diff --git a/ROADMAP.md b/ROADMAP.md index 878e041..30dbb1f 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -22,16 +22,16 @@ __Objective:__ Establish a clean, testable core with `clap` and a minimal `ratat __Objective:__ Parse markdown documents into a rich `Slide` struct. -| Task | Description | Key Crates | -| ---------------------- | --------------------------------------------------------------- | -------------------- | -| __✓ Parser Core__ | Split files on `---` separators. | `pulldown-cmark`[^4] | -| | Detect title blocks, lists, and code fences. | | -| | Represent as `Vec`. | | -| __✓ Slide Model__ | Define structs: `Slide`, `Block`, `TextSpan`, `CodeBlock`, etc. | Internal | -| __✓ Metadata Parsing__ | Optional front matter (YAML/TOML) for theme, author, etc. | `serde_yml`[^5] | -| __Error & Validation__ | Provide friendly parser errors with file/line info. | `thiserror`[^6] | -| __✓ Basic CLI UX__ | `slides present file.md` runs full TUI. | `clap` | -| | `slides print` renders to stdout with width constraint. | | +| Task | Description | Key Crates | +| ----------------------- | --------------------------------------------------------------- | -------------------- | +| __✓ Parser Core__ | Split files on `---` separators. | `pulldown-cmark`[^4] | +| | Detect title blocks, lists, and code fences. | | +| | Represent as `Vec`. | | +| __✓ Slide Model__ | Define structs: `Slide`, `Block`, `TextSpan`, `CodeBlock`, etc. | Internal | +| __✓ Metadata Parsing__ | Optional front matter (YAML/TOML) for theme, author, etc. | `serde_yml`[^5] | +| __Error & Validation__ | Provide friendly parser errors with file/line info. | `thiserror`[^6] | +| __✓ Basic CLI UX__ | `lantern present file.md` runs full TUI. | `clap` | +| | `lantern print` renders to stdout with width constraint. | | ## Rendering & Navigation @@ -44,7 +44,8 @@ __Objective:__ Build the interactive slide renderer with navigation. | __✓ Status Bar__ | Display slide count, filename, clock, and theme name. | `ratatui` | | __✓ Color Styling__ | Apply consistent color palette via `owo-colors`. Define traits like `ThemeColor`. | `owo-colors` | | __✓ Unicode Headings__ | Use Unicode block symbols (▉▓▒░▌) for h1-h6 instead of markdown `#` syntax. | Unicode constants | -| __Configurable Themes__ | Base16 YAML theme system with 10 prebuilt themes. Add user theme loading from config directory and CLI `--theme-file` flag. | `serde_yml`, `serde`, `dirs` | +| __Configurable Themes__ | Base16 YAML theme system with 10 prebuilt themes. | `serde_yml`, `serde` | +| | Add user theme loading from config directory and CLI `--theme-file` flag. | `dirs` | --- @@ -60,7 +61,7 @@ __Objective:__ Add first-class syntax highlighting using Syntect. | | Render syntax-highlighted text with color spans mapped to `owo-colors`. | | | __✓ Theming__ | Map terminal theme choice to Syntect theme (e.g., `"OneDark"`, `"Monokai"`). | `syntect` | | __✓ Performance__ | Lazy-load themes and syntaxes; use `OnceLock` for caching. | `std::sync::OnceLock` | -| __✓ Mode__ | Render to ANSI-colored plain text output (for `slides print`). | `owo-colors` | +| __✓ Mode__ | Render to ANSI-colored plain text output (for `lantern print`). | `owo-colors` | ## Presenter @@ -78,12 +79,12 @@ __Objective:__ Introduce features for live presentations and authoring convenien __Objective:__ Add richness and visual polish to text and layout. -| Task | Description | Key Crates | -| -------------------- | ------------------------------------------------------------ | ----------------------------- | -| __Tables & Lists__ | Render GitHub-style tables, bullets, and task lists. | `pulldown-cmark`, `ratatui` | -| __Admonitions__ | Highlighted boxes with icons | `owo-colors`, internal glyphs | -| __Horizontal Rules__ | Use box-drawing (`─`, `═`) and shading (`░`, `▓`). | Unicode constants | -| __Generators__ | `slides init` scaffolds an example deck with code and notes. | `include_str!`, `fs` | +| Task | Description | Key Crates | +| -------------------- | ------------------------------------------------------------- | ----------------------------- | +| __Tables & Lists__ | Render GitHub-style tables, bullets, and task lists. | `pulldown-cmark`, `ratatui` | +| __Admonitions__ | Highlighted boxes with icons | `owo-colors`, internal glyphs | +| __Horizontal Rules__ | Use box-drawing (`─`, `═`) and shading (`░`, `▓`). | Unicode constants | +| __Generators__ | `lantern init` scaffolds an example deck with code and notes. | `include_str!`, `fs` | ## RC @@ -113,7 +114,7 @@ __Objective:__ Generate high-quality PNG/SVG snapshots of any slide (Freeze-styl | __Canvas → Pixmap__ | Implement a `FrameRasterizer` that turns a `Frame` + layout into an RGBA pixmap (background, panes, etc). | `tiny-skia` | | __Text Rendering__ | Render slide titles/body text via glyph rasterization and simple layout (left/center, line wrapping). | `ab_glyph` | | __Terminal Snapshot Mode__ | Convert `TerminalBuffer` into a rendered terminal "window" (frame, tabs, padding, cursor). | `tiny-skia`, `ab_glyph` | -| __Slide Screenshot CLI__ | `slides export-image deck.md --slide 5 --output slide-5.png` (PNG by default, optional SVG/WebP). | `clap`, `image` | +| __Slide Screenshot CLI__ | `lantern export-image deck.md --slide 5 --output slide-5.png` (PNG by default, optional SVG/WebP). | `clap`, `image` | | __Batch Export__ | `--all` / `--range 3..7` to dump multiple slides, naming convention like `deck-003.png`. | `image` | | __Deterministic Layout Test__ | Golden tests comparing generated PNGs against fixtures for regression in layout and text. | `image`, integration test harness | @@ -126,7 +127,7 @@ __Objective:__ Produce MP4/WebM/GIF recordings of a scripted terminal+slides run | __Timeline Scheduling__ | Extend `Event` to carry timestamps or durations; implement `Scheduler` to emit frames at target FPS. | internal `timeline` module | | | __Frame Capture Loop__ | Drive the same layout/rasterizer used for images at N FPS, yielding a sequence of RGBA frames. | `tiny-skia`, `image` | | | __FFmpeg Binding Layer__ | Wrap `ffmpeg-next` to open an encoder, configure codec/container, and accept raw frames. | `ffmpeg-next` | | -| __Video Export CLI__ | `slides export-video deck.md --output demo.mp4 --fps 30 --duration 120s` (or auto-duration from events). | `clap`, internal encoder | | +| __Video Export CLI__ | `lantern export-video deck.md --output demo.mp4 --fps 30 --duration 120s` (or auto-duration from events). | `clap`, internal encoder | | | __GIF / WebM Variants__ | Add `--format gif | webm` mapping to appropriate ffmpeg muxer/codec presets. | `ffmpeg-next`[^7] | | __Typing & Cursor Effects__ | Represent typing, deletes, cursor blinks as timeline events, so video export matches live presentation feel. | internal `timeline`, terminal core | | | __Audio-less Simplification__ | Keep V1 video export silent (no audio tracks) for simpler ffmpeg integration and smaller binaries. | `ffmpeg-next` | | @@ -140,7 +141,7 @@ __Objective:__ Generate vertical (portrait) slides optimized for short-form vert | -------------------------- | ------------------------------------------------------------------------------------------------ | ----------------------------- | | __Portrait Layout Engine__ | Implement 9:16 aspect ratio layout with vertical constraints (1080x1920, 720x1280). | internal `layout` module | | __Mobile-Optimized Text__ | Larger font sizes, reduced content density, and simplified layouts for mobile readability. | `ab_glyph`, `tiny-skia` | -| __Vertical Export CLI__ | `slides export-vertical deck.md --output reel.mp4` with preset dimensions for each platform. | `clap`, internal encoder | +| __Vertical Export CLI__ | `lantern export-vertical deck.md --output reel.mp4` with preset dimensions for each platform. | `clap`, internal encoder | | __Platform Presets__ | Built-in presets: `instagram-reel`, `tiktok`, `youtube-shorts` with optimal resolution/duration. | internal preset registry | | __Content Adaptation__ | Auto-scale or warn when horizontal content doesn't fit portrait orientation. | internal `layout` module | | __Safe Zones__ | Respect platform UI overlays (captions, profile pics) with configurable safe zones. | internal `layout` module | @@ -152,7 +153,7 @@ __Objective:__ Make "slides → image/video" a natural extension of your current | Task | Description | Key Crates | | ------------------------ | ------------------------------------------------------------------------------------------------ | ---------------------------- | -| __Export Subcommands__ | Add `slides export-image` and `slides export-video` commands with shared flags (theme, range). | `clap` | +| __Export Subcommands__ | Add `lantern export-image` and `lantern export-video` commands with shared flags (theme, range). | `clap` | | __Frontmatter Controls__ | Support per-deck/per-slide frontmatter: `fps`, `default_duration`, `transition`, `record: true`. | `pulldown-cmark-frontmatter` | | __Deterministic Seeds__ | Add `--seed` for any animations (typing jitter, cursor blink timing) to keep exports repeatable. | internal `timeline` | | __Preset Profiles__ | Presets like `social-card`, `doc-screenshot`, `talk-demo` mapping to resolution + theme. | internal profile registry | diff --git a/core/src/theme.rs b/core/src/theme.rs index 5765cb8..8d97eb5 100644 --- a/core/src/theme.rs +++ b/core/src/theme.rs @@ -129,9 +129,8 @@ pub fn detect_is_dark() -> bool { /// Color theme abstraction for slides with semantic roles for consistent theming across the application. /// -/// Stores RGB colors that can be converted to both owo-colors Style (for terminal output) -/// and ratatui Color (for TUI rendering) via Into implementations. -#[derive(Debug, Clone)] +/// Stores RGB colors that can be converted to both owo-colors Style (for terminal output) and ratatui Color (for TUI rendering). +#[derive(Debug, Clone, Copy)] pub struct ThemeColors { pub heading: Color, pub heading_bold: bool, @@ -652,7 +651,6 @@ palette: assert!(theme.link(&"Test").to_string().contains("Test")); assert!(theme.inline_code_bg(&"Test").to_string().contains("Test")); - // UI colors don't need style methods, just verify they exist let _ = theme.ui_border; let _ = theme.ui_title; let _ = theme.ui_text; diff --git a/ui/src/viewer.rs b/ui/src/viewer.rs index fa5dfac..ad613d2 100644 --- a/ui/src/viewer.rs +++ b/ui/src/viewer.rs @@ -1,15 +1,67 @@ +use lantern_core::{slide::Slide, theme::ThemeColors}; use ratatui::{ Frame, layout::Rect, style::{Color, Modifier, Style}, text::{Line, Span}, - widgets::{Block, Borders, Paragraph, Wrap}, + widgets::{Block, Borders, Padding, Paragraph, Wrap}, }; -use lantern_core::{slide::Slide, theme::ThemeColors}; use std::time::Instant; use crate::renderer::render_slide_content; +#[derive(Clone, Copy)] +struct Stylesheet { + theme: ThemeColors, +} + +impl Stylesheet { + fn new(theme: ThemeColors) -> Self { + Self { theme } + } + + fn slide_padding() -> Padding { + Padding::new(4, 4, 2, 2) + } + + fn status_bar(&self) -> Style { + Style::default() + .bg(self.bg_color()) + .fg(self.ui_text_color()) + .add_modifier(Modifier::BOLD) + } + + fn border_color(&self) -> Color { + Color::Rgb(self.theme.ui_border.r, self.theme.ui_border.g, self.theme.ui_border.b) + } + + fn title_color(&self) -> Color { + Color::Rgb(self.theme.ui_title.r, self.theme.ui_title.g, self.theme.ui_title.b) + } + + fn text_color(&self) -> Color { + Color::Rgb(self.theme.body.r, self.theme.body.g, self.theme.body.b) + } + + fn bg_color(&self) -> Color { + Color::Rgb( + self.theme.ui_background.r, + self.theme.ui_background.g, + self.theme.ui_background.b, + ) + } + + fn ui_text_color(&self) -> Color { + Color::Rgb(self.theme.ui_text.r, self.theme.ui_text.g, self.theme.ui_text.b) + } +} + +impl From for Stylesheet { + fn from(value: ThemeColors) -> Self { + Self::new(value) + } +} + /// Slide viewer state manager /// /// Manages current slide index, navigation, and speaker notes visibility. @@ -17,8 +69,8 @@ pub struct SlideViewer { slides: Vec, current_index: usize, show_notes: bool, - theme: ThemeColors, filename: Option, + stylesheet: Stylesheet, theme_name: String, start_time: Option, } @@ -30,7 +82,7 @@ impl SlideViewer { slides, current_index: 0, show_notes: false, - theme, + stylesheet: theme.into(), filename: None, theme_name: "default".to_string(), start_time: None, @@ -42,7 +94,15 @@ impl SlideViewer { slides: Vec, theme: ThemeColors, filename: Option, theme_name: String, start_time: Option, ) -> Self { - Self { slides, current_index: 0, show_notes: false, theme, filename, theme_name, start_time } + Self { + slides, + current_index: 0, + show_notes: false, + stylesheet: theme.into(), + filename, + theme_name, + start_time, + } } /// Navigate to the next slide @@ -91,16 +151,23 @@ impl SlideViewer { self.show_notes } + fn theme(&self) -> ThemeColors { + self.stylesheet.theme.clone() + } + /// Render the current slide to the frame pub fn render(&self, frame: &mut Frame, area: Rect) { if let Some(slide) = self.current_slide() { - let content = render_slide_content(&slide.blocks, &self.theme); + let content = render_slide_content(&slide.blocks, &self.theme()); + let border_color = self.stylesheet.border_color(); + let title_color = self.stylesheet.title_color(); let block = Block::default() .borders(Borders::ALL) - .border_style(Style::default().fg(Color::DarkGray)) + .border_style(Style::default().fg(border_color)) .title(format!(" Slide {}/{} ", self.current_index + 1, self.total_slides())) - .title_style(Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)); + .title_style(Style::default().fg(title_color).add_modifier(Modifier::BOLD)) + .padding(Stylesheet::slide_padding()); let paragraph = Paragraph::new(content).block(block).wrap(Wrap { trim: false }); @@ -116,16 +183,21 @@ impl SlideViewer { if let Some(slide) = self.current_slide() { if let Some(notes) = &slide.notes { + let border_color = self.stylesheet.border_color(); + let title_color = self.stylesheet.title_color(); + let text_color = self.stylesheet.text_color(); + let block = Block::default() .borders(Borders::ALL) - .border_style(Style::default().fg(Color::Yellow)) + .border_style(Style::default().fg(border_color)) .title(" Speaker Notes ") - .title_style(Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)); + .title_style(Style::default().fg(title_color).add_modifier(Modifier::BOLD)) + .padding(Stylesheet::slide_padding()); let paragraph = Paragraph::new(notes.clone()) .block(block) .wrap(Wrap { trim: false }) - .style(Style::default().fg(Color::Gray)); + .style(Style::default().fg(text_color)); frame.render_widget(paragraph, area); } @@ -160,10 +232,7 @@ impl SlideViewer { let status = Paragraph::new(Line::from(vec![Span::styled( status_text, - Style::default() - .bg(Color::DarkGray) - .fg(Color::White) - .add_modifier(Modifier::BOLD), + self.stylesheet.status_bar(), )])); frame.render_widget(status, area);