Skip to main content

vtcode_tui/ui/
theme.rs

1use anstyle::{Color, Effects, RgbColor, Style};
2use anyhow::{Context, Result, anyhow};
3use catppuccin::PALETTE;
4use once_cell::sync::Lazy;
5use parking_lot::RwLock;
6use std::collections::HashMap;
7
8use crate::config::constants::{defaults, ui};
9
10/// Identifier for the default theme.
11pub const DEFAULT_THEME_ID: &str = defaults::DEFAULT_THEME;
12
13/// Default minimum contrast ratio (WCAG AA standard)
14const DEFAULT_MIN_CONTRAST: f64 = ui::THEME_MIN_CONTRAST_RATIO;
15const MAX_DARK_BG_TEXT_LUMINANCE: f64 = 0.92;
16const MIN_DARK_BG_TEXT_LUMINANCE: f64 = 0.20;
17const MAX_LIGHT_BG_TEXT_LUMINANCE: f64 = 0.68;
18
19/// Runtime configuration for color accessibility settings.
20/// These can be updated from vtcode.toml [ui] section.
21static COLOR_CONFIG: Lazy<RwLock<ColorAccessibilityConfig>> =
22    Lazy::new(|| RwLock::new(ColorAccessibilityConfig::default()));
23
24/// Color accessibility configuration loaded from vtcode.toml
25#[derive(Clone, Debug)]
26pub struct ColorAccessibilityConfig {
27    /// Minimum contrast ratio for text (WCAG standard)
28    pub minimum_contrast: f64,
29    /// Whether to treat bold as bright (legacy terminal compat)
30    pub bold_is_bright: bool,
31    /// Whether to restrict to safe ANSI colors only
32    pub safe_colors_only: bool,
33}
34
35impl Default for ColorAccessibilityConfig {
36    fn default() -> Self {
37        Self {
38            minimum_contrast: DEFAULT_MIN_CONTRAST,
39            bold_is_bright: false,
40            safe_colors_only: false,
41        }
42    }
43}
44
45/// Update the global color accessibility configuration.
46/// Call this after loading vtcode.toml to apply user preferences.
47pub fn set_color_accessibility_config(config: ColorAccessibilityConfig) {
48    *COLOR_CONFIG.write() = config;
49}
50
51/// Get the current minimum contrast ratio setting.
52pub fn get_minimum_contrast() -> f64 {
53    COLOR_CONFIG.read().minimum_contrast
54}
55
56/// Check if bold-is-bright compatibility mode is enabled.
57pub fn is_bold_bright_mode() -> bool {
58    COLOR_CONFIG.read().bold_is_bright
59}
60
61/// Check if safe colors only mode is enabled.
62pub fn is_safe_colors_only() -> bool {
63    COLOR_CONFIG.read().safe_colors_only
64}
65
66/// Palette describing UI colors for the terminal experience.
67#[derive(Clone, Debug)]
68pub struct ThemePalette {
69    pub primary_accent: RgbColor,
70    pub background: RgbColor,
71    pub foreground: RgbColor,
72    pub secondary_accent: RgbColor,
73    pub alert: RgbColor,
74    pub logo_accent: RgbColor,
75}
76
77impl ThemePalette {
78    /// Create a style with foreground color, respecting bold_is_bright setting.
79    /// When bold_is_bright is enabled and bold is requested, we skip bold
80    /// to prevent unintended bright color mapping in legacy terminals.
81    fn style_from(color: RgbColor, bold: bool) -> Style {
82        let mut style = Style::new().fg_color(Some(Color::Rgb(color)));
83        // Only apply bold if not in bold_is_bright compatibility mode
84        if bold && !is_bold_bright_mode() {
85            style = style.bold();
86        }
87        style
88    }
89
90    fn build_styles(&self) -> ThemeStyles {
91        self.build_styles_with_contrast(get_minimum_contrast())
92    }
93
94    /// Build styles with a specific minimum contrast ratio.
95    /// This allows runtime configuration of contrast requirements.
96    fn build_styles_with_contrast(&self, min_contrast: f64) -> ThemeStyles {
97        let primary = self.primary_accent;
98        let background = self.background;
99        let secondary = self.secondary_accent;
100        let logo_accent = self.logo_accent;
101
102        let fallback_light = RgbColor(
103            ui::THEME_COLOR_WHITE_RED,
104            ui::THEME_COLOR_WHITE_GREEN,
105            ui::THEME_COLOR_WHITE_BLUE,
106        );
107
108        let text_color = ensure_contrast(
109            self.foreground,
110            background,
111            min_contrast,
112            &[
113                lighten(self.foreground, ui::THEME_FOREGROUND_LIGHTEN_RATIO),
114                lighten(secondary, ui::THEME_SECONDARY_LIGHTEN_RATIO),
115                fallback_light,
116            ],
117        );
118        let text_color = balance_text_luminance(text_color, background, min_contrast);
119
120        let info_color = ensure_contrast(
121            secondary,
122            background,
123            min_contrast,
124            &[
125                lighten(secondary, ui::THEME_SECONDARY_LIGHTEN_RATIO),
126                text_color,
127                fallback_light,
128            ],
129        );
130        let info_color = balance_text_luminance(info_color, background, min_contrast);
131
132        // Light gray for tool output derived from theme colors
133        let light_tool_color = lighten(text_color, ui::THEME_MIX_RATIO); // Lighter version of the text color
134        let tool_color = ensure_contrast(
135            light_tool_color,
136            background,
137            min_contrast,
138            &[
139                lighten(light_tool_color, ui::THEME_TOOL_BODY_LIGHTEN_RATIO),
140                info_color,
141                text_color,
142            ],
143        );
144        let tool_body_candidate = mix(light_tool_color, text_color, ui::THEME_TOOL_BODY_MIX_RATIO);
145        let tool_body_color = ensure_contrast(
146            tool_body_candidate,
147            background,
148            min_contrast,
149            &[
150                lighten(light_tool_color, ui::THEME_TOOL_BODY_LIGHTEN_RATIO),
151                text_color,
152                fallback_light,
153            ],
154        );
155        let tool_style = Style::new().fg_color(Some(Color::Rgb(tool_color)));
156        let tool_detail_style = Style::new().fg_color(Some(Color::Rgb(tool_body_color)));
157        let response_color = ensure_contrast(
158            text_color,
159            background,
160            min_contrast,
161            &[
162                lighten(text_color, ui::THEME_RESPONSE_COLOR_LIGHTEN_RATIO),
163                fallback_light,
164            ],
165        );
166        let response_color = balance_text_luminance(response_color, background, min_contrast);
167
168        // Reasoning color: Use text color with dimmed effect for placeholder-like appearance
169        let reasoning_color = ensure_contrast(
170            lighten(text_color, 0.25), // Lighter for placeholder-like appearance
171            background,
172            min_contrast,
173            &[lighten(text_color, 0.15), text_color, fallback_light],
174        );
175        let reasoning_color = balance_text_luminance(reasoning_color, background, min_contrast);
176        // Reasoning style: Dimmed and italic for placeholder-like thinking output
177        let reasoning_style =
178            Self::style_from(reasoning_color, false).effects(Effects::DIMMED | Effects::ITALIC);
179        // Make user messages more distinct using secondary accent color
180
181        let user_color = ensure_contrast(
182            lighten(secondary, ui::THEME_USER_COLOR_LIGHTEN_RATIO),
183            background,
184            min_contrast,
185            &[
186                lighten(secondary, ui::THEME_SECONDARY_USER_COLOR_LIGHTEN_RATIO),
187                info_color,
188                text_color,
189            ],
190        );
191        let user_color = balance_text_luminance(user_color, background, min_contrast);
192
193        let alert_color = ensure_contrast(
194            self.alert,
195            background,
196            min_contrast,
197            &[
198                lighten(self.alert, ui::THEME_LUMINANCE_LIGHTEN_RATIO),
199                fallback_light,
200                text_color,
201            ],
202        );
203
204        let alert_color = balance_text_luminance(alert_color, background, min_contrast);
205
206        // Tool output style: use default terminal styling (no color/bold/dim effects)
207        let tool_output_style = Style::new();
208
209        // PTY output style: subdued foreground for terminal output that's readable
210        // but visually distinct from agent/user text — avoids terminal DIM modifier
211        // which can be too faint on many terminals
212        let pty_output_candidate = lighten(tool_body_color, ui::THEME_PTY_OUTPUT_LIGHTEN_RATIO);
213        let pty_output_color = ensure_contrast(
214            pty_output_candidate,
215            background,
216            min_contrast,
217            &[
218                lighten(text_color, ui::THEME_PTY_OUTPUT_LIGHTEN_RATIO),
219                tool_body_color,
220                text_color,
221            ],
222        );
223        let pty_output_style = Style::new().fg_color(Some(Color::Rgb(pty_output_color)));
224
225        let primary_style_color = balance_text_luminance(
226            ensure_contrast(primary, background, min_contrast, &[text_color]),
227            background,
228            min_contrast,
229        );
230        let secondary_style_color = balance_text_luminance(
231            ensure_contrast(
232                secondary,
233                background,
234                min_contrast,
235                &[info_color, text_color],
236            ),
237            background,
238            min_contrast,
239        );
240        let logo_style_color = balance_text_luminance(
241            ensure_contrast(
242                logo_accent,
243                background,
244                min_contrast,
245                &[secondary_style_color, text_color],
246            ),
247            background,
248            min_contrast,
249        );
250
251        ThemeStyles {
252            info: Self::style_from(info_color, true),
253            error: Self::style_from(alert_color, true),
254            output: Self::style_from(text_color, false),
255            response: Self::style_from(response_color, false),
256            reasoning: reasoning_style,
257            tool: tool_style,
258            tool_detail: tool_detail_style,
259            tool_output: tool_output_style,
260            pty_output: pty_output_style,
261            status: Self::style_from(
262                ensure_contrast(
263                    lighten(primary_style_color, ui::THEME_PRIMARY_STATUS_LIGHTEN_RATIO),
264                    background,
265                    min_contrast,
266                    &[
267                        lighten(
268                            primary_style_color,
269                            ui::THEME_PRIMARY_STATUS_SECONDARY_LIGHTEN_RATIO,
270                        ),
271                        info_color,
272                        text_color,
273                    ],
274                ),
275                true,
276            ),
277            mcp: Self::style_from(
278                ensure_contrast(
279                    lighten(logo_style_color, ui::THEME_SECONDARY_LIGHTEN_RATIO),
280                    background,
281                    min_contrast,
282                    &[
283                        lighten(logo_style_color, ui::THEME_LOGO_ACCENT_BANNER_LIGHTEN_RATIO),
284                        info_color,
285                        fallback_light,
286                    ],
287                ),
288                true,
289            ),
290            user: Self::style_from(user_color, false),
291            primary: Self::style_from(primary_style_color, false),
292            secondary: Self::style_from(secondary_style_color, false),
293            background: Color::Rgb(background),
294            foreground: Color::Rgb(text_color),
295        }
296    }
297}
298
299/// Styles computed from palette colors.
300#[derive(Clone, Debug)]
301pub struct ThemeStyles {
302    pub info: Style,
303    pub error: Style,
304    pub output: Style,
305    pub response: Style,
306    pub reasoning: Style,
307    pub tool: Style,
308    pub tool_detail: Style,
309    pub tool_output: Style,
310    pub pty_output: Style,
311    pub status: Style,
312    pub mcp: Style,
313    pub user: Style,
314    pub primary: Style,
315    pub secondary: Style,
316    pub background: Color,
317    pub foreground: Color,
318}
319
320#[derive(Clone, Debug)]
321pub struct ThemeDefinition {
322    pub id: &'static str,
323    pub label: &'static str,
324    pub palette: ThemePalette,
325}
326
327/// Logical grouping of built-in themes.
328#[derive(Clone, Debug, PartialEq, Eq)]
329pub struct ThemeSuite {
330    pub id: &'static str,
331    pub label: &'static str,
332    pub theme_ids: Vec<&'static str>,
333}
334
335#[derive(Clone, Debug)]
336struct ActiveTheme {
337    id: String,
338    label: String,
339    palette: ThemePalette,
340    styles: ThemeStyles,
341}
342
343#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
344enum CatppuccinFlavorKind {
345    Latte,
346    Frappe,
347    Macchiato,
348    Mocha,
349}
350
351impl CatppuccinFlavorKind {
352    const fn id(self) -> &'static str {
353        match self {
354            CatppuccinFlavorKind::Latte => "catppuccin-latte",
355            CatppuccinFlavorKind::Frappe => "catppuccin-frappe",
356            CatppuccinFlavorKind::Macchiato => "catppuccin-macchiato",
357            CatppuccinFlavorKind::Mocha => "catppuccin-mocha",
358        }
359    }
360
361    const fn label(self) -> &'static str {
362        match self {
363            CatppuccinFlavorKind::Latte => "Catppuccin Latte",
364            CatppuccinFlavorKind::Frappe => "Catppuccin Frappé",
365            CatppuccinFlavorKind::Macchiato => "Catppuccin Macchiato",
366            CatppuccinFlavorKind::Mocha => "Catppuccin Mocha",
367        }
368    }
369
370    fn flavor(self) -> catppuccin::Flavor {
371        match self {
372            CatppuccinFlavorKind::Latte => PALETTE.latte,
373            CatppuccinFlavorKind::Frappe => PALETTE.frappe,
374            CatppuccinFlavorKind::Macchiato => PALETTE.macchiato,
375            CatppuccinFlavorKind::Mocha => PALETTE.mocha,
376        }
377    }
378}
379
380static CATPPUCCIN_FLAVORS: &[CatppuccinFlavorKind] = &[
381    CatppuccinFlavorKind::Latte,
382    CatppuccinFlavorKind::Frappe,
383    CatppuccinFlavorKind::Macchiato,
384    CatppuccinFlavorKind::Mocha,
385];
386
387static REGISTRY: Lazy<HashMap<&'static str, ThemeDefinition>> = Lazy::new(|| {
388    let mut map = HashMap::new();
389    map.insert(
390        "ciapre-dark",
391        ThemeDefinition {
392            id: "ciapre-dark",
393            label: "Ciapre Dark",
394            palette: ThemePalette {
395                primary_accent: RgbColor(0xBF, 0xB3, 0x8F),
396                background: RgbColor(0x26, 0x26, 0x26),
397                foreground: RgbColor(0xBF, 0xB3, 0x8F),
398                secondary_accent: RgbColor(0xD9, 0x9A, 0x4E),
399                alert: RgbColor(0xFF, 0x8A, 0x8A),
400                logo_accent: RgbColor(0xD9, 0x9A, 0x4E),
401            },
402        },
403    );
404    map.insert(
405        "ciapre-blue",
406        ThemeDefinition {
407            id: "ciapre-blue",
408            label: "Ciapre Blue",
409            palette: ThemePalette {
410                primary_accent: RgbColor(0xBF, 0xB3, 0x8F),
411                background: RgbColor(0x17, 0x1C, 0x26),
412                foreground: RgbColor(0xBF, 0xB3, 0x8F),
413                secondary_accent: RgbColor(0xBF, 0xB3, 0x8F),
414                alert: RgbColor(0xFF, 0x8A, 0x8A),
415                logo_accent: RgbColor(0xD9, 0x9A, 0x4E),
416            },
417        },
418    );
419    map.insert(
420        "ciapre",
421        ThemeDefinition {
422            id: "ciapre",
423            label: "Ciapre",
424            palette: ThemePalette {
425                primary_accent: RgbColor(0xAE, 0xA4, 0x7F), // White (#AEA47F)
426                background: RgbColor(0x18, 0x18, 0x18),     // Black (#181818)
427                foreground: RgbColor(0xAE, 0xA4, 0x7F),     // White (#AEA47F)
428                secondary_accent: RgbColor(0xCC, 0x8A, 0x3E), // Yellow (#CC8A3E)
429                alert: RgbColor(0xAC, 0x38, 0x35),          // Bright Red (#AC3835)
430                logo_accent: RgbColor(0xCC, 0x8A, 0x3E),    // Yellow (#CC8A3E)
431            },
432        },
433    );
434    map.insert(
435        "solarized-dark",
436        ThemeDefinition {
437            id: "solarized-dark",
438            label: "Solarized Dark",
439            palette: ThemePalette {
440                primary_accent: RgbColor(0x83, 0x94, 0x96), // Base 0 (#839496)
441                background: RgbColor(0x00, 0x2B, 0x36),     // Base 03 (#002b36)
442                foreground: RgbColor(0x83, 0x94, 0x96),     // Base 0 (#839496)
443                secondary_accent: RgbColor(0x26, 0x8B, 0xD2), // Blue (#268bd2)
444                alert: RgbColor(0xDC, 0x32, 0x2F),          // Red (#dc322f)
445                logo_accent: RgbColor(0xB5, 0x89, 0x00),    // Yellow (#b58900)
446            },
447        },
448    );
449    map.insert(
450        "solarized-light",
451        ThemeDefinition {
452            id: "solarized-light",
453            label: "Solarized Light",
454            palette: ThemePalette {
455                primary_accent: RgbColor(0x58, 0x6E, 0x75), // Base 00 (#586e75)
456                background: RgbColor(0xFD, 0xF6, 0xE3),     // Base 3 (#fdf6e3)
457                foreground: RgbColor(0x58, 0x6E, 0x75),     // Base 00 (#586e75)
458                secondary_accent: RgbColor(0x26, 0x8B, 0xD2), // Blue (#268bd2)
459                alert: RgbColor(0xDC, 0x32, 0x2F),          // Red (#dc322f)
460                logo_accent: RgbColor(0xB5, 0x89, 0x00),    // Yellow (#b58900)
461            },
462        },
463    );
464    map.insert(
465        "solarized-dark-hc",
466        ThemeDefinition {
467            id: "solarized-dark-hc",
468            label: "Solarized Dark Higher Contrast",
469            palette: ThemePalette {
470                primary_accent: RgbColor(0x83, 0x94, 0x96), // Base 0 (#839496)
471                background: RgbColor(0x00, 0x28, 0x31),     // Base 03 (#002831)
472                foreground: RgbColor(0xE9, 0xE3, 0xCC),     // Base 1 (#e9e3cc)
473                secondary_accent: RgbColor(0x20, 0x76, 0xC7), // Blue (#2076c7)
474                alert: RgbColor(0xD1, 0x1C, 0x24),          // Red (#d11c24)
475                logo_accent: RgbColor(0xA5, 0x77, 0x06),    // Yellow (#a57706)
476            },
477        },
478    );
479
480    // Gruvbox themes
481    map.insert(
482        "gruvbox-dark",
483        ThemeDefinition {
484            id: "gruvbox-dark",
485            label: "Gruvbox Dark",
486            palette: ThemePalette {
487                primary_accent: RgbColor(0xA8, 0x99, 0x84), // Light gray (#a89984)
488                background: RgbColor(0x28, 0x28, 0x28),     // Dark background (#282828)
489                foreground: RgbColor(0xA8, 0x99, 0x84),     // Light gray (#a89984)
490                secondary_accent: RgbColor(0x45, 0x85, 0x88), // Cyan (#458588)
491                alert: RgbColor(0xCC, 0x24, 0x1D),          // Red (#cc241d)
492                logo_accent: RgbColor(0xD7, 0x99, 0x21),    // Yellow (#d79921)
493            },
494        },
495    );
496    map.insert(
497        "gruvbox-dark-hard",
498        ThemeDefinition {
499            id: "gruvbox-dark-hard",
500            label: "Gruvbox Dark Hard",
501            palette: ThemePalette {
502                primary_accent: RgbColor(0xA8, 0x99, 0x84), // Light gray (#a89984)
503                background: RgbColor(0x1D, 0x20, 0x21),     // Hard dark (#1d2021)
504                foreground: RgbColor(0xA8, 0x99, 0x84),     // Light gray (#a89984)
505                secondary_accent: RgbColor(0x45, 0x85, 0x88), // Cyan (#458588)
506                alert: RgbColor(0xCC, 0x24, 0x1D),          // Red (#cc241d)
507                logo_accent: RgbColor(0xD7, 0x99, 0x21),    // Yellow (#d79921)
508            },
509        },
510    );
511    map.insert(
512        "gruvbox-light",
513        ThemeDefinition {
514            id: "gruvbox-light",
515            label: "Gruvbox Light",
516            palette: ThemePalette {
517                primary_accent: RgbColor(0x7C, 0x6F, 0x64), // Dark gray (#7c6f64)
518                background: RgbColor(0xFB, 0xF4, 0xE8),     // Light background (#fbf4e8)
519                foreground: RgbColor(0x7C, 0x6F, 0x64),     // Dark gray (#7c6f64)
520                secondary_accent: RgbColor(0x45, 0x85, 0x88), // Cyan (#458588)
521                alert: RgbColor(0xCC, 0x24, 0x1D),          // Red (#cc241d)
522                logo_accent: RgbColor(0xD7, 0x99, 0x21),    // Yellow (#d79921)
523            },
524        },
525    );
526    map.insert(
527        "gruvbox-light-hard",
528        ThemeDefinition {
529            id: "gruvbox-light-hard",
530            label: "Gruvbox Light Hard",
531            palette: ThemePalette {
532                primary_accent: RgbColor(0x7C, 0x6F, 0x64), // Dark gray (#7c6f64)
533                background: RgbColor(0xF9, 0xF5, 0xD7),     // Hard light (#f9f5d7)
534                foreground: RgbColor(0x7C, 0x6F, 0x64),     // Dark gray (#7c6f64)
535                secondary_accent: RgbColor(0x45, 0x85, 0x88), // Cyan (#458588)
536                alert: RgbColor(0xCC, 0x24, 0x1D),          // Red (#cc241d)
537                logo_accent: RgbColor(0xD7, 0x99, 0x21),    // Yellow (#d79921)
538            },
539        },
540    );
541    map.insert(
542        "gruvbox-material",
543        ThemeDefinition {
544            id: "gruvbox-material",
545            label: "Gruvbox Material",
546            palette: ThemePalette {
547                primary_accent: RgbColor(0xFF, 0xFF, 0xFF), // White (#ffffff)
548                background: RgbColor(0x14, 0x16, 0x17),     // Dark background (#141617)
549                foreground: RgbColor(0xFF, 0xFF, 0xFF),     // White (#ffffff)
550                secondary_accent: RgbColor(0x6D, 0xA3, 0xED), // Blue (#6da3ed)
551                alert: RgbColor(0xEA, 0x69, 0x26),          // Orange (#ea6926)
552                logo_accent: RgbColor(0xEE, 0xCE, 0x5B),    // Yellow (#eece5b)
553            },
554        },
555    );
556    map.insert(
557        "gruvbox-material-dark",
558        ThemeDefinition {
559            id: "gruvbox-material-dark",
560            label: "Gruvbox Material Dark",
561            palette: ThemePalette {
562                primary_accent: RgbColor(0xD4, 0xBE, 0x98), // Light tan (#d4be98)
563                background: RgbColor(0x28, 0x28, 0x28),     // Dark background (#282828)
564                foreground: RgbColor(0xD4, 0xBE, 0x98),     // Light tan (#d4be98)
565                secondary_accent: RgbColor(0x7D, 0xAE, 0xA3), // Cyan (#7daea3)
566                alert: RgbColor(0xEA, 0x69, 0x62),          // Red (#ea6962)
567                logo_accent: RgbColor(0xD8, 0xA6, 0x57),    // Yellow (#d8a657)
568            },
569        },
570    );
571    map.insert(
572        "gruvbox-material-light",
573        ThemeDefinition {
574            id: "gruvbox-material-light",
575            label: "Gruvbox Material Light",
576            palette: ThemePalette {
577                primary_accent: RgbColor(0x65, 0x47, 0x35), // Dark brown (#654735)
578                background: RgbColor(0xFB, 0xF1, 0xC7),     // Light background (#fbf1c7)
579                foreground: RgbColor(0x65, 0x47, 0x35),     // Dark brown (#654735)
580                secondary_accent: RgbColor(0x45, 0x70, 0x7A), // Cyan (#45707a)
581                alert: RgbColor(0xC1, 0x4A, 0x4A),          // Red (#c14a4a)
582                logo_accent: RgbColor(0xB4, 0x71, 0x09),    // Yellow (#b47109)
583            },
584        },
585    );
586
587    // Zenburn theme
588    map.insert(
589        "zenburn",
590        ThemeDefinition {
591            id: "zenburn",
592            label: "Zenburn",
593            palette: ThemePalette {
594                primary_accent: RgbColor(0xDC, 0xDC, 0xCC), // White (#dcdccc)
595                background: RgbColor(0x4D, 0x4D, 0x4D),     // Dark background (#4d4d4d)
596                foreground: RgbColor(0xDC, 0xDC, 0xCC),     // White (#dcdccc)
597                secondary_accent: RgbColor(0x8C, 0xD0, 0xD3), // Cyan (#8cd0d3)
598                alert: RgbColor(0x70, 0x50, 0x50),          // Red (#705050)
599                logo_accent: RgbColor(0xF0, 0xDF, 0xAF),    // Yellow (#f0dfaf)
600            },
601        },
602    );
603
604    // Tomorrow themes
605    map.insert(
606        "tomorrow",
607        ThemeDefinition {
608            id: "tomorrow",
609            label: "Tomorrow",
610            palette: ThemePalette {
611                primary_accent: RgbColor(0x4D, 0x4D, 0x4D), // Dark gray (#4d4d4d)
612                background: RgbColor(0xFF, 0xFF, 0xFF),     // White (#ffffff)
613                foreground: RgbColor(0x4D, 0x4D, 0x4D),     // Dark gray (#4d4d4d)
614                secondary_accent: RgbColor(0x42, 0x71, 0xAE), // Blue (#4271ae)
615                alert: RgbColor(0xC8, 0x28, 0x29),          // Red (#c82829)
616                logo_accent: RgbColor(0xEA, 0xB7, 0x00),    // Yellow (#eab700)
617            },
618        },
619    );
620    map.insert(
621        "tomorrow-night",
622        ThemeDefinition {
623            id: "tomorrow-night",
624            label: "Tomorrow Night",
625            palette: ThemePalette {
626                primary_accent: RgbColor(0xDE, 0xDE, 0xDE), // Light gray (#dedede)
627                background: RgbColor(0x1D, 0x1F, 0x21),     // Dark (#1d1f21)
628                foreground: RgbColor(0xDE, 0xDE, 0xDE),     // Light gray (#dedede)
629                secondary_accent: RgbColor(0x81, 0xA2, 0xBE), // Blue (#81a2be)
630                alert: RgbColor(0xCC, 0x66, 0x66),          // Red (#cc6666)
631                logo_accent: RgbColor(0xF0, 0xC6, 0x74),    // Yellow (#f0c674)
632            },
633        },
634    );
635    map.insert(
636        "tomorrow-night-blue",
637        ThemeDefinition {
638            id: "tomorrow-night-blue",
639            label: "Tomorrow Night Blue",
640            palette: ThemePalette {
641                primary_accent: RgbColor(0xFF, 0xFF, 0xFF), // White (#ffffff)
642                background: RgbColor(0x00, 0x24, 0x51),     // Dark blue (#002451)
643                foreground: RgbColor(0xFF, 0xFF, 0xFF),     // White (#ffffff)
644                secondary_accent: RgbColor(0xBB, 0xDA, 0xFF), // Blue (#bbdaff)
645                alert: RgbColor(0xFF, 0x9D, 0xA4),          // Red (#ff9da4)
646                logo_accent: RgbColor(0xFF, 0xEE, 0xAD),    // Yellow (#ffeead)
647            },
648        },
649    );
650    map.insert(
651        "tomorrow-night-bright",
652        ThemeDefinition {
653            id: "tomorrow-night-bright",
654            label: "Tomorrow Night Bright",
655            palette: ThemePalette {
656                primary_accent: RgbColor(0xE0, 0xE0, 0xE0), // Light gray (#e0e0e0)
657                background: RgbColor(0x00, 0x00, 0x00),     // Black (#000000)
658                foreground: RgbColor(0xE0, 0xE0, 0xE0),     // Light gray (#e0e0e0)
659                secondary_accent: RgbColor(0x7A, 0xA6, 0xDA), // Blue (#7aa6da)
660                alert: RgbColor(0xD5, 0x4E, 0x53),          // Red (#d54e53)
661                logo_accent: RgbColor(0xE7, 0xC5, 0x47),    // Yellow (#e7c547)
662            },
663        },
664    );
665    map.insert(
666        "tomorrow-night-burns",
667        ThemeDefinition {
668            id: "tomorrow-night-burns",
669            label: "Tomorrow Night Burns",
670            palette: ThemePalette {
671                primary_accent: RgbColor(0xF5, 0xF5, 0xF5), // White (#f5f5f5)
672                background: RgbColor(0x25, 0x25, 0x25),     // Dark (#252525)
673                foreground: RgbColor(0xF5, 0xF5, 0xF5),     // White (#f5f5f5)
674                secondary_accent: RgbColor(0xFC, 0x59, 0x5F), // Red (#fc595f)
675                alert: RgbColor(0xFC, 0x59, 0x5F),          // Red (#fc595f)
676                logo_accent: RgbColor(0xE0, 0x93, 0x95),    // Light red (#e09395)
677            },
678        },
679    );
680    map.insert(
681        "tomorrow-night-eighties",
682        ThemeDefinition {
683            id: "tomorrow-night-eighties",
684            label: "Tomorrow Night Eighties",
685            palette: ThemePalette {
686                primary_accent: RgbColor(0xCC, 0xCC, 0xCC), // Light gray (#cccccc)
687                background: RgbColor(0x2D, 0x2D, 0x2D),     // Dark (#2d2d2d)
688                foreground: RgbColor(0xCC, 0xCC, 0xCC),     // Light gray (#cccccc)
689                secondary_accent: RgbColor(0x66, 0x99, 0xCC), // Blue (#6699cc)
690                alert: RgbColor(0xF2, 0x77, 0x7A),          // Red (#f2777a)
691                logo_accent: RgbColor(0xFF, 0xCC, 0x66),    // Yellow (#ffcc66)
692            },
693        },
694    );
695
696    // Ayu themes
697    map.insert(
698        "ayu",
699        ThemeDefinition {
700            id: "ayu",
701            label: "Ayu",
702            palette: ThemePalette {
703                primary_accent: RgbColor(0xC7, 0xC7, 0xC7), // Gray (#c7c7c7)
704                background: RgbColor(0x11, 0x15, 0x1C),     // Dark (#11151c)
705                foreground: RgbColor(0xC7, 0xC7, 0xC7),     // Gray (#c7c7c7)
706                secondary_accent: RgbColor(0x53, 0xBD, 0xFA), // Blue (#53bdfa)
707                alert: RgbColor(0xEA, 0x6C, 0x73),          // Red (#ea6c73)
708                logo_accent: RgbColor(0xF9, 0xAF, 0x4F),    // Orange (#f9af4f)
709            },
710        },
711    );
712    map.insert(
713        "ayu-mirage",
714        ThemeDefinition {
715            id: "ayu-mirage",
716            label: "Ayu Mirage",
717            palette: ThemePalette {
718                primary_accent: RgbColor(0xC7, 0xC7, 0xC7), // Gray (#c7c7c7)
719                background: RgbColor(0x17, 0x1B, 0x24),     // Dark blue (#171b24)
720                foreground: RgbColor(0xC7, 0xC7, 0xC7),     // Gray (#c7c7c7)
721                secondary_accent: RgbColor(0x6D, 0xCB, 0xFA), // Blue (#6dcbfa)
722                alert: RgbColor(0xED, 0x82, 0x74),          // Red (#ed8274)
723                logo_accent: RgbColor(0xFA, 0xCC, 0x6E),    // Orange (#facc6e)
724            },
725        },
726    );
727
728    // Material themes
729    map.insert(
730        "material-ocean",
731        ThemeDefinition {
732            id: "material-ocean",
733            label: "Material Ocean",
734            palette: ThemePalette {
735                primary_accent: RgbColor(0xFF, 0xFF, 0xFF), // White (#ffffff)
736                background: RgbColor(0x0F, 0x11, 0x1A),     // Dark blue (#0f111a)
737                foreground: RgbColor(0xFF, 0xFF, 0xFF),     // White (#ffffff)
738                secondary_accent: RgbColor(0x82, 0xAA, 0xFF), // Blue (#82aaff)
739                alert: RgbColor(0xFF, 0x53, 0x70),          // Red (#ff5370)
740                logo_accent: RgbColor(0xFF, 0xCB, 0x6B),    // Yellow (#ffcb6b)
741            },
742        },
743    );
744    map.insert(
745        "material-dark",
746        ThemeDefinition {
747            id: "material-dark",
748            label: "Material Dark",
749            palette: ThemePalette {
750                primary_accent: RgbColor(0xEF, 0xEF, 0xEF), // Light gray (#efefef)
751                background: RgbColor(0x21, 0x21, 0x21),     // Dark (#212121)
752                foreground: RgbColor(0xEF, 0xEF, 0xEF),     // Light gray (#efefef)
753                secondary_accent: RgbColor(0x13, 0x4E, 0xB2), // Blue (#134eb2)
754                alert: RgbColor(0xB7, 0x14, 0x1F),          // Red (#b7141f)
755                logo_accent: RgbColor(0xF6, 0x98, 0x1E),    // Yellow (#f6981e)
756            },
757        },
758    );
759    map.insert(
760        "material",
761        ThemeDefinition {
762            id: "material",
763            label: "Material",
764            palette: ThemePalette {
765                primary_accent: RgbColor(0xEF, 0xEF, 0xEF), // Light gray (#efefef)
766                background: RgbColor(0x21, 0x21, 0x21),     // Dark (#212121)
767                foreground: RgbColor(0xEF, 0xEF, 0xEF),     // Light gray (#efefef)
768                secondary_accent: RgbColor(0x14, 0x4E, 0xB2), // Blue (#144eb2)
769                alert: RgbColor(0xB7, 0x14, 0x1F),          // Red (#b7141f)
770                logo_accent: RgbColor(0xF6, 0x98, 0x1E),    // Yellow (#f6981e)
771            },
772        },
773    );
774
775    // GitHub themes
776    map.insert(
777        "github-dark",
778        ThemeDefinition {
779            id: "github-dark",
780            label: "GitHub Dark",
781            palette: ThemePalette {
782                primary_accent: RgbColor(0xFF, 0xFF, 0xFF), // White (#ffffff)
783                background: RgbColor(0x0D, 0x11, 0x17),     // Dark (#0d1117)
784                foreground: RgbColor(0xFF, 0xFF, 0xFF),     // White (#ffffff)
785                secondary_accent: RgbColor(0x6C, 0xA4, 0xF8), // Blue (#6ca4f8)
786                alert: RgbColor(0xF7, 0x81, 0x66),          // Red (#f78166)
787                logo_accent: RgbColor(0xE3, 0xB3, 0x41),    // Yellow (#e3b341)
788            },
789        },
790    );
791    map.insert(
792        "github",
793        ThemeDefinition {
794            id: "github",
795            label: "GitHub",
796            palette: ThemePalette {
797                primary_accent: RgbColor(0x3E, 0x3E, 0x3E), // Dark gray (#3e3e3e)
798                background: RgbColor(0xFF, 0xFF, 0xFF),     // White (#ffffff)
799                foreground: RgbColor(0x3E, 0x3E, 0x3E),     // Dark gray (#3e3e3e)
800                secondary_accent: RgbColor(0x00, 0x3E, 0x8A), // Blue (#003e8a)
801                alert: RgbColor(0x97, 0x0B, 0x16),          // Red (#970b16)
802                logo_accent: RgbColor(0xF8, 0xEE, 0xC7),    // Yellow (#f8eec7)
803            },
804        },
805    );
806
807    // Dracula theme
808    map.insert(
809        "dracula",
810        ThemeDefinition {
811            id: "dracula",
812            label: "Dracula",
813            palette: ThemePalette {
814                primary_accent: RgbColor(0xF8, 0xF8, 0xF2), // Light gray (#f8f8f2)
815                background: RgbColor(0x21, 0x22, 0x2C),     // Dark (#21222c)
816                foreground: RgbColor(0xF8, 0xF8, 0xF2),     // Light gray (#f8f8f2)
817                secondary_accent: RgbColor(0xBD, 0x93, 0xF9), // Purple (#bd93f9)
818                alert: RgbColor(0xFF, 0x55, 0x55),          // Red (#ff5555)
819                logo_accent: RgbColor(0xF1, 0xFA, 0x8C),    // Yellow (#f1fa8c)
820            },
821        },
822    );
823
824    // Monokai Classic theme
825    map.insert(
826        "monokai-classic",
827        ThemeDefinition {
828            id: "monokai-classic",
829            label: "Monokai Classic",
830            palette: ThemePalette {
831                primary_accent: RgbColor(0xF8, 0xF8, 0xF2), // Light gray (#f8f8f2)
832                background: RgbColor(0x27, 0x28, 0x22),     // Dark (#272822)
833                foreground: RgbColor(0xF8, 0xF8, 0xF2),     // Light gray (#f8f8f2)
834                secondary_accent: RgbColor(0x66, 0xD9, 0xEF), // Cyan (#66d9ef)
835                alert: RgbColor(0xF9, 0x26, 0x72),          // Red (#f92672)
836                logo_accent: RgbColor(0xE6, 0xDB, 0x74),    // Yellow (#e6db74)
837            },
838        },
839    );
840
841    // Night Owl theme
842    map.insert(
843        "night-owl",
844        ThemeDefinition {
845            id: "night-owl",
846            label: "Night Owl",
847            palette: ThemePalette {
848                primary_accent: RgbColor(0xFF, 0xFF, 0xFF), // White (#ffffff)
849                background: RgbColor(0x00, 0x16, 0x26),     // Dark blue (#001626)
850                foreground: RgbColor(0xFF, 0xFF, 0xFF),     // White (#ffffff)
851                secondary_accent: RgbColor(0x82, 0xAA, 0xFF), // Blue (#82aaff)
852                alert: RgbColor(0xEF, 0x53, 0x50),          // Red (#ef5350)
853                logo_accent: RgbColor(0xAD, 0xDB, 0x89),    // Yellow (#addd89)
854            },
855        },
856    );
857
858    // Spacegray themes
859    map.insert(
860        "spacegray",
861        ThemeDefinition {
862            id: "spacegray",
863            label: "Spacegray",
864            palette: ThemePalette {
865                primary_accent: RgbColor(0xB3, 0xB8, 0xC3), // Light gray (#b3b8c3)
866                background: RgbColor(0x00, 0x00, 0x00),     // Black (#000000)
867                foreground: RgbColor(0xB3, 0xB8, 0xC3),     // Light gray (#b3b8c3)
868                secondary_accent: RgbColor(0x7D, 0x8F, 0xA4), // Blue (#7d8fa4)
869                alert: RgbColor(0xB0, 0x4B, 0x57),          // Red (#b04b57)
870                logo_accent: RgbColor(0xE5, 0xC1, 0x79),    // Yellow (#e5c179)
871            },
872        },
873    );
874    map.insert(
875        "spacegray-bright",
876        ThemeDefinition {
877            id: "spacegray-bright",
878            label: "Spacegray Bright",
879            palette: ThemePalette {
880                primary_accent: RgbColor(0xD8, 0xD8, 0xD8), // Light gray (#d8d8d8)
881                background: RgbColor(0x08, 0x08, 0x08),     // Dark (#080808)
882                foreground: RgbColor(0xD8, 0xD8, 0xD8),     // Light gray (#d8d8d8)
883                secondary_accent: RgbColor(0x7B, 0xAE, 0xBC), // Blue (#7baebc)
884                alert: RgbColor(0xBD, 0x55, 0x53),          // Red (#bd5553)
885                logo_accent: RgbColor(0xF6, 0xC9, 0x73),    // Yellow (#f6c973)
886            },
887        },
888    );
889    map.insert(
890        "spacegray-eighties",
891        ThemeDefinition {
892            id: "spacegray-eighties",
893            label: "Spacegray Eighties",
894            palette: ThemePalette {
895                primary_accent: RgbColor(0xEF, 0xEC, 0xE7), // Light gray (#efec e7)
896                background: RgbColor(0x15, 0x17, 0x1D),     // Dark (#15171d)
897                foreground: RgbColor(0xEF, 0xEC, 0xE7),     // Light gray (#efec e7)
898                secondary_accent: RgbColor(0x54, 0x86, 0xC0), // Blue (#5486c0)
899                alert: RgbColor(0xEC, 0x5F, 0x67),          // Red (#ec5f67)
900                logo_accent: RgbColor(0xFE, 0xC2, 0x54),    // Yellow (#fec254)
901            },
902        },
903    );
904    map.insert(
905        "spacegray-eighties-dull",
906        ThemeDefinition {
907            id: "spacegray-eighties-dull",
908            label: "Spacegray Eighties Dull",
909            palette: ThemePalette {
910                primary_accent: RgbColor(0xB3, 0xB8, 0xBC), // Light gray (#b3b8bc)
911                background: RgbColor(0x15, 0x17, 0x1C),     // Dark (#15171c)
912                foreground: RgbColor(0xB3, 0xB8, 0xBC),     // Light gray (#b3b8bc)
913                secondary_accent: RgbColor(0x7C, 0x8F, 0x9E), // Blue (#7c8f9e)
914                alert: RgbColor(0xB2, 0x4A, 0x56),          // Red (#b24a56)
915                logo_accent: RgbColor(0xC6, 0x73, 0x44),    // Orange (#c67344)
916            },
917        },
918    );
919
920    // Atom themes
921    map.insert(
922        "atom",
923        ThemeDefinition {
924            id: "atom",
925            label: "Atom",
926            palette: ThemePalette {
927                primary_accent: RgbColor(0xE0, 0xE0, 0xE0), // Light gray (#e0e0e0)
928                background: RgbColor(0x00, 0x00, 0x00),     // Black (#000000)
929                foreground: RgbColor(0xE0, 0xE0, 0xE0),     // Light gray (#e0e0e0)
930                secondary_accent: RgbColor(0x85, 0xBE, 0xFE), // Blue (#85befe)
931                alert: RgbColor(0xFD, 0x5F, 0xF1),          // Magenta (#fd5ff1)
932                logo_accent: RgbColor(0xFF, 0xD7, 0xB1),    // Yellow (#ffd7b1)
933            },
934        },
935    );
936    map.insert(
937        "atom-one-dark",
938        ThemeDefinition {
939            id: "atom-one-dark",
940            label: "Atom One Dark",
941            palette: ThemePalette {
942                primary_accent: RgbColor(0xAB, 0xB2, 0xBF), // Light gray (#abb2bf)
943                background: RgbColor(0x21, 0x25, 0x2B),     // Dark (#21252b)
944                foreground: RgbColor(0xAB, 0xB2, 0xBF),     // Light gray (#abb2bf)
945                secondary_accent: RgbColor(0x61, 0xAF, 0xEF), // Blue (#61afef)
946                alert: RgbColor(0xE0, 0x6C, 0x75),          // Red (#e06c75)
947                logo_accent: RgbColor(0xE5, 0xC0, 0x7B),    // Yellow (#e5c07b)
948            },
949        },
950    );
951    map.insert(
952        "atom-one-light",
953        ThemeDefinition {
954            id: "atom-one-light",
955            label: "Atom One Light",
956            palette: ThemePalette {
957                primary_accent: RgbColor(0x3E, 0x3E, 0x3E), // Dark gray (#3e3e3e)
958                background: RgbColor(0xFF, 0xFF, 0xFF),     // White (#ffffff)
959                foreground: RgbColor(0x3E, 0x3E, 0x3E),     // Dark gray (#3e3e3e)
960                secondary_accent: RgbColor(0x2F, 0x5A, 0xF3), // Blue (#2f5af3)
961                alert: RgbColor(0xDE, 0x3E, 0x35),          // Red (#de3e35)
962                logo_accent: RgbColor(0xD2, 0xB6, 0x7C),    // Yellow (#d2b67c)
963            },
964        },
965    );
966
967    // Other popular themes
968    map.insert(
969        "man-page",
970        ThemeDefinition {
971            id: "man-page",
972            label: "Man Page",
973            palette: ThemePalette {
974                primary_accent: RgbColor(0xCC, 0xCC, 0xCC), // Light gray (#cccccc)
975                background: RgbColor(0xFF, 0xFF, 0xFF),     // White (#ffffff)
976                foreground: RgbColor(0xCC, 0xCC, 0xCC),     // Light gray (#cccccc)
977                secondary_accent: RgbColor(0x00, 0x00, 0xB2), // Blue (#0000b2)
978                alert: RgbColor(0xCC, 0x00, 0x00),          // Red (#cc0000)
979                logo_accent: RgbColor(0x99, 0x99, 0x00),    // Yellow (#999900)
980            },
981        },
982    );
983    map.insert(
984        "jetbrains-darcula",
985        ThemeDefinition {
986            id: "jetbrains-darcula",
987            label: "JetBrains Darcula",
988            palette: ThemePalette {
989                primary_accent: RgbColor(0xAD, 0xAD, 0xAD), // Gray (#adadad)
990                background: RgbColor(0x1E, 0x1E, 0x1E),     // Dark (#1e1e1e)
991                foreground: RgbColor(0xAD, 0xAD, 0xAD),     // Gray (#adadad)
992                secondary_accent: RgbColor(0x45, 0x82, 0xEB), // Blue (#4582eb)
993                alert: RgbColor(0xFB, 0x54, 0x54),          // Red (#fb5454)
994                logo_accent: RgbColor(0xC2, 0xC2, 0x00),    // Yellow (#c2c200)
995            },
996        },
997    );
998    map.insert(
999        "homebrew",
1000        ThemeDefinition {
1001            id: "homebrew",
1002            label: "Homebrew",
1003            palette: ThemePalette {
1004                primary_accent: RgbColor(0xBF, 0xBF, 0xBF), // Light gray (#bfbfbf)
1005                background: RgbColor(0x00, 0x00, 0x00),     // Black (#000000)
1006                foreground: RgbColor(0xBF, 0xBF, 0xBF),     // Light gray (#bfbfbf)
1007                secondary_accent: RgbColor(0x00, 0x00, 0xB2), // Blue (#0000b2)
1008                alert: RgbColor(0x99, 0x00, 0x00),          // Red (#990000)
1009                logo_accent: RgbColor(0x99, 0x99, 0x00),    // Yellow (#999900)
1010            },
1011        },
1012    );
1013    map.insert(
1014        "framer",
1015        ThemeDefinition {
1016            id: "framer",
1017            label: "Framer",
1018            palette: ThemePalette {
1019                primary_accent: RgbColor(0xCC, 0xCC, 0xCC), // Light gray (#cccccc)
1020                background: RgbColor(0x14, 0x14, 0x14),     // Dark (#141414)
1021                foreground: RgbColor(0xCC, 0xCC, 0xCC),     // Light gray (#cccccc)
1022                secondary_accent: RgbColor(0x00, 0xAA, 0xFF), // Blue (#00aaff)
1023                alert: RgbColor(0xFF, 0x55, 0x55),          // Red (#ff5555)
1024                logo_accent: RgbColor(0xFF, 0xCC, 0x33),    // Yellow (#ffcc33)
1025            },
1026        },
1027    );
1028    map.insert(
1029        "espresso",
1030        ThemeDefinition {
1031            id: "espresso",
1032            label: "Espresso",
1033            palette: ThemePalette {
1034                primary_accent: RgbColor(0xEE, 0xEE, 0xEF), // Light gray (#eeeeef)
1035                background: RgbColor(0x35, 0x35, 0x35),     // Dark (#353535)
1036                foreground: RgbColor(0xEE, 0xEE, 0xEF),     // Light gray (#eeeeef)
1037                secondary_accent: RgbColor(0x6C, 0x99, 0xBB), // Blue (#6c99bb)
1038                alert: RgbColor(0xD2, 0x52, 0x52),          // Red (#d25252)
1039                logo_accent: RgbColor(0xFF, 0xC6, 0x6D),    // Yellow (#ffc66d)
1040            },
1041        },
1042    );
1043    map.insert(
1044        "adventure-time",
1045        ThemeDefinition {
1046            id: "adventure-time",
1047            label: "Adventure Time",
1048            palette: ThemePalette {
1049                primary_accent: RgbColor(0xF8, 0xDC, 0xC0), // Light tan (#f8dcc0)
1050                background: RgbColor(0x05, 0x04, 0x04),     // Dark (#050404)
1051                foreground: RgbColor(0xF8, 0xDC, 0xC0),     // Light tan (#f8dcc0)
1052                secondary_accent: RgbColor(0x0E, 0x49, 0xC6), // Blue (#0e49c6)
1053                alert: RgbColor(0xBD, 0x00, 0x13),          // Red (#bd0013)
1054                logo_accent: RgbColor(0xE8, 0x74, 0x1D),    // Orange (#e8741d)
1055            },
1056        },
1057    );
1058    map.insert(
1059        "afterglow",
1060        ThemeDefinition {
1061            id: "afterglow",
1062            label: "Afterglow",
1063            palette: ThemePalette {
1064                primary_accent: RgbColor(0xD0, 0xD0, 0xD0), // Light gray (#d0d0d0)
1065                background: RgbColor(0x15, 0x15, 0x15),     // Dark (#151515)
1066                foreground: RgbColor(0xD0, 0xD0, 0xD0),     // Light gray (#d0d0d0)
1067                secondary_accent: RgbColor(0x6C, 0x99, 0xBB), // Blue (#6c99bb)
1068                alert: RgbColor(0xAC, 0x41, 0x42),          // Red (#ac4142)
1069                logo_accent: RgbColor(0xE5, 0xB5, 0x67),    // Yellow (#e5b567)
1070            },
1071        },
1072    );
1073    map.insert(
1074        "apple-classic",
1075        ThemeDefinition {
1076            id: "apple-classic",
1077            label: "Apple Classic",
1078            palette: ThemePalette {
1079                primary_accent: RgbColor(0xC7, 0xC7, 0xC7), // Light gray (#c7c7c7)
1080                background: RgbColor(0x00, 0x00, 0x00),     // Black (#000000)
1081                foreground: RgbColor(0xC7, 0xC7, 0xC7),     // Light gray (#c7c7c7)
1082                secondary_accent: RgbColor(0x01, 0x25, 0xC8), // Blue (#0125c8)
1083                alert: RgbColor(0xCA, 0x1B, 0x11),          // Red (#ca1b11)
1084                logo_accent: RgbColor(0xC7, 0xC5, 0x00),    // Yellow (#c7c500)
1085            },
1086        },
1087    );
1088    map.insert(
1089        "apple-system-colors",
1090        ThemeDefinition {
1091            id: "apple-system-colors",
1092            label: "Apple System Colors",
1093            palette: ThemePalette {
1094                primary_accent: RgbColor(0x98, 0x98, 0x9D), // Gray (#98989d)
1095                background: RgbColor(0x1A, 0x1A, 0x1A),     // Dark (#1a1a1a)
1096                foreground: RgbColor(0x98, 0x98, 0x9D),     // Gray (#98989d)
1097                secondary_accent: RgbColor(0x08, 0x69, 0xC9), // Blue (#0869c9)
1098                alert: RgbColor(0xCC, 0x37, 0x2E),          // Red (#cc372e)
1099                logo_accent: RgbColor(0xCD, 0xAB, 0x1E),    // Yellow (#cdab1e)
1100            },
1101        },
1102    );
1103    map.insert(
1104        "apple-system-colors-light",
1105        ThemeDefinition {
1106            id: "apple-system-colors-light",
1107            label: "Apple System Colors Light",
1108            palette: ThemePalette {
1109                primary_accent: RgbColor(0x1A, 0x1A, 0x1A), // Dark gray (#1a1a1a)
1110                background: RgbColor(0xFF, 0xFF, 0xFF),     // White (#ffffff)
1111                foreground: RgbColor(0x1A, 0x1A, 0x1A),     // Dark gray (#1a1a1a)
1112                secondary_accent: RgbColor(0x2E, 0x68, 0xC5), // Blue (#2e68c5)
1113                alert: RgbColor(0xBC, 0x44, 0x37),          // Red (#bc4437)
1114                logo_accent: RgbColor(0xC8, 0xAD, 0x3A),    // Yellow (#c8ad3a)
1115            },
1116        },
1117    );
1118
1119    // Vitesse themes
1120    map.insert(
1121        "vitesse-black",
1122        ThemeDefinition {
1123            id: "vitesse-black",
1124            label: "Vitesse Black",
1125            palette: ThemePalette {
1126                primary_accent: RgbColor(0xDB, 0xD7, 0xCA), // Light gray foreground
1127                background: RgbColor(0x00, 0x00, 0x00),     // Black
1128                foreground: RgbColor(0xDB, 0xD7, 0xCA),     // Light gray
1129                secondary_accent: RgbColor(0x4D, 0x93, 0x75), // Green (selection color)
1130                alert: RgbColor(0xCB, 0x76, 0x76),          // Red for errors
1131                logo_accent: RgbColor(0xDB, 0xD7, 0xCA),    // Light gray for logo accent
1132            },
1133        },
1134    );
1135    map.insert(
1136        "vitesse-dark",
1137        ThemeDefinition {
1138            id: "vitesse-dark",
1139            label: "Vitesse Dark",
1140            palette: ThemePalette {
1141                primary_accent: RgbColor(0xDB, 0xD7, 0xCA), // Light gray foreground
1142                background: RgbColor(0x12, 0x12, 0x12),     // Very dark gray
1143                foreground: RgbColor(0xDB, 0xD7, 0xCA),     // Light gray
1144                secondary_accent: RgbColor(0x4D, 0x93, 0x75), // Green (selection color)
1145                alert: RgbColor(0xCB, 0x76, 0x76),          // Red for errors
1146                logo_accent: RgbColor(0xDB, 0xD7, 0xCA),    // Light gray for logo accent
1147            },
1148        },
1149    );
1150    map.insert(
1151        "vitesse-dark-soft",
1152        ThemeDefinition {
1153            id: "vitesse-dark-soft",
1154            label: "Vitesse Dark Soft",
1155            palette: ThemePalette {
1156                primary_accent: RgbColor(0xDB, 0xD7, 0xCA), // Light gray foreground
1157                background: RgbColor(0x22, 0x22, 0x22),     // Very dark gray (soft)
1158                foreground: RgbColor(0xDB, 0xD7, 0xCA),     // Light gray
1159                secondary_accent: RgbColor(0x4D, 0x93, 0x75), // Green (selection color)
1160                alert: RgbColor(0xCB, 0x76, 0x76),          // Red for errors
1161                logo_accent: RgbColor(0xDB, 0xD7, 0xCA),    // Light gray for logo accent
1162            },
1163        },
1164    );
1165    map.insert(
1166        "vitesse-light",
1167        ThemeDefinition {
1168            id: "vitesse-light",
1169            label: "Vitesse Light",
1170            palette: ThemePalette {
1171                primary_accent: RgbColor(0x39, 0x3A, 0x34), // Dark gray foreground
1172                background: RgbColor(0xFF, 0xFF, 0xFF),     // White
1173                foreground: RgbColor(0x39, 0x3A, 0x34),     // Dark gray
1174                secondary_accent: RgbColor(0x1C, 0x6B, 0x48), // Green (selection color)
1175                alert: RgbColor(0xAB, 0x59, 0x59),          // Red for errors
1176                logo_accent: RgbColor(0x39, 0x3A, 0x34),    // Dark gray for logo accent
1177            },
1178        },
1179    );
1180    map.insert(
1181        "vitesse-light-soft",
1182        ThemeDefinition {
1183            id: "vitesse-light-soft",
1184            label: "Vitesse Light Soft",
1185            palette: ThemePalette {
1186                primary_accent: RgbColor(0x39, 0x3A, 0x34), // Dark gray foreground
1187                background: RgbColor(0xF1, 0xF0, 0xE9),     // Soft cream
1188                foreground: RgbColor(0x39, 0x3A, 0x34),     // Dark gray
1189                secondary_accent: RgbColor(0x1C, 0x6B, 0x48), // Green (selection color)
1190                alert: RgbColor(0xAB, 0x59, 0x59),          // Red for errors
1191                logo_accent: RgbColor(0x39, 0x3A, 0x34),    // Dark gray for logo accent
1192            },
1193        },
1194    );
1195
1196    map.insert(
1197        "mono",
1198        ThemeDefinition {
1199            id: "mono",
1200            label: "Mono",
1201            palette: ThemePalette {
1202                primary_accent: RgbColor(0xFF, 0xFF, 0xFF),   // Pure white
1203                background: RgbColor(0x00, 0x00, 0x00),       // Black
1204                foreground: RgbColor(0xDB, 0xD7, 0xCA), // Soft light gray (borrowed from vitesse)
1205                secondary_accent: RgbColor(0xBB, 0xBB, 0xBB), // Medium gray
1206                alert: RgbColor(0xFF, 0xFF, 0xFF),      // High contrast white for alerts
1207                logo_accent: RgbColor(0xFF, 0xFF, 0xFF), // White for logo
1208            },
1209        },
1210    );
1211
1212    register_catppuccin_themes(&mut map);
1213    map
1214});
1215
1216fn register_catppuccin_themes(map: &mut HashMap<&'static str, ThemeDefinition>) {
1217    for &flavor_kind in CATPPUCCIN_FLAVORS {
1218        let flavor = flavor_kind.flavor();
1219        let theme_definition = ThemeDefinition {
1220            id: flavor_kind.id(),
1221            label: flavor_kind.label(),
1222            palette: catppuccin_palette(flavor),
1223        };
1224        map.insert(flavor_kind.id(), theme_definition);
1225    }
1226}
1227
1228fn catppuccin_palette(flavor: catppuccin::Flavor) -> ThemePalette {
1229    let colors = flavor.colors;
1230    ThemePalette {
1231        primary_accent: catppuccin_rgb(colors.lavender),
1232        background: catppuccin_rgb(colors.base),
1233        foreground: catppuccin_rgb(colors.text),
1234        secondary_accent: catppuccin_rgb(colors.sapphire),
1235        alert: catppuccin_rgb(colors.red),
1236        logo_accent: catppuccin_rgb(colors.peach),
1237    }
1238}
1239
1240fn catppuccin_rgb(color: catppuccin::Color) -> RgbColor {
1241    RgbColor(color.rgb.r, color.rgb.g, color.rgb.b)
1242}
1243
1244static ACTIVE: Lazy<RwLock<ActiveTheme>> = Lazy::new(|| {
1245    let default = REGISTRY
1246        .get(DEFAULT_THEME_ID)
1247        .expect("default theme must exist");
1248    let styles = default.palette.build_styles();
1249    RwLock::new(ActiveTheme {
1250        id: default.id.to_string(),
1251        label: default.label.to_string(),
1252        palette: default.palette.clone(),
1253        styles,
1254    })
1255});
1256
1257/// Set the active theme by identifier.
1258pub fn set_active_theme(theme_id: &str) -> Result<()> {
1259    let id_lc = theme_id.trim().to_lowercase();
1260    let theme = REGISTRY
1261        .get(id_lc.as_str())
1262        .ok_or_else(|| anyhow!("Unknown theme '{theme_id}'"))?;
1263
1264    let styles = theme.palette.build_styles();
1265    let mut guard = ACTIVE.write();
1266    guard.id = theme.id.to_string();
1267    guard.label = theme.label.to_string();
1268    guard.palette = theme.palette.clone();
1269    guard.styles = styles;
1270    Ok(())
1271}
1272
1273/// Get the identifier of the active theme.
1274pub fn active_theme_id() -> String {
1275    ACTIVE.read().id.clone()
1276}
1277
1278/// Get the human-readable label of the active theme.
1279pub fn active_theme_label() -> String {
1280    ACTIVE.read().label.clone()
1281}
1282
1283/// Get the current styles cloned from the active theme.
1284pub fn active_styles() -> ThemeStyles {
1285    ACTIVE.read().styles.clone()
1286}
1287
1288/// Slightly adjusted accent color for banner-like copy.
1289pub fn banner_color() -> RgbColor {
1290    let guard = ACTIVE.read();
1291    let accent = guard.palette.logo_accent;
1292    let secondary = guard.palette.secondary_accent;
1293    let background = guard.palette.background;
1294    drop(guard);
1295
1296    let min_contrast = get_minimum_contrast();
1297    let candidate = lighten(accent, ui::THEME_LOGO_ACCENT_BANNER_LIGHTEN_RATIO);
1298    ensure_contrast(
1299        candidate,
1300        background,
1301        min_contrast,
1302        &[
1303            lighten(accent, ui::THEME_PRIMARY_STATUS_SECONDARY_LIGHTEN_RATIO),
1304            lighten(
1305                secondary,
1306                ui::THEME_LOGO_ACCENT_BANNER_SECONDARY_LIGHTEN_RATIO,
1307            ),
1308            accent,
1309        ],
1310    )
1311}
1312
1313/// Slightly darkened accent style for banner-like copy.
1314pub fn banner_style() -> Style {
1315    let accent = banner_color();
1316    Style::new().fg_color(Some(Color::Rgb(accent))).bold()
1317}
1318
1319/// Accent color for the startup banner logo.
1320pub fn logo_accent_color() -> RgbColor {
1321    ACTIVE.read().palette.logo_accent
1322}
1323
1324/// Enumerate available theme identifiers.
1325pub fn available_themes() -> Vec<&'static str> {
1326    let mut keys: Vec<_> = REGISTRY.keys().copied().collect();
1327    keys.sort();
1328    keys
1329}
1330
1331/// Look up a theme label for display.
1332pub fn theme_label(theme_id: &str) -> Option<&'static str> {
1333    REGISTRY.get(theme_id).map(|definition| definition.label)
1334}
1335
1336fn suite_id_for_theme(theme_id: &str) -> Option<&'static str> {
1337    if theme_id.starts_with("catppuccin-") {
1338        Some("catppuccin")
1339    } else if theme_id.starts_with("vitesse-") {
1340        Some("vitesse")
1341    } else if theme_id.starts_with("ciapre-") {
1342        Some("ciapre")
1343    } else if theme_id == "mono" {
1344        Some("mono")
1345    } else {
1346        None
1347    }
1348}
1349
1350fn suite_label(suite_id: &str) -> Option<&'static str> {
1351    match suite_id {
1352        "catppuccin" => Some("Catppuccin"),
1353        "vitesse" => Some("Vitesse"),
1354        "ciapre" => Some("Ciapre"),
1355        "mono" => Some("Mono"),
1356        _ => None,
1357    }
1358}
1359
1360/// Resolve the suite identifier for a theme id.
1361pub fn theme_suite_id(theme_id: &str) -> Option<&'static str> {
1362    suite_id_for_theme(theme_id)
1363}
1364
1365/// Resolve the suite label for a theme id.
1366pub fn theme_suite_label(theme_id: &str) -> Option<&'static str> {
1367    suite_id_for_theme(theme_id).and_then(suite_label)
1368}
1369
1370/// Enumerate built-in theme suites and their member theme ids.
1371pub fn available_theme_suites() -> Vec<ThemeSuite> {
1372    const ORDER: &[&str] = &["ciapre", "vitesse", "catppuccin", "mono"];
1373
1374    ORDER
1375        .iter()
1376        .filter_map(|suite_id| {
1377            let mut theme_ids: Vec<&'static str> = available_themes()
1378                .into_iter()
1379                .filter(|theme_id| suite_id_for_theme(theme_id) == Some(*suite_id))
1380                .collect();
1381            if theme_ids.is_empty() {
1382                return None;
1383            }
1384            theme_ids.sort_unstable();
1385            Some(ThemeSuite {
1386                id: suite_id,
1387                label: suite_label(suite_id).expect("known suite id must have label"),
1388                theme_ids,
1389            })
1390        })
1391        .collect()
1392}
1393
1394fn relative_luminance(color: RgbColor) -> f64 {
1395    fn channel(value: u8) -> f64 {
1396        let c = (value as f64) / 255.0;
1397        if c <= ui::THEME_RELATIVE_LUMINANCE_CUTOFF {
1398            c / ui::THEME_RELATIVE_LUMINANCE_LOW_FACTOR
1399        } else {
1400            ((c + ui::THEME_RELATIVE_LUMINANCE_OFFSET)
1401                / (1.0 + ui::THEME_RELATIVE_LUMINANCE_OFFSET))
1402                .powf(ui::THEME_RELATIVE_LUMINANCE_EXPONENT)
1403        }
1404    }
1405    let r = channel(color.0);
1406    let g = channel(color.1);
1407    let b = channel(color.2);
1408    ui::THEME_RED_LUMINANCE_COEFFICIENT * r
1409        + ui::THEME_GREEN_LUMINANCE_COEFFICIENT * g
1410        + ui::THEME_BLUE_LUMINANCE_COEFFICIENT * b
1411}
1412
1413fn contrast_ratio(foreground: RgbColor, background: RgbColor) -> f64 {
1414    let fg = relative_luminance(foreground);
1415    let bg = relative_luminance(background);
1416    let (lighter, darker) = if fg > bg { (fg, bg) } else { (bg, fg) };
1417    (lighter + ui::THEME_CONTRAST_RATIO_OFFSET) / (darker + ui::THEME_CONTRAST_RATIO_OFFSET)
1418}
1419
1420fn darken(color: RgbColor, ratio: f64) -> RgbColor {
1421    mix(color, RgbColor(0, 0, 0), ratio)
1422}
1423
1424fn adjust_luminance_to_target(color: RgbColor, target: f64) -> RgbColor {
1425    let current = relative_luminance(color);
1426    if (current - target).abs() < 1e-3 {
1427        return color;
1428    }
1429
1430    if current < target {
1431        // Raise luminance by blending toward white.
1432        let denom = (1.0 - current).max(1e-6);
1433        let ratio = ((target - current) / denom).clamp(0.0, 1.0);
1434        lighten(color, ratio)
1435    } else {
1436        // Lower luminance by blending toward black.
1437        let denom = current.max(1e-6);
1438        let ratio = ((current - target) / denom).clamp(0.0, 1.0);
1439        darken(color, ratio)
1440    }
1441}
1442
1443fn balance_text_luminance(color: RgbColor, background: RgbColor, min_contrast: f64) -> RgbColor {
1444    let bg_luminance = relative_luminance(background);
1445    let mut candidate = color;
1446    let current = relative_luminance(candidate);
1447    if bg_luminance < 0.5 {
1448        if current < MIN_DARK_BG_TEXT_LUMINANCE {
1449            candidate = adjust_luminance_to_target(candidate, MIN_DARK_BG_TEXT_LUMINANCE);
1450        } else if current > MAX_DARK_BG_TEXT_LUMINANCE {
1451            candidate = adjust_luminance_to_target(candidate, MAX_DARK_BG_TEXT_LUMINANCE);
1452        }
1453    } else if current > MAX_LIGHT_BG_TEXT_LUMINANCE {
1454        candidate = adjust_luminance_to_target(candidate, MAX_LIGHT_BG_TEXT_LUMINANCE);
1455    }
1456
1457    ensure_contrast(candidate, background, min_contrast, &[color])
1458}
1459
1460fn ensure_contrast(
1461    candidate: RgbColor,
1462    background: RgbColor,
1463    min_ratio: f64,
1464    fallbacks: &[RgbColor],
1465) -> RgbColor {
1466    if contrast_ratio(candidate, background) >= min_ratio {
1467        return candidate;
1468    }
1469    for &fallback in fallbacks {
1470        if contrast_ratio(fallback, background) >= min_ratio {
1471            return fallback;
1472        }
1473    }
1474
1475    // Final accessibility fallback: choose the higher-contrast endpoint.
1476    let black = RgbColor(0, 0, 0);
1477    let white = RgbColor(255, 255, 255);
1478    if contrast_ratio(black, background) >= contrast_ratio(white, background) {
1479        black
1480    } else {
1481        white
1482    }
1483}
1484
1485pub(crate) fn mix(color: RgbColor, target: RgbColor, ratio: f64) -> RgbColor {
1486    let ratio = ratio.clamp(ui::THEME_MIX_RATIO_MIN, ui::THEME_MIX_RATIO_MAX);
1487    let blend = |c: u8, t: u8| -> u8 {
1488        let c = c as f64;
1489        let t = t as f64;
1490        ((c + (t - c) * ratio).round()).clamp(ui::THEME_BLEND_CLAMP_MIN, ui::THEME_BLEND_CLAMP_MAX)
1491            as u8
1492    };
1493    RgbColor(
1494        blend(color.0, target.0),
1495        blend(color.1, target.1),
1496        blend(color.2, target.2),
1497    )
1498}
1499
1500fn lighten(color: RgbColor, ratio: f64) -> RgbColor {
1501    mix(
1502        color,
1503        RgbColor(
1504            ui::THEME_COLOR_WHITE_RED,
1505            ui::THEME_COLOR_WHITE_GREEN,
1506            ui::THEME_COLOR_WHITE_BLUE,
1507        ),
1508        ratio,
1509    )
1510}
1511
1512/// Resolve a theme identifier from configuration or CLI input.
1513pub fn resolve_theme(preferred: Option<String>) -> String {
1514    preferred
1515        .and_then(|candidate| {
1516            let trimmed = candidate.trim().to_lowercase();
1517            if trimmed.is_empty() {
1518                None
1519            } else if REGISTRY.contains_key(trimmed.as_str()) {
1520                Some(trimmed)
1521            } else {
1522                None
1523            }
1524        })
1525        .unwrap_or_else(|| DEFAULT_THEME_ID.to_string())
1526}
1527
1528/// Validate a theme and return its label for messaging.
1529pub fn ensure_theme(theme_id: &str) -> Result<&'static str> {
1530    REGISTRY
1531        .get(theme_id)
1532        .map(|definition| definition.label)
1533        .context("Theme not found")
1534}
1535
1536/// Rebuild the active theme's styles with current accessibility settings.
1537/// Call this after updating color accessibility configuration.
1538pub fn rebuild_active_styles() {
1539    let mut guard = ACTIVE.write();
1540    guard.styles = guard.palette.build_styles();
1541}
1542
1543/// Theme validation result
1544#[derive(Debug, Clone)]
1545pub struct ThemeValidationResult {
1546    /// Whether the theme passed validation
1547    pub is_valid: bool,
1548    /// List of warnings (non-fatal issues)
1549    pub warnings: Vec<String>,
1550    /// List of errors (fatal issues)
1551    pub errors: Vec<String>,
1552}
1553
1554/// Validate a theme's color contrast ratios.
1555/// Returns warnings for colors that don't meet WCAG AA standards.
1556pub fn validate_theme_contrast(theme_id: &str) -> ThemeValidationResult {
1557    let mut result = ThemeValidationResult {
1558        is_valid: true,
1559        warnings: Vec::new(),
1560        errors: Vec::new(),
1561    };
1562
1563    let theme = match REGISTRY.get(theme_id) {
1564        Some(t) => t,
1565        None => {
1566            result.is_valid = false;
1567            result.errors.push(format!("Unknown theme: {}", theme_id));
1568            return result;
1569        }
1570    };
1571
1572    let palette = &theme.palette;
1573    let bg = palette.background;
1574    let min_contrast = get_minimum_contrast();
1575
1576    // Check main text colors
1577    let checks = [
1578        ("foreground", palette.foreground),
1579        ("primary_accent", palette.primary_accent),
1580        ("secondary_accent", palette.secondary_accent),
1581        ("alert", palette.alert),
1582        ("logo_accent", palette.logo_accent),
1583    ];
1584
1585    for (name, color) in checks {
1586        let ratio = contrast_ratio(color, bg);
1587        if ratio < min_contrast {
1588            result.warnings.push(format!(
1589                "{} ({:02X}{:02X}{:02X}) has contrast ratio {:.2} < {:.1} against background",
1590                name, color.0, color.1, color.2, ratio, min_contrast
1591            ));
1592        }
1593    }
1594
1595    result
1596}
1597
1598/// Check if a theme is suitable for the detected terminal color scheme.
1599/// Returns true if the theme matches (light theme for light terminal, dark for dark).
1600pub fn theme_matches_terminal_scheme(theme_id: &str) -> bool {
1601    use crate::utils::ansi_capabilities::ColorScheme;
1602    use crate::utils::ansi_capabilities::detect_color_scheme;
1603
1604    let scheme = detect_color_scheme();
1605    let theme_is_light = is_light_theme(theme_id);
1606
1607    match scheme {
1608        ColorScheme::Light => theme_is_light,
1609        ColorScheme::Dark | ColorScheme::Unknown => !theme_is_light,
1610    }
1611}
1612
1613/// Determine if a theme is a light theme based on its background luminance.
1614pub fn is_light_theme(theme_id: &str) -> bool {
1615    REGISTRY
1616        .get(theme_id)
1617        .map(|theme| {
1618            let bg = theme.palette.background;
1619            let luminance = relative_luminance(bg);
1620            // If background luminance > 0.5, it's a light theme
1621            luminance > 0.5
1622        })
1623        .unwrap_or(false)
1624}
1625
1626/// Get a suggested theme based on terminal color scheme detection.
1627/// Returns a light or dark theme depending on detected terminal background.
1628pub fn suggest_theme_for_terminal() -> &'static str {
1629    use crate::utils::ansi_capabilities::ColorScheme;
1630    use crate::utils::ansi_capabilities::detect_color_scheme;
1631
1632    match detect_color_scheme() {
1633        ColorScheme::Light => "vitesse-light",
1634        ColorScheme::Dark | ColorScheme::Unknown => DEFAULT_THEME_ID,
1635    }
1636}
1637
1638/// Get the recommended syntax highlighting theme for a given UI theme.
1639/// This ensures that code highlighting colors complement the UI theme's background.
1640/// Based on: https://github.com/openai/codex/pull/11447, https://github.com/openai/codex/pull/12581
1641///
1642/// # Usage
1643///
1644/// For code blocks and syntax highlighting:
1645/// ```rust
1646/// use vtcode_tui::ui::theme::{get_syntax_theme_for_ui_theme, active_theme_id};
1647/// let ui_theme = active_theme_id();
1648/// let syntax_theme = get_syntax_theme_for_ui_theme(&ui_theme);
1649/// // Use `syntax_theme` with syntect's ThemeSet
1650/// ```
1651///
1652/// For PTY/shell output highlighting, the same mapping applies.
1653/// The shell command highlighter should use the same color palette
1654/// as the syntax highlighting theme for visual consistency.
1655pub fn get_syntax_theme_for_ui_theme(ui_theme: &str) -> &'static str {
1656    match ui_theme.to_lowercase().as_str() {
1657        // Ayu themes - use matching syntect themes
1658        "ayu" => "ayu-dark",
1659        "ayu-mirage" => "ayu-mirage",
1660
1661        // Catppuccin themes - use matching syntect themes
1662        "catppuccin-latte" => "catppuccin-latte",
1663        "catppuccin-frappe" => "catppuccin-frappe",
1664        "catppuccin-macchiato" => "catppuccin-macchiato",
1665        "catppuccin-mocha" => "catppuccin-mocha",
1666
1667        // Solarized themes - exact TextMate theme names
1668        "solarized-dark" | "solarized-dark-hc" => "Solarized (dark)",
1669        "solarized-light" => "Solarized (light)",
1670
1671        // Gruvbox themes
1672        "gruvbox-dark" | "gruvbox-dark-hard" => "gruvbox-dark",
1673        "gruvbox-light" | "gruvbox-light-hard" => "gruvbox-light",
1674        "gruvbox-material" | "gruvbox-material-dark" => "gruvbox-dark",
1675        "gruvbox-material-light" => "gruvbox-light",
1676
1677        // Tomorrow themes - exact TextMate theme names
1678        "tomorrow" => "Tomorrow",
1679        "tomorrow-night" => "Tomorrow Night",
1680        "tomorrow-night-blue" => "Tomorrow Night Blue",
1681        "tomorrow-night-bright" => "Tomorrow Night Bright",
1682        "tomorrow-night-eighties" => "Tomorrow Night Eighties",
1683        "tomorrow-night-burns" => "Tomorrow Night",
1684
1685        // GitHub themes - exact TextMate theme names
1686        "github-dark" => "GitHub Dark",
1687        "github" => "GitHub",
1688
1689        // Atom themes - exact TextMate theme names
1690        "atom-one-dark" => "OneDark",
1691        "atom-one-light" => "OneLight",
1692        "atom" => "base16-ocean.dark",
1693
1694        // Spacegray themes - use base16-ocean.dark as closest match
1695        "spacegray" | "spacegray-bright" | "spacegray-eighties" | "spacegray-eighties-dull" => {
1696            "base16-ocean.dark"
1697        }
1698
1699        // Material themes - exact TextMate theme names
1700        "material-ocean" | "material-dark" | "material" => "Material Dark",
1701
1702        // Other popular dark themes - exact TextMate theme names where available
1703        "dracula" => "Dracula",
1704        "monokai-classic" => "monokai-classic",
1705        "night-owl" => "Night Owl",
1706        "zenburn" => "Zenburn",
1707
1708        // Fallback themes - use base16-ocean as a good general-purpose dark theme
1709        "jetbrains-darcula" => "base16-ocean.dark",
1710        "man-page" => "base16-ocean.dark",
1711        "homebrew" => "base16-ocean.dark",
1712        "framer" => "base16-ocean.dark",
1713        "espresso" => "base16-ocean.dark",
1714        "adventure-time" => "base16-ocean.dark",
1715        "afterglow" => "base16-ocean.dark",
1716        "apple-classic" => "base16-ocean.dark",
1717        "apple-system-colors" => "base16-ocean.dark",
1718
1719        // Light themes - use base16-ocean.light as fallback
1720        "apple-system-colors-light" => "base16-ocean.light",
1721        "vitesse-light" | "vitesse-light-soft" => "base16-ocean.light",
1722
1723        // Default dark themes
1724        "ciapre" | "ciapre-dark" | "ciapre-blue" => "base16-ocean.dark",
1725        "vitesse-black" | "vitesse-dark" | "vitesse-dark-soft" => "base16-ocean.dark",
1726        "mono" => "base16-ocean.dark",
1727        "ansi-classic" => "base16-ocean.dark",
1728
1729        // Fallback to dark theme for unknown themes
1730        _ => "base16-ocean.dark",
1731    }
1732}
1733
1734/// Get the recommended syntax highlighting theme for the currently active UI theme.
1735/// Convenience wrapper around `get_syntax_theme_for_ui_theme`.
1736pub fn get_active_syntax_theme() -> &'static str {
1737    get_syntax_theme_for_ui_theme(&active_theme_id())
1738}
1739
1740#[cfg(test)]
1741mod tests {
1742    use super::*;
1743
1744    #[test]
1745    fn test_mono_theme_exists() {
1746        let result = ensure_theme("mono");
1747        assert!(result.is_ok(), "Mono theme should be registered");
1748        assert_eq!(result.unwrap(), "Mono");
1749    }
1750
1751    #[test]
1752    fn test_mono_theme_contrast() {
1753        let result = validate_theme_contrast("mono");
1754        // We expect it to be valid, but we check if there are any major contrast issues
1755        assert!(result.errors.is_empty(), "Mono theme should have no errors");
1756        // Mono themes might have some warnings if grays are close, but pure black/white should be fine.
1757        assert!(result.is_valid);
1758    }
1759
1760    #[test]
1761    fn test_all_themes_resolvable() {
1762        for id in available_themes() {
1763            assert!(
1764                ensure_theme(id).is_ok(),
1765                "Theme {} should be resolvable",
1766                id
1767            );
1768        }
1769    }
1770
1771    #[test]
1772    fn test_available_theme_suites_contains_expected_groups() {
1773        let suites = available_theme_suites();
1774        let suite_ids: Vec<&str> = suites.iter().map(|suite| suite.id).collect();
1775        assert!(suite_ids.contains(&"ciapre"));
1776        assert!(suite_ids.contains(&"vitesse"));
1777        assert!(suite_ids.contains(&"catppuccin"));
1778        assert!(suite_ids.contains(&"mono"));
1779    }
1780
1781    #[test]
1782    fn test_theme_suite_resolution() {
1783        assert_eq!(theme_suite_id("catppuccin-mocha"), Some("catppuccin"));
1784        assert_eq!(theme_suite_id("vitesse-light"), Some("vitesse"));
1785        assert_eq!(theme_suite_id("ciapre-dark"), Some("ciapre"));
1786        assert_eq!(theme_suite_id("mono"), Some("mono"));
1787        assert_eq!(theme_suite_id("unknown-theme"), None);
1788    }
1789
1790    #[test]
1791    fn test_all_themes_have_readable_foreground_and_accents() {
1792        let min_contrast = get_minimum_contrast();
1793        for definition in REGISTRY.values() {
1794            let styles = definition.palette.build_styles_with_contrast(min_contrast);
1795            let bg = definition.palette.background;
1796
1797            for (name, color) in [
1798                ("foreground", style_rgb(styles.output)),
1799                ("primary", style_rgb(styles.primary)),
1800                ("secondary", style_rgb(styles.secondary)),
1801                ("user", style_rgb(styles.user)),
1802                ("response", style_rgb(styles.response)),
1803            ] {
1804                let color = color
1805                    .unwrap_or_else(|| panic!("{} missing fg color for {}", name, definition.id));
1806                let ratio = contrast_ratio(color, bg);
1807                assert!(
1808                    ratio >= min_contrast,
1809                    "theme={} style={} contrast {:.2} < {:.1}",
1810                    definition.id,
1811                    name,
1812                    ratio,
1813                    min_contrast
1814                );
1815
1816                let luminance = relative_luminance(color);
1817                if relative_luminance(bg) < 0.5 {
1818                    assert!(
1819                        (MIN_DARK_BG_TEXT_LUMINANCE..=MAX_DARK_BG_TEXT_LUMINANCE)
1820                            .contains(&luminance),
1821                        "theme={} style={} luminance {:.3} outside dark-theme readability bounds",
1822                        definition.id,
1823                        name,
1824                        luminance
1825                    );
1826                } else {
1827                    assert!(
1828                        luminance <= MAX_LIGHT_BG_TEXT_LUMINANCE,
1829                        "theme={} style={} luminance {:.3} too bright for light theme",
1830                        definition.id,
1831                        name,
1832                        luminance
1833                    );
1834                }
1835            }
1836        }
1837    }
1838
1839    fn style_rgb(style: Style) -> Option<RgbColor> {
1840        match style.get_fg_color() {
1841            Some(Color::Rgb(rgb)) => Some(rgb),
1842            _ => None,
1843        }
1844    }
1845}