Skip to main content

fresh/view/theme/
types.rs

1//! Pure theme types without I/O operations.
2//!
3//! This module contains all theme-related data structures that can be used
4//! without filesystem access. This enables WASM compatibility and easier testing.
5
6use ratatui::style::Color;
7use schemars::JsonSchema;
8use serde::{Deserialize, Serialize};
9
10pub const THEME_DARK: &str = "dark";
11pub const THEME_LIGHT: &str = "light";
12pub const THEME_HIGH_CONTRAST: &str = "high-contrast";
13pub const THEME_NOSTALGIA: &str = "nostalgia";
14pub const THEME_DRACULA: &str = "dracula";
15pub const THEME_NORD: &str = "nord";
16pub const THEME_SOLARIZED_DARK: &str = "solarized-dark";
17
18/// A builtin theme with its name, pack, and embedded JSON content.
19pub struct BuiltinTheme {
20    pub name: &'static str,
21    /// Pack name (subdirectory path, empty for root themes)
22    pub pack: &'static str,
23    pub json: &'static str,
24}
25
26// Include the auto-generated BUILTIN_THEMES array from build.rs
27include!(concat!(env!("OUT_DIR"), "/builtin_themes.rs"));
28
29/// Information about an available theme.
30#[derive(Debug, Clone, PartialEq, Eq)]
31pub struct ThemeInfo {
32    /// Theme display name (e.g., "dark", "adwaita-dark")
33    pub name: String,
34    /// Pack name (subdirectory path, empty for root themes)
35    pub pack: String,
36    /// Unique key used as the registry identifier.
37    ///
38    /// Derivation priority:
39    /// 1. Package themes: `{repository_url}#{theme_name}`
40    /// 2. User-saved themes (theme editor): `file://{absolute_path}`
41    /// 3. Loose user themes: `{pack}/{name}` or just `{name}` if pack is empty
42    /// 4. Builtins: just the name
43    pub key: String,
44}
45
46impl ThemeInfo {
47    /// Create a new ThemeInfo. The key defaults to `pack/name` (or just `name`
48    /// when pack is empty).
49    pub fn new(name: impl Into<String>, pack: impl Into<String>) -> Self {
50        let name = name.into();
51        let pack = pack.into();
52        let key = if pack.is_empty() {
53            name.clone()
54        } else {
55            format!("{}/{}", pack, name)
56        };
57        Self { name, pack, key }
58    }
59
60    /// Create a ThemeInfo with an explicit key (e.g. a repository URL).
61    pub fn with_key(
62        name: impl Into<String>,
63        pack: impl Into<String>,
64        key: impl Into<String>,
65    ) -> Self {
66        Self {
67            name: name.into(),
68            pack: pack.into(),
69            key: key.into(),
70        }
71    }
72
73    /// Get display name showing pack if present
74    pub fn display_name(&self) -> String {
75        if self.pack.is_empty() {
76            self.name.clone()
77        } else {
78            format!("{} ({})", self.name, self.pack)
79        }
80    }
81}
82
83/// Convert a ratatui Color to RGB values.
84/// Returns None for Reset or Indexed colors.
85pub fn color_to_rgb(color: Color) -> Option<(u8, u8, u8)> {
86    match color {
87        Color::Rgb(r, g, b) => Some((r, g, b)),
88        Color::White => Some((255, 255, 255)),
89        Color::Black => Some((0, 0, 0)),
90        Color::Red => Some((205, 0, 0)),
91        Color::Green => Some((0, 205, 0)),
92        Color::Blue => Some((0, 0, 238)),
93        Color::Yellow => Some((205, 205, 0)),
94        Color::Magenta => Some((205, 0, 205)),
95        Color::Cyan => Some((0, 205, 205)),
96        Color::Gray => Some((229, 229, 229)),
97        Color::DarkGray => Some((127, 127, 127)),
98        Color::LightRed => Some((255, 0, 0)),
99        Color::LightGreen => Some((0, 255, 0)),
100        Color::LightBlue => Some((92, 92, 255)),
101        Color::LightYellow => Some((255, 255, 0)),
102        Color::LightMagenta => Some((255, 0, 255)),
103        Color::LightCyan => Some((0, 255, 255)),
104        Color::Reset | Color::Indexed(_) => None,
105    }
106}
107
108/// Brighten a color by adding an amount to each RGB component.
109/// Clamps values to 255.
110pub fn brighten_color(color: Color, amount: u8) -> Color {
111    if let Some((r, g, b)) = color_to_rgb(color) {
112        Color::Rgb(
113            r.saturating_add(amount),
114            g.saturating_add(amount),
115            b.saturating_add(amount),
116        )
117    } else {
118        color
119    }
120}
121
122/// Shift an RGB color a small amount toward the opposite end of the
123/// brightness spectrum: dark colors become slightly brighter, light colors
124/// slightly darker. Non-RGB colors are returned unchanged.
125///
126/// Used to derive subtle visual cues (e.g. post-EOF background shade) from
127/// a theme's editor background without requiring theme authors to pick an
128/// explicit color.
129pub fn shade_toward_contrast(color: Color, amount: u8) -> Color {
130    if let Some((r, g, b)) = color_to_rgb(color) {
131        let avg = (u16::from(r) + u16::from(g) + u16::from(b)) / 3;
132        if avg < 128 {
133            Color::Rgb(
134                r.saturating_add(amount),
135                g.saturating_add(amount),
136                b.saturating_add(amount),
137            )
138        } else {
139            Color::Rgb(
140                r.saturating_sub(amount),
141                g.saturating_sub(amount),
142                b.saturating_sub(amount),
143            )
144        }
145    } else {
146        color
147    }
148}
149
150/// Serializable color representation
151#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
152#[serde(untagged)]
153pub enum ColorDef {
154    /// RGB color as [r, g, b]
155    Rgb(u8, u8, u8),
156    /// Named color
157    Named(String),
158}
159
160impl From<ColorDef> for Color {
161    fn from(def: ColorDef) -> Self {
162        match def {
163            ColorDef::Rgb(r, g, b) => Color::Rgb(r, g, b),
164            ColorDef::Named(name) => match name.as_str() {
165                "Black" => Color::Black,
166                "Red" => Color::Red,
167                "Green" => Color::Green,
168                "Yellow" => Color::Yellow,
169                "Blue" => Color::Blue,
170                "Magenta" => Color::Magenta,
171                "Cyan" => Color::Cyan,
172                "Gray" => Color::Gray,
173                "DarkGray" => Color::DarkGray,
174                "LightRed" => Color::LightRed,
175                "LightGreen" => Color::LightGreen,
176                "LightYellow" => Color::LightYellow,
177                "LightBlue" => Color::LightBlue,
178                "LightMagenta" => Color::LightMagenta,
179                "LightCyan" => Color::LightCyan,
180                "White" => Color::White,
181                // Default/Reset uses the terminal's default color (preserves transparency)
182                "Default" | "Reset" => Color::Reset,
183                _ => Color::White, // Default fallback
184            },
185        }
186    }
187}
188
189/// Convert a named color string (e.g. "Yellow", "Red") to a ratatui Color.
190/// Returns None if the string is not a recognized named color.
191pub fn named_color_from_str(name: &str) -> Option<Color> {
192    match name {
193        "Black" => Some(Color::Black),
194        "Red" => Some(Color::Red),
195        "Green" => Some(Color::Green),
196        "Yellow" => Some(Color::Yellow),
197        "Blue" => Some(Color::Blue),
198        "Magenta" => Some(Color::Magenta),
199        "Cyan" => Some(Color::Cyan),
200        "Gray" => Some(Color::Gray),
201        "DarkGray" => Some(Color::DarkGray),
202        "LightRed" => Some(Color::LightRed),
203        "LightGreen" => Some(Color::LightGreen),
204        "LightYellow" => Some(Color::LightYellow),
205        "LightBlue" => Some(Color::LightBlue),
206        "LightMagenta" => Some(Color::LightMagenta),
207        "LightCyan" => Some(Color::LightCyan),
208        "White" => Some(Color::White),
209        "Default" | "Reset" => Some(Color::Reset),
210        _ => None,
211    }
212}
213
214impl From<Color> for ColorDef {
215    fn from(color: Color) -> Self {
216        match color {
217            Color::Rgb(r, g, b) => ColorDef::Rgb(r, g, b),
218            Color::White => ColorDef::Named("White".to_string()),
219            Color::Black => ColorDef::Named("Black".to_string()),
220            Color::Red => ColorDef::Named("Red".to_string()),
221            Color::Green => ColorDef::Named("Green".to_string()),
222            Color::Blue => ColorDef::Named("Blue".to_string()),
223            Color::Yellow => ColorDef::Named("Yellow".to_string()),
224            Color::Magenta => ColorDef::Named("Magenta".to_string()),
225            Color::Cyan => ColorDef::Named("Cyan".to_string()),
226            Color::Gray => ColorDef::Named("Gray".to_string()),
227            Color::DarkGray => ColorDef::Named("DarkGray".to_string()),
228            Color::LightRed => ColorDef::Named("LightRed".to_string()),
229            Color::LightGreen => ColorDef::Named("LightGreen".to_string()),
230            Color::LightBlue => ColorDef::Named("LightBlue".to_string()),
231            Color::LightYellow => ColorDef::Named("LightYellow".to_string()),
232            Color::LightMagenta => ColorDef::Named("LightMagenta".to_string()),
233            Color::LightCyan => ColorDef::Named("LightCyan".to_string()),
234            Color::Reset => ColorDef::Named("Default".to_string()),
235            Color::Indexed(_) => {
236                // Fallback for indexed colors
237                if let Some((r, g, b)) = color_to_rgb(color) {
238                    ColorDef::Rgb(r, g, b)
239                } else {
240                    ColorDef::Named("Default".to_string())
241                }
242            }
243        }
244    }
245}
246
247/// Serializable theme definition (matches JSON structure)
248#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
249pub struct ThemeFile {
250    /// Theme name
251    pub name: String,
252    /// Editor area colors
253    pub editor: EditorColors,
254    /// UI element colors (tabs, menus, status bar, etc.)
255    pub ui: UiColors,
256    /// Search result highlighting colors
257    pub search: SearchColors,
258    /// LSP diagnostic colors (errors, warnings, etc.)
259    pub diagnostic: DiagnosticColors,
260    /// Syntax highlighting colors
261    pub syntax: SyntaxColors,
262}
263
264/// Editor area colors
265#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
266pub struct EditorColors {
267    /// Editor background color
268    #[serde(default = "default_editor_bg")]
269    pub bg: ColorDef,
270    /// Default text color
271    #[serde(default = "default_editor_fg")]
272    pub fg: ColorDef,
273    /// Cursor color
274    #[serde(default = "default_cursor")]
275    pub cursor: ColorDef,
276    /// Cursor color in unfocused splits
277    #[serde(default = "default_inactive_cursor")]
278    pub inactive_cursor: ColorDef,
279    /// Selected text background
280    #[serde(default = "default_selection_bg")]
281    pub selection_bg: ColorDef,
282    /// Background of the line containing cursor
283    #[serde(default = "default_current_line_bg")]
284    pub current_line_bg: ColorDef,
285    /// Line number text color
286    #[serde(default = "default_line_number_fg")]
287    pub line_number_fg: ColorDef,
288    /// Line number gutter background
289    #[serde(default = "default_line_number_bg")]
290    pub line_number_bg: ColorDef,
291    /// Diff added line background
292    #[serde(default = "default_diff_add_bg")]
293    pub diff_add_bg: ColorDef,
294    /// Diff removed line background
295    #[serde(default = "default_diff_remove_bg")]
296    pub diff_remove_bg: ColorDef,
297    /// Diff added word-level highlight background (optional override)
298    /// When not set, computed by brightening diff_add_bg
299    #[serde(default)]
300    pub diff_add_highlight_bg: Option<ColorDef>,
301    /// Diff removed word-level highlight background (optional override)
302    /// When not set, computed by brightening diff_remove_bg
303    #[serde(default)]
304    pub diff_remove_highlight_bg: Option<ColorDef>,
305    /// Diff modified line background
306    #[serde(default = "default_diff_modify_bg")]
307    pub diff_modify_bg: ColorDef,
308    /// Vertical ruler background color
309    #[serde(default = "default_ruler_bg")]
310    pub ruler_bg: ColorDef,
311    /// Whitespace indicator foreground color (for tab arrows and space dots)
312    #[serde(default = "default_whitespace_indicator_fg")]
313    pub whitespace_indicator_fg: ColorDef,
314    /// Background color for lines after end-of-file (optional override).
315    /// When not set, computed as a slightly contrasting shade of `bg`
316    /// (lighter for dark themes, darker for light themes) to give post-EOF
317    /// rows a subtle visual separation from the buffer content.
318    #[serde(default)]
319    pub after_eof_bg: Option<ColorDef>,
320}
321
322// Default editor colors (for minimal themes)
323fn default_editor_bg() -> ColorDef {
324    ColorDef::Rgb(30, 30, 30)
325}
326fn default_editor_fg() -> ColorDef {
327    ColorDef::Rgb(212, 212, 212)
328}
329fn default_cursor() -> ColorDef {
330    ColorDef::Rgb(255, 255, 255)
331}
332fn default_inactive_cursor() -> ColorDef {
333    ColorDef::Named("DarkGray".to_string())
334}
335fn default_selection_bg() -> ColorDef {
336    ColorDef::Rgb(38, 79, 120)
337}
338fn default_current_line_bg() -> ColorDef {
339    ColorDef::Rgb(40, 40, 40)
340}
341fn default_line_number_fg() -> ColorDef {
342    ColorDef::Rgb(100, 100, 100)
343}
344fn default_line_number_bg() -> ColorDef {
345    ColorDef::Rgb(30, 30, 30)
346}
347fn default_diff_add_bg() -> ColorDef {
348    ColorDef::Rgb(35, 60, 35) // Dark green
349}
350fn default_diff_remove_bg() -> ColorDef {
351    ColorDef::Rgb(70, 35, 35) // Dark red
352}
353fn default_diff_modify_bg() -> ColorDef {
354    ColorDef::Rgb(40, 38, 30) // Very subtle yellow tint, close to dark bg
355}
356fn default_ruler_bg() -> ColorDef {
357    ColorDef::Rgb(50, 50, 50) // Subtle dark gray, slightly lighter than default editor bg
358}
359fn default_whitespace_indicator_fg() -> ColorDef {
360    ColorDef::Rgb(70, 70, 70) // Subdued dark gray, subtle but visible
361}
362
363/// UI element colors (tabs, menus, status bar, etc.)
364#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
365pub struct UiColors {
366    /// Active tab text color
367    #[serde(default = "default_tab_active_fg")]
368    pub tab_active_fg: ColorDef,
369    /// Active tab background color
370    #[serde(default = "default_tab_active_bg")]
371    pub tab_active_bg: ColorDef,
372    /// Inactive tab text color
373    #[serde(default = "default_tab_inactive_fg")]
374    pub tab_inactive_fg: ColorDef,
375    /// Inactive tab background color
376    #[serde(default = "default_tab_inactive_bg")]
377    pub tab_inactive_bg: ColorDef,
378    /// Tab bar separator color
379    #[serde(default = "default_tab_separator_bg")]
380    pub tab_separator_bg: ColorDef,
381    /// Tab close button hover color
382    #[serde(default = "default_tab_close_hover_fg")]
383    pub tab_close_hover_fg: ColorDef,
384    /// Tab hover background color
385    #[serde(default = "default_tab_hover_bg")]
386    pub tab_hover_bg: ColorDef,
387    /// Menu bar background
388    #[serde(default = "default_menu_bg")]
389    pub menu_bg: ColorDef,
390    /// Menu bar text color
391    #[serde(default = "default_menu_fg")]
392    pub menu_fg: ColorDef,
393    /// Active menu item background
394    #[serde(default = "default_menu_active_bg")]
395    pub menu_active_bg: ColorDef,
396    /// Active menu item text color
397    #[serde(default = "default_menu_active_fg")]
398    pub menu_active_fg: ColorDef,
399    /// Dropdown menu background
400    #[serde(default = "default_menu_dropdown_bg")]
401    pub menu_dropdown_bg: ColorDef,
402    /// Dropdown menu text color
403    #[serde(default = "default_menu_dropdown_fg")]
404    pub menu_dropdown_fg: ColorDef,
405    /// Highlighted menu item background
406    #[serde(default = "default_menu_highlight_bg")]
407    pub menu_highlight_bg: ColorDef,
408    /// Highlighted menu item text color
409    #[serde(default = "default_menu_highlight_fg")]
410    pub menu_highlight_fg: ColorDef,
411    /// Menu border color
412    #[serde(default = "default_menu_border_fg")]
413    pub menu_border_fg: ColorDef,
414    /// Menu separator line color
415    #[serde(default = "default_menu_separator_fg")]
416    pub menu_separator_fg: ColorDef,
417    /// Menu item hover background
418    #[serde(default = "default_menu_hover_bg")]
419    pub menu_hover_bg: ColorDef,
420    /// Menu item hover text color
421    #[serde(default = "default_menu_hover_fg")]
422    pub menu_hover_fg: ColorDef,
423    /// Disabled menu item text color
424    #[serde(default = "default_menu_disabled_fg")]
425    pub menu_disabled_fg: ColorDef,
426    /// Disabled menu item background
427    #[serde(default = "default_menu_disabled_bg")]
428    pub menu_disabled_bg: ColorDef,
429    /// Status bar text color
430    #[serde(default = "default_status_bar_fg")]
431    pub status_bar_fg: ColorDef,
432    /// Status bar background color
433    #[serde(default = "default_status_bar_bg")]
434    pub status_bar_bg: ColorDef,
435    /// Command palette shortcut hint text color in status bar (falls back to status_bar_fg)
436    #[serde(default)]
437    pub status_palette_fg: Option<ColorDef>,
438    /// Command palette shortcut hint background in status bar (falls back to status_bar_bg)
439    #[serde(default)]
440    pub status_palette_bg: Option<ColorDef>,
441    /// Status bar LSP indicator text color when LSP is running (falls back to status_bar_fg)
442    #[serde(default)]
443    pub status_lsp_on_fg: Option<ColorDef>,
444    /// Status bar LSP indicator background when LSP is running (falls back to status_bar_bg)
445    #[serde(default)]
446    pub status_lsp_on_bg: Option<ColorDef>,
447    /// Command prompt text color
448    #[serde(default = "default_prompt_fg")]
449    pub prompt_fg: ColorDef,
450    /// Command prompt background
451    #[serde(default = "default_prompt_bg")]
452    pub prompt_bg: ColorDef,
453    /// Prompt selected text color
454    #[serde(default = "default_prompt_selection_fg")]
455    pub prompt_selection_fg: ColorDef,
456    /// Prompt selection background
457    #[serde(default = "default_prompt_selection_bg")]
458    pub prompt_selection_bg: ColorDef,
459    /// Popup window border color
460    #[serde(default = "default_popup_border_fg")]
461    pub popup_border_fg: ColorDef,
462    /// Popup window background
463    #[serde(default = "default_popup_bg")]
464    pub popup_bg: ColorDef,
465    /// Popup selected item background
466    #[serde(default = "default_popup_selection_bg")]
467    pub popup_selection_bg: ColorDef,
468    /// Popup selected item text color
469    #[serde(default = "default_popup_selection_fg")]
470    pub popup_selection_fg: ColorDef,
471    /// Popup window text color
472    #[serde(default = "default_popup_text_fg")]
473    pub popup_text_fg: ColorDef,
474    /// Autocomplete suggestion background
475    #[serde(default = "default_suggestion_bg")]
476    pub suggestion_bg: ColorDef,
477    /// Selected suggestion background
478    #[serde(default = "default_suggestion_selected_bg")]
479    pub suggestion_selected_bg: ColorDef,
480    /// Help panel background
481    #[serde(default = "default_help_bg")]
482    pub help_bg: ColorDef,
483    /// Help panel text color
484    #[serde(default = "default_help_fg")]
485    pub help_fg: ColorDef,
486    /// Help keybinding text color
487    #[serde(default = "default_help_key_fg")]
488    pub help_key_fg: ColorDef,
489    /// Help panel separator color
490    #[serde(default = "default_help_separator_fg")]
491    pub help_separator_fg: ColorDef,
492    /// Help indicator text color
493    #[serde(default = "default_help_indicator_fg")]
494    pub help_indicator_fg: ColorDef,
495    /// Help indicator background
496    #[serde(default = "default_help_indicator_bg")]
497    pub help_indicator_bg: ColorDef,
498    /// Inline code block background
499    #[serde(default = "default_inline_code_bg")]
500    pub inline_code_bg: ColorDef,
501    /// Split pane separator color
502    #[serde(default = "default_split_separator_fg")]
503    pub split_separator_fg: ColorDef,
504    /// Split separator hover color
505    #[serde(default = "default_split_separator_hover_fg")]
506    pub split_separator_hover_fg: ColorDef,
507    /// Scrollbar track color
508    #[serde(default = "default_scrollbar_track_fg")]
509    pub scrollbar_track_fg: ColorDef,
510    /// Scrollbar thumb color
511    #[serde(default = "default_scrollbar_thumb_fg")]
512    pub scrollbar_thumb_fg: ColorDef,
513    /// Scrollbar track hover color
514    #[serde(default = "default_scrollbar_track_hover_fg")]
515    pub scrollbar_track_hover_fg: ColorDef,
516    /// Scrollbar thumb hover color
517    #[serde(default = "default_scrollbar_thumb_hover_fg")]
518    pub scrollbar_thumb_hover_fg: ColorDef,
519    /// Compose mode margin background
520    #[serde(default = "default_compose_margin_bg")]
521    pub compose_margin_bg: ColorDef,
522    /// Word under cursor highlight
523    #[serde(default = "default_semantic_highlight_bg")]
524    pub semantic_highlight_bg: ColorDef,
525    /// Embedded terminal background (use Default for transparency)
526    #[serde(default = "default_terminal_bg")]
527    pub terminal_bg: ColorDef,
528    /// Embedded terminal default text color
529    #[serde(default = "default_terminal_fg")]
530    pub terminal_fg: ColorDef,
531    /// Warning indicator background in status bar
532    #[serde(default = "default_status_warning_indicator_bg")]
533    pub status_warning_indicator_bg: ColorDef,
534    /// Warning indicator text color in status bar
535    #[serde(default = "default_status_warning_indicator_fg")]
536    pub status_warning_indicator_fg: ColorDef,
537    /// Error indicator background in status bar
538    #[serde(default = "default_status_error_indicator_bg")]
539    pub status_error_indicator_bg: ColorDef,
540    /// Error indicator text color in status bar
541    #[serde(default = "default_status_error_indicator_fg")]
542    pub status_error_indicator_fg: ColorDef,
543    /// Warning indicator hover background
544    #[serde(default = "default_status_warning_indicator_hover_bg")]
545    pub status_warning_indicator_hover_bg: ColorDef,
546    /// Warning indicator hover text color
547    #[serde(default = "default_status_warning_indicator_hover_fg")]
548    pub status_warning_indicator_hover_fg: ColorDef,
549    /// Error indicator hover background
550    #[serde(default = "default_status_error_indicator_hover_bg")]
551    pub status_error_indicator_hover_bg: ColorDef,
552    /// Error indicator hover text color
553    #[serde(default = "default_status_error_indicator_hover_fg")]
554    pub status_error_indicator_hover_fg: ColorDef,
555    /// Tab drop zone background during drag
556    #[serde(default = "default_tab_drop_zone_bg")]
557    pub tab_drop_zone_bg: ColorDef,
558    /// Tab drop zone border during drag
559    #[serde(default = "default_tab_drop_zone_border")]
560    pub tab_drop_zone_border: ColorDef,
561    /// Settings UI selected item background
562    #[serde(default = "default_settings_selected_bg")]
563    pub settings_selected_bg: ColorDef,
564    /// Settings UI selected item foreground (text on selected background)
565    #[serde(default = "default_settings_selected_fg")]
566    pub settings_selected_fg: ColorDef,
567    /// File status: added file color in file explorer (falls back to diagnostic.info_fg)
568    #[serde(default)]
569    pub file_status_added_fg: Option<ColorDef>,
570    /// File status: modified file color in file explorer (falls back to diagnostic.warning_fg)
571    #[serde(default)]
572    pub file_status_modified_fg: Option<ColorDef>,
573    /// File status: deleted file color in file explorer (falls back to diagnostic.error_fg)
574    #[serde(default)]
575    pub file_status_deleted_fg: Option<ColorDef>,
576    /// File status: renamed file color in file explorer (falls back to diagnostic.info_fg)
577    #[serde(default)]
578    pub file_status_renamed_fg: Option<ColorDef>,
579    /// File status: untracked file color in file explorer (falls back to diagnostic.hint_fg)
580    #[serde(default)]
581    pub file_status_untracked_fg: Option<ColorDef>,
582    /// File status: conflicted file color in file explorer (falls back to diagnostic.error_fg)
583    #[serde(default)]
584    pub file_status_conflicted_fg: Option<ColorDef>,
585}
586
587// Default tab close hover color (for backward compatibility with existing themes)
588// Default tab colors (for minimal themes)
589fn default_tab_active_fg() -> ColorDef {
590    ColorDef::Named("Yellow".to_string())
591}
592fn default_tab_active_bg() -> ColorDef {
593    ColorDef::Named("Blue".to_string())
594}
595fn default_tab_inactive_fg() -> ColorDef {
596    ColorDef::Named("White".to_string())
597}
598fn default_tab_inactive_bg() -> ColorDef {
599    ColorDef::Named("DarkGray".to_string())
600}
601fn default_tab_separator_bg() -> ColorDef {
602    ColorDef::Named("Black".to_string())
603}
604fn default_tab_close_hover_fg() -> ColorDef {
605    ColorDef::Rgb(255, 100, 100) // Red-ish color for close button hover
606}
607fn default_tab_hover_bg() -> ColorDef {
608    ColorDef::Rgb(70, 70, 75) // Slightly lighter than inactive tab bg for hover
609}
610
611// Default menu colors (for backward compatibility with existing themes)
612fn default_menu_bg() -> ColorDef {
613    ColorDef::Rgb(60, 60, 65)
614}
615fn default_menu_fg() -> ColorDef {
616    ColorDef::Rgb(220, 220, 220)
617}
618fn default_menu_active_bg() -> ColorDef {
619    ColorDef::Rgb(60, 60, 60)
620}
621fn default_menu_active_fg() -> ColorDef {
622    ColorDef::Rgb(255, 255, 255)
623}
624fn default_menu_dropdown_bg() -> ColorDef {
625    ColorDef::Rgb(50, 50, 50)
626}
627fn default_menu_dropdown_fg() -> ColorDef {
628    ColorDef::Rgb(220, 220, 220)
629}
630fn default_menu_highlight_bg() -> ColorDef {
631    ColorDef::Rgb(70, 130, 180)
632}
633fn default_menu_highlight_fg() -> ColorDef {
634    ColorDef::Rgb(255, 255, 255)
635}
636fn default_menu_border_fg() -> ColorDef {
637    ColorDef::Rgb(100, 100, 100)
638}
639fn default_menu_separator_fg() -> ColorDef {
640    ColorDef::Rgb(80, 80, 80)
641}
642fn default_menu_hover_bg() -> ColorDef {
643    ColorDef::Rgb(55, 55, 55)
644}
645fn default_menu_hover_fg() -> ColorDef {
646    ColorDef::Rgb(255, 255, 255)
647}
648fn default_menu_disabled_fg() -> ColorDef {
649    ColorDef::Rgb(100, 100, 100) // Gray for disabled items
650}
651fn default_menu_disabled_bg() -> ColorDef {
652    ColorDef::Rgb(50, 50, 50) // Same as dropdown bg
653}
654// Default status bar colors
655fn default_status_bar_fg() -> ColorDef {
656    ColorDef::Named("White".to_string())
657}
658fn default_status_bar_bg() -> ColorDef {
659    ColorDef::Named("DarkGray".to_string())
660}
661
662// Default prompt colors
663fn default_prompt_fg() -> ColorDef {
664    ColorDef::Named("White".to_string())
665}
666fn default_prompt_bg() -> ColorDef {
667    ColorDef::Named("Black".to_string())
668}
669fn default_prompt_selection_fg() -> ColorDef {
670    ColorDef::Named("White".to_string())
671}
672fn default_prompt_selection_bg() -> ColorDef {
673    ColorDef::Rgb(58, 79, 120)
674}
675
676// Default popup colors
677fn default_popup_border_fg() -> ColorDef {
678    ColorDef::Named("Gray".to_string())
679}
680fn default_popup_bg() -> ColorDef {
681    ColorDef::Rgb(30, 30, 30)
682}
683fn default_popup_selection_bg() -> ColorDef {
684    ColorDef::Rgb(58, 79, 120)
685}
686fn default_popup_selection_fg() -> ColorDef {
687    ColorDef::Rgb(255, 255, 255) // White text on selected popup item
688}
689fn default_popup_text_fg() -> ColorDef {
690    ColorDef::Named("White".to_string())
691}
692
693// Default suggestion colors
694fn default_suggestion_bg() -> ColorDef {
695    ColorDef::Rgb(30, 30, 30)
696}
697fn default_suggestion_selected_bg() -> ColorDef {
698    ColorDef::Rgb(58, 79, 120)
699}
700
701// Default help colors
702fn default_help_bg() -> ColorDef {
703    ColorDef::Named("Black".to_string())
704}
705fn default_help_fg() -> ColorDef {
706    ColorDef::Named("White".to_string())
707}
708fn default_help_key_fg() -> ColorDef {
709    ColorDef::Named("Cyan".to_string())
710}
711fn default_help_separator_fg() -> ColorDef {
712    ColorDef::Named("DarkGray".to_string())
713}
714fn default_help_indicator_fg() -> ColorDef {
715    ColorDef::Named("Red".to_string())
716}
717fn default_help_indicator_bg() -> ColorDef {
718    ColorDef::Named("Black".to_string())
719}
720
721fn default_inline_code_bg() -> ColorDef {
722    ColorDef::Named("DarkGray".to_string())
723}
724
725// Default split separator colors
726fn default_split_separator_fg() -> ColorDef {
727    ColorDef::Rgb(100, 100, 100)
728}
729fn default_split_separator_hover_fg() -> ColorDef {
730    ColorDef::Rgb(100, 149, 237) // Cornflower blue for visibility
731}
732fn default_scrollbar_track_fg() -> ColorDef {
733    ColorDef::Named("DarkGray".to_string())
734}
735fn default_scrollbar_thumb_fg() -> ColorDef {
736    ColorDef::Named("Gray".to_string())
737}
738fn default_scrollbar_track_hover_fg() -> ColorDef {
739    ColorDef::Named("Gray".to_string())
740}
741fn default_scrollbar_thumb_hover_fg() -> ColorDef {
742    ColorDef::Named("White".to_string())
743}
744fn default_compose_margin_bg() -> ColorDef {
745    ColorDef::Rgb(18, 18, 18) // Darker than editor_bg for "desk" effect
746}
747fn default_semantic_highlight_bg() -> ColorDef {
748    ColorDef::Rgb(60, 60, 80) // Subtle dark highlight for word occurrences
749}
750fn default_terminal_bg() -> ColorDef {
751    ColorDef::Named("Default".to_string()) // Use terminal's default background (preserves transparency)
752}
753fn default_terminal_fg() -> ColorDef {
754    ColorDef::Named("Default".to_string()) // Use terminal's default foreground
755}
756fn default_status_warning_indicator_bg() -> ColorDef {
757    ColorDef::Rgb(181, 137, 0) // Solarized yellow/amber - noticeable but not harsh
758}
759fn default_status_warning_indicator_fg() -> ColorDef {
760    ColorDef::Rgb(0, 0, 0) // Black text on amber background
761}
762fn default_status_error_indicator_bg() -> ColorDef {
763    ColorDef::Rgb(220, 50, 47) // Solarized red - clearly an error
764}
765fn default_status_error_indicator_fg() -> ColorDef {
766    ColorDef::Rgb(255, 255, 255) // White text on red background
767}
768fn default_status_warning_indicator_hover_bg() -> ColorDef {
769    ColorDef::Rgb(211, 167, 30) // Lighter amber for hover
770}
771fn default_status_warning_indicator_hover_fg() -> ColorDef {
772    ColorDef::Rgb(0, 0, 0) // Black text on hover
773}
774fn default_status_error_indicator_hover_bg() -> ColorDef {
775    ColorDef::Rgb(250, 80, 77) // Lighter red for hover
776}
777fn default_status_error_indicator_hover_fg() -> ColorDef {
778    ColorDef::Rgb(255, 255, 255) // White text on hover
779}
780fn default_tab_drop_zone_bg() -> ColorDef {
781    ColorDef::Rgb(70, 130, 180) // Steel blue with transparency effect
782}
783fn default_tab_drop_zone_border() -> ColorDef {
784    ColorDef::Rgb(100, 149, 237) // Cornflower blue for border
785}
786fn default_settings_selected_bg() -> ColorDef {
787    ColorDef::Rgb(60, 60, 70) // Subtle highlight for selected settings item
788}
789fn default_settings_selected_fg() -> ColorDef {
790    ColorDef::Rgb(255, 255, 255) // White text on selected background
791}
792/// Search result highlighting colors
793#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
794pub struct SearchColors {
795    /// Search match background color
796    #[serde(default = "default_search_match_bg")]
797    pub match_bg: ColorDef,
798    /// Search match text color
799    #[serde(default = "default_search_match_fg")]
800    pub match_fg: ColorDef,
801    /// Background color for jump labels (e.g. flash plugin labels).
802    /// Should be visually distinct from `match_bg` so labels stand
803    /// out against highlighted matches.  Default: bright magenta.
804    #[serde(default = "default_search_label_bg")]
805    pub label_bg: ColorDef,
806    /// Foreground color for jump labels.  Should be high contrast
807    /// against `label_bg` so the single label letter is unambiguous
808    /// even on small terminal cells.  Default: white.
809    #[serde(default = "default_search_label_fg")]
810    pub label_fg: ColorDef,
811}
812
813// Default search colors
814fn default_search_match_bg() -> ColorDef {
815    ColorDef::Rgb(100, 100, 20)
816}
817fn default_search_match_fg() -> ColorDef {
818    ColorDef::Rgb(255, 255, 255)
819}
820// Mirrors flash.nvim's default FlashLabel (links to Substitute, which
821// is a magenta-family colour in most colorschemes).  The pairing is
822// chosen so labels pop visually distinct from `search.match_bg`
823// (typically yellow / orange).
824fn default_search_label_bg() -> ColorDef {
825    ColorDef::Rgb(199, 78, 189)
826}
827fn default_search_label_fg() -> ColorDef {
828    ColorDef::Rgb(255, 255, 255)
829}
830
831/// LSP diagnostic colors (errors, warnings, etc.)
832#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
833pub struct DiagnosticColors {
834    /// Error message text color
835    #[serde(default = "default_diagnostic_error_fg")]
836    pub error_fg: ColorDef,
837    /// Error highlight background
838    #[serde(default = "default_diagnostic_error_bg")]
839    pub error_bg: ColorDef,
840    /// Warning message text color
841    #[serde(default = "default_diagnostic_warning_fg")]
842    pub warning_fg: ColorDef,
843    /// Warning highlight background
844    #[serde(default = "default_diagnostic_warning_bg")]
845    pub warning_bg: ColorDef,
846    /// Info message text color
847    #[serde(default = "default_diagnostic_info_fg")]
848    pub info_fg: ColorDef,
849    /// Info highlight background
850    #[serde(default = "default_diagnostic_info_bg")]
851    pub info_bg: ColorDef,
852    /// Hint message text color
853    #[serde(default = "default_diagnostic_hint_fg")]
854    pub hint_fg: ColorDef,
855    /// Hint highlight background
856    #[serde(default = "default_diagnostic_hint_bg")]
857    pub hint_bg: ColorDef,
858}
859
860// Default diagnostic colors
861fn default_diagnostic_error_fg() -> ColorDef {
862    ColorDef::Named("Red".to_string())
863}
864fn default_diagnostic_error_bg() -> ColorDef {
865    ColorDef::Rgb(60, 20, 20)
866}
867fn default_diagnostic_warning_fg() -> ColorDef {
868    ColorDef::Named("Yellow".to_string())
869}
870fn default_diagnostic_warning_bg() -> ColorDef {
871    ColorDef::Rgb(60, 50, 0)
872}
873fn default_diagnostic_info_fg() -> ColorDef {
874    ColorDef::Named("Blue".to_string())
875}
876fn default_diagnostic_info_bg() -> ColorDef {
877    ColorDef::Rgb(0, 30, 60)
878}
879fn default_diagnostic_hint_fg() -> ColorDef {
880    ColorDef::Named("Gray".to_string())
881}
882fn default_diagnostic_hint_bg() -> ColorDef {
883    ColorDef::Rgb(30, 30, 30)
884}
885
886/// Syntax highlighting colors
887#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
888pub struct SyntaxColors {
889    /// Language keywords (if, for, fn, etc.)
890    #[serde(default = "default_syntax_keyword")]
891    pub keyword: ColorDef,
892    /// String literals
893    #[serde(default = "default_syntax_string")]
894    pub string: ColorDef,
895    /// Code comments
896    #[serde(default = "default_syntax_comment")]
897    pub comment: ColorDef,
898    /// Function names
899    #[serde(default = "default_syntax_function")]
900    pub function: ColorDef,
901    /// Type names
902    #[serde(rename = "type", default = "default_syntax_type")]
903    pub type_: ColorDef,
904    /// Variable names
905    #[serde(default = "default_syntax_variable")]
906    pub variable: ColorDef,
907    /// Constants and literals
908    #[serde(default = "default_syntax_constant")]
909    pub constant: ColorDef,
910    /// Operators (+, -, =, etc.)
911    #[serde(default = "default_syntax_operator")]
912    pub operator: ColorDef,
913    /// Punctuation brackets ({, }, (, ), [, ])
914    #[serde(default = "default_syntax_punctuation_bracket")]
915    pub punctuation_bracket: ColorDef,
916    /// Punctuation delimiters (;, ,, .)
917    #[serde(default = "default_syntax_punctuation_delimiter")]
918    pub punctuation_delimiter: ColorDef,
919}
920
921// Default syntax colors (VSCode Dark+ inspired)
922fn default_syntax_keyword() -> ColorDef {
923    ColorDef::Rgb(86, 156, 214)
924}
925fn default_syntax_string() -> ColorDef {
926    ColorDef::Rgb(206, 145, 120)
927}
928fn default_syntax_comment() -> ColorDef {
929    ColorDef::Rgb(106, 153, 85)
930}
931fn default_syntax_function() -> ColorDef {
932    ColorDef::Rgb(220, 220, 170)
933}
934fn default_syntax_type() -> ColorDef {
935    ColorDef::Rgb(78, 201, 176)
936}
937fn default_syntax_variable() -> ColorDef {
938    ColorDef::Rgb(156, 220, 254)
939}
940fn default_syntax_constant() -> ColorDef {
941    ColorDef::Rgb(79, 193, 255)
942}
943fn default_syntax_operator() -> ColorDef {
944    ColorDef::Rgb(212, 212, 212)
945}
946fn default_syntax_punctuation_bracket() -> ColorDef {
947    ColorDef::Rgb(212, 212, 212) // default foreground — brackets blend with text
948}
949fn default_syntax_punctuation_delimiter() -> ColorDef {
950    ColorDef::Rgb(212, 212, 212) // default foreground — delimiters blend with text
951}
952
953/// Comprehensive theme structure with all UI colors
954#[derive(Debug, Clone)]
955pub struct Theme {
956    /// Theme name (e.g., "dark", "light", "high-contrast")
957    pub name: String,
958
959    // Editor colors
960    pub editor_bg: Color,
961    pub editor_fg: Color,
962    pub cursor: Color,
963    pub inactive_cursor: Color,
964    pub selection_bg: Color,
965    pub current_line_bg: Color,
966    pub line_number_fg: Color,
967    pub line_number_bg: Color,
968
969    /// Background color for rows past end-of-file
970    pub after_eof_bg: Color,
971
972    // Vertical ruler color
973    pub ruler_bg: Color,
974
975    // Whitespace indicator color (tab arrows, space dots)
976    pub whitespace_indicator_fg: Color,
977
978    // Diff highlighting colors
979    pub diff_add_bg: Color,
980    pub diff_remove_bg: Color,
981    pub diff_modify_bg: Color,
982    /// Brighter background for inline diff highlighting on added content
983    pub diff_add_highlight_bg: Color,
984    /// Brighter background for inline diff highlighting on removed content
985    pub diff_remove_highlight_bg: Color,
986
987    // UI element colors
988    pub tab_active_fg: Color,
989    pub tab_active_bg: Color,
990    pub tab_inactive_fg: Color,
991    pub tab_inactive_bg: Color,
992    pub tab_separator_bg: Color,
993    pub tab_close_hover_fg: Color,
994    pub tab_hover_bg: Color,
995
996    // Menu bar colors
997    pub menu_bg: Color,
998    pub menu_fg: Color,
999    pub menu_active_bg: Color,
1000    pub menu_active_fg: Color,
1001    pub menu_dropdown_bg: Color,
1002    pub menu_dropdown_fg: Color,
1003    pub menu_highlight_bg: Color,
1004    pub menu_highlight_fg: Color,
1005    pub menu_border_fg: Color,
1006    pub menu_separator_fg: Color,
1007    pub menu_hover_bg: Color,
1008    pub menu_hover_fg: Color,
1009    pub menu_disabled_fg: Color,
1010    pub menu_disabled_bg: Color,
1011
1012    pub status_bar_fg: Color,
1013    pub status_bar_bg: Color,
1014    /// Status bar palette shortcut hint colors (default: same as status bar)
1015    pub status_palette_fg: Color,
1016    pub status_palette_bg: Color,
1017    /// Status bar LSP indicator colors when running (default: same as status bar)
1018    pub status_lsp_on_fg: Color,
1019    pub status_lsp_on_bg: Color,
1020    pub prompt_fg: Color,
1021    pub prompt_bg: Color,
1022    pub prompt_selection_fg: Color,
1023    pub prompt_selection_bg: Color,
1024
1025    pub popup_border_fg: Color,
1026    pub popup_bg: Color,
1027    pub popup_selection_bg: Color,
1028    pub popup_selection_fg: Color,
1029    pub popup_text_fg: Color,
1030
1031    pub suggestion_bg: Color,
1032    pub suggestion_selected_bg: Color,
1033
1034    pub help_bg: Color,
1035    pub help_fg: Color,
1036    pub help_key_fg: Color,
1037    pub help_separator_fg: Color,
1038
1039    pub help_indicator_fg: Color,
1040    pub help_indicator_bg: Color,
1041
1042    /// Background color for inline code in help popups
1043    pub inline_code_bg: Color,
1044
1045    pub split_separator_fg: Color,
1046    pub split_separator_hover_fg: Color,
1047
1048    // Scrollbar colors
1049    pub scrollbar_track_fg: Color,
1050    pub scrollbar_thumb_fg: Color,
1051    pub scrollbar_track_hover_fg: Color,
1052    pub scrollbar_thumb_hover_fg: Color,
1053
1054    // Compose mode colors
1055    pub compose_margin_bg: Color,
1056
1057    // Semantic highlighting (word under cursor)
1058    pub semantic_highlight_bg: Color,
1059
1060    // Terminal colors (for embedded terminal buffers)
1061    pub terminal_bg: Color,
1062    pub terminal_fg: Color,
1063
1064    // Status bar warning/error indicator colors
1065    pub status_warning_indicator_bg: Color,
1066    pub status_warning_indicator_fg: Color,
1067    pub status_error_indicator_bg: Color,
1068    pub status_error_indicator_fg: Color,
1069    pub status_warning_indicator_hover_bg: Color,
1070    pub status_warning_indicator_hover_fg: Color,
1071    pub status_error_indicator_hover_bg: Color,
1072    pub status_error_indicator_hover_fg: Color,
1073
1074    // Tab drag-and-drop colors
1075    pub tab_drop_zone_bg: Color,
1076    pub tab_drop_zone_border: Color,
1077
1078    // Settings UI colors
1079    pub settings_selected_bg: Color,
1080    pub settings_selected_fg: Color,
1081
1082    // File status colors (git status indicators in file explorer)
1083    pub file_status_added_fg: Color,
1084    pub file_status_modified_fg: Color,
1085    pub file_status_deleted_fg: Color,
1086    pub file_status_renamed_fg: Color,
1087    pub file_status_untracked_fg: Color,
1088    pub file_status_conflicted_fg: Color,
1089
1090    // Search colors
1091    pub search_match_bg: Color,
1092    pub search_match_fg: Color,
1093    pub search_label_bg: Color,
1094    pub search_label_fg: Color,
1095
1096    // Diagnostic colors
1097    pub diagnostic_error_fg: Color,
1098    pub diagnostic_error_bg: Color,
1099    pub diagnostic_warning_fg: Color,
1100    pub diagnostic_warning_bg: Color,
1101    pub diagnostic_info_fg: Color,
1102    pub diagnostic_info_bg: Color,
1103    pub diagnostic_hint_fg: Color,
1104    pub diagnostic_hint_bg: Color,
1105
1106    // Syntax highlighting colors
1107    pub syntax_keyword: Color,
1108    pub syntax_string: Color,
1109    pub syntax_comment: Color,
1110    pub syntax_function: Color,
1111    pub syntax_type: Color,
1112    pub syntax_variable: Color,
1113    pub syntax_constant: Color,
1114    pub syntax_operator: Color,
1115    pub syntax_punctuation_bracket: Color,
1116    pub syntax_punctuation_delimiter: Color,
1117}
1118
1119impl From<ThemeFile> for Theme {
1120    fn from(file: ThemeFile) -> Self {
1121        Self {
1122            name: file.name,
1123            editor_bg: file.editor.bg.clone().into(),
1124            editor_fg: file.editor.fg.into(),
1125            cursor: file.editor.cursor.into(),
1126            inactive_cursor: file.editor.inactive_cursor.into(),
1127            selection_bg: file.editor.selection_bg.into(),
1128            current_line_bg: file.editor.current_line_bg.into(),
1129            line_number_fg: file.editor.line_number_fg.into(),
1130            line_number_bg: file.editor.line_number_bg.into(),
1131            // Use explicit override if provided, otherwise derive a subtle
1132            // contrasting shade from the editor background.
1133            after_eof_bg: file
1134                .editor
1135                .after_eof_bg
1136                .clone()
1137                .map(|c| c.into())
1138                .unwrap_or_else(|| shade_toward_contrast(file.editor.bg.clone().into(), 10)),
1139            ruler_bg: file.editor.ruler_bg.into(),
1140            whitespace_indicator_fg: file.editor.whitespace_indicator_fg.into(),
1141            diff_add_bg: file.editor.diff_add_bg.clone().into(),
1142            diff_remove_bg: file.editor.diff_remove_bg.clone().into(),
1143            diff_modify_bg: file.editor.diff_modify_bg.into(),
1144            // Use explicit override if provided, otherwise brighten from base
1145            diff_add_highlight_bg: file
1146                .editor
1147                .diff_add_highlight_bg
1148                .map(|c| c.into())
1149                .unwrap_or_else(|| brighten_color(file.editor.diff_add_bg.into(), 40)),
1150            diff_remove_highlight_bg: file
1151                .editor
1152                .diff_remove_highlight_bg
1153                .map(|c| c.into())
1154                .unwrap_or_else(|| brighten_color(file.editor.diff_remove_bg.into(), 40)),
1155            tab_active_fg: file.ui.tab_active_fg.into(),
1156            tab_active_bg: file.ui.tab_active_bg.into(),
1157            tab_inactive_fg: file.ui.tab_inactive_fg.into(),
1158            tab_inactive_bg: file.ui.tab_inactive_bg.into(),
1159            tab_separator_bg: file.ui.tab_separator_bg.into(),
1160            tab_close_hover_fg: file.ui.tab_close_hover_fg.into(),
1161            tab_hover_bg: file.ui.tab_hover_bg.into(),
1162            menu_bg: file.ui.menu_bg.into(),
1163            menu_fg: file.ui.menu_fg.into(),
1164            menu_active_bg: file.ui.menu_active_bg.into(),
1165            menu_active_fg: file.ui.menu_active_fg.into(),
1166            menu_dropdown_bg: file.ui.menu_dropdown_bg.into(),
1167            menu_dropdown_fg: file.ui.menu_dropdown_fg.into(),
1168            menu_highlight_bg: file.ui.menu_highlight_bg.into(),
1169            menu_highlight_fg: file.ui.menu_highlight_fg.into(),
1170            menu_border_fg: file.ui.menu_border_fg.into(),
1171            menu_separator_fg: file.ui.menu_separator_fg.into(),
1172            menu_hover_bg: file.ui.menu_hover_bg.into(),
1173            menu_hover_fg: file.ui.menu_hover_fg.into(),
1174            menu_disabled_fg: file.ui.menu_disabled_fg.into(),
1175            menu_disabled_bg: file.ui.menu_disabled_bg.into(),
1176            status_bar_fg: file.ui.status_bar_fg.clone().into(),
1177            status_bar_bg: file.ui.status_bar_bg.clone().into(),
1178            status_palette_fg: file
1179                .ui
1180                .status_palette_fg
1181                .clone()
1182                .map(|c| c.into())
1183                .unwrap_or_else(|| file.ui.status_bar_fg.clone().into()),
1184            status_palette_bg: file
1185                .ui
1186                .status_palette_bg
1187                .clone()
1188                .map(|c| c.into())
1189                .unwrap_or_else(|| file.ui.status_bar_bg.clone().into()),
1190            status_lsp_on_fg: file
1191                .ui
1192                .status_lsp_on_fg
1193                .clone()
1194                .map(|c| c.into())
1195                .unwrap_or_else(|| file.ui.status_bar_fg.clone().into()),
1196            status_lsp_on_bg: file
1197                .ui
1198                .status_lsp_on_bg
1199                .clone()
1200                .map(|c| c.into())
1201                .unwrap_or_else(|| file.ui.status_bar_bg.clone().into()),
1202            prompt_fg: file.ui.prompt_fg.into(),
1203            prompt_bg: file.ui.prompt_bg.into(),
1204            prompt_selection_fg: file.ui.prompt_selection_fg.into(),
1205            prompt_selection_bg: file.ui.prompt_selection_bg.into(),
1206            popup_border_fg: file.ui.popup_border_fg.into(),
1207            popup_bg: file.ui.popup_bg.into(),
1208            popup_selection_bg: file.ui.popup_selection_bg.into(),
1209            popup_selection_fg: file.ui.popup_selection_fg.into(),
1210            popup_text_fg: file.ui.popup_text_fg.into(),
1211            suggestion_bg: file.ui.suggestion_bg.into(),
1212            suggestion_selected_bg: file.ui.suggestion_selected_bg.into(),
1213            help_bg: file.ui.help_bg.into(),
1214            help_fg: file.ui.help_fg.into(),
1215            help_key_fg: file.ui.help_key_fg.into(),
1216            help_separator_fg: file.ui.help_separator_fg.into(),
1217            help_indicator_fg: file.ui.help_indicator_fg.into(),
1218            help_indicator_bg: file.ui.help_indicator_bg.into(),
1219            inline_code_bg: file.ui.inline_code_bg.into(),
1220            split_separator_fg: file.ui.split_separator_fg.into(),
1221            split_separator_hover_fg: file.ui.split_separator_hover_fg.into(),
1222            scrollbar_track_fg: file.ui.scrollbar_track_fg.into(),
1223            scrollbar_thumb_fg: file.ui.scrollbar_thumb_fg.into(),
1224            scrollbar_track_hover_fg: file.ui.scrollbar_track_hover_fg.into(),
1225            scrollbar_thumb_hover_fg: file.ui.scrollbar_thumb_hover_fg.into(),
1226            compose_margin_bg: file.ui.compose_margin_bg.into(),
1227            semantic_highlight_bg: file.ui.semantic_highlight_bg.into(),
1228            terminal_bg: file.ui.terminal_bg.into(),
1229            terminal_fg: file.ui.terminal_fg.into(),
1230            status_warning_indicator_bg: file.ui.status_warning_indicator_bg.into(),
1231            status_warning_indicator_fg: file.ui.status_warning_indicator_fg.into(),
1232            status_error_indicator_bg: file.ui.status_error_indicator_bg.into(),
1233            status_error_indicator_fg: file.ui.status_error_indicator_fg.into(),
1234            status_warning_indicator_hover_bg: file.ui.status_warning_indicator_hover_bg.into(),
1235            status_warning_indicator_hover_fg: file.ui.status_warning_indicator_hover_fg.into(),
1236            status_error_indicator_hover_bg: file.ui.status_error_indicator_hover_bg.into(),
1237            status_error_indicator_hover_fg: file.ui.status_error_indicator_hover_fg.into(),
1238            tab_drop_zone_bg: file.ui.tab_drop_zone_bg.into(),
1239            tab_drop_zone_border: file.ui.tab_drop_zone_border.into(),
1240            settings_selected_bg: file.ui.settings_selected_bg.into(),
1241            settings_selected_fg: file.ui.settings_selected_fg.into(),
1242            file_status_added_fg: file
1243                .ui
1244                .file_status_added_fg
1245                .map(|c| c.into())
1246                .unwrap_or_else(|| file.diagnostic.info_fg.clone().into()),
1247            file_status_modified_fg: file
1248                .ui
1249                .file_status_modified_fg
1250                .map(|c| c.into())
1251                .unwrap_or_else(|| file.diagnostic.warning_fg.clone().into()),
1252            file_status_deleted_fg: file
1253                .ui
1254                .file_status_deleted_fg
1255                .map(|c| c.into())
1256                .unwrap_or_else(|| file.diagnostic.error_fg.clone().into()),
1257            file_status_renamed_fg: file
1258                .ui
1259                .file_status_renamed_fg
1260                .map(|c| c.into())
1261                .unwrap_or_else(|| file.diagnostic.info_fg.clone().into()),
1262            file_status_untracked_fg: file
1263                .ui
1264                .file_status_untracked_fg
1265                .map(|c| c.into())
1266                .unwrap_or_else(|| file.diagnostic.hint_fg.clone().into()),
1267            file_status_conflicted_fg: file
1268                .ui
1269                .file_status_conflicted_fg
1270                .map(|c| c.into())
1271                .unwrap_or_else(|| file.diagnostic.error_fg.clone().into()),
1272            search_match_bg: file.search.match_bg.into(),
1273            search_match_fg: file.search.match_fg.into(),
1274            search_label_bg: file.search.label_bg.into(),
1275            search_label_fg: file.search.label_fg.into(),
1276            diagnostic_error_fg: file.diagnostic.error_fg.into(),
1277            diagnostic_error_bg: file.diagnostic.error_bg.into(),
1278            diagnostic_warning_fg: file.diagnostic.warning_fg.into(),
1279            diagnostic_warning_bg: file.diagnostic.warning_bg.into(),
1280            diagnostic_info_fg: file.diagnostic.info_fg.into(),
1281            diagnostic_info_bg: file.diagnostic.info_bg.into(),
1282            diagnostic_hint_fg: file.diagnostic.hint_fg.into(),
1283            diagnostic_hint_bg: file.diagnostic.hint_bg.into(),
1284            syntax_keyword: file.syntax.keyword.into(),
1285            syntax_string: file.syntax.string.into(),
1286            syntax_comment: file.syntax.comment.into(),
1287            syntax_function: file.syntax.function.into(),
1288            syntax_type: file.syntax.type_.into(),
1289            syntax_variable: file.syntax.variable.into(),
1290            syntax_constant: file.syntax.constant.into(),
1291            syntax_operator: file.syntax.operator.into(),
1292            syntax_punctuation_bracket: file.syntax.punctuation_bracket.into(),
1293            syntax_punctuation_delimiter: file.syntax.punctuation_delimiter.into(),
1294        }
1295    }
1296}
1297
1298impl From<Theme> for ThemeFile {
1299    fn from(theme: Theme) -> Self {
1300        Self {
1301            name: theme.name,
1302            editor: EditorColors {
1303                bg: theme.editor_bg.into(),
1304                fg: theme.editor_fg.into(),
1305                cursor: theme.cursor.into(),
1306                inactive_cursor: theme.inactive_cursor.into(),
1307                selection_bg: theme.selection_bg.into(),
1308                current_line_bg: theme.current_line_bg.into(),
1309                line_number_fg: theme.line_number_fg.into(),
1310                line_number_bg: theme.line_number_bg.into(),
1311                diff_add_bg: theme.diff_add_bg.into(),
1312                diff_remove_bg: theme.diff_remove_bg.into(),
1313                diff_add_highlight_bg: Some(theme.diff_add_highlight_bg.into()),
1314                diff_remove_highlight_bg: Some(theme.diff_remove_highlight_bg.into()),
1315                diff_modify_bg: theme.diff_modify_bg.into(),
1316                ruler_bg: theme.ruler_bg.into(),
1317                whitespace_indicator_fg: theme.whitespace_indicator_fg.into(),
1318                after_eof_bg: Some(theme.after_eof_bg.into()),
1319            },
1320            ui: UiColors {
1321                tab_active_fg: theme.tab_active_fg.into(),
1322                tab_active_bg: theme.tab_active_bg.into(),
1323                tab_inactive_fg: theme.tab_inactive_fg.into(),
1324                tab_inactive_bg: theme.tab_inactive_bg.into(),
1325                tab_separator_bg: theme.tab_separator_bg.into(),
1326                tab_close_hover_fg: theme.tab_close_hover_fg.into(),
1327                tab_hover_bg: theme.tab_hover_bg.into(),
1328                menu_bg: theme.menu_bg.into(),
1329                menu_fg: theme.menu_fg.into(),
1330                menu_active_bg: theme.menu_active_bg.into(),
1331                menu_active_fg: theme.menu_active_fg.into(),
1332                menu_dropdown_bg: theme.menu_dropdown_bg.into(),
1333                menu_dropdown_fg: theme.menu_dropdown_fg.into(),
1334                menu_highlight_bg: theme.menu_highlight_bg.into(),
1335                menu_highlight_fg: theme.menu_highlight_fg.into(),
1336                menu_border_fg: theme.menu_border_fg.into(),
1337                menu_separator_fg: theme.menu_separator_fg.into(),
1338                menu_hover_bg: theme.menu_hover_bg.into(),
1339                menu_hover_fg: theme.menu_hover_fg.into(),
1340                menu_disabled_fg: theme.menu_disabled_fg.into(),
1341                menu_disabled_bg: theme.menu_disabled_bg.into(),
1342                status_bar_fg: theme.status_bar_fg.into(),
1343                status_bar_bg: theme.status_bar_bg.into(),
1344                status_palette_fg: Some(theme.status_palette_fg.into()),
1345                status_palette_bg: Some(theme.status_palette_bg.into()),
1346                status_lsp_on_fg: Some(theme.status_lsp_on_fg.into()),
1347                status_lsp_on_bg: Some(theme.status_lsp_on_bg.into()),
1348                prompt_fg: theme.prompt_fg.into(),
1349                prompt_bg: theme.prompt_bg.into(),
1350                prompt_selection_fg: theme.prompt_selection_fg.into(),
1351                prompt_selection_bg: theme.prompt_selection_bg.into(),
1352                popup_border_fg: theme.popup_border_fg.into(),
1353                popup_bg: theme.popup_bg.into(),
1354                popup_selection_bg: theme.popup_selection_bg.into(),
1355                popup_selection_fg: theme.popup_selection_fg.into(),
1356                popup_text_fg: theme.popup_text_fg.into(),
1357                suggestion_bg: theme.suggestion_bg.into(),
1358                suggestion_selected_bg: theme.suggestion_selected_bg.into(),
1359                help_bg: theme.help_bg.into(),
1360                help_fg: theme.help_fg.into(),
1361                help_key_fg: theme.help_key_fg.into(),
1362                help_separator_fg: theme.help_separator_fg.into(),
1363                help_indicator_fg: theme.help_indicator_fg.into(),
1364                help_indicator_bg: theme.help_indicator_bg.into(),
1365                inline_code_bg: theme.inline_code_bg.into(),
1366                split_separator_fg: theme.split_separator_fg.into(),
1367                split_separator_hover_fg: theme.split_separator_hover_fg.into(),
1368                scrollbar_track_fg: theme.scrollbar_track_fg.into(),
1369                scrollbar_thumb_fg: theme.scrollbar_thumb_fg.into(),
1370                scrollbar_track_hover_fg: theme.scrollbar_track_hover_fg.into(),
1371                scrollbar_thumb_hover_fg: theme.scrollbar_thumb_hover_fg.into(),
1372                compose_margin_bg: theme.compose_margin_bg.into(),
1373                semantic_highlight_bg: theme.semantic_highlight_bg.into(),
1374                terminal_bg: theme.terminal_bg.into(),
1375                terminal_fg: theme.terminal_fg.into(),
1376                status_warning_indicator_bg: theme.status_warning_indicator_bg.into(),
1377                status_warning_indicator_fg: theme.status_warning_indicator_fg.into(),
1378                status_error_indicator_bg: theme.status_error_indicator_bg.into(),
1379                status_error_indicator_fg: theme.status_error_indicator_fg.into(),
1380                status_warning_indicator_hover_bg: theme.status_warning_indicator_hover_bg.into(),
1381                status_warning_indicator_hover_fg: theme.status_warning_indicator_hover_fg.into(),
1382                status_error_indicator_hover_bg: theme.status_error_indicator_hover_bg.into(),
1383                status_error_indicator_hover_fg: theme.status_error_indicator_hover_fg.into(),
1384                tab_drop_zone_bg: theme.tab_drop_zone_bg.into(),
1385                tab_drop_zone_border: theme.tab_drop_zone_border.into(),
1386                settings_selected_bg: theme.settings_selected_bg.into(),
1387                settings_selected_fg: theme.settings_selected_fg.into(),
1388                file_status_added_fg: Some(theme.file_status_added_fg.into()),
1389                file_status_modified_fg: Some(theme.file_status_modified_fg.into()),
1390                file_status_deleted_fg: Some(theme.file_status_deleted_fg.into()),
1391                file_status_renamed_fg: Some(theme.file_status_renamed_fg.into()),
1392                file_status_untracked_fg: Some(theme.file_status_untracked_fg.into()),
1393                file_status_conflicted_fg: Some(theme.file_status_conflicted_fg.into()),
1394            },
1395            search: SearchColors {
1396                match_bg: theme.search_match_bg.into(),
1397                match_fg: theme.search_match_fg.into(),
1398                label_bg: theme.search_label_bg.into(),
1399                label_fg: theme.search_label_fg.into(),
1400            },
1401            diagnostic: DiagnosticColors {
1402                error_fg: theme.diagnostic_error_fg.into(),
1403                error_bg: theme.diagnostic_error_bg.into(),
1404                warning_fg: theme.diagnostic_warning_fg.into(),
1405                warning_bg: theme.diagnostic_warning_bg.into(),
1406                info_fg: theme.diagnostic_info_fg.into(),
1407                info_bg: theme.diagnostic_info_bg.into(),
1408                hint_fg: theme.diagnostic_hint_fg.into(),
1409                hint_bg: theme.diagnostic_hint_bg.into(),
1410            },
1411            syntax: SyntaxColors {
1412                keyword: theme.syntax_keyword.into(),
1413                string: theme.syntax_string.into(),
1414                comment: theme.syntax_comment.into(),
1415                function: theme.syntax_function.into(),
1416                type_: theme.syntax_type.into(),
1417                variable: theme.syntax_variable.into(),
1418                constant: theme.syntax_constant.into(),
1419                operator: theme.syntax_operator.into(),
1420                punctuation_bracket: theme.syntax_punctuation_bracket.into(),
1421                punctuation_delimiter: theme.syntax_punctuation_delimiter.into(),
1422            },
1423        }
1424    }
1425}
1426
1427impl Theme {
1428    /// Returns `true` when the theme has a light background.
1429    ///
1430    /// Uses the relative luminance of `editor_bg` (perceived brightness).
1431    /// A threshold of 0.5 separates dark from light; for `Color::Reset` or
1432    /// unresolvable colors, falls back to `false` (dark).
1433    pub fn is_light(&self) -> bool {
1434        if let Some((r, g, b)) = color_to_rgb(self.editor_bg) {
1435            // sRGB relative luminance (ITU-R BT.709)
1436            let lum = 0.2126 * (r as f64 / 255.0)
1437                + 0.7152 * (g as f64 / 255.0)
1438                + 0.0722 * (b as f64 / 255.0);
1439            lum > 0.5
1440        } else {
1441            false
1442        }
1443    }
1444
1445    /// Load a builtin theme by name (no I/O, uses embedded JSON).
1446    pub fn load_builtin(name: &str) -> Option<Self> {
1447        BUILTIN_THEMES
1448            .iter()
1449            .find(|t| t.name == name)
1450            .and_then(|t| serde_json::from_str::<ThemeFile>(t.json).ok())
1451            .map(|tf| tf.into())
1452    }
1453
1454    /// Parse theme from JSON string (no I/O).
1455    pub fn from_json(json: &str) -> Result<Self, String> {
1456        let theme_file: ThemeFile =
1457            serde_json::from_str(json).map_err(|e| format!("Failed to parse theme JSON: {}", e))?;
1458        Ok(theme_file.into())
1459    }
1460
1461    /// Resolve a theme key to a Color.
1462    ///
1463    /// Theme keys use dot notation: "section.field"
1464    /// Examples:
1465    /// - "ui.status_bar_fg" -> status_bar_fg
1466    /// - "editor.selection_bg" -> selection_bg
1467    /// - "syntax.keyword" -> syntax_keyword
1468    /// - "diagnostic.error_fg" -> diagnostic_error_fg
1469    ///
1470    /// Returns None if the key is not recognized.
1471    pub fn resolve_theme_key(&self, key: &str) -> Option<Color> {
1472        // Parse "section.field" format
1473        let parts: Vec<&str> = key.split('.').collect();
1474        if parts.len() != 2 {
1475            return None;
1476        }
1477
1478        let (section, field) = (parts[0], parts[1]);
1479
1480        match section {
1481            "editor" => match field {
1482                "bg" => Some(self.editor_bg),
1483                "fg" => Some(self.editor_fg),
1484                "cursor" => Some(self.cursor),
1485                "inactive_cursor" => Some(self.inactive_cursor),
1486                "selection_bg" => Some(self.selection_bg),
1487                "current_line_bg" => Some(self.current_line_bg),
1488                "line_number_fg" => Some(self.line_number_fg),
1489                "line_number_bg" => Some(self.line_number_bg),
1490                "diff_add_bg" => Some(self.diff_add_bg),
1491                "diff_remove_bg" => Some(self.diff_remove_bg),
1492                "diff_modify_bg" => Some(self.diff_modify_bg),
1493                "ruler_bg" => Some(self.ruler_bg),
1494                "whitespace_indicator_fg" => Some(self.whitespace_indicator_fg),
1495                _ => None,
1496            },
1497            "ui" => match field {
1498                "tab_active_fg" => Some(self.tab_active_fg),
1499                "tab_active_bg" => Some(self.tab_active_bg),
1500                "tab_inactive_fg" => Some(self.tab_inactive_fg),
1501                "tab_inactive_bg" => Some(self.tab_inactive_bg),
1502                "status_bar_fg" => Some(self.status_bar_fg),
1503                "status_bar_bg" => Some(self.status_bar_bg),
1504                "status_palette_fg" => Some(self.status_palette_fg),
1505                "status_palette_bg" => Some(self.status_palette_bg),
1506                "status_lsp_on_fg" => Some(self.status_lsp_on_fg),
1507                "status_lsp_on_bg" => Some(self.status_lsp_on_bg),
1508                "prompt_fg" => Some(self.prompt_fg),
1509                "prompt_bg" => Some(self.prompt_bg),
1510                "prompt_selection_fg" => Some(self.prompt_selection_fg),
1511                "prompt_selection_bg" => Some(self.prompt_selection_bg),
1512                "popup_bg" => Some(self.popup_bg),
1513                "popup_border_fg" => Some(self.popup_border_fg),
1514                "popup_selection_bg" => Some(self.popup_selection_bg),
1515                "popup_selection_fg" => Some(self.popup_selection_fg),
1516                "popup_text_fg" => Some(self.popup_text_fg),
1517                "menu_bg" => Some(self.menu_bg),
1518                "menu_fg" => Some(self.menu_fg),
1519                "menu_active_bg" => Some(self.menu_active_bg),
1520                "menu_active_fg" => Some(self.menu_active_fg),
1521                "help_bg" => Some(self.help_bg),
1522                "help_fg" => Some(self.help_fg),
1523                "help_key_fg" => Some(self.help_key_fg),
1524                "split_separator_fg" => Some(self.split_separator_fg),
1525                "scrollbar_thumb_fg" => Some(self.scrollbar_thumb_fg),
1526                "semantic_highlight_bg" => Some(self.semantic_highlight_bg),
1527                "file_status_added_fg" => Some(self.file_status_added_fg),
1528                "file_status_modified_fg" => Some(self.file_status_modified_fg),
1529                "file_status_deleted_fg" => Some(self.file_status_deleted_fg),
1530                "file_status_renamed_fg" => Some(self.file_status_renamed_fg),
1531                "file_status_untracked_fg" => Some(self.file_status_untracked_fg),
1532                "file_status_conflicted_fg" => Some(self.file_status_conflicted_fg),
1533                _ => None,
1534            },
1535            "syntax" => match field {
1536                "keyword" => Some(self.syntax_keyword),
1537                "string" => Some(self.syntax_string),
1538                "comment" => Some(self.syntax_comment),
1539                "function" => Some(self.syntax_function),
1540                "type" => Some(self.syntax_type),
1541                "variable" => Some(self.syntax_variable),
1542                "constant" => Some(self.syntax_constant),
1543                "operator" => Some(self.syntax_operator),
1544                "punctuation_bracket" => Some(self.syntax_punctuation_bracket),
1545                "punctuation_delimiter" => Some(self.syntax_punctuation_delimiter),
1546                _ => None,
1547            },
1548            "diagnostic" => match field {
1549                "error_fg" => Some(self.diagnostic_error_fg),
1550                "error_bg" => Some(self.diagnostic_error_bg),
1551                "warning_fg" => Some(self.diagnostic_warning_fg),
1552                "warning_bg" => Some(self.diagnostic_warning_bg),
1553                "info_fg" => Some(self.diagnostic_info_fg),
1554                "info_bg" => Some(self.diagnostic_info_bg),
1555                "hint_fg" => Some(self.diagnostic_hint_fg),
1556                "hint_bg" => Some(self.diagnostic_hint_bg),
1557                _ => None,
1558            },
1559            "search" => match field {
1560                "match_bg" => Some(self.search_match_bg),
1561                "match_fg" => Some(self.search_match_fg),
1562                "label_bg" => Some(self.search_label_bg),
1563                "label_fg" => Some(self.search_label_fg),
1564                _ => None,
1565            },
1566            _ => None,
1567        }
1568    }
1569
1570    /// Mutable companion to [`resolve_theme_key`]. Keep the two matches in
1571    /// lock-step: any key readable by `resolve_theme_key` should also be
1572    /// writable here, and vice versa.
1573    pub fn resolve_theme_key_mut(&mut self, key: &str) -> Option<&mut Color> {
1574        let parts: Vec<&str> = key.split('.').collect();
1575        if parts.len() != 2 {
1576            return None;
1577        }
1578        let (section, field) = (parts[0], parts[1]);
1579        match section {
1580            "editor" => match field {
1581                "bg" => Some(&mut self.editor_bg),
1582                "fg" => Some(&mut self.editor_fg),
1583                "cursor" => Some(&mut self.cursor),
1584                "inactive_cursor" => Some(&mut self.inactive_cursor),
1585                "selection_bg" => Some(&mut self.selection_bg),
1586                "current_line_bg" => Some(&mut self.current_line_bg),
1587                "line_number_fg" => Some(&mut self.line_number_fg),
1588                "line_number_bg" => Some(&mut self.line_number_bg),
1589                "diff_add_bg" => Some(&mut self.diff_add_bg),
1590                "diff_remove_bg" => Some(&mut self.diff_remove_bg),
1591                "diff_modify_bg" => Some(&mut self.diff_modify_bg),
1592                "ruler_bg" => Some(&mut self.ruler_bg),
1593                "whitespace_indicator_fg" => Some(&mut self.whitespace_indicator_fg),
1594                _ => None,
1595            },
1596            "ui" => match field {
1597                "tab_active_fg" => Some(&mut self.tab_active_fg),
1598                "tab_active_bg" => Some(&mut self.tab_active_bg),
1599                "tab_inactive_fg" => Some(&mut self.tab_inactive_fg),
1600                "tab_inactive_bg" => Some(&mut self.tab_inactive_bg),
1601                "status_bar_fg" => Some(&mut self.status_bar_fg),
1602                "status_bar_bg" => Some(&mut self.status_bar_bg),
1603                "status_palette_fg" => Some(&mut self.status_palette_fg),
1604                "status_palette_bg" => Some(&mut self.status_palette_bg),
1605                "status_lsp_on_fg" => Some(&mut self.status_lsp_on_fg),
1606                "status_lsp_on_bg" => Some(&mut self.status_lsp_on_bg),
1607                "prompt_fg" => Some(&mut self.prompt_fg),
1608                "prompt_bg" => Some(&mut self.prompt_bg),
1609                "prompt_selection_fg" => Some(&mut self.prompt_selection_fg),
1610                "prompt_selection_bg" => Some(&mut self.prompt_selection_bg),
1611                "popup_bg" => Some(&mut self.popup_bg),
1612                "popup_border_fg" => Some(&mut self.popup_border_fg),
1613                "popup_selection_bg" => Some(&mut self.popup_selection_bg),
1614                "popup_selection_fg" => Some(&mut self.popup_selection_fg),
1615                "popup_text_fg" => Some(&mut self.popup_text_fg),
1616                "menu_bg" => Some(&mut self.menu_bg),
1617                "menu_fg" => Some(&mut self.menu_fg),
1618                "menu_active_bg" => Some(&mut self.menu_active_bg),
1619                "menu_active_fg" => Some(&mut self.menu_active_fg),
1620                "help_bg" => Some(&mut self.help_bg),
1621                "help_fg" => Some(&mut self.help_fg),
1622                "help_key_fg" => Some(&mut self.help_key_fg),
1623                "split_separator_fg" => Some(&mut self.split_separator_fg),
1624                "scrollbar_thumb_fg" => Some(&mut self.scrollbar_thumb_fg),
1625                "semantic_highlight_bg" => Some(&mut self.semantic_highlight_bg),
1626                "file_status_added_fg" => Some(&mut self.file_status_added_fg),
1627                "file_status_modified_fg" => Some(&mut self.file_status_modified_fg),
1628                "file_status_deleted_fg" => Some(&mut self.file_status_deleted_fg),
1629                "file_status_renamed_fg" => Some(&mut self.file_status_renamed_fg),
1630                "file_status_untracked_fg" => Some(&mut self.file_status_untracked_fg),
1631                "file_status_conflicted_fg" => Some(&mut self.file_status_conflicted_fg),
1632                _ => None,
1633            },
1634            "syntax" => match field {
1635                "keyword" => Some(&mut self.syntax_keyword),
1636                "string" => Some(&mut self.syntax_string),
1637                "comment" => Some(&mut self.syntax_comment),
1638                "function" => Some(&mut self.syntax_function),
1639                "type" => Some(&mut self.syntax_type),
1640                "variable" => Some(&mut self.syntax_variable),
1641                "constant" => Some(&mut self.syntax_constant),
1642                "operator" => Some(&mut self.syntax_operator),
1643                "punctuation_bracket" => Some(&mut self.syntax_punctuation_bracket),
1644                "punctuation_delimiter" => Some(&mut self.syntax_punctuation_delimiter),
1645                _ => None,
1646            },
1647            "diagnostic" => match field {
1648                "error_fg" => Some(&mut self.diagnostic_error_fg),
1649                "error_bg" => Some(&mut self.diagnostic_error_bg),
1650                "warning_fg" => Some(&mut self.diagnostic_warning_fg),
1651                "warning_bg" => Some(&mut self.diagnostic_warning_bg),
1652                "info_fg" => Some(&mut self.diagnostic_info_fg),
1653                "info_bg" => Some(&mut self.diagnostic_info_bg),
1654                "hint_fg" => Some(&mut self.diagnostic_hint_fg),
1655                "hint_bg" => Some(&mut self.diagnostic_hint_bg),
1656                _ => None,
1657            },
1658            "search" => match field {
1659                "match_bg" => Some(&mut self.search_match_bg),
1660                "match_fg" => Some(&mut self.search_match_fg),
1661                "label_bg" => Some(&mut self.search_label_bg),
1662                "label_fg" => Some(&mut self.search_label_fg),
1663                _ => None,
1664            },
1665            _ => None,
1666        }
1667    }
1668
1669    /// Apply a map of `"section.field" -> Color` overrides to the running
1670    /// theme in-place. Returns the number of keys that matched a known
1671    /// theme field. Unknown keys are silently dropped so a typo in a fast
1672    /// animation loop doesn't crash the caller.
1673    pub fn override_colors<I, K>(&mut self, overrides: I) -> usize
1674    where
1675        I: IntoIterator<Item = (K, Color)>,
1676        K: AsRef<str>,
1677    {
1678        let mut applied = 0;
1679        for (key, color) in overrides {
1680            if let Some(slot) = self.resolve_theme_key_mut(key.as_ref()) {
1681                *slot = color;
1682                applied += 1;
1683            }
1684        }
1685        applied
1686    }
1687}
1688
1689// =============================================================================
1690// Theme Schema Generation for Plugin API
1691// =============================================================================
1692
1693/// Returns the raw JSON Schema for ThemeFile, generated by schemars.
1694/// The schema uses standard JSON Schema format with $ref for type references.
1695/// Plugins are responsible for parsing and resolving $ref references.
1696pub fn get_theme_schema() -> serde_json::Value {
1697    use schemars::schema_for;
1698    let schema = schema_for!(ThemeFile);
1699    serde_json::to_value(&schema).unwrap_or_default()
1700}
1701
1702/// Returns a map of built-in theme names to their JSON content.
1703pub fn get_builtin_themes() -> serde_json::Value {
1704    let mut map = serde_json::Map::new();
1705    for theme in BUILTIN_THEMES {
1706        map.insert(
1707            theme.name.to_string(),
1708            serde_json::Value::String(theme.json.to_string()),
1709        );
1710    }
1711    serde_json::Value::Object(map)
1712}
1713
1714#[cfg(test)]
1715mod tests {
1716    use super::*;
1717
1718    #[test]
1719    fn test_load_builtin_theme() {
1720        let dark = Theme::load_builtin(THEME_DARK).expect("Dark theme must exist");
1721        assert_eq!(dark.name, THEME_DARK);
1722
1723        let light = Theme::load_builtin(THEME_LIGHT).expect("Light theme must exist");
1724        assert_eq!(light.name, THEME_LIGHT);
1725
1726        let high_contrast =
1727            Theme::load_builtin(THEME_HIGH_CONTRAST).expect("High contrast theme must exist");
1728        assert_eq!(high_contrast.name, THEME_HIGH_CONTRAST);
1729    }
1730
1731    #[test]
1732    fn test_builtin_themes_match_schema() {
1733        for theme in BUILTIN_THEMES {
1734            let _: ThemeFile = serde_json::from_str(theme.json)
1735                .unwrap_or_else(|_| panic!("Theme '{}' does not match schema", theme.name));
1736        }
1737    }
1738
1739    #[test]
1740    fn test_from_json() {
1741        let json = r#"{"name":"test","editor":{},"ui":{},"search":{},"diagnostic":{},"syntax":{}}"#;
1742        let theme = Theme::from_json(json).expect("Should parse minimal theme");
1743        assert_eq!(theme.name, "test");
1744    }
1745
1746    #[test]
1747    fn test_default_reset_color() {
1748        // Test that "Default" maps to Color::Reset
1749        let color: Color = ColorDef::Named("Default".to_string()).into();
1750        assert_eq!(color, Color::Reset);
1751
1752        // Test that "Reset" also maps to Color::Reset
1753        let color: Color = ColorDef::Named("Reset".to_string()).into();
1754        assert_eq!(color, Color::Reset);
1755    }
1756
1757    #[test]
1758    fn test_file_status_colors_fall_back_to_diagnostic_colors() {
1759        // A theme with NO file_status_* keys should inherit from diagnostic colors
1760        let json = r#"{
1761            "name": "test-fallback",
1762            "editor": {},
1763            "ui": {},
1764            "search": {},
1765            "diagnostic": {
1766                "error_fg": [220, 50, 47],
1767                "warning_fg": [181, 137, 0],
1768                "info_fg": [38, 139, 210],
1769                "hint_fg": [101, 123, 131]
1770            },
1771            "syntax": {}
1772        }"#;
1773        let theme = Theme::from_json(json).expect("Should parse theme without file_status keys");
1774
1775        // Verify fallback: added/renamed -> info_fg
1776        assert_eq!(theme.file_status_added_fg, Color::Rgb(38, 139, 210));
1777        assert_eq!(theme.file_status_renamed_fg, Color::Rgb(38, 139, 210));
1778        // modified -> warning_fg
1779        assert_eq!(theme.file_status_modified_fg, Color::Rgb(181, 137, 0));
1780        // deleted/conflicted -> error_fg
1781        assert_eq!(theme.file_status_deleted_fg, Color::Rgb(220, 50, 47));
1782        assert_eq!(theme.file_status_conflicted_fg, Color::Rgb(220, 50, 47));
1783        // untracked -> hint_fg
1784        assert_eq!(theme.file_status_untracked_fg, Color::Rgb(101, 123, 131));
1785    }
1786
1787    #[test]
1788    fn test_file_status_colors_explicit_override() {
1789        // A theme WITH explicit file_status keys should use those, not the fallback
1790        let json = r#"{
1791            "name": "test-override",
1792            "editor": {},
1793            "ui": {
1794                "file_status_added_fg": [80, 250, 123],
1795                "file_status_modified_fg": [255, 184, 108]
1796            },
1797            "search": {},
1798            "diagnostic": {
1799                "info_fg": [38, 139, 210],
1800                "warning_fg": [181, 137, 0]
1801            },
1802            "syntax": {}
1803        }"#;
1804        let theme = Theme::from_json(json).expect("Should parse theme with file_status overrides");
1805
1806        // Explicit overrides should win
1807        assert_eq!(theme.file_status_added_fg, Color::Rgb(80, 250, 123));
1808        assert_eq!(theme.file_status_modified_fg, Color::Rgb(255, 184, 108));
1809        // Non-overridden should still fall back
1810        assert_eq!(theme.file_status_renamed_fg, Color::Rgb(38, 139, 210));
1811    }
1812
1813    #[test]
1814    fn test_file_status_colors_resolve_via_theme_key() {
1815        let json = r#"{
1816            "name": "test-resolve",
1817            "editor": {},
1818            "ui": {
1819                "file_status_added_fg": [80, 250, 123]
1820            },
1821            "search": {},
1822            "diagnostic": {
1823                "warning_fg": [181, 137, 0]
1824            },
1825            "syntax": {}
1826        }"#;
1827        let theme = Theme::from_json(json).expect("Should parse theme");
1828
1829        // Theme key resolution should work for file_status keys
1830        assert_eq!(
1831            theme.resolve_theme_key("ui.file_status_added_fg"),
1832            Some(Color::Rgb(80, 250, 123))
1833        );
1834        assert_eq!(
1835            theme.resolve_theme_key("ui.file_status_modified_fg"),
1836            Some(Color::Rgb(181, 137, 0))
1837        );
1838    }
1839
1840    #[test]
1841    fn override_colors_writes_known_keys_and_drops_unknowns() {
1842        let mut theme = Theme::load_builtin(THEME_DARK).expect("dark builtin");
1843        let applied = theme.override_colors([
1844            ("editor.bg".to_string(), Color::Rgb(10, 20, 30)),
1845            ("ui.status_bar_fg".to_string(), Color::Rgb(1, 2, 3)),
1846            ("does.not_exist".to_string(), Color::Rgb(9, 9, 9)),
1847            ("garbage_no_dot".to_string(), Color::Rgb(9, 9, 9)),
1848        ]);
1849        assert_eq!(applied, 2, "only the two valid keys should be applied");
1850        assert_eq!(
1851            theme.resolve_theme_key("editor.bg"),
1852            Some(Color::Rgb(10, 20, 30))
1853        );
1854        assert_eq!(
1855            theme.resolve_theme_key("ui.status_bar_fg"),
1856            Some(Color::Rgb(1, 2, 3))
1857        );
1858    }
1859
1860    #[test]
1861    fn resolve_theme_key_mut_matches_resolve_theme_key_domain() {
1862        // If a key resolves readably, it must also resolve as a mutable
1863        // slot — the two matches must stay in lock-step.
1864        let mut theme = Theme::load_builtin(THEME_DARK).expect("dark builtin");
1865        let probe = [
1866            "editor.bg",
1867            "editor.fg",
1868            "ui.status_bar_fg",
1869            "ui.tab_active_bg",
1870            "syntax.keyword",
1871            "diagnostic.error_fg",
1872            "search.match_bg",
1873        ];
1874        for key in probe {
1875            assert!(
1876                theme.resolve_theme_key(key).is_some(),
1877                "reader lost key {key}"
1878            );
1879            assert!(
1880                theme.resolve_theme_key_mut(key).is_some(),
1881                "mutator missing key {key}"
1882            );
1883        }
1884    }
1885
1886    #[test]
1887    fn test_all_builtin_themes_set_prominent_palette_indicator() {
1888        // Issue #1711: the Ctrl+P palette hint should be a *prominent*
1889        // accent drawn from each theme's own palette, not the neutral
1890        // status-bar colors. The fallback to status_bar_* exists for
1891        // user themes that don't opt in, but every shipped theme must
1892        // set explicit values that differ from the bar so the hint
1893        // pops as intended.
1894        for builtin in BUILTIN_THEMES {
1895            let theme = Theme::from_json(builtin.json)
1896                .unwrap_or_else(|e| panic!("Theme '{}' failed to parse: {}", builtin.name, e));
1897            assert!(
1898                theme.status_palette_fg != theme.status_bar_fg
1899                    || theme.status_palette_bg != theme.status_bar_bg,
1900                "Theme '{}' must set status_palette_fg/bg to a prominent \
1901                 accent distinct from status_bar_fg/bg",
1902                builtin.name
1903            );
1904        }
1905    }
1906
1907    #[test]
1908    fn test_all_builtin_themes_have_file_status_colors() {
1909        // Every builtin theme must produce valid file_status colors (via fallback or explicit)
1910        for builtin in BUILTIN_THEMES {
1911            let theme = Theme::from_json(builtin.json)
1912                .unwrap_or_else(|e| panic!("Theme '{}' failed to parse: {}", builtin.name, e));
1913
1914            // All six keys must resolve to Some via resolve_theme_key
1915            for key in &[
1916                "ui.file_status_added_fg",
1917                "ui.file_status_modified_fg",
1918                "ui.file_status_deleted_fg",
1919                "ui.file_status_renamed_fg",
1920                "ui.file_status_untracked_fg",
1921                "ui.file_status_conflicted_fg",
1922            ] {
1923                assert!(
1924                    theme.resolve_theme_key(key).is_some(),
1925                    "Theme '{}' missing resolution for '{}'",
1926                    builtin.name,
1927                    key
1928                );
1929            }
1930        }
1931    }
1932}