Skip to main content

vtcode_tui/ui/
theme.rs

1use anstyle::{Color, Effects, RgbColor, Style};
2use anyhow::{Context, Result, anyhow};
3use catppuccin::PALETTE;
4use hashbrown::HashMap;
5use once_cell::sync::Lazy;
6use parking_lot::RwLock;
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 = match REGISTRY.get(DEFAULT_THEME_ID) {
1246        Some(theme) => theme,
1247        None => panic!("default theme must exist"),
1248    };
1249    let styles = default.palette.build_styles();
1250    RwLock::new(ActiveTheme {
1251        id: default.id.to_string(),
1252        label: default.label.to_string(),
1253        palette: default.palette.clone(),
1254        styles,
1255    })
1256});
1257
1258/// Set the active theme by identifier.
1259pub fn set_active_theme(theme_id: &str) -> Result<()> {
1260    let id_lc = theme_id.trim().to_lowercase();
1261    let theme = REGISTRY
1262        .get(id_lc.as_str())
1263        .ok_or_else(|| anyhow!("Unknown theme '{theme_id}'"))?;
1264
1265    let styles = theme.palette.build_styles();
1266    let mut guard = ACTIVE.write();
1267    guard.id = theme.id.to_string();
1268    guard.label = theme.label.to_string();
1269    guard.palette = theme.palette.clone();
1270    guard.styles = styles;
1271    Ok(())
1272}
1273
1274/// Get the identifier of the active theme.
1275pub fn active_theme_id() -> String {
1276    ACTIVE.read().id.clone()
1277}
1278
1279/// Get the human-readable label of the active theme.
1280pub fn active_theme_label() -> String {
1281    ACTIVE.read().label.clone()
1282}
1283
1284/// Get the current styles cloned from the active theme.
1285pub fn active_styles() -> ThemeStyles {
1286    ACTIVE.read().styles.clone()
1287}
1288
1289/// Slightly adjusted accent color for banner-like copy.
1290pub fn banner_color() -> RgbColor {
1291    let guard = ACTIVE.read();
1292    let accent = guard.palette.logo_accent;
1293    let secondary = guard.palette.secondary_accent;
1294    let background = guard.palette.background;
1295    drop(guard);
1296
1297    let min_contrast = get_minimum_contrast();
1298    let candidate = lighten(accent, ui::THEME_LOGO_ACCENT_BANNER_LIGHTEN_RATIO);
1299    ensure_contrast(
1300        candidate,
1301        background,
1302        min_contrast,
1303        &[
1304            lighten(accent, ui::THEME_PRIMARY_STATUS_SECONDARY_LIGHTEN_RATIO),
1305            lighten(
1306                secondary,
1307                ui::THEME_LOGO_ACCENT_BANNER_SECONDARY_LIGHTEN_RATIO,
1308            ),
1309            accent,
1310        ],
1311    )
1312}
1313
1314/// Slightly darkened accent style for banner-like copy.
1315pub fn banner_style() -> Style {
1316    let accent = banner_color();
1317    Style::new().fg_color(Some(Color::Rgb(accent))).bold()
1318}
1319
1320/// Accent color for the startup banner logo.
1321pub fn logo_accent_color() -> RgbColor {
1322    ACTIVE.read().palette.logo_accent
1323}
1324
1325/// Enumerate available theme identifiers.
1326pub fn available_themes() -> Vec<&'static str> {
1327    let mut keys: Vec<_> = REGISTRY.keys().copied().collect();
1328    keys.sort();
1329    keys
1330}
1331
1332/// Look up a theme label for display.
1333pub fn theme_label(theme_id: &str) -> Option<&'static str> {
1334    REGISTRY.get(theme_id).map(|definition| definition.label)
1335}
1336
1337fn suite_id_for_theme(theme_id: &str) -> Option<&'static str> {
1338    if theme_id.starts_with("catppuccin-") {
1339        Some("catppuccin")
1340    } else if theme_id.starts_with("vitesse-") {
1341        Some("vitesse")
1342    } else if theme_id.starts_with("ciapre-") {
1343        Some("ciapre")
1344    } else if theme_id == "mono" {
1345        Some("mono")
1346    } else {
1347        None
1348    }
1349}
1350
1351fn suite_label(suite_id: &str) -> Option<&'static str> {
1352    match suite_id {
1353        "catppuccin" => Some("Catppuccin"),
1354        "vitesse" => Some("Vitesse"),
1355        "ciapre" => Some("Ciapre"),
1356        "mono" => Some("Mono"),
1357        _ => None,
1358    }
1359}
1360
1361/// Resolve the suite identifier for a theme id.
1362pub fn theme_suite_id(theme_id: &str) -> Option<&'static str> {
1363    suite_id_for_theme(theme_id)
1364}
1365
1366/// Resolve the suite label for a theme id.
1367pub fn theme_suite_label(theme_id: &str) -> Option<&'static str> {
1368    suite_id_for_theme(theme_id).and_then(suite_label)
1369}
1370
1371/// Enumerate built-in theme suites and their member theme ids.
1372pub fn available_theme_suites() -> Vec<ThemeSuite> {
1373    const ORDER: &[&str] = &["ciapre", "vitesse", "catppuccin", "mono"];
1374
1375    ORDER
1376        .iter()
1377        .filter_map(|suite_id| {
1378            let mut theme_ids: Vec<&'static str> = available_themes()
1379                .into_iter()
1380                .filter(|theme_id| suite_id_for_theme(theme_id) == Some(*suite_id))
1381                .collect();
1382            if theme_ids.is_empty() {
1383                return None;
1384            }
1385            theme_ids.sort_unstable();
1386            let label = suite_label(suite_id)?;
1387            Some(ThemeSuite {
1388                id: suite_id,
1389                label,
1390                theme_ids,
1391            })
1392        })
1393        .collect()
1394}
1395
1396fn relative_luminance(color: RgbColor) -> f64 {
1397    fn channel(value: u8) -> f64 {
1398        let c = (value as f64) / 255.0;
1399        if c <= ui::THEME_RELATIVE_LUMINANCE_CUTOFF {
1400            c / ui::THEME_RELATIVE_LUMINANCE_LOW_FACTOR
1401        } else {
1402            ((c + ui::THEME_RELATIVE_LUMINANCE_OFFSET)
1403                / (1.0 + ui::THEME_RELATIVE_LUMINANCE_OFFSET))
1404                .powf(ui::THEME_RELATIVE_LUMINANCE_EXPONENT)
1405        }
1406    }
1407    let r = channel(color.0);
1408    let g = channel(color.1);
1409    let b = channel(color.2);
1410    ui::THEME_RED_LUMINANCE_COEFFICIENT * r
1411        + ui::THEME_GREEN_LUMINANCE_COEFFICIENT * g
1412        + ui::THEME_BLUE_LUMINANCE_COEFFICIENT * b
1413}
1414
1415fn contrast_ratio(foreground: RgbColor, background: RgbColor) -> f64 {
1416    let fg = relative_luminance(foreground);
1417    let bg = relative_luminance(background);
1418    let (lighter, darker) = if fg > bg { (fg, bg) } else { (bg, fg) };
1419    (lighter + ui::THEME_CONTRAST_RATIO_OFFSET) / (darker + ui::THEME_CONTRAST_RATIO_OFFSET)
1420}
1421
1422fn darken(color: RgbColor, ratio: f64) -> RgbColor {
1423    mix(color, RgbColor(0, 0, 0), ratio)
1424}
1425
1426fn adjust_luminance_to_target(color: RgbColor, target: f64) -> RgbColor {
1427    let current = relative_luminance(color);
1428    if (current - target).abs() < 1e-3 {
1429        return color;
1430    }
1431
1432    if current < target {
1433        // Raise luminance by blending toward white.
1434        let denom = (1.0 - current).max(1e-6);
1435        let ratio = ((target - current) / denom).clamp(0.0, 1.0);
1436        lighten(color, ratio)
1437    } else {
1438        // Lower luminance by blending toward black.
1439        let denom = current.max(1e-6);
1440        let ratio = ((current - target) / denom).clamp(0.0, 1.0);
1441        darken(color, ratio)
1442    }
1443}
1444
1445fn balance_text_luminance(color: RgbColor, background: RgbColor, min_contrast: f64) -> RgbColor {
1446    let bg_luminance = relative_luminance(background);
1447    let mut candidate = color;
1448    let current = relative_luminance(candidate);
1449    if bg_luminance < 0.5 {
1450        if current < MIN_DARK_BG_TEXT_LUMINANCE {
1451            candidate = adjust_luminance_to_target(candidate, MIN_DARK_BG_TEXT_LUMINANCE);
1452        } else if current > MAX_DARK_BG_TEXT_LUMINANCE {
1453            candidate = adjust_luminance_to_target(candidate, MAX_DARK_BG_TEXT_LUMINANCE);
1454        }
1455    } else if current > MAX_LIGHT_BG_TEXT_LUMINANCE {
1456        candidate = adjust_luminance_to_target(candidate, MAX_LIGHT_BG_TEXT_LUMINANCE);
1457    }
1458
1459    ensure_contrast(candidate, background, min_contrast, &[color])
1460}
1461
1462fn ensure_contrast(
1463    candidate: RgbColor,
1464    background: RgbColor,
1465    min_ratio: f64,
1466    fallbacks: &[RgbColor],
1467) -> RgbColor {
1468    if contrast_ratio(candidate, background) >= min_ratio {
1469        return candidate;
1470    }
1471    for &fallback in fallbacks {
1472        if contrast_ratio(fallback, background) >= min_ratio {
1473            return fallback;
1474        }
1475    }
1476
1477    // Final accessibility fallback: choose the higher-contrast endpoint.
1478    let black = RgbColor(0, 0, 0);
1479    let white = RgbColor(255, 255, 255);
1480    if contrast_ratio(black, background) >= contrast_ratio(white, background) {
1481        black
1482    } else {
1483        white
1484    }
1485}
1486
1487pub(crate) fn mix(color: RgbColor, target: RgbColor, ratio: f64) -> RgbColor {
1488    let ratio = ratio.clamp(ui::THEME_MIX_RATIO_MIN, ui::THEME_MIX_RATIO_MAX);
1489    let blend = |c: u8, t: u8| -> u8 {
1490        let c = c as f64;
1491        let t = t as f64;
1492        ((c + (t - c) * ratio).round()).clamp(ui::THEME_BLEND_CLAMP_MIN, ui::THEME_BLEND_CLAMP_MAX)
1493            as u8
1494    };
1495    RgbColor(
1496        blend(color.0, target.0),
1497        blend(color.1, target.1),
1498        blend(color.2, target.2),
1499    )
1500}
1501
1502fn lighten(color: RgbColor, ratio: f64) -> RgbColor {
1503    mix(
1504        color,
1505        RgbColor(
1506            ui::THEME_COLOR_WHITE_RED,
1507            ui::THEME_COLOR_WHITE_GREEN,
1508            ui::THEME_COLOR_WHITE_BLUE,
1509        ),
1510        ratio,
1511    )
1512}
1513
1514/// Resolve a theme identifier from configuration or CLI input.
1515pub fn resolve_theme(preferred: Option<String>) -> String {
1516    preferred
1517        .and_then(|candidate| {
1518            let trimmed = candidate.trim().to_lowercase();
1519            if trimmed.is_empty() {
1520                None
1521            } else if REGISTRY.contains_key(trimmed.as_str()) {
1522                Some(trimmed)
1523            } else {
1524                None
1525            }
1526        })
1527        .unwrap_or_else(|| DEFAULT_THEME_ID.to_string())
1528}
1529
1530/// Validate a theme and return its label for messaging.
1531pub fn ensure_theme(theme_id: &str) -> Result<&'static str> {
1532    REGISTRY
1533        .get(theme_id)
1534        .map(|definition| definition.label)
1535        .context("Theme not found")
1536}
1537
1538/// Rebuild the active theme's styles with current accessibility settings.
1539/// Call this after updating color accessibility configuration.
1540pub fn rebuild_active_styles() {
1541    let mut guard = ACTIVE.write();
1542    guard.styles = guard.palette.build_styles();
1543}
1544
1545/// Theme validation result
1546#[derive(Debug, Clone)]
1547pub struct ThemeValidationResult {
1548    /// Whether the theme passed validation
1549    pub is_valid: bool,
1550    /// List of warnings (non-fatal issues)
1551    pub warnings: Vec<String>,
1552    /// List of errors (fatal issues)
1553    pub errors: Vec<String>,
1554}
1555
1556/// Validate a theme's color contrast ratios.
1557/// Returns warnings for colors that don't meet WCAG AA standards.
1558pub fn validate_theme_contrast(theme_id: &str) -> ThemeValidationResult {
1559    let mut result = ThemeValidationResult {
1560        is_valid: true,
1561        warnings: Vec::new(),
1562        errors: Vec::new(),
1563    };
1564
1565    let theme = match REGISTRY.get(theme_id) {
1566        Some(t) => t,
1567        None => {
1568            result.is_valid = false;
1569            result.errors.push(format!("Unknown theme: {}", theme_id));
1570            return result;
1571        }
1572    };
1573
1574    let palette = &theme.palette;
1575    let bg = palette.background;
1576    let min_contrast = get_minimum_contrast();
1577
1578    // Check main text colors
1579    let checks = [
1580        ("foreground", palette.foreground),
1581        ("primary_accent", palette.primary_accent),
1582        ("secondary_accent", palette.secondary_accent),
1583        ("alert", palette.alert),
1584        ("logo_accent", palette.logo_accent),
1585    ];
1586
1587    for (name, color) in checks {
1588        let ratio = contrast_ratio(color, bg);
1589        if ratio < min_contrast {
1590            result.warnings.push(format!(
1591                "{} ({:02X}{:02X}{:02X}) has contrast ratio {:.2} < {:.1} against background",
1592                name, color.0, color.1, color.2, ratio, min_contrast
1593            ));
1594        }
1595    }
1596
1597    result
1598}
1599
1600/// Check if a theme is suitable for the detected terminal color scheme.
1601/// Returns true if the theme matches (light theme for light terminal, dark for dark).
1602pub fn theme_matches_terminal_scheme(theme_id: &str) -> bool {
1603    use crate::utils::ansi_capabilities::ColorScheme;
1604    use crate::utils::ansi_capabilities::detect_color_scheme;
1605
1606    let scheme = detect_color_scheme();
1607    let theme_is_light = is_light_theme(theme_id);
1608
1609    match scheme {
1610        ColorScheme::Light => theme_is_light,
1611        ColorScheme::Dark | ColorScheme::Unknown => !theme_is_light,
1612    }
1613}
1614
1615/// Determine if a theme is a light theme based on its background luminance.
1616pub fn is_light_theme(theme_id: &str) -> bool {
1617    REGISTRY
1618        .get(theme_id)
1619        .map(|theme| {
1620            let bg = theme.palette.background;
1621            let luminance = relative_luminance(bg);
1622            // If background luminance > 0.5, it's a light theme
1623            luminance > 0.5
1624        })
1625        .unwrap_or(false)
1626}
1627
1628/// Get a suggested theme based on terminal color scheme detection.
1629/// Returns a light or dark theme depending on detected terminal background.
1630pub fn suggest_theme_for_terminal() -> &'static str {
1631    use crate::utils::ansi_capabilities::ColorScheme;
1632    use crate::utils::ansi_capabilities::detect_color_scheme;
1633
1634    match detect_color_scheme() {
1635        ColorScheme::Light => "vitesse-light",
1636        ColorScheme::Dark | ColorScheme::Unknown => DEFAULT_THEME_ID,
1637    }
1638}
1639
1640/// Get the recommended syntax highlighting theme for a given UI theme.
1641/// This ensures that code highlighting colors complement the UI theme's background.
1642/// Based on: https://github.com/openai/codex/pull/11447, https://github.com/openai/codex/pull/12581
1643///
1644/// # Usage
1645///
1646/// For code blocks and syntax highlighting:
1647/// ```rust
1648/// use vtcode_tui::ui::theme::{get_syntax_theme_for_ui_theme, active_theme_id};
1649/// let ui_theme = active_theme_id();
1650/// let syntax_theme = get_syntax_theme_for_ui_theme(&ui_theme);
1651/// // Use `syntax_theme` with syntect's ThemeSet
1652/// ```
1653///
1654/// For PTY/shell output highlighting, the same mapping applies.
1655/// The shell command highlighter should use the same color palette
1656/// as the syntax highlighting theme for visual consistency.
1657pub fn get_syntax_theme_for_ui_theme(ui_theme: &str) -> &'static str {
1658    match ui_theme.to_lowercase().as_str() {
1659        // Ayu themes - use matching syntect themes
1660        "ayu" => "ayu-dark",
1661        "ayu-mirage" => "ayu-mirage",
1662
1663        // Catppuccin themes - use matching syntect themes
1664        "catppuccin-latte" => "catppuccin-latte",
1665        "catppuccin-frappe" => "catppuccin-frappe",
1666        "catppuccin-macchiato" => "catppuccin-macchiato",
1667        "catppuccin-mocha" => "catppuccin-mocha",
1668
1669        // Solarized themes - exact TextMate theme names
1670        "solarized-dark" | "solarized-dark-hc" => "Solarized (dark)",
1671        "solarized-light" => "Solarized (light)",
1672
1673        // Gruvbox themes
1674        "gruvbox-dark" | "gruvbox-dark-hard" => "gruvbox-dark",
1675        "gruvbox-light" | "gruvbox-light-hard" => "gruvbox-light",
1676        "gruvbox-material" | "gruvbox-material-dark" => "gruvbox-dark",
1677        "gruvbox-material-light" => "gruvbox-light",
1678
1679        // Tomorrow themes - exact TextMate theme names
1680        "tomorrow" => "Tomorrow",
1681        "tomorrow-night" => "Tomorrow Night",
1682        "tomorrow-night-blue" => "Tomorrow Night Blue",
1683        "tomorrow-night-bright" => "Tomorrow Night Bright",
1684        "tomorrow-night-eighties" => "Tomorrow Night Eighties",
1685        "tomorrow-night-burns" => "Tomorrow Night",
1686
1687        // GitHub themes - exact TextMate theme names
1688        "github-dark" => "GitHub Dark",
1689        "github" => "GitHub",
1690
1691        // Atom themes - exact TextMate theme names
1692        "atom-one-dark" => "OneDark",
1693        "atom-one-light" => "OneLight",
1694        "atom" => "base16-ocean.dark",
1695
1696        // Spacegray themes - use base16-ocean.dark as closest match
1697        "spacegray" | "spacegray-bright" | "spacegray-eighties" | "spacegray-eighties-dull" => {
1698            "base16-ocean.dark"
1699        }
1700
1701        // Material themes - exact TextMate theme names
1702        "material-ocean" | "material-dark" | "material" => "Material Dark",
1703
1704        // Other popular dark themes - exact TextMate theme names where available
1705        "dracula" => "Dracula",
1706        "monokai-classic" => "monokai-classic",
1707        "night-owl" => "Night Owl",
1708        "zenburn" => "Zenburn",
1709
1710        // Fallback themes - use base16-ocean as a good general-purpose dark theme
1711        "jetbrains-darcula" => "base16-ocean.dark",
1712        "man-page" => "base16-ocean.dark",
1713        "homebrew" => "base16-ocean.dark",
1714        "framer" => "base16-ocean.dark",
1715        "espresso" => "base16-ocean.dark",
1716        "adventure-time" => "base16-ocean.dark",
1717        "afterglow" => "base16-ocean.dark",
1718        "apple-classic" => "base16-ocean.dark",
1719        "apple-system-colors" => "base16-ocean.dark",
1720
1721        // Light themes - use base16-ocean.light as fallback
1722        "apple-system-colors-light" => "base16-ocean.light",
1723        "vitesse-light" | "vitesse-light-soft" => "base16-ocean.light",
1724
1725        // Default dark themes
1726        "ciapre" | "ciapre-dark" | "ciapre-blue" => "base16-ocean.dark",
1727        "vitesse-black" | "vitesse-dark" | "vitesse-dark-soft" => "base16-ocean.dark",
1728        "mono" => "base16-ocean.dark",
1729        "ansi-classic" => "base16-ocean.dark",
1730
1731        // Fallback to dark theme for unknown themes
1732        _ => "base16-ocean.dark",
1733    }
1734}
1735
1736/// Get the recommended syntax highlighting theme for the currently active UI theme.
1737/// Convenience wrapper around `get_syntax_theme_for_ui_theme`.
1738pub fn get_active_syntax_theme() -> &'static str {
1739    get_syntax_theme_for_ui_theme(&active_theme_id())
1740}
1741
1742#[cfg(test)]
1743mod tests {
1744    use super::*;
1745
1746    #[test]
1747    fn test_mono_theme_exists() {
1748        let result = ensure_theme("mono");
1749        assert!(result.is_ok(), "Mono theme should be registered");
1750        assert_eq!(result.unwrap(), "Mono");
1751    }
1752
1753    #[test]
1754    fn test_mono_theme_contrast() {
1755        let result = validate_theme_contrast("mono");
1756        // We expect it to be valid, but we check if there are any major contrast issues
1757        assert!(result.errors.is_empty(), "Mono theme should have no errors");
1758        // Mono themes might have some warnings if grays are close, but pure black/white should be fine.
1759        assert!(result.is_valid);
1760    }
1761
1762    #[test]
1763    fn test_all_themes_resolvable() {
1764        for id in available_themes() {
1765            assert!(
1766                ensure_theme(id).is_ok(),
1767                "Theme {} should be resolvable",
1768                id
1769            );
1770        }
1771    }
1772
1773    #[test]
1774    fn test_available_theme_suites_contains_expected_groups() {
1775        let suites = available_theme_suites();
1776        let suite_ids: Vec<&str> = suites.iter().map(|suite| suite.id).collect();
1777        assert!(suite_ids.contains(&"ciapre"));
1778        assert!(suite_ids.contains(&"vitesse"));
1779        assert!(suite_ids.contains(&"catppuccin"));
1780        assert!(suite_ids.contains(&"mono"));
1781    }
1782
1783    #[test]
1784    fn test_theme_suite_resolution() {
1785        assert_eq!(theme_suite_id("catppuccin-mocha"), Some("catppuccin"));
1786        assert_eq!(theme_suite_id("vitesse-light"), Some("vitesse"));
1787        assert_eq!(theme_suite_id("ciapre-dark"), Some("ciapre"));
1788        assert_eq!(theme_suite_id("mono"), Some("mono"));
1789        assert_eq!(theme_suite_id("unknown-theme"), None);
1790    }
1791
1792    #[test]
1793    fn test_all_themes_have_readable_foreground_and_accents() {
1794        let min_contrast = get_minimum_contrast();
1795        for definition in REGISTRY.values() {
1796            let styles = definition.palette.build_styles_with_contrast(min_contrast);
1797            let bg = definition.palette.background;
1798
1799            for (name, color) in [
1800                ("foreground", style_rgb(styles.output)),
1801                ("primary", style_rgb(styles.primary)),
1802                ("secondary", style_rgb(styles.secondary)),
1803                ("user", style_rgb(styles.user)),
1804                ("response", style_rgb(styles.response)),
1805            ] {
1806                let color = color
1807                    .unwrap_or_else(|| panic!("{} missing fg color for {}", name, definition.id));
1808                let ratio = contrast_ratio(color, bg);
1809                assert!(
1810                    ratio >= min_contrast,
1811                    "theme={} style={} contrast {:.2} < {:.1}",
1812                    definition.id,
1813                    name,
1814                    ratio,
1815                    min_contrast
1816                );
1817
1818                let luminance = relative_luminance(color);
1819                if relative_luminance(bg) < 0.5 {
1820                    assert!(
1821                        (MIN_DARK_BG_TEXT_LUMINANCE..=MAX_DARK_BG_TEXT_LUMINANCE)
1822                            .contains(&luminance),
1823                        "theme={} style={} luminance {:.3} outside dark-theme readability bounds",
1824                        definition.id,
1825                        name,
1826                        luminance
1827                    );
1828                } else {
1829                    assert!(
1830                        luminance <= MAX_LIGHT_BG_TEXT_LUMINANCE,
1831                        "theme={} style={} luminance {:.3} too bright for light theme",
1832                        definition.id,
1833                        name,
1834                        luminance
1835                    );
1836                }
1837            }
1838        }
1839    }
1840
1841    fn style_rgb(style: Style) -> Option<RgbColor> {
1842        match style.get_fg_color() {
1843            Some(Color::Rgb(rgb)) => Some(rgb),
1844            _ => None,
1845        }
1846    }
1847}