The main functionality works but only in cosmic desktop. So there are some issues that need to be worked out yet in regards to libcosmic.
764 lines
24 KiB
Rust
764 lines
24 KiB
Rust
use clap::{command, Parser};
|
|
use core::service_items::{ServiceItem, ServiceItemModel};
|
|
use cosmic::app::context_drawer::ContextDrawer;
|
|
use cosmic::app::{Core, Settings, Task};
|
|
use cosmic::iced::clipboard::dnd::DndAction;
|
|
use cosmic::iced::keyboard::{Key, Modifiers};
|
|
use cosmic::iced::window::{Mode, Position};
|
|
use cosmic::iced::{self, event, window, Length, Padding, Point};
|
|
use cosmic::iced_futures::Subscription;
|
|
use cosmic::iced_widget::{column, row};
|
|
use cosmic::prelude::ElementExt;
|
|
use cosmic::prelude::*;
|
|
use cosmic::widget::dnd_destination::DragId;
|
|
use cosmic::widget::nav_bar::nav_bar_style;
|
|
use cosmic::widget::segmented_button::Entity;
|
|
use cosmic::widget::tooltip::Position as TPosition;
|
|
use cosmic::widget::{
|
|
button, nav_bar, text, tooltip, DndDestination, DndSource, Id,
|
|
Space,
|
|
};
|
|
use cosmic::widget::{icon, slider};
|
|
use cosmic::{executor, Application, ApplicationExt, Element};
|
|
use cosmic::{widget::Container, Theme};
|
|
use crisp::types::Value;
|
|
use lisp::parse_lisp;
|
|
use miette::{miette, Result};
|
|
use std::fs::read_to_string;
|
|
use std::path::PathBuf;
|
|
use tracing::{debug, level_filters::LevelFilter};
|
|
use tracing::{error, warn};
|
|
use tracing_subscriber::EnvFilter;
|
|
use ui::editor::SongEditor;
|
|
use ui::library::{self, Library};
|
|
|
|
pub mod core;
|
|
pub mod lisp;
|
|
pub mod ui;
|
|
use core::slide::*;
|
|
use ui::presenter::{self, Presenter};
|
|
|
|
#[derive(Debug, Parser)]
|
|
#[command(name = "lumina", version, about)]
|
|
struct Cli {
|
|
#[arg(short, long)]
|
|
watch: bool,
|
|
#[arg(short = 'i', long)]
|
|
ui: bool,
|
|
file: PathBuf,
|
|
}
|
|
|
|
fn main() -> Result<()> {
|
|
let timer = tracing_subscriber::fmt::time::ChronoLocal::new(
|
|
"%Y-%m-%d_%I:%M:%S%.6f %P".to_owned(),
|
|
);
|
|
let filter = EnvFilter::builder()
|
|
.with_default_directive(LevelFilter::WARN.into())
|
|
.parse_lossy("lumina=debug");
|
|
tracing_subscriber::FmtSubscriber::builder()
|
|
.pretty()
|
|
.with_line_number(true)
|
|
.with_level(true)
|
|
.with_target(true)
|
|
.with_env_filter(filter)
|
|
.with_target(true)
|
|
.with_timer(timer)
|
|
.init();
|
|
|
|
let args = Cli::parse();
|
|
|
|
let settings;
|
|
if args.ui {
|
|
debug!("main view");
|
|
settings = Settings::default().debug(false);
|
|
} else {
|
|
debug!("window view");
|
|
settings =
|
|
Settings::default().debug(false).no_main_window(true);
|
|
}
|
|
|
|
cosmic::app::run::<App>(settings, args)
|
|
.map_err(|e| miette!("Invalid things... {}", e))
|
|
}
|
|
|
|
fn theme(_state: &App) -> Theme {
|
|
Theme::dark()
|
|
}
|
|
|
|
struct App {
|
|
core: Core,
|
|
nav_model: nav_bar::Model,
|
|
file: PathBuf,
|
|
presenter: Presenter,
|
|
windows: Vec<window::Id>,
|
|
slides: Vec<Slide>,
|
|
current_slide: Slide,
|
|
presentation_open: bool,
|
|
cli_mode: bool,
|
|
library: Option<Library>,
|
|
library_open: bool,
|
|
library_width: f32,
|
|
song_editor: SongEditor,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
enum Message {
|
|
Present(presenter::Message),
|
|
Library(library::Message),
|
|
SongEditor(editor::Message),
|
|
File(PathBuf),
|
|
DndEnter(Entity, Vec<String>),
|
|
DndDrop(Entity, Option<ServiceItem>, DndAction),
|
|
OpenWindow,
|
|
CloseWindow(Option<window::Id>),
|
|
WindowOpened(window::Id, Option<Point>),
|
|
WindowClosed(window::Id),
|
|
AddLibrary(Library),
|
|
Quit,
|
|
Key(Key, Modifiers),
|
|
None,
|
|
DndLeave(Entity),
|
|
}
|
|
|
|
impl cosmic::Application for App {
|
|
type Executor = executor::Default;
|
|
type Flags = Cli;
|
|
type Message = Message;
|
|
const APP_ID: &'static str = "lumina";
|
|
fn core(&self) -> &Core {
|
|
&self.core
|
|
}
|
|
fn core_mut(&mut self) -> &mut Core {
|
|
&mut self.core
|
|
}
|
|
fn init(
|
|
core: Core,
|
|
input: Self::Flags,
|
|
) -> (Self, Task<Self::Message>) {
|
|
debug!("init");
|
|
let mut nav_model = nav_bar::Model::default();
|
|
|
|
let mut windows = vec![];
|
|
|
|
if input.ui {
|
|
windows.push(core.main_window_id().unwrap());
|
|
}
|
|
|
|
let items = match read_to_string(input.file) {
|
|
Ok(lisp) => {
|
|
let mut slide_vector = vec![];
|
|
let lisp = crisp::reader::read(&lisp);
|
|
match lisp {
|
|
Value::List(vec) => {
|
|
for value in vec {
|
|
let mut inner_vector = parse_lisp(value);
|
|
slide_vector.append(&mut inner_vector);
|
|
}
|
|
}
|
|
_ => todo!(),
|
|
}
|
|
slide_vector
|
|
}
|
|
Err(e) => {
|
|
warn!("Missing file or could not read: {e}");
|
|
vec![]
|
|
}
|
|
};
|
|
|
|
let items = ServiceItemModel::from(items);
|
|
let presenter = Presenter::with_items(items.clone());
|
|
let slides = items.to_slides().unwrap_or_default();
|
|
let current_slide = slides[0].clone();
|
|
let song_editor = SongEditor::new();
|
|
|
|
for item in items.iter() {
|
|
nav_model.insert().text(item.title()).data(item.clone());
|
|
}
|
|
|
|
nav_model.activate_position(0);
|
|
|
|
let mut app = App {
|
|
presenter,
|
|
song_editor,
|
|
core,
|
|
nav_model,
|
|
file: PathBuf::default(),
|
|
windows,
|
|
slides,
|
|
current_slide,
|
|
presentation_open: false,
|
|
cli_mode: !input.ui,
|
|
library: None,
|
|
library_open: true,
|
|
library_width: 60.0,
|
|
};
|
|
|
|
let mut batch = vec![];
|
|
|
|
if input.ui {
|
|
debug!("main view");
|
|
batch.push(app.update_title())
|
|
} else {
|
|
debug!("window view");
|
|
batch.push(app.show_window())
|
|
};
|
|
|
|
batch.push(app.add_library());
|
|
let batch = Task::batch(batch);
|
|
(app, batch)
|
|
}
|
|
|
|
/// Allows COSMIC to integrate with your application's [`nav_bar::Model`].
|
|
fn nav_model(&self) -> Option<&nav_bar::Model> {
|
|
Some(&self.nav_model)
|
|
}
|
|
|
|
fn nav_bar(
|
|
&self,
|
|
) -> Option<Element<cosmic::app::Message<Message>>> {
|
|
if !self.core().nav_bar_active() {
|
|
return None;
|
|
}
|
|
|
|
let nav_model = self.nav_model()?;
|
|
|
|
let mut nav = cosmic::widget::nav_bar(nav_model, |id| {
|
|
cosmic::app::Message::Cosmic(
|
|
cosmic::app::cosmic::Message::NavBar(id),
|
|
)
|
|
})
|
|
.on_dnd_enter(|entity, data| {
|
|
debug!("entered");
|
|
cosmic::app::Message::App(Message::DndEnter(entity, data))
|
|
})
|
|
.on_dnd_leave(|entity| {
|
|
debug!("left");
|
|
cosmic::app::Message::App(Message::DndLeave(entity))
|
|
})
|
|
.drag_id(DragId::new())
|
|
.on_context(|id| {
|
|
cosmic::app::Message::Cosmic(
|
|
cosmic::app::cosmic::Message::NavBarContext(id),
|
|
)
|
|
})
|
|
.on_dnd_drop::<ServiceItem>(|entity, data, action| {
|
|
debug!("dropped");
|
|
cosmic::app::Message::App(Message::DndDrop(
|
|
entity, data, action,
|
|
))
|
|
})
|
|
.context_menu(None)
|
|
.into_container()
|
|
// XXX both must be shrink to avoid flex layout from ignoring it
|
|
.width(Length::Shrink)
|
|
.height(Length::Shrink);
|
|
|
|
if !self.core().is_condensed() {
|
|
nav = nav.max_width(280);
|
|
}
|
|
|
|
// let dnd = DndDestination::new(
|
|
// nav,
|
|
// vec!["application/service-item".to_string().into()],
|
|
// )
|
|
// .data_received_for(|item| {
|
|
// cosmic::app::Message::App(Message::DndDrop(item))
|
|
// });
|
|
|
|
let column = column![
|
|
text::heading("Service List").center().width(280),
|
|
nav
|
|
]
|
|
.spacing(10);
|
|
let padding = Padding::new(0.0).top(20);
|
|
let container = Container::new(column)
|
|
.style(nav_bar_style)
|
|
.padding(padding);
|
|
Some(container.into())
|
|
}
|
|
|
|
/// Called when a navigation item is selected.
|
|
fn on_nav_select(
|
|
&mut self,
|
|
id: nav_bar::Id,
|
|
) -> Task<Self::Message> {
|
|
self.nav_model.activate(id);
|
|
// debug!(?id);
|
|
self.update_title()
|
|
}
|
|
|
|
fn header_start(&self) -> Vec<Element<Self::Message>> {
|
|
vec![]
|
|
}
|
|
fn header_center(&self) -> Vec<Element<Self::Message>> {
|
|
vec![]
|
|
}
|
|
fn header_end(&self) -> Vec<Element<Self::Message>> {
|
|
let presenter_window = self.windows.get(1);
|
|
let text = if self.presentation_open {
|
|
text::body("Close Presentation")
|
|
} else {
|
|
text::body("Open Presentation")
|
|
};
|
|
vec![tooltip(
|
|
button::custom(
|
|
row!(
|
|
Container::new(
|
|
icon::from_name(if self.presentation_open {
|
|
"dialog-close"
|
|
} else {
|
|
"view-presentation-symbolic"
|
|
})
|
|
.scale(3)
|
|
)
|
|
.center_y(Length::Fill),
|
|
text
|
|
)
|
|
.padding(5)
|
|
.spacing(5),
|
|
)
|
|
.class(cosmic::theme::style::Button::HeaderBar)
|
|
.on_press({
|
|
if self.presentation_open {
|
|
Message::CloseWindow(presenter_window.copied())
|
|
} else {
|
|
Message::OpenWindow
|
|
}
|
|
}),
|
|
"Start Presentation",
|
|
TPosition::Bottom,
|
|
)
|
|
.into()]
|
|
}
|
|
|
|
fn footer(&self) -> Option<Element<Self::Message>> {
|
|
Some(text::body("Sux").into())
|
|
}
|
|
|
|
fn subscription(&self) -> Subscription<Self::Message> {
|
|
event::listen_with(|event, _, id| {
|
|
// debug!(?event);
|
|
match event {
|
|
iced::Event::Keyboard(event) => match event {
|
|
iced::keyboard::Event::KeyPressed {
|
|
key,
|
|
modifiers,
|
|
..
|
|
} => Some(Message::Key(key, modifiers)),
|
|
_ => None,
|
|
},
|
|
iced::Event::Mouse(event) => None,
|
|
iced::Event::Window(window_event) => {
|
|
match window_event {
|
|
window::Event::CloseRequested => {
|
|
debug!("Closing window");
|
|
Some(Message::CloseWindow(Some(id)))
|
|
}
|
|
window::Event::Opened {
|
|
position, ..
|
|
} => {
|
|
debug!(?window_event, ?id);
|
|
Some(Message::WindowOpened(id, position))
|
|
}
|
|
window::Event::Closed => {
|
|
debug!("Closed window");
|
|
Some(Message::WindowClosed(id))
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
iced::Event::Touch(event) => None,
|
|
iced::Event::A11y(id, action_request) => None,
|
|
iced::Event::Dnd(dnd_event) => None,
|
|
iced::Event::PlatformSpecific(platform_specific) => {
|
|
None
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
fn context_drawer(
|
|
&self,
|
|
) -> Option<
|
|
cosmic::app::context_drawer::ContextDrawer<Self::Message>,
|
|
> {
|
|
ContextDrawer {
|
|
title: Some("Context".into()),
|
|
header_actions: vec![],
|
|
header: Some("hi".into()),
|
|
content: "Sup".into(),
|
|
footer: Some("foot".into()),
|
|
on_close: Message::None,
|
|
};
|
|
None
|
|
}
|
|
|
|
fn update(&mut self, message: Message) -> Task<Message> {
|
|
match message {
|
|
Message::Key(key, modifiers) => {
|
|
debug!(?key, ?modifiers);
|
|
match (key, modifiers) {
|
|
(
|
|
Key::Named(
|
|
iced::keyboard::key::Named::ArrowRight,
|
|
),
|
|
_,
|
|
) => self.update(Message::Present(
|
|
presenter::Message::NextSlide,
|
|
)),
|
|
(
|
|
Key::Named(
|
|
iced::keyboard::key::Named::ArrowLeft,
|
|
),
|
|
_,
|
|
) => self.update(Message::Present(
|
|
presenter::Message::PrevSlide,
|
|
)),
|
|
(
|
|
Key::Named(iced::keyboard::key::Named::Space),
|
|
_,
|
|
) => self.update(Message::Present(
|
|
presenter::Message::NextSlide,
|
|
)),
|
|
(Key::Character(k), _)
|
|
if k == *"j" || k == *"l" =>
|
|
{
|
|
self.update(Message::Present(
|
|
presenter::Message::NextSlide,
|
|
))
|
|
}
|
|
(Key::Character(k), _)
|
|
if k == *"k" || k == *"h" =>
|
|
{
|
|
self.update(Message::Present(
|
|
presenter::Message::PrevSlide,
|
|
))
|
|
}
|
|
(Key::Character(k), _) if k == *"q" => {
|
|
self.update(Message::Quit)
|
|
}
|
|
_ => Task::none(),
|
|
}
|
|
}
|
|
Message::SongEditor(message) => {
|
|
todo!()
|
|
}
|
|
Message::Present(message) => {
|
|
// debug!(?message);
|
|
if self.presentation_open {
|
|
if let Some(video) = &mut self.presenter.video {
|
|
video.set_muted(false);
|
|
}
|
|
}
|
|
self.presenter.update(message).map(|x| {
|
|
debug!(?x);
|
|
cosmic::app::Message::App(Message::None)
|
|
})
|
|
}
|
|
|
|
Message::Library(message) => {
|
|
// debug!(?message);
|
|
if let Some(library) = &mut self.library {
|
|
library.update(message).map(|x| {
|
|
debug!(?x);
|
|
cosmic::app::Message::App(Message::None)
|
|
})
|
|
} else {
|
|
Task::none()
|
|
}
|
|
}
|
|
Message::File(file) => {
|
|
self.file = file;
|
|
Task::none()
|
|
}
|
|
Message::OpenWindow => {
|
|
let count = self.windows.len() + 1;
|
|
|
|
let (id, spawn_window) =
|
|
window::open(window::Settings {
|
|
position: Position::Centered,
|
|
exit_on_close_request: count % 2 == 0,
|
|
decorations: false,
|
|
..Default::default()
|
|
});
|
|
|
|
self.windows.push(id);
|
|
_ = self.set_window_title(
|
|
format!("window_{}", count),
|
|
id,
|
|
);
|
|
|
|
spawn_window.map(|id| {
|
|
cosmic::app::Message::App(Message::WindowOpened(
|
|
id, None,
|
|
))
|
|
})
|
|
}
|
|
Message::CloseWindow(id) => {
|
|
if let Some(id) = id {
|
|
window::close(id)
|
|
} else {
|
|
Task::none()
|
|
}
|
|
}
|
|
Message::WindowOpened(id, _) => {
|
|
debug!(?id, "Window opened");
|
|
if self.cli_mode
|
|
|| id > self.core.main_window_id().expect("Cosmic core seems to be missing a main window, was this started in cli mode?")
|
|
{
|
|
self.presentation_open = true;
|
|
if let Some(video) = &mut self.presenter.video {
|
|
video.set_muted(false);
|
|
}
|
|
window::change_mode(id, Mode::Fullscreen)
|
|
} else {
|
|
Task::none()
|
|
}
|
|
}
|
|
Message::WindowClosed(id) => {
|
|
warn!("Closing window: {id}");
|
|
let Some(window) =
|
|
self.windows.iter().position(|w| *w == id)
|
|
else {
|
|
error!("Nothing matches this window id: {id}");
|
|
return Task::none();
|
|
};
|
|
self.windows.remove(window);
|
|
// This closes the app if using the cli example
|
|
if self.windows.is_empty() {
|
|
self.update(Message::Quit)
|
|
} else {
|
|
self.presentation_open = false;
|
|
if let Some(video) = &mut self.presenter.video {
|
|
video.set_muted(true);
|
|
}
|
|
Task::none()
|
|
}
|
|
}
|
|
Message::Quit => cosmic::iced::exit(),
|
|
Message::DndEnter(entity, data) => {
|
|
debug!(?entity);
|
|
debug!(?data);
|
|
Task::none()
|
|
}
|
|
Message::DndDrop(entity, service_item, action) => {
|
|
debug!(?entity);
|
|
debug!(?action);
|
|
debug!(?service_item);
|
|
Task::none()
|
|
}
|
|
Message::AddLibrary(library) => {
|
|
self.library = Some(library);
|
|
Task::none()
|
|
}
|
|
Message::None => Task::none(),
|
|
Message::DndLeave(entity) => {
|
|
// debug!(?entity);
|
|
Task::none()
|
|
}
|
|
}
|
|
}
|
|
|
|
// Main window view
|
|
fn view(&self) -> Element<Message> {
|
|
let icon_left = icon::from_name("arrow-left");
|
|
let icon_right = icon::from_name("arrow-right");
|
|
|
|
let video_range = self.presenter.video.as_ref().map_or_else(
|
|
|| 0.0,
|
|
|video| video.duration().as_secs_f32(),
|
|
);
|
|
|
|
let video_button_icon =
|
|
if let Some(video) = &self.presenter.video {
|
|
let (icon_name, tooltip) = if video.paused() {
|
|
("media-play", "Play")
|
|
} else {
|
|
("media-pause", "Pause")
|
|
};
|
|
button::icon(icon::from_name(icon_name))
|
|
.tooltip(tooltip)
|
|
.on_press(Message::Present(
|
|
presenter::Message::StartVideo,
|
|
))
|
|
} else {
|
|
button::icon(icon::from_name("media-play"))
|
|
.tooltip("Play")
|
|
.on_press(Message::Present(
|
|
presenter::Message::StartVideo,
|
|
))
|
|
};
|
|
|
|
let slide_preview = column![
|
|
Space::with_height(Length::Fill),
|
|
Container::new(
|
|
self.presenter.view_preview().map(Message::Present),
|
|
)
|
|
.align_bottom(Length::Fill),
|
|
Container::new(if self.presenter.video.is_some() {
|
|
row![
|
|
video_button_icon,
|
|
Container::new(
|
|
slider(
|
|
0.0..=video_range,
|
|
self.presenter.video_position,
|
|
|pos| {
|
|
Message::Present(
|
|
presenter::Message::VideoPos(pos),
|
|
)
|
|
}
|
|
)
|
|
.step(0.1)
|
|
)
|
|
.center_x(Length::Fill)
|
|
.padding([7, 0, 0, 0])
|
|
]
|
|
.padding(5)
|
|
} else {
|
|
row![]
|
|
})
|
|
.center_x(Length::Fill),
|
|
Space::with_height(Length::Fill),
|
|
]
|
|
.spacing(3);
|
|
|
|
let mut drag_item = None;
|
|
|
|
let library =
|
|
Container::new(if let Some(library) = &self.library {
|
|
let (view, item) = library.view();
|
|
drag_item = item;
|
|
view.map(|m| Message::Library(m))
|
|
} else {
|
|
Space::new(0, 0).into()
|
|
})
|
|
.style(nav_bar_style)
|
|
.center(Length::Fill);
|
|
|
|
let song_editor =
|
|
self.song_editor.view().map(|m| Message::SongEditor(m));
|
|
// let dnd_source: DndSource<Message, _> = DndSource::with_id(
|
|
// drag_item.expect("errors").map(|m| Message::Library(m)),
|
|
// Id::new("item"),
|
|
// );
|
|
// let drag_handle = Container::new(Space::new(1, Length::Fill))
|
|
// .style(|t| nav_bar_style(t));
|
|
// let dragger = MouseArea::new(drag_handle)
|
|
// .on_drag(Message::LibraryWidth);
|
|
|
|
let row = row![
|
|
Container::new(
|
|
button::icon(icon_left)
|
|
.icon_size(128)
|
|
.tooltip("Previous Slide")
|
|
.width(128)
|
|
.on_press(Message::Present(
|
|
presenter::Message::PrevSlide
|
|
))
|
|
)
|
|
.center_y(Length::Fill)
|
|
.align_right(Length::Fill)
|
|
.width(Length::FillPortion(2)),
|
|
Container::new(slide_preview)
|
|
.center_y(Length::Fill)
|
|
.width(Length::FillPortion(3)),
|
|
Container::new(
|
|
button::icon(icon_right)
|
|
.icon_size(128)
|
|
.tooltip("Next Slide")
|
|
.width(128)
|
|
.on_press(Message::Present(
|
|
presenter::Message::NextSlide
|
|
))
|
|
)
|
|
.center_y(Length::Fill)
|
|
.align_left(Length::Fill)
|
|
.width(Length::FillPortion(2)),
|
|
library.width(Length::FillPortion(2))
|
|
]
|
|
.width(Length::Fill)
|
|
.height(Length::Fill)
|
|
.spacing(20);
|
|
|
|
let column = column![
|
|
Container::new(row).center_y(Length::Fill),
|
|
Container::new(
|
|
self.presenter.preview_bar().map(Message::Present)
|
|
)
|
|
.clip(true)
|
|
.width(Length::Fill)
|
|
.center_y(130)
|
|
];
|
|
|
|
let element: Element<Message> = column.into();
|
|
element
|
|
}
|
|
|
|
// View for presentation
|
|
fn view_window(&self, _id: window::Id) -> Element<Message> {
|
|
self.presenter.view().map(Message::Present)
|
|
}
|
|
}
|
|
|
|
impl App
|
|
where
|
|
Self: cosmic::Application,
|
|
{
|
|
fn active_page_title(&self) -> &str {
|
|
let Some(label) =
|
|
self.nav_model.text(self.nav_model.active())
|
|
else {
|
|
return "Lumina";
|
|
};
|
|
label
|
|
}
|
|
|
|
fn update_title(&mut self) -> Task<Message> {
|
|
let header_title = self.active_page_title().to_owned();
|
|
let window_title = format!("{header_title} — Lumina");
|
|
self.core.main_window_id().map_or_else(Task::none, |id| {
|
|
self.set_window_title(window_title, id)
|
|
})
|
|
}
|
|
|
|
fn show_window(&mut self) -> Task<Message> {
|
|
let (id, spawn_window) = window::open(window::Settings {
|
|
position: Position::Centered,
|
|
exit_on_close_request: true,
|
|
decorations: false,
|
|
..Default::default()
|
|
});
|
|
self.windows.push(id);
|
|
_ = self.set_window_title("Lumina Presenter".to_owned(), id);
|
|
spawn_window.map(|id| {
|
|
cosmic::app::Message::App(Message::WindowOpened(id, None))
|
|
})
|
|
}
|
|
|
|
fn add_library(&mut self) -> Task<Message> {
|
|
Task::perform(async { Library::new().await }, |x| {
|
|
cosmic::app::Message::App(Message::AddLibrary(x))
|
|
})
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use super::*;
|
|
use pretty_assertions::assert_eq;
|
|
|
|
fn test_slide() -> String {
|
|
let slide = r#"(slide (image :source "./somehting.jpg" :fill cover
|
|
(text "Something cooler" :font-size 50)))"#;
|
|
String::from(slide)
|
|
}
|
|
// #[test]
|
|
// fn test_lisp() {
|
|
// let slide = test_slide();
|
|
// if let Ok(data) = lexpr::parse::from_str_elisp(slide.as_str()) {
|
|
// assert_eq!(slide, data)
|
|
// } else {
|
|
// assert!(false)
|
|
// }
|
|
// }
|
|
}
|