magical markdown slides
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor: abstract colors to stylesheet struct

+109 -41
+23 -22
ROADMAP.md
··· 22 22 23 23 __Objective:__ Parse markdown documents into a rich `Slide` struct. 24 24 25 - | Task | Description | Key Crates | 26 - | ---------------------- | --------------------------------------------------------------- | -------------------- | 27 - | __✓ Parser Core__ | Split files on `---` separators. | `pulldown-cmark`[^4] | 28 - | | Detect title blocks, lists, and code fences. | | 29 - | | Represent as `Vec<Slide>`. | | 30 - | __✓ Slide Model__ | Define structs: `Slide`, `Block`, `TextSpan`, `CodeBlock`, etc. | Internal | 31 - | __✓ Metadata Parsing__ | Optional front matter (YAML/TOML) for theme, author, etc. | `serde_yml`[^5] | 32 - | __Error & Validation__ | Provide friendly parser errors with file/line info. | `thiserror`[^6] | 33 - | __✓ Basic CLI UX__ | `slides present file.md` runs full TUI. | `clap` | 34 - | | `slides print` renders to stdout with width constraint. | | 25 + | Task | Description | Key Crates | 26 + | ----------------------- | --------------------------------------------------------------- | -------------------- | 27 + | __✓ Parser Core__ | Split files on `---` separators. | `pulldown-cmark`[^4] | 28 + | | Detect title blocks, lists, and code fences. | | 29 + | | Represent as `Vec<Slide>`. | | 30 + | __✓ Slide Model__ | Define structs: `Slide`, `Block`, `TextSpan`, `CodeBlock`, etc. | Internal | 31 + | __✓ Metadata Parsing__ | Optional front matter (YAML/TOML) for theme, author, etc. | `serde_yml`[^5] | 32 + | __Error & Validation__ | Provide friendly parser errors with file/line info. | `thiserror`[^6] | 33 + | __✓ Basic CLI UX__ | `lantern present file.md` runs full TUI. | `clap` | 34 + | | `lantern print` renders to stdout with width constraint. | | 35 35 36 36 ## Rendering & Navigation 37 37 ··· 44 44 | __✓ Status Bar__ | Display slide count, filename, clock, and theme name. | `ratatui` | 45 45 | __✓ Color Styling__ | Apply consistent color palette via `owo-colors`. Define traits like `ThemeColor`. | `owo-colors` | 46 46 | __✓ Unicode Headings__ | Use Unicode block symbols (▉▓▒░▌) for h1-h6 instead of markdown `#` syntax. | Unicode constants | 47 - | __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` | 47 + | __Configurable Themes__ | Base16 YAML theme system with 10 prebuilt themes. | `serde_yml`, `serde` | 48 + | | Add user theme loading from config directory and CLI `--theme-file` flag. | `dirs` | 48 49 49 50 --- 50 51 ··· 60 61 | | Render syntax-highlighted text with color spans mapped to `owo-colors`. | | 61 62 | __✓ Theming__ | Map terminal theme choice to Syntect theme (e.g., `"OneDark"`, `"Monokai"`). | `syntect` | 62 63 | __✓ Performance__ | Lazy-load themes and syntaxes; use `OnceLock` for caching. | `std::sync::OnceLock` | 63 - | __✓ Mode__ | Render to ANSI-colored plain text output (for `slides print`). | `owo-colors` | 64 + | __✓ Mode__ | Render to ANSI-colored plain text output (for `lantern print`). | `owo-colors` | 64 65 65 66 ## Presenter 66 67 ··· 78 79 79 80 __Objective:__ Add richness and visual polish to text and layout. 80 81 81 - | Task | Description | Key Crates | 82 - | -------------------- | ------------------------------------------------------------ | ----------------------------- | 83 - | __Tables & Lists__ | Render GitHub-style tables, bullets, and task lists. | `pulldown-cmark`, `ratatui` | 84 - | __Admonitions__ | Highlighted boxes with icons | `owo-colors`, internal glyphs | 85 - | __Horizontal Rules__ | Use box-drawing (`─`, `═`) and shading (`░`, `▓`). | Unicode constants | 86 - | __Generators__ | `slides init` scaffolds an example deck with code and notes. | `include_str!`, `fs` | 82 + | Task | Description | Key Crates | 83 + | -------------------- | ------------------------------------------------------------- | ----------------------------- | 84 + | __Tables & Lists__ | Render GitHub-style tables, bullets, and task lists. | `pulldown-cmark`, `ratatui` | 85 + | __Admonitions__ | Highlighted boxes with icons | `owo-colors`, internal glyphs | 86 + | __Horizontal Rules__ | Use box-drawing (`─`, `═`) and shading (`░`, `▓`). | Unicode constants | 87 + | __Generators__ | `lantern init` scaffolds an example deck with code and notes. | `include_str!`, `fs` | 87 88 88 89 ## RC 89 90 ··· 113 114 | __Canvas → Pixmap__ | Implement a `FrameRasterizer` that turns a `Frame` + layout into an RGBA pixmap (background, panes, etc). | `tiny-skia` | 114 115 | __Text Rendering__ | Render slide titles/body text via glyph rasterization and simple layout (left/center, line wrapping). | `ab_glyph` | 115 116 | __Terminal Snapshot Mode__ | Convert `TerminalBuffer` into a rendered terminal "window" (frame, tabs, padding, cursor). | `tiny-skia`, `ab_glyph` | 116 - | __Slide Screenshot CLI__ | `slides export-image deck.md --slide 5 --output slide-5.png` (PNG by default, optional SVG/WebP). | `clap`, `image` | 117 + | __Slide Screenshot CLI__ | `lantern export-image deck.md --slide 5 --output slide-5.png` (PNG by default, optional SVG/WebP). | `clap`, `image` | 117 118 | __Batch Export__ | `--all` / `--range 3..7` to dump multiple slides, naming convention like `deck-003.png`. | `image` | 118 119 | __Deterministic Layout Test__ | Golden tests comparing generated PNGs against fixtures for regression in layout and text. | `image`, integration test harness | 119 120 ··· 126 127 | __Timeline Scheduling__ | Extend `Event` to carry timestamps or durations; implement `Scheduler` to emit frames at target FPS. | internal `timeline` module | | 127 128 | __Frame Capture Loop__ | Drive the same layout/rasterizer used for images at N FPS, yielding a sequence of RGBA frames. | `tiny-skia`, `image` | | 128 129 | __FFmpeg Binding Layer__ | Wrap `ffmpeg-next` to open an encoder, configure codec/container, and accept raw frames. | `ffmpeg-next` | | 129 - | __Video Export CLI__ | `slides export-video deck.md --output demo.mp4 --fps 30 --duration 120s` (or auto-duration from events). | `clap`, internal encoder | | 130 + | __Video Export CLI__ | `lantern export-video deck.md --output demo.mp4 --fps 30 --duration 120s` (or auto-duration from events). | `clap`, internal encoder | | 130 131 | __GIF / WebM Variants__ | Add `--format gif | webm` mapping to appropriate ffmpeg muxer/codec presets. | `ffmpeg-next`[^7] | 131 132 | __Typing & Cursor Effects__ | Represent typing, deletes, cursor blinks as timeline events, so video export matches live presentation feel. | internal `timeline`, terminal core | | 132 133 | __Audio-less Simplification__ | Keep V1 video export silent (no audio tracks) for simpler ffmpeg integration and smaller binaries. | `ffmpeg-next` | | ··· 140 141 | -------------------------- | ------------------------------------------------------------------------------------------------ | ----------------------------- | 141 142 | __Portrait Layout Engine__ | Implement 9:16 aspect ratio layout with vertical constraints (1080x1920, 720x1280). | internal `layout` module | 142 143 | __Mobile-Optimized Text__ | Larger font sizes, reduced content density, and simplified layouts for mobile readability. | `ab_glyph`, `tiny-skia` | 143 - | __Vertical Export CLI__ | `slides export-vertical deck.md --output reel.mp4` with preset dimensions for each platform. | `clap`, internal encoder | 144 + | __Vertical Export CLI__ | `lantern export-vertical deck.md --output reel.mp4` with preset dimensions for each platform. | `clap`, internal encoder | 144 145 | __Platform Presets__ | Built-in presets: `instagram-reel`, `tiktok`, `youtube-shorts` with optimal resolution/duration. | internal preset registry | 145 146 | __Content Adaptation__ | Auto-scale or warn when horizontal content doesn't fit portrait orientation. | internal `layout` module | 146 147 | __Safe Zones__ | Respect platform UI overlays (captions, profile pics) with configurable safe zones. | internal `layout` module | ··· 152 153 153 154 | Task | Description | Key Crates | 154 155 | ------------------------ | ------------------------------------------------------------------------------------------------ | ---------------------------- | 155 - | __Export Subcommands__ | Add `slides export-image` and `slides export-video` commands with shared flags (theme, range). | `clap` | 156 + | __Export Subcommands__ | Add `lantern export-image` and `lantern export-video` commands with shared flags (theme, range). | `clap` | 156 157 | __Frontmatter Controls__ | Support per-deck/per-slide frontmatter: `fps`, `default_duration`, `transition`, `record: true`. | `pulldown-cmark-frontmatter` | 157 158 | __Deterministic Seeds__ | Add `--seed` for any animations (typing jitter, cursor blink timing) to keep exports repeatable. | internal `timeline` | 158 159 | __Preset Profiles__ | Presets like `social-card`, `doc-screenshot`, `talk-demo` mapping to resolution + theme. | internal profile registry |
+2 -4
core/src/theme.rs
··· 129 129 130 130 /// Color theme abstraction for slides with semantic roles for consistent theming across the application. 131 131 /// 132 - /// Stores RGB colors that can be converted to both owo-colors Style (for terminal output) 133 - /// and ratatui Color (for TUI rendering) via Into implementations. 134 - #[derive(Debug, Clone)] 132 + /// Stores RGB colors that can be converted to both owo-colors Style (for terminal output) and ratatui Color (for TUI rendering). 133 + #[derive(Debug, Clone, Copy)] 135 134 pub struct ThemeColors { 136 135 pub heading: Color, 137 136 pub heading_bold: bool, ··· 652 651 assert!(theme.link(&"Test").to_string().contains("Test")); 653 652 assert!(theme.inline_code_bg(&"Test").to_string().contains("Test")); 654 653 655 - // UI colors don't need style methods, just verify they exist 656 654 let _ = theme.ui_border; 657 655 let _ = theme.ui_title; 658 656 let _ = theme.ui_text;
+84 -15
ui/src/viewer.rs
··· 1 + use lantern_core::{slide::Slide, theme::ThemeColors}; 1 2 use ratatui::{ 2 3 Frame, 3 4 layout::Rect, 4 5 style::{Color, Modifier, Style}, 5 6 text::{Line, Span}, 6 - widgets::{Block, Borders, Paragraph, Wrap}, 7 + widgets::{Block, Borders, Padding, Paragraph, Wrap}, 7 8 }; 8 - use lantern_core::{slide::Slide, theme::ThemeColors}; 9 9 use std::time::Instant; 10 10 11 11 use crate::renderer::render_slide_content; 12 12 13 + #[derive(Clone, Copy)] 14 + struct Stylesheet { 15 + theme: ThemeColors, 16 + } 17 + 18 + impl Stylesheet { 19 + fn new(theme: ThemeColors) -> Self { 20 + Self { theme } 21 + } 22 + 23 + fn slide_padding() -> Padding { 24 + Padding::new(4, 4, 2, 2) 25 + } 26 + 27 + fn status_bar(&self) -> Style { 28 + Style::default() 29 + .bg(self.bg_color()) 30 + .fg(self.ui_text_color()) 31 + .add_modifier(Modifier::BOLD) 32 + } 33 + 34 + fn border_color(&self) -> Color { 35 + Color::Rgb(self.theme.ui_border.r, self.theme.ui_border.g, self.theme.ui_border.b) 36 + } 37 + 38 + fn title_color(&self) -> Color { 39 + Color::Rgb(self.theme.ui_title.r, self.theme.ui_title.g, self.theme.ui_title.b) 40 + } 41 + 42 + fn text_color(&self) -> Color { 43 + Color::Rgb(self.theme.body.r, self.theme.body.g, self.theme.body.b) 44 + } 45 + 46 + fn bg_color(&self) -> Color { 47 + Color::Rgb( 48 + self.theme.ui_background.r, 49 + self.theme.ui_background.g, 50 + self.theme.ui_background.b, 51 + ) 52 + } 53 + 54 + fn ui_text_color(&self) -> Color { 55 + Color::Rgb(self.theme.ui_text.r, self.theme.ui_text.g, self.theme.ui_text.b) 56 + } 57 + } 58 + 59 + impl From<ThemeColors> for Stylesheet { 60 + fn from(value: ThemeColors) -> Self { 61 + Self::new(value) 62 + } 63 + } 64 + 13 65 /// Slide viewer state manager 14 66 /// 15 67 /// Manages current slide index, navigation, and speaker notes visibility. ··· 17 69 slides: Vec<Slide>, 18 70 current_index: usize, 19 71 show_notes: bool, 20 - theme: ThemeColors, 21 72 filename: Option<String>, 73 + stylesheet: Stylesheet, 22 74 theme_name: String, 23 75 start_time: Option<Instant>, 24 76 } ··· 30 82 slides, 31 83 current_index: 0, 32 84 show_notes: false, 33 - theme, 85 + stylesheet: theme.into(), 34 86 filename: None, 35 87 theme_name: "default".to_string(), 36 88 start_time: None, ··· 42 94 slides: Vec<Slide>, theme: ThemeColors, filename: Option<String>, theme_name: String, 43 95 start_time: Option<Instant>, 44 96 ) -> Self { 45 - Self { slides, current_index: 0, show_notes: false, theme, filename, theme_name, start_time } 97 + Self { 98 + slides, 99 + current_index: 0, 100 + show_notes: false, 101 + stylesheet: theme.into(), 102 + filename, 103 + theme_name, 104 + start_time, 105 + } 46 106 } 47 107 48 108 /// Navigate to the next slide ··· 91 151 self.show_notes 92 152 } 93 153 154 + fn theme(&self) -> ThemeColors { 155 + self.stylesheet.theme.clone() 156 + } 157 + 94 158 /// Render the current slide to the frame 95 159 pub fn render(&self, frame: &mut Frame, area: Rect) { 96 160 if let Some(slide) = self.current_slide() { 97 - let content = render_slide_content(&slide.blocks, &self.theme); 161 + let content = render_slide_content(&slide.blocks, &self.theme()); 162 + let border_color = self.stylesheet.border_color(); 163 + let title_color = self.stylesheet.title_color(); 98 164 99 165 let block = Block::default() 100 166 .borders(Borders::ALL) 101 - .border_style(Style::default().fg(Color::DarkGray)) 167 + .border_style(Style::default().fg(border_color)) 102 168 .title(format!(" Slide {}/{} ", self.current_index + 1, self.total_slides())) 103 - .title_style(Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)); 169 + .title_style(Style::default().fg(title_color).add_modifier(Modifier::BOLD)) 170 + .padding(Stylesheet::slide_padding()); 104 171 105 172 let paragraph = Paragraph::new(content).block(block).wrap(Wrap { trim: false }); 106 173 ··· 116 183 117 184 if let Some(slide) = self.current_slide() { 118 185 if let Some(notes) = &slide.notes { 186 + let border_color = self.stylesheet.border_color(); 187 + let title_color = self.stylesheet.title_color(); 188 + let text_color = self.stylesheet.text_color(); 189 + 119 190 let block = Block::default() 120 191 .borders(Borders::ALL) 121 - .border_style(Style::default().fg(Color::Yellow)) 192 + .border_style(Style::default().fg(border_color)) 122 193 .title(" Speaker Notes ") 123 - .title_style(Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)); 194 + .title_style(Style::default().fg(title_color).add_modifier(Modifier::BOLD)) 195 + .padding(Stylesheet::slide_padding()); 124 196 125 197 let paragraph = Paragraph::new(notes.clone()) 126 198 .block(block) 127 199 .wrap(Wrap { trim: false }) 128 - .style(Style::default().fg(Color::Gray)); 200 + .style(Style::default().fg(text_color)); 129 201 130 202 frame.render_widget(paragraph, area); 131 203 } ··· 160 232 161 233 let status = Paragraph::new(Line::from(vec![Span::styled( 162 234 status_text, 163 - Style::default() 164 - .bg(Color::DarkGray) 165 - .fg(Color::White) 166 - .add_modifier(Modifier::BOLD), 235 + self.stylesheet.status_bar(), 167 236 )])); 168 237 169 238 frame.render_widget(status, area);