magical markdown slides

refactor(theme): migrate to base16 YAML themes with compile-time embedding

+776 -396
+75 -10
ROADMAP.md
··· 43 43 | __✓ Input & State__ | Support `←/→`, `j/k`, `q`, numeric jumps, and window resize. | `crossterm`, `ratatui` | 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 - | __Configurable Themes__ | Support themes via TOML files mapping semantic roles (`heading`, `body`, `accent`) → color pairs. | `toml`, `serde` | 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 48 48 49 --- 49 50 ··· 51 52 52 53 __Objective:__ Add first-class syntax highlighting using Syntect. 53 54 54 - | Task | Description | Key Crates | 55 - | --------------- | ---------------------------------------------------------------------------- | ----------------------- | 56 - | __Syntect__ | Load `.tmTheme` / `.sublime-syntax` definitions on startup. | `syntect`[^8] | 57 - | | Cache `SyntaxSet` + `ThemeSet`. | | 58 - | __Code Blocks__ | Detect fenced code blocks with language tags. | `syntect`, `owo-colors` | 59 - | | Render syntax-highlighted text with color spans mapped to `owo-colors`. | | 60 - | __Theming__ | Map terminal theme choice to Syntect theme (e.g., `"OneDark"`, `"Monokai"`). | `syntect` | 61 - | __Performance__ | Lazy-load themes and syntaxes; use `once_cell` for caching. | `once_cell` | 62 - | __Mode__ | Render to ANSI-colored plain text output (for `slides print`). | `owo-colors` | 55 + | Task | Description | Key Crates | 56 + | ------------------- | ---------------------------------------------------------------------------- | ----------------------- | 57 + | __✓ Syntect__ | Load `.tmTheme` / `.sublime-syntax` definitions on startup. | `syntect`[^8] | 58 + | | Cache `SyntaxSet` + `ThemeSet`. | | 59 + | __✓ Code Blocks__ | Detect fenced code blocks with language tags. | `syntect`, `owo-colors` | 60 + | | Render syntax-highlighted text with color spans mapped to `owo-colors`. | | 61 + | __✓ Theming__ | Map terminal theme choice to Syntect theme (e.g., `"OneDark"`, `"Monokai"`). | `syntect` | 62 + | __✓ 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` | 63 64 64 65 ## Presenter 65 66 ··· 91 92 | __Config Discovery__ | Read from `$XDG_CONFIG_HOME/slides/config.toml` for defaults. | `dirs`, `serde` | 92 93 | __Theme Registry__ | Built-in theme manifest (e.g., `onedark`, `solarized`, `plain`). | Internal | 93 94 | __Release__ | Tag `v1.0.0-rc.1` with changelog and binaries for major platforms. | `cargo-dist`, GitHub Actions | 95 + 96 + ## Rendering Core Extension 97 + 98 + __Objective:__ Make live, image, and video modes all run on the same slide/timeline + frame renderer pipeline. 99 + 100 + | Task | Description | Key Crates | 101 + | ------------------------- | --------------------------------------------------------------------------------------------------- | ------------------------------------------ | 102 + | __Event Timeline Core__ | Compile slides into an `Event` timeline (show slide, type, run command, wait, transition, capture). | internal `timeline` module | 103 + | __Virtual Terminal Core__ | Implement PTY + ANSI parser → `TerminalBuffer { cells, colors, attrs }` shared by live/video/image. | `portable-pty` (or similar), internal ANSI | 104 + | __Frame Layout Engine__ | Map title/body/terminal regions into a logical canvas (cells or pixels) for all renderers. | internal `layout` module | 105 + | __Renderer Trait__ | Define `Renderer` trait (`begin`, `handle_event`, `end`) with impls for Live, Image, and Video. | internal `renderer` module | 106 + 107 + ## Export: Images 108 + 109 + __Objective:__ Generate high-quality PNG/SVG snapshots of any slide (Freeze-style) directly from the slide + layout + terminal state. 110 + 111 + | Task | Description | Key Crates | 112 + | ----------------------------- | --------------------------------------------------------------------------------------------------------- | --------------------------------- | 113 + | __Canvas → Pixmap__ | Implement a `FrameRasterizer` that turns a `Frame` + layout into an RGBA pixmap (background, panes, etc). | `tiny-skia` | 114 + | __Text Rendering__ | Render slide titles/body text via glyph rasterization and simple layout (left/center, line wrapping). | `ab_glyph` | 115 + | __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 + | __Batch Export__ | `--all` / `--range 3..7` to dump multiple slides, naming convention like `deck-003.png`. | `image` | 118 + | __Deterministic Layout Test__ | Golden tests comparing generated PNGs against fixtures for regression in layout and text. | `image`, integration test harness | 119 + 120 + ## Export: Video 121 + 122 + __Objective:__ Produce MP4/WebM/GIF recordings of a scripted terminal+slides run (VHS-style) directly from the markdown deck. 123 + 124 + | Task | Description | Key Crates | | 125 + | ----------------------------- | -------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- | ----------------- | 126 + | __Timeline Scheduling__ | Extend `Event` to carry timestamps or durations; implement `Scheduler` to emit frames at target FPS. | internal `timeline` module | | 127 + | __Frame Capture Loop__ | Drive the same layout/rasterizer used for images at N FPS, yielding a sequence of RGBA frames. | `tiny-skia`, `image` | | 128 + | __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 + | __GIF / WebM Variants__ | Add `--format gif | webm` mapping to appropriate ffmpeg muxer/codec presets. | `ffmpeg-next`[^7] | 131 + | __Typing & Cursor Effects__ | Represent typing, deletes, cursor blinks as timeline events, so video export matches live presentation feel. | internal `timeline`, terminal core | | 132 + | __Audio-less Simplification__ | Keep V1 video export silent (no audio tracks) for simpler ffmpeg integration and smaller binaries. | `ffmpeg-next` | | 133 + | __Performance Tuning__ | Measure memory/CPU for long decks; stream frames to ffmpeg (no full buffering) and expose `--quality` presets. | `ffmpeg-next`, `image` | | 134 + 135 + ## Export: Social Media 136 + 137 + __Objective:__ Generate vertical (portrait) slides optimized for short-form vertical video. 138 + 139 + | Task | Description | Key Crates | 140 + | -------------------------- | ------------------------------------------------------------------------------------------------ | ----------------------------- | 141 + | __Portrait Layout Engine__ | Implement 9:16 aspect ratio layout with vertical constraints (1080x1920, 720x1280). | internal `layout` module | 142 + | __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 + | __Platform Presets__ | Built-in presets: `instagram-reel`, `tiktok`, `youtube-shorts` with optimal resolution/duration. | internal preset registry | 145 + | __Content Adaptation__ | Auto-scale or warn when horizontal content doesn't fit portrait orientation. | internal `layout` module | 146 + | __Safe Zones__ | Respect platform UI overlays (captions, profile pics) with configurable safe zones. | internal `layout` module | 147 + | __Swipe Animations__ | Optional slide transition effects optimized for vertical scrolling behavior. | internal `timeline`, `ffmpeg` | 148 + 149 + ## Authoring & UX for Export 150 + 151 + __Objective:__ Make "slides → image/video" a natural extension of your current CLI and authoring workflow. 152 + 153 + | Task | Description | Key Crates | 154 + | ------------------------ | ------------------------------------------------------------------------------------------------ | ---------------------------- | 155 + | __Export Subcommands__ | Add `slides export-image` and `slides export-video` commands with shared flags (theme, range). | `clap` | 156 + | __Frontmatter Controls__ | Support per-deck/per-slide frontmatter: `fps`, `default_duration`, `transition`, `record: true`. | `pulldown-cmark-frontmatter` | 157 + | __Deterministic Seeds__ | Add `--seed` for any animations (typing jitter, cursor blink timing) to keep exports repeatable. | internal `timeline` | 158 + | __Preset Profiles__ | Presets like `social-card`, `doc-screenshot`, `talk-demo` mapping to resolution + theme. | internal profile registry | 94 159 95 160 [^1]: <https://docs.rs/clap/latest/clap/> 96 161 [^2]: <https://docs.rs/owo-colors/latest/owo_colors/>
+16
core/src/highlighter.rs
··· 196 196 list_marker: Color::new(150, 150, 150), 197 197 blockquote_border: Color::new(120, 120, 120), 198 198 table_border: Color::new(120, 120, 120), 199 + emphasis: Color::new(160, 160, 160), 200 + strong: Color::new(190, 190, 190), 201 + link: Color::new(140, 180, 220), 202 + inline_code_bg: Color::new(50, 50, 50), 203 + ui_border: Color::new(80, 80, 80), 204 + ui_title: Color::new(200, 200, 200), 205 + ui_text: Color::new(220, 220, 220), 206 + ui_background: Color::new(30, 30, 30), 199 207 }; 200 208 201 209 assert!(is_dark_theme(&dark_theme)); ··· 215 223 list_marker: Color::new(50, 50, 50), 216 224 blockquote_border: Color::new(80, 80, 80), 217 225 table_border: Color::new(80, 80, 80), 226 + emphasis: Color::new(70, 70, 70), 227 + strong: Color::new(40, 40, 40), 228 + link: Color::new(0, 80, 160), 229 + inline_code_bg: Color::new(240, 240, 240), 230 + ui_border: Color::new(180, 180, 180), 231 + ui_title: Color::new(40, 40, 40), 232 + ui_text: Color::new(20, 20, 20), 233 + ui_background: Color::new(250, 250, 250), 218 234 }; 219 235 220 236 assert!(!is_dark_theme(&light_theme));
+369 -274
core/src/theme.rs
··· 1 1 use owo_colors::{OwoColorize, Style}; 2 + use serde::Deserialize; 2 3 use terminal_colorsaurus::{QueryOptions, background_color}; 3 4 5 + /// Parses a hex color string to RGB values. 6 + /// 7 + /// Supports both `#RRGGBB` and `RRGGBB` formats. 8 + fn parse_hex_color(hex: &str) -> Option<(u8, u8, u8)> { 9 + let hex = hex.trim_start_matches('#'); 10 + 11 + if hex.len() != 6 { 12 + return None; 13 + } 14 + 15 + let r = u8::from_str_radix(&hex[0..2], 16).ok()?; 16 + let g = u8::from_str_radix(&hex[2..4], 16).ok()?; 17 + let b = u8::from_str_radix(&hex[4..6], 16).ok()?; 18 + 19 + Some((r, g, b)) 20 + } 21 + 22 + /// Base16 color scheme specification. 23 + /// 24 + /// Defines a standard 16-color palette that can be mapped to semantic theme roles. 25 + #[derive(Debug, Clone, Deserialize)] 26 + struct Base16Scheme { 27 + #[allow(dead_code)] 28 + system: String, 29 + #[allow(dead_code)] 30 + name: String, 31 + #[allow(dead_code)] 32 + author: String, 33 + #[allow(dead_code)] 34 + variant: String, 35 + palette: Base16Palette, 36 + } 37 + 38 + /// Base16 color palette with 16 standardized color slots. 39 + /// 40 + /// Each base color serves a semantic purpose in the base16 specification: 41 + /// - base00-03: Background shades (darkest to lighter) 42 + /// - base04-07: Foreground shades (darker to lightest) 43 + /// - base08-0F: Accent colors (red, orange, yellow, green, cyan, blue, magenta, brown) 44 + #[derive(Debug, Clone, Deserialize)] 45 + #[allow(dead_code)] 46 + struct Base16Palette { 47 + base00: String, 48 + base01: String, 49 + base02: String, 50 + base03: String, 51 + base04: String, 52 + base05: String, 53 + base06: String, 54 + base07: String, 55 + base08: String, 56 + base09: String, 57 + #[serde(rename = "base0A")] 58 + base0a: String, 59 + #[serde(rename = "base0B")] 60 + base0b: String, 61 + #[serde(rename = "base0C")] 62 + base0c: String, 63 + #[serde(rename = "base0D")] 64 + base0d: String, 65 + #[serde(rename = "base0E")] 66 + base0e: String, 67 + #[serde(rename = "base0F")] 68 + base0f: String, 69 + } 70 + 71 + static CATPPUCCIN_LATTE: &str = include_str!("themes/catppuccin-latte.yml"); 72 + static CATPPUCCIN_MOCHA: &str = include_str!("themes/catppuccin-mocha.yml"); 73 + static GRUVBOX_MATERIAL_DARK: &str = include_str!("themes/gruvbox-material-dark-medium.yml"); 74 + static GRUVBOX_MATERIAL_LIGHT: &str = include_str!("themes/gruvbox-material-light-medium.yml"); 75 + static NORD_LIGHT: &str = include_str!("themes/nord-light.yml"); 76 + static NORD: &str = include_str!("themes/nord.yml"); 77 + static OXOCARBON_DARK: &str = include_str!("themes/oxocarbon-dark.yml"); 78 + static OXOCARBON_LIGHT: &str = include_str!("themes/oxocarbon-light.yml"); 79 + static SOLARIZED_DARK: &str = include_str!("themes/solarized-dark.yml"); 80 + static SOLARIZED_LIGHT: &str = include_str!("themes/solarized-light.yml"); 81 + 4 82 /// RGB color value for use with both owo-colors and ratatui 5 83 #[derive(Debug, Clone, Copy)] 6 84 pub struct Color { ··· 66 144 pub list_marker: Color, 67 145 pub blockquote_border: Color, 68 146 pub table_border: Color, 147 + pub emphasis: Color, 148 + pub strong: Color, 149 + pub link: Color, 150 + pub inline_code_bg: Color, 151 + pub ui_border: Color, 152 + pub ui_title: Color, 153 + pub ui_text: Color, 154 + pub ui_background: Color, 69 155 } 70 156 71 157 impl Default for ThemeColors { 72 158 fn default() -> Self { 73 - Self::basic(detect_is_dark()) 159 + let is_dark = detect_is_dark(); 160 + let theme_name = if is_dark { "nord" } else { "nord-light" }; 161 + ThemeRegistry::get(theme_name) 74 162 } 75 163 } 76 164 77 165 impl ThemeColors { 166 + /// Create a ThemeColors from a base16 color scheme. 167 + /// 168 + /// Maps base16 colors to semantic theme roles following base16 styling guidelines: 169 + /// 170 + /// Content colors: 171 + /// - base05: body text (main foreground) 172 + /// - base0D: headings (blue - classes/functions) 173 + /// - base0E: strong emphasis (magenta/purple - keywords) 174 + /// - base0B: code blocks (green - strings) 175 + /// - base03: dimmed/borders (comment color) 176 + /// - base0A: list markers (yellow - classes/constants) 177 + /// - base09: emphasis/italics (orange - integers/constants) 178 + /// - base0C: links (cyan - support/regex) 179 + /// - base08: accents (red - variables/tags) 180 + /// - base02: inline code background (selection background) 181 + /// 182 + /// UI chrome colors: 183 + /// - base00: UI background (darkest background) 184 + /// - base04: UI borders (dim foreground) 185 + /// - base06: UI titles (bright foreground) 186 + /// - base07: UI text (brightest foreground) 187 + fn from_base16(scheme: &Base16Scheme) -> Option<Self> { 188 + let palette = &scheme.palette; 189 + 190 + let heading = parse_hex_color(&palette.base0d)?; 191 + let body = parse_hex_color(&palette.base05)?; 192 + let accent = parse_hex_color(&palette.base08)?; 193 + let code = parse_hex_color(&palette.base0b)?; 194 + let dimmed = parse_hex_color(&palette.base03)?; 195 + let code_fence = dimmed; 196 + let rule = dimmed; 197 + let list_marker = parse_hex_color(&palette.base0a)?; 198 + let blockquote_border = dimmed; 199 + let table_border = dimmed; 200 + let emphasis = parse_hex_color(&palette.base09)?; 201 + let strong = parse_hex_color(&palette.base0e)?; 202 + let link = parse_hex_color(&palette.base0c)?; 203 + let inline_code_bg = parse_hex_color(&palette.base02)?; 204 + let ui_background = parse_hex_color(&palette.base00)?; 205 + let ui_border = parse_hex_color(&palette.base04)?; 206 + let ui_title = parse_hex_color(&palette.base06)?; 207 + let ui_text = parse_hex_color(&palette.base07)?; 208 + 209 + Some(Self { 210 + heading: Color::new(heading.0, heading.1, heading.2), 211 + heading_bold: true, 212 + body: Color::new(body.0, body.1, body.2), 213 + accent: Color::new(accent.0, accent.1, accent.2), 214 + code: Color::new(code.0, code.1, code.2), 215 + dimmed: Color::new(dimmed.0, dimmed.1, dimmed.2), 216 + code_fence: Color::new(code_fence.0, code_fence.1, code_fence.2), 217 + rule: Color::new(rule.0, rule.1, rule.2), 218 + list_marker: Color::new(list_marker.0, list_marker.1, list_marker.2), 219 + blockquote_border: Color::new(blockquote_border.0, blockquote_border.1, blockquote_border.2), 220 + table_border: Color::new(table_border.0, table_border.1, table_border.2), 221 + emphasis: Color::new(emphasis.0, emphasis.1, emphasis.2), 222 + strong: Color::new(strong.0, strong.1, strong.2), 223 + link: Color::new(link.0, link.1, link.2), 224 + inline_code_bg: Color::new(inline_code_bg.0, inline_code_bg.1, inline_code_bg.2), 225 + ui_border: Color::new(ui_border.0, ui_border.1, ui_border.2), 226 + ui_title: Color::new(ui_title.0, ui_title.1, ui_title.2), 227 + ui_text: Color::new(ui_text.0, ui_text.1, ui_text.2), 228 + ui_background: Color::new(ui_background.0, ui_background.1, ui_background.2), 229 + }) 230 + } 231 + 78 232 /// Apply heading style to text 79 233 pub fn heading<'a, T: OwoColorize>(&self, text: &'a T) -> owo_colors::Styled<&'a T> { 80 234 let mut style: Style = (&self.heading).into(); ··· 129 283 text.style((&self.table_border).into()) 130 284 } 131 285 132 - /// Create an oxocarbon-based theme. 133 - pub fn basic(is_dark: bool) -> Self { 134 - if is_dark { 135 - Self { 136 - heading: Color::new(66, 190, 101), // green 137 - heading_bold: true, 138 - body: Color::new(242, 244, 248), 139 - accent: Color::new(238, 83, 150), // pink 140 - code: Color::new(51, 177, 255), // blue 141 - dimmed: Color::new(82, 82, 82), // gray 142 - code_fence: Color::new(82, 82, 82), 143 - rule: Color::new(82, 82, 82), 144 - list_marker: Color::new(120, 169, 255), // light blue 145 - blockquote_border: Color::new(82, 82, 82), 146 - table_border: Color::new(82, 82, 82), 147 - } 148 - } else { 149 - Self { 150 - heading: Color::new(66, 190, 101), // green 151 - heading_bold: true, 152 - body: Color::new(57, 57, 57), 153 - accent: Color::new(255, 111, 0), // orange 154 - code: Color::new(15, 98, 254), // blue 155 - dimmed: Color::new(22, 22, 22), // dark gray 156 - code_fence: Color::new(22, 22, 22), 157 - rule: Color::new(22, 22, 22), 158 - list_marker: Color::new(238, 83, 150), // pink 159 - blockquote_border: Color::new(22, 22, 22), 160 - table_border: Color::new(22, 22, 22), 161 - } 162 - } 286 + /// Apply emphasis (italic) style to text 287 + pub fn emphasis<'a, T: OwoColorize>(&self, text: &'a T) -> owo_colors::Styled<&'a T> { 288 + text.style((&self.emphasis).into()) 163 289 } 164 290 165 - /// Create a Monokai-inspired theme. 166 - /// 167 - /// Dark variant uses classic Monokai colors optimized for dark backgrounds. 168 - /// Light variant uses adjusted colors optimized for light backgrounds. 169 - pub fn monokai(is_dark: bool) -> Self { 170 - if is_dark { 171 - Self { 172 - heading: Color::new(249, 38, 114), // pink 173 - heading_bold: true, 174 - body: Color::new(248, 248, 242), // off-white 175 - accent: Color::new(230, 219, 116), // yellow 176 - code: Color::new(166, 226, 46), // green 177 - dimmed: Color::new(117, 113, 94), // brown-gray 178 - code_fence: Color::new(117, 113, 94), 179 - rule: Color::new(117, 113, 94), 180 - list_marker: Color::new(230, 219, 116), 181 - blockquote_border: Color::new(117, 113, 94), 182 - table_border: Color::new(117, 113, 94), 183 - } 184 - } else { 185 - Self { 186 - heading: Color::new(200, 30, 90), // darker pink 187 - heading_bold: true, 188 - body: Color::new(39, 40, 34), // dark gray 189 - accent: Color::new(180, 170, 80), // darker yellow 190 - code: Color::new(100, 150, 30), // darker green 191 - dimmed: Color::new(150, 150, 150), // light gray 192 - code_fence: Color::new(150, 150, 150), 193 - rule: Color::new(150, 150, 150), 194 - list_marker: Color::new(180, 170, 80), 195 - blockquote_border: Color::new(150, 150, 150), 196 - table_border: Color::new(150, 150, 150), 197 - } 198 - } 291 + /// Apply strong (bold) style to text 292 + pub fn strong<'a, T: OwoColorize>(&self, text: &'a T) -> owo_colors::Styled<&'a T> { 293 + let style: Style = (&self.strong).into(); 294 + text.style(style.bold()) 199 295 } 200 296 201 - /// Create a Dracula-inspired theme. 202 - /// 203 - /// Dark variant uses classic Dracula colors optimized for dark backgrounds. 204 - /// Light variant uses adjusted colors optimized for light backgrounds. 205 - pub fn dracula(is_dark: bool) -> Self { 206 - if is_dark { 207 - Self { 208 - heading: Color::new(255, 121, 198), // pink 209 - heading_bold: true, 210 - body: Color::new(248, 248, 242), 211 - accent: Color::new(139, 233, 253), // cyan 212 - code: Color::new(80, 250, 123), // green 213 - dimmed: Color::new(98, 114, 164), 214 - code_fence: Color::new(98, 114, 164), 215 - rule: Color::new(98, 114, 164), 216 - list_marker: Color::new(241, 250, 140), // yellow 217 - blockquote_border: Color::new(98, 114, 164), 218 - table_border: Color::new(98, 114, 164), 219 - } 220 - } else { 221 - Self { 222 - heading: Color::new(200, 80, 160), // darker pink 223 - heading_bold: true, 224 - body: Color::new(40, 42, 54), 225 - accent: Color::new(80, 150, 180), // darker cyan 226 - code: Color::new(50, 160, 80), // darker green 227 - dimmed: Color::new(150, 150, 150), // light gray 228 - code_fence: Color::new(150, 150, 150), 229 - rule: Color::new(150, 150, 150), 230 - list_marker: Color::new(180, 170, 90), // darker yellow 231 - blockquote_border: Color::new(150, 150, 150), 232 - table_border: Color::new(150, 150, 150), 233 - } 234 - } 297 + /// Apply link style to text 298 + pub fn link<'a, T: OwoColorize>(&self, text: &'a T) -> owo_colors::Styled<&'a T> { 299 + text.style((&self.link).into()) 235 300 } 236 301 237 - /// Create a Solarized theme. 238 - /// 239 - /// Uses Ethan Schoonover's Solarized color palette. 240 - pub fn solarized(is_dark: bool) -> Self { 241 - if is_dark { 242 - Self { 243 - heading: Color::new(38, 139, 210), 244 - heading_bold: true, 245 - body: Color::new(131, 148, 150), 246 - accent: Color::new(42, 161, 152), 247 - code: Color::new(133, 153, 0), 248 - dimmed: Color::new(88, 110, 117), 249 - code_fence: Color::new(88, 110, 117), 250 - rule: Color::new(88, 110, 117), 251 - list_marker: Color::new(181, 137, 0), 252 - blockquote_border: Color::new(88, 110, 117), 253 - table_border: Color::new(88, 110, 117), 254 - } 255 - } else { 256 - Self { 257 - heading: Color::new(38, 139, 210), 258 - heading_bold: true, 259 - body: Color::new(101, 123, 131), 260 - accent: Color::new(42, 161, 152), 261 - code: Color::new(133, 153, 0), 262 - dimmed: Color::new(147, 161, 161), 263 - code_fence: Color::new(147, 161, 161), 264 - rule: Color::new(147, 161, 161), 265 - list_marker: Color::new(181, 137, 0), 266 - blockquote_border: Color::new(147, 161, 161), 267 - table_border: Color::new(147, 161, 161), 268 - } 269 - } 270 - } 271 - 272 - /// Create a Nord theme instance 273 - pub fn nord(is_dark: bool) -> Self { 274 - if is_dark { 275 - Self { 276 - heading: Color::new(136, 192, 208), // nord8 - light blue 277 - heading_bold: true, 278 - body: Color::new(216, 222, 233), // nord4 279 - accent: Color::new(143, 188, 187), // nord7 - teal 280 - code: Color::new(163, 190, 140), // nord14 - green 281 - dimmed: Color::new(76, 86, 106), // nord3 282 - code_fence: Color::new(76, 86, 106), 283 - rule: Color::new(76, 86, 106), 284 - list_marker: Color::new(235, 203, 139), // nord13 - yellow 285 - blockquote_border: Color::new(76, 86, 106), 286 - table_border: Color::new(76, 86, 106), 287 - } 288 - } else { 289 - Self { 290 - heading: Color::new(94, 129, 172), // darker blue 291 - heading_bold: true, 292 - body: Color::new(46, 52, 64), 293 - accent: Color::new(136, 192, 208), // blue 294 - code: Color::new(163, 190, 140), // green 295 - dimmed: Color::new(143, 157, 175), 296 - code_fence: Color::new(143, 157, 175), 297 - rule: Color::new(143, 157, 175), 298 - list_marker: Color::new(235, 203, 139), // yellow 299 - blockquote_border: Color::new(143, 157, 175), 300 - table_border: Color::new(143, 157, 175), 301 - } 302 - } 302 + /// Apply inline code background style to text 303 + pub fn inline_code_bg<'a, T: OwoColorize>(&self, text: &'a T) -> owo_colors::Styled<&'a T> { 304 + text.style((&self.inline_code_bg).into()) 303 305 } 304 306 } 305 307 306 - /// Theme registry for loading themes by name with automatic light/dark variant selection. 308 + /// Theme registry for loading prebuilt base16 themes from YAML files. 309 + /// 310 + /// Themes are embedded at compile time using include_str! for zero runtime I/O. 311 + /// Supports all base16 color schemes in the themes directory. 307 312 pub struct ThemeRegistry; 308 313 309 314 impl ThemeRegistry { 310 - /// Get a theme by name with automatic variant detection or explicit override. 315 + /// Get a theme by name. 316 + /// 317 + /// Loads and parses the corresponding YAML theme file embedded at compile time. 318 + /// Falls back to Nord theme if the requested theme is not found or parsing fails. 311 319 pub fn get(name: &str) -> ThemeColors { 312 - let (theme_name, explicit_variant) = if let Some((scheme, variant)) = name.split_once(':') { 313 - let is_dark = match variant.to_lowercase().as_str() { 314 - "light" => false, 315 - "dark" => true, 316 - _ => detect_is_dark(), 317 - }; 318 - (scheme, Some(is_dark)) 319 - } else { 320 - (name, None) 320 + let yaml = match name.to_lowercase().as_str() { 321 + "catppuccin-latte" => CATPPUCCIN_LATTE, 322 + "catppuccin-mocha" => CATPPUCCIN_MOCHA, 323 + "gruvbox-material-dark" => GRUVBOX_MATERIAL_DARK, 324 + "gruvbox-material-light" => GRUVBOX_MATERIAL_LIGHT, 325 + "nord-light" => NORD_LIGHT, 326 + "nord" => NORD, 327 + "oxocarbon-dark" => OXOCARBON_DARK, 328 + "oxocarbon-light" => OXOCARBON_LIGHT, 329 + "solarized-dark" => SOLARIZED_DARK, 330 + "solarized-light" => SOLARIZED_LIGHT, 331 + _ => NORD, 321 332 }; 322 333 323 - let is_dark = explicit_variant.unwrap_or_else(detect_is_dark); 324 - 325 - match theme_name.to_lowercase().as_str() { 326 - "basic" => ThemeColors::basic(is_dark), 327 - "monokai" => ThemeColors::monokai(is_dark), 328 - "dracula" => ThemeColors::dracula(is_dark), 329 - "solarized" => ThemeColors::solarized(is_dark), 330 - "nord" => ThemeColors::nord(is_dark), 331 - _ => ThemeColors::basic(is_dark), 332 - } 334 + serde_yml::from_str::<Base16Scheme>(yaml) 335 + .ok() 336 + .and_then(|scheme| ThemeColors::from_base16(&scheme)) 337 + .unwrap_or_else(|| { 338 + serde_yml::from_str::<Base16Scheme>(NORD) 339 + .ok() 340 + .and_then(|scheme| ThemeColors::from_base16(&scheme)) 341 + .expect("Failed to parse fallback Nord theme") 342 + }) 333 343 } 334 344 335 - /// List all available theme scheme names. 345 + /// List all available theme names. 336 346 pub fn available_themes() -> Vec<&'static str> { 337 - vec!["basic", "monokai", "dracula", "solarized", "nord"] 347 + vec![ 348 + "catppuccin-latte", 349 + "catppuccin-mocha", 350 + "gruvbox-material-dark", 351 + "gruvbox-material-light", 352 + "nord-light", 353 + "nord", 354 + "oxocarbon-dark", 355 + "oxocarbon-light", 356 + "solarized-dark", 357 + "solarized-light", 358 + ] 338 359 } 339 360 } 340 361 ··· 343 364 use super::*; 344 365 345 366 #[test] 367 + fn parse_hex_color_with_hash() { 368 + let result = parse_hex_color("#FF8040"); 369 + assert_eq!(result, Some((255, 128, 64))); 370 + } 371 + 372 + #[test] 373 + fn parse_hex_color_without_hash() { 374 + let result = parse_hex_color("FF8040"); 375 + assert_eq!(result, Some((255, 128, 64))); 376 + } 377 + 378 + #[test] 379 + fn parse_hex_color_lowercase() { 380 + let result = parse_hex_color("#ff8040"); 381 + assert_eq!(result, Some((255, 128, 64))); 382 + } 383 + 384 + #[test] 385 + fn parse_hex_color_invalid_length() { 386 + assert_eq!(parse_hex_color("#FFF"), None); 387 + assert_eq!(parse_hex_color("#FFFFFFF"), None); 388 + } 389 + 390 + #[test] 391 + fn parse_hex_color_invalid_chars() { 392 + assert_eq!(parse_hex_color("#GGGGGG"), None); 393 + assert_eq!(parse_hex_color("#XYZ123"), None); 394 + } 395 + 396 + #[test] 346 397 fn color_new() { 347 398 let color = Color::new(255, 128, 64); 348 399 assert_eq!(color.r, 255); ··· 369 420 } 370 421 371 422 #[test] 372 - fn color_into_style_preserves_rgb() { 373 - let color = Color::new(255, 0, 128); 374 - let style: Style = color.into(); 375 - let styled_text = "Test".style(style); 376 - let output = styled_text.to_string(); 377 - assert!(output.contains("Test")); 423 + fn base16_scheme_deserializes() { 424 + let yaml = r##" 425 + system: "base16" 426 + name: "Test Theme" 427 + author: "Test Author" 428 + variant: "dark" 429 + palette: 430 + base00: "#000000" 431 + base01: "#111111" 432 + base02: "#222222" 433 + base03: "#333333" 434 + base04: "#444444" 435 + base05: "#555555" 436 + base06: "#666666" 437 + base07: "#777777" 438 + base08: "#888888" 439 + base09: "#999999" 440 + base0A: "#aaaaaa" 441 + base0B: "#bbbbbb" 442 + base0C: "#cccccc" 443 + base0D: "#dddddd" 444 + base0E: "#eeeeee" 445 + base0F: "#ffffff" 446 + "##; 447 + let scheme: Result<Base16Scheme, _> = serde_yml::from_str(yaml); 448 + assert!(scheme.is_ok()); 449 + } 450 + 451 + #[test] 452 + fn theme_colors_from_base16() { 453 + let yaml = r##" 454 + system: "base16" 455 + name: "Test Theme" 456 + author: "Test Author" 457 + variant: "dark" 458 + palette: 459 + base00: "#000000" 460 + base01: "#111111" 461 + base02: "#222222" 462 + base03: "#333333" 463 + base04: "#444444" 464 + base05: "#555555" 465 + base06: "#666666" 466 + base07: "#777777" 467 + base08: "#ff0000" 468 + base09: "#ff7f00" 469 + base0A: "#ffff00" 470 + base0B: "#00ff00" 471 + base0C: "#00ffff" 472 + base0D: "#0000ff" 473 + base0E: "#ff00ff" 474 + base0F: "#ffffff" 475 + "##; 476 + let scheme: Base16Scheme = serde_yml::from_str(yaml).unwrap(); 477 + let theme = ThemeColors::from_base16(&scheme); 478 + assert!(theme.is_some()); 479 + 480 + let theme = theme.unwrap(); 481 + assert_eq!(theme.body.r, 85); // base05 - #555555 482 + assert_eq!(theme.heading.r, 0); // base0D - #0000ff 483 + assert_eq!(theme.code.r, 0); // base0B - #00ff00 484 + assert_eq!(theme.accent.r, 255); // base08 - #ff0000 485 + assert_eq!(theme.emphasis.r, 255); // base09 - #ff7f00 486 + assert_eq!(theme.strong.r, 255); // base0E - #ff00ff 487 + assert_eq!(theme.link.r, 0); // base0C - #00ffff 488 + assert_eq!(theme.inline_code_bg.r, 34); // base02 - #222222 489 + assert_eq!(theme.ui_background.r, 0); // base00 - #000000 490 + assert_eq!(theme.ui_border.r, 68); // base04 - #444444 491 + assert_eq!(theme.ui_title.r, 102); // base06 - #666666 492 + assert_eq!(theme.ui_text.r, 119); // base07 - #777777 378 493 } 379 494 380 495 #[test] ··· 397 512 } 398 513 399 514 #[test] 400 - fn theme_basic_dark_variant() { 401 - let theme = ThemeColors::basic(true); 515 + fn theme_registry_get_nord() { 516 + let theme = ThemeRegistry::get("nord"); 402 517 let text = "Test"; 403 518 let styled = theme.heading(&text); 404 519 assert!(styled.to_string().contains("Test")); 520 + assert_eq!(theme.heading.r, 129); 521 + assert_eq!(theme.heading.g, 161); 522 + assert_eq!(theme.heading.b, 193); 405 523 } 406 524 407 525 #[test] 408 - fn theme_basic_light_variant() { 409 - let theme = ThemeColors::basic(false); 526 + fn theme_registry_get_nord_light() { 527 + let theme = ThemeRegistry::get("nord-light"); 410 528 let text = "Test"; 411 529 let styled = theme.heading(&text); 412 530 assert!(styled.to_string().contains("Test")); 413 531 } 414 532 415 533 #[test] 416 - fn theme_monokai_variants() { 417 - let dark = ThemeColors::monokai(true); 418 - let light = ThemeColors::monokai(false); 419 - assert!(dark.heading(&"Test").to_string().contains("Test")); 420 - assert!(light.heading(&"Test").to_string().contains("Test")); 421 - } 422 - 423 - #[test] 424 - fn theme_dracula_variants() { 425 - let dark = ThemeColors::dracula(true); 426 - let light = ThemeColors::dracula(false); 427 - assert!(dark.heading(&"Test").to_string().contains("Test")); 428 - assert!(light.heading(&"Test").to_string().contains("Test")); 429 - } 430 - 431 - #[test] 432 - fn theme_solarized_variants() { 433 - let dark = ThemeColors::solarized(true); 434 - let light = ThemeColors::solarized(false); 435 - assert!(dark.heading(&"Test").to_string().contains("Test")); 436 - assert!(light.heading(&"Test").to_string().contains("Test")); 437 - } 438 - 439 - #[test] 440 - fn theme_nord_variants() { 441 - let dark = ThemeColors::nord(true); 442 - let light = ThemeColors::nord(false); 443 - assert!(dark.heading(&"Test").to_string().contains("Test")); 444 - assert!(light.heading(&"Test").to_string().contains("Test")); 534 + fn theme_registry_get_catppuccin_mocha() { 535 + let theme = ThemeRegistry::get("catppuccin-mocha"); 536 + let text = "Test"; 537 + let styled = theme.heading(&text); 538 + assert!(styled.to_string().contains("Test")); 445 539 } 446 540 447 541 #[test] 448 - fn theme_registry_get_basic() { 449 - let theme = ThemeRegistry::get("basic"); 542 + fn theme_registry_get_catppuccin_latte() { 543 + let theme = ThemeRegistry::get("catppuccin-latte"); 450 544 let text = "Test"; 451 545 let styled = theme.heading(&text); 452 546 assert!(styled.to_string().contains("Test")); 453 547 } 454 548 455 549 #[test] 456 - fn theme_registry_explicit_variant_dark() { 457 - let theme = ThemeRegistry::get("basic:dark"); 550 + fn theme_registry_get_gruvbox_dark() { 551 + let theme = ThemeRegistry::get("gruvbox-material-dark"); 458 552 let text = "Test"; 459 553 let styled = theme.heading(&text); 460 554 assert!(styled.to_string().contains("Test")); 461 555 } 462 556 463 557 #[test] 464 - fn theme_registry_explicit_variant_light() { 465 - let theme = ThemeRegistry::get("solarized:light"); 558 + fn theme_registry_get_gruvbox_light() { 559 + let theme = ThemeRegistry::get("gruvbox-material-light"); 466 560 let text = "Test"; 467 561 let styled = theme.heading(&text); 468 562 assert!(styled.to_string().contains("Test")); 469 563 } 470 564 471 565 #[test] 472 - fn theme_registry_get_monokai() { 473 - let theme = ThemeRegistry::get("monokai"); 566 + fn theme_registry_get_oxocarbon_dark() { 567 + let theme = ThemeRegistry::get("oxocarbon-dark"); 474 568 let text = "Test"; 475 569 let styled = theme.heading(&text); 476 570 assert!(styled.to_string().contains("Test")); 477 571 } 478 572 479 573 #[test] 480 - fn theme_registry_get_dracula() { 481 - let theme = ThemeRegistry::get("dracula"); 574 + fn theme_registry_get_oxocarbon_light() { 575 + let theme = ThemeRegistry::get("oxocarbon-light"); 482 576 let text = "Test"; 483 577 let styled = theme.heading(&text); 484 578 assert!(styled.to_string().contains("Test")); 485 579 } 486 580 487 581 #[test] 488 - fn theme_registry_get_solarized() { 489 - let theme = ThemeRegistry::get("solarized"); 582 + fn theme_registry_get_solarized_dark() { 583 + let theme = ThemeRegistry::get("solarized-dark"); 490 584 let text = "Test"; 491 585 let styled = theme.heading(&text); 492 586 assert!(styled.to_string().contains("Test")); 493 587 } 494 588 495 589 #[test] 496 - fn theme_registry_get_nord() { 497 - let theme = ThemeRegistry::get("nord"); 590 + fn theme_registry_get_solarized_light() { 591 + let theme = ThemeRegistry::get("solarized-light"); 498 592 let text = "Test"; 499 593 let styled = theme.heading(&text); 500 594 assert!(styled.to_string().contains("Test")); ··· 510 604 511 605 #[test] 512 606 fn theme_registry_case_insensitive() { 513 - let theme1 = ThemeRegistry::get("BASIC"); 514 - let theme2 = ThemeRegistry::get("basic"); 607 + let theme1 = ThemeRegistry::get("NORD"); 608 + let theme2 = ThemeRegistry::get("nord"); 515 609 let text = "Test"; 516 610 assert!(theme1.heading(&text).to_string().contains("Test")); 517 611 assert!(theme2.heading(&text).to_string().contains("Test")); ··· 520 614 #[test] 521 615 fn theme_registry_available_themes() { 522 616 let themes = ThemeRegistry::available_themes(); 523 - assert!(themes.contains(&"basic")); 524 - assert!(themes.contains(&"monokai")); 525 - assert!(themes.contains(&"dracula")); 526 - assert!(themes.contains(&"solarized")); 527 617 assert!(themes.contains(&"nord")); 528 - assert_eq!(themes.len(), 5); 618 + assert!(themes.contains(&"nord-light")); 619 + assert!(themes.contains(&"catppuccin-mocha")); 620 + assert!(themes.contains(&"catppuccin-latte")); 621 + assert!(themes.contains(&"gruvbox-material-dark")); 622 + assert!(themes.contains(&"gruvbox-material-light")); 623 + assert!(themes.contains(&"oxocarbon-dark")); 624 + assert!(themes.contains(&"oxocarbon-light")); 625 + assert!(themes.contains(&"solarized-dark")); 626 + assert!(themes.contains(&"solarized-light")); 627 + assert_eq!(themes.len(), 10); 529 628 } 530 629 531 630 #[test] 532 631 fn detect_is_dark_returns_bool() { 533 632 let result = detect_is_dark(); 534 - assert!(result == true || result == false); 535 - } 536 - 537 - #[test] 538 - fn theme_colors_stores_correct_colors() { 539 - let theme = ThemeColors::basic(true); 540 - assert_eq!(theme.heading.r, 66); 541 - assert_eq!(theme.heading.g, 190); 542 - assert_eq!(theme.heading.b, 101); 543 - assert!(theme.heading_bold); 544 - } 545 - 546 - #[test] 547 - fn theme_colors_heading_applies_bold() { 548 - let theme = ThemeColors::basic(true); 549 - let text = "Bold Heading"; 550 - let styled = theme.heading(&text); 551 - assert!(styled.to_string().contains("Bold Heading")); 633 + assert!(result || !result); 552 634 } 553 635 554 636 #[test] ··· 565 647 assert!(theme.list_marker(&"Test").to_string().contains("Test")); 566 648 assert!(theme.blockquote_border(&"Test").to_string().contains("Test")); 567 649 assert!(theme.table_border(&"Test").to_string().contains("Test")); 650 + assert!(theme.emphasis(&"Test").to_string().contains("Test")); 651 + assert!(theme.strong(&"Test").to_string().contains("Test")); 652 + assert!(theme.link(&"Test").to_string().contains("Test")); 653 + assert!(theme.inline_code_bg(&"Test").to_string().contains("Test")); 654 + 655 + // UI colors don't need style methods, just verify they exist 656 + let _ = theme.ui_border; 657 + let _ = theme.ui_title; 658 + let _ = theme.ui_text; 659 + let _ = theme.ui_background; 568 660 } 569 661 570 662 #[test] 571 - fn theme_colors_light_vs_dark() { 572 - let dark = ThemeColors::basic(true); 573 - let light = ThemeColors::basic(false); 574 - 575 - assert_eq!(dark.body.r, 242); 576 - assert_eq!(light.body.r, 57); 577 - assert_ne!(dark.body.r, light.body.r); 663 + fn all_embedded_themes_parse() { 664 + for theme_name in ThemeRegistry::available_themes() { 665 + let theme = ThemeRegistry::get(theme_name); 666 + let styled = theme.heading(&"Test"); 667 + assert!( 668 + styled.to_string().contains("Test"), 669 + "Theme '{}' failed to parse or apply styles", 670 + theme_name 671 + ); 672 + } 578 673 } 579 674 }
+21
core/src/themes/catppuccin-latte.yml
··· 1 + system: "base16" 2 + name: "Catppuccin Latte" 3 + author: "https://github.com/catppuccin/catppuccin" 4 + variant: "light" 5 + palette: 6 + base00: "#eff1f5" # base 7 + base01: "#e6e9ef" # mantle 8 + base02: "#ccd0da" # surface0 9 + base03: "#bcc0cc" # surface1 10 + base04: "#acb0be" # surface2 11 + base05: "#4c4f69" # text 12 + base06: "#dc8a78" # rosewater 13 + base07: "#7287fd" # lavender 14 + base08: "#d20f39" # red 15 + base09: "#fe640b" # peach 16 + base0A: "#df8e1d" # yellow 17 + base0B: "#40a02b" # green 18 + base0C: "#179299" # teal 19 + base0D: "#1e66f5" # blue 20 + base0E: "#8839ef" # mauve 21 + base0F: "#dd7878" # flamingo
+21
core/src/themes/catppuccin-mocha.yml
··· 1 + system: "base16" 2 + name: "Catppuccin Mocha" 3 + author: "https://github.com/catppuccin/catppuccin" 4 + variant: "dark" 5 + palette: 6 + base00: "#1e1e2e" # base 7 + base01: "#181825" # mantle 8 + base02: "#313244" # surface0 9 + base03: "#45475a" # surface1 10 + base04: "#585b70" # surface2 11 + base05: "#cdd6f4" # text 12 + base06: "#f5e0dc" # rosewater 13 + base07: "#b4befe" # lavender 14 + base08: "#f38ba8" # red 15 + base09: "#fab387" # peach 16 + base0A: "#f9e2af" # yellow 17 + base0B: "#a6e3a1" # green 18 + base0C: "#94e2d5" # teal 19 + base0D: "#89b4fa" # blue 20 + base0E: "#cba6f7" # mauve 21 + base0F: "#f2cdcd" # flamingo
+21
core/src/themes/gruvbox-material-dark-medium.yml
··· 1 + system: "base16" 2 + name: "Gruvbox Material Dark, Medium" 3 + author: "Mayush Kumar (https://github.com/MayushKumar), sainnhe (https://github.com/sainnhe/gruvbox-material-vscode)" 4 + variant: "dark" 5 + palette: 6 + base00: "#292828" 7 + base01: "#32302f" 8 + base02: "#504945" 9 + base03: "#665c54" 10 + base04: "#bdae93" 11 + base05: "#ddc7a1" 12 + base06: "#ebdbb2" 13 + base07: "#fbf1c7" 14 + base08: "#ea6962" 15 + base09: "#e78a4e" 16 + base0A: "#d8a657" 17 + base0B: "#a9b665" 18 + base0C: "#89b482" 19 + base0D: "#7daea3" 20 + base0E: "#d3869b" 21 + base0F: "#bd6f3e"
+21
core/src/themes/gruvbox-material-light-medium.yml
··· 1 + system: "base16" 2 + name: "Gruvbox Material Light, Medium" 3 + author: "Mayush Kumar (https://github.com/MayushKumar), sainnhe (https://github.com/sainnhe/gruvbox-material-vscode)" 4 + variant: "light" 5 + palette: 6 + base00: "#fbf1c7" 7 + base01: "#f2e5bc" 8 + base02: "#d5c4a1" 9 + base03: "#bdae93" 10 + base04: "#665c54" 11 + base05: "#654735" 12 + base06: "#3c3836" 13 + base07: "#282828" 14 + base08: "#c14a4a" 15 + base09: "#c35e0a" 16 + base0A: "#b47109" 17 + base0B: "#6c782e" 18 + base0C: "#4c7a5d" 19 + base0D: "#45707a" 20 + base0E: "#945e80" 21 + base0F: "#e78a4e"
+21
core/src/themes/nord-light.yml
··· 1 + system: "base16" 2 + name: "Nord Light" 3 + author: "threddast, based on fuxialexander's doom-nord-light-theme (Doom Emacs)" 4 + variant: "light" 5 + palette: 6 + base00: "#e5e9f0" 7 + base01: "#c2d0e7" 8 + base02: "#b8c5db" 9 + base03: "#aebacf" 10 + base04: "#60728c" 11 + base05: "#2e3440" 12 + base06: "#3b4252" 13 + base07: "#29838d" 14 + base08: "#99324b" 15 + base09: "#ac4426" 16 + base0A: "#9a7500" 17 + base0B: "#4f894c" 18 + base0C: "#398eac" 19 + base0D: "#3b6ea8" 20 + base0E: "#97365b" 21 + base0F: "#5272af"
+21
core/src/themes/nord.yml
··· 1 + system: "base16" 2 + name: "Nord" 3 + author: "arcticicestudio" 4 + variant: "dark" 5 + palette: 6 + base00: "#2E3440" 7 + base01: "#3B4252" 8 + base02: "#434C5E" 9 + base03: "#4C566A" 10 + base04: "#D8DEE9" 11 + base05: "#E5E9F0" 12 + base06: "#ECEFF4" 13 + base07: "#8FBCBB" 14 + base08: "#BF616A" 15 + base09: "#D08770" 16 + base0A: "#EBCB8B" 17 + base0B: "#A3BE8C" 18 + base0C: "#88C0D0" 19 + base0D: "#81A1C1" 20 + base0E: "#B48EAD" 21 + base0F: "#5E81AC"
+21
core/src/themes/oxocarbon-dark.yml
··· 1 + system: "base16" 2 + name: "Oxocarbon Dark" 3 + author: "shaunsingh/IBM" 4 + variant: "dark" 5 + palette: 6 + base00: "#161616" 7 + base01: "#262626" 8 + base02: "#393939" 9 + base03: "#525252" 10 + base04: "#dde1e6" 11 + base05: "#f2f4f8" 12 + base06: "#ffffff" 13 + base07: "#08bdba" 14 + base08: "#3ddbd9" 15 + base09: "#78a9ff" 16 + base0A: "#ee5396" 17 + base0B: "#33b1ff" 18 + base0C: "#ff7eb6" 19 + base0D: "#42be65" 20 + base0E: "#be95ff" 21 + base0F: "#82cfff"
+21
core/src/themes/oxocarbon-light.yml
··· 1 + system: "base16" 2 + name: "Oxocarbon Light" 3 + author: "shaunsingh/IBM" 4 + variant: "light" 5 + palette: 6 + base00: "#f2f4f8" 7 + base01: "#dde1e6" 8 + base02: "#525252" 9 + base03: "#161616" 10 + base04: "#262626" 11 + base05: "#393939" 12 + base06: "#525252" 13 + base07: "#08bdba" 14 + base08: "#ff7eb6" 15 + base09: "#ee5396" 16 + base0A: "#FF6F00" 17 + base0B: "#0f62fe" 18 + base0C: "#673AB7" 19 + base0D: "#42be65" 20 + base0E: "#be95ff" 21 + base0F: "#37474F"
+21
core/src/themes/solarized-dark.yml
··· 1 + system: "base16" 2 + name: "Solarized Dark" 3 + author: "Ethan Schoonover (modified by aramisgithub)" 4 + variant: "dark" 5 + palette: 6 + base00: "#002b36" 7 + base01: "#073642" 8 + base02: "#586e75" 9 + base03: "#657b83" 10 + base04: "#839496" 11 + base05: "#93a1a1" 12 + base06: "#eee8d5" 13 + base07: "#fdf6e3" 14 + base08: "#dc322f" 15 + base09: "#cb4b16" 16 + base0A: "#b58900" 17 + base0B: "#859900" 18 + base0C: "#2aa198" 19 + base0D: "#268bd2" 20 + base0E: "#6c71c4" 21 + base0F: "#d33682"
+21
core/src/themes/solarized-light.yml
··· 1 + system: "base16" 2 + name: "Solarized Light" 3 + author: "Ethan Schoonover (modified by aramisgithub)" 4 + variant: "light" 5 + palette: 6 + base00: "#fdf6e3" 7 + base01: "#eee8d5" 8 + base02: "#93a1a1" 9 + base03: "#839496" 10 + base04: "#657b83" 11 + base05: "#586e75" 12 + base06: "#073642" 13 + base07: "#002b36" 14 + base08: "#dc322f" 15 + base09: "#cb4b16" 16 + base0A: "#b58900" 17 + base0B: "#859900" 18 + base0C: "#2aa198" 19 + base0D: "#268bd2" 20 + base0E: "#6c71c4" 21 + base0F: "#d33682"
+106 -112
docs/src/appendices/themes.md
··· 1 1 # Themes 2 2 3 - slides.rs provides a theme system for customizing the appearance of your presentations. Themes control colors and styling for headings, body text, code blocks, and UI elements. 3 + slides.rs uses the [Base16](https://github.com/chriskempson/base16) theming system for customizing the appearance of your presentations. Base16 provides a standardized way to define color schemes that work consistently across dark and light backgrounds. 4 + 5 + ## Base16 Color System 6 + 7 + Base16 defines 16 semantic colors (base00 through base0F) that serve specific purposes: 8 + 9 + ### Background Shades 10 + 11 + - **base00-03**: Background colors from darkest to lighter (or lightest to darker in light themes) 12 + 13 + ### Foreground Shades 14 + 15 + - **base04-07**: Foreground colors from darker to lightest (or lightest to darker in light themes) 16 + 17 + ### Accent Colors 18 + 19 + - **base08**: Red (variables, deletion) 20 + - **base09**: Orange (integers, constants, emphasis) 21 + - **base0A**: Yellow (classes, list markers) 22 + - **base0B**: Green (strings, code blocks) 23 + - **base0C**: Cyan (links, support functions) 24 + - **base0D**: Blue (functions, headings) 25 + - **base0E**: Magenta (keywords, strong emphasis) 26 + - **base0F**: Brown (deprecated, special) 27 + 28 + ## Color Mapping 29 + 30 + slides.rs maps base16 colors to semantic roles: 31 + 32 + ### Content Colors 33 + 34 + - **Headings** (base0D): Blue accent for slide titles 35 + - **Body text** (base05): Main foreground color 36 + - **Strong/Bold** (base0E): Magenta for emphasis 37 + - **Emphasis/Italic** (base09): Orange for subtle emphasis 38 + - **Code blocks** (base0B): Green for fenced code 39 + - **Inline code background** (base02): Selection background 40 + - **Links** (base0C): Cyan for hyperlinks 41 + - **Accents** (base08): Red for highlights 42 + - **List markers** (base0A): Yellow for bullets/numbers 43 + - **Dimmed elements** (base03): Comments, borders, rules 4 44 5 - ## Automatic Light/Dark Detection 45 + ### UI Chrome Colors 6 46 7 - Each theme automatically detects your terminal background and selects the appropriate light or dark variant. This ensures optimal contrast and readability regardless of your terminal settings. 47 + - **UI background** (base00): Status bar and UI backgrounds 48 + - **UI borders** (base04): Window and panel borders 49 + - **UI titles** (base06): Bright text for UI elements 50 + - **UI text** (base07): Brightest text for status bars 8 51 9 - ## Available Theme Schemes 52 + ## Available Themes 10 53 11 - The following color schemes are built-in: 54 + slides.rs includes 10 prebuilt base16 themes, embedded at compile time: 12 55 13 - **basic** (default) - IBM's Oxocarbon color palette with clean, modern styling 56 + ### Catppuccin 14 57 15 - - Dark variant: Light text on dark background with vibrant accents 16 - - Light variant: Dark text on light background with adjusted colors 58 + - **catppuccin-mocha** - Dark theme with pastel colors 59 + - **catppuccin-latte** - Light theme with warm tones 17 60 18 - **monokai** - Inspired by the popular Monokai editor theme 61 + ### Gruvbox Material 19 62 20 - - Dark variant: Classic Monokai with pink headings and green code 21 - - Light variant: Adjusted colors for light backgrounds 63 + - **gruvbox-material-dark** - Retro dark theme with warm colors 64 + - **gruvbox-material-light** - Retro light theme 22 65 23 - **dracula** - Based on the Dracula color scheme 66 + ### Nord 24 67 25 - - Dark variant: Purple and cyan tones optimized for dark backgrounds 26 - - Light variant: Darker variants of Dracula colors for light backgrounds 68 + - **nord** - Arctic-inspired dark theme with cool blues 69 + - **nord-light** - Nord palette adapted for light backgrounds 27 70 28 - **solarized** - Ethan Schoonover's Solarized palette 71 + ### Oxocarbon 29 72 30 - - Dark variant: Solarized Dark with blue headings 31 - - Light variant: Solarized Light with adjusted foreground colors 73 + - **oxocarbon-dark** - IBM's modern dark theme (default) 74 + - **oxocarbon-light** - IBM's modern light theme 32 75 33 - **nord** - Arctic-inspired theme with cool tones 76 + ### Solarized 34 77 35 - - Dark variant: Subtle blues and greens on dark background 36 - - Light variant: Nord colors adjusted for light backgrounds 78 + - **solarized-dark** - Ethan Schoonover's precision dark palette 79 + - **solarized-light** - Solarized adapted for light backgrounds 37 80 38 81 ## Using Themes 39 82 ··· 41 84 42 85 Specify a theme in your slide deck's YAML frontmatter: 43 86 44 - ````markdown 87 + ```markdown 45 88 --- 46 - theme: monokai 89 + theme: catppuccin-mocha 47 90 --- 48 91 49 92 # Your First Slide ··· 51 94 Content here 52 95 ``` 53 96 54 - The terminal background will be automatically detected. To force a specific variant: 55 - 56 - ```markdown 57 - --- 58 - theme: solarized:light 59 - --- 60 - ``` 61 - 62 97 Or with TOML frontmatter: 63 98 64 99 ```markdown 65 100 +++ 66 - theme = "dracula:dark" 101 + theme = "nord" 67 102 +++ 68 103 69 104 # Your First Slide 70 - 71 - Content here 72 - ```` 105 + ``` 73 106 74 107 ### Via Command Line 75 108 76 109 Override the theme with the `--theme` flag: 77 110 78 111 ```bash 79 - # Auto-detect terminal background 80 112 slides present slides.md --theme nord 81 - slides print slides.md --theme solarized 82 - 83 - # Force a specific variant 84 - slides present slides.md --theme monokai:light 85 - slides print slides.md --theme nord:dark 113 + slides print slides.md --theme catppuccin-latte 86 114 ``` 87 115 88 116 ### Via Environment Variable ··· 90 118 Set a default theme using the `SLIDES_THEME` environment variable: 91 119 92 120 ```bash 93 - # Auto-detect variant 94 - export SLIDES_THEME=basic 95 - slides present slides.md 96 - 97 - # Force specific variant 98 - export SLIDES_THEME=dracula:dark 121 + export SLIDES_THEME=gruvbox-material-dark 99 122 slides present slides.md 100 123 ``` 101 124 ··· 106 129 1. Command line flag (`--theme`) 107 130 2. Frontmatter metadata (`theme:` field) 108 131 3. Environment variable (`SLIDES_THEME`) 109 - 4. Default theme 132 + 4. Default theme (nord for dark terminals, nord-light for light terminals) 110 133 111 - ## Theme Components 134 + ## Custom Themes (Coming Soon) 112 135 113 - Each theme defines colors for: 136 + Future versions will support loading custom base16 YAML themes from: 137 + 138 + - `~/.config/slides/themes/` directory 139 + - `--theme-file` command line flag 140 + 141 + Base16 YAML format: 142 + 143 + ```yaml 144 + system: "base16" 145 + name: "My Custom Theme" 146 + author: "Your Name" 147 + variant: "dark" # or "light" 148 + palette: 149 + base00: "#1a1b26" 150 + base01: "#16161e" 151 + base02: "#2f3549" 152 + base03: "#444b6a" 153 + base04: "#787c99" 154 + base05: "#a9b1d6" 155 + base06: "#cbccd1" 156 + base07: "#d5d6db" 157 + base08: "#c0caf5" 158 + base09: "#a9b1d6" 159 + base0A: "#0db9d7" 160 + base0B: "#9ece6a" 161 + base0C: "#b4f9f8" 162 + base0D: "#2ac3de" 163 + base0E: "#bb9af7" 164 + base0F: "#f7768e" 165 + ``` 114 166 115 - - Headings (level 1-6) 116 - - Body text 117 - - Accent colors 118 - - Code blocks and inline code 119 - - Code fence markers 120 - - Horizontal rules 121 - - List markers (bullets and numbers) 122 - - Blockquote borders 123 - - Table borders 167 + You can find thousands of base16 themes at the [Base16 Gallery](https://tinted-theming.github.io/tinted-gallery/). 124 168 125 169 ## Rendering Features 126 170 127 171 The printer uses Unicode box-drawing characters for clean visual output: 128 172 129 - - `─` and `═` for horizontal lines 130 - - `│` for vertical borders 131 - - `┼` for table intersections 173 + - `▉ ▓ ▒ ░ ▌` for heading levels (h1-h6) 174 + - `─` and `═` for horizontal rules 175 + - `│` for blockquote borders and table dividers 132 176 - `•` for unordered list markers 133 177 134 178 Tables automatically calculate column widths based on content and available terminal width. 135 179 136 - ## Default Theme 137 - 138 - The application's default slide theme is based on Oxocarbon 139 - 140 - ### Dark 141 - 142 - ```yml 143 - - scheme: "Oxocarbon Dark" 144 - author: "shaunsingh/IBM" 145 - palette: 146 - base00: "#161616" 147 - base01: "#262626" 148 - base02: "#393939" 149 - base03: "#525252" 150 - base04: "#dde1e6" 151 - base05: "#f2f4f8" 152 - base06: "#ffffff" 153 - base07: "#08bdba" 154 - base08: "#3ddbd9" 155 - base09: "#78a9ff" 156 - base0A: "#ee5396" 157 - base0B: "#33b1ff" 158 - base0C: "#ff7eb6" 159 - base0D: "#42be65" 160 - base0E: "#be95ff" 161 - base0F: "#82cfff" 162 - ``` 163 - 164 - ### Light 165 - 166 - ```yml 167 - - scheme: "Oxocarbon Light" 168 - author: "shaunsingh/IBM" 169 - palette: 170 - base00: "#f2f4f8" 171 - base01: "#dde1e6" 172 - base02: "#525252" 173 - base03: "#161616" 174 - base04: "#262626" 175 - base05: "#393939" 176 - base06: "#525252" 177 - base07: "#08bdba" 178 - base08: "#ff7eb6" 179 - base09: "#ee5396" 180 - base0A: "#FF6F00" 181 - base0B: "#0f62fe" 182 - base0C: "#673AB7" 183 - base0D: "#42be65" 184 - base0E: "#be95ff" 185 - base0F: "#37474F" 186 - ``` 180 + Code blocks support syntax highlighting through [Syntect](https://github.com/trishume/syntect), which automatically adapts to your selected theme's light/dark variant.