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 prompt text color
436    #[serde(default = "default_prompt_fg")]
437    pub prompt_fg: ColorDef,
438    /// Command prompt background
439    #[serde(default = "default_prompt_bg")]
440    pub prompt_bg: ColorDef,
441    /// Prompt selected text color
442    #[serde(default = "default_prompt_selection_fg")]
443    pub prompt_selection_fg: ColorDef,
444    /// Prompt selection background
445    #[serde(default = "default_prompt_selection_bg")]
446    pub prompt_selection_bg: ColorDef,
447    /// Popup window border color
448    #[serde(default = "default_popup_border_fg")]
449    pub popup_border_fg: ColorDef,
450    /// Popup window background
451    #[serde(default = "default_popup_bg")]
452    pub popup_bg: ColorDef,
453    /// Popup selected item background
454    #[serde(default = "default_popup_selection_bg")]
455    pub popup_selection_bg: ColorDef,
456    /// Popup selected item text color
457    #[serde(default = "default_popup_selection_fg")]
458    pub popup_selection_fg: ColorDef,
459    /// Popup window text color
460    #[serde(default = "default_popup_text_fg")]
461    pub popup_text_fg: ColorDef,
462    /// Autocomplete suggestion background
463    #[serde(default = "default_suggestion_bg")]
464    pub suggestion_bg: ColorDef,
465    /// Selected suggestion background
466    #[serde(default = "default_suggestion_selected_bg")]
467    pub suggestion_selected_bg: ColorDef,
468    /// Help panel background
469    #[serde(default = "default_help_bg")]
470    pub help_bg: ColorDef,
471    /// Help panel text color
472    #[serde(default = "default_help_fg")]
473    pub help_fg: ColorDef,
474    /// Help keybinding text color
475    #[serde(default = "default_help_key_fg")]
476    pub help_key_fg: ColorDef,
477    /// Help panel separator color
478    #[serde(default = "default_help_separator_fg")]
479    pub help_separator_fg: ColorDef,
480    /// Help indicator text color
481    #[serde(default = "default_help_indicator_fg")]
482    pub help_indicator_fg: ColorDef,
483    /// Help indicator background
484    #[serde(default = "default_help_indicator_bg")]
485    pub help_indicator_bg: ColorDef,
486    /// Inline code block background
487    #[serde(default = "default_inline_code_bg")]
488    pub inline_code_bg: ColorDef,
489    /// Split pane separator color
490    #[serde(default = "default_split_separator_fg")]
491    pub split_separator_fg: ColorDef,
492    /// Split separator hover color
493    #[serde(default = "default_split_separator_hover_fg")]
494    pub split_separator_hover_fg: ColorDef,
495    /// Scrollbar track color
496    #[serde(default = "default_scrollbar_track_fg")]
497    pub scrollbar_track_fg: ColorDef,
498    /// Scrollbar thumb color
499    #[serde(default = "default_scrollbar_thumb_fg")]
500    pub scrollbar_thumb_fg: ColorDef,
501    /// Scrollbar track hover color
502    #[serde(default = "default_scrollbar_track_hover_fg")]
503    pub scrollbar_track_hover_fg: ColorDef,
504    /// Scrollbar thumb hover color
505    #[serde(default = "default_scrollbar_thumb_hover_fg")]
506    pub scrollbar_thumb_hover_fg: ColorDef,
507    /// Compose mode margin background
508    #[serde(default = "default_compose_margin_bg")]
509    pub compose_margin_bg: ColorDef,
510    /// Word under cursor highlight
511    #[serde(default = "default_semantic_highlight_bg")]
512    pub semantic_highlight_bg: ColorDef,
513    /// Embedded terminal background (use Default for transparency)
514    #[serde(default = "default_terminal_bg")]
515    pub terminal_bg: ColorDef,
516    /// Embedded terminal default text color
517    #[serde(default = "default_terminal_fg")]
518    pub terminal_fg: ColorDef,
519    /// Warning indicator background in status bar
520    #[serde(default = "default_status_warning_indicator_bg")]
521    pub status_warning_indicator_bg: ColorDef,
522    /// Warning indicator text color in status bar
523    #[serde(default = "default_status_warning_indicator_fg")]
524    pub status_warning_indicator_fg: ColorDef,
525    /// Error indicator background in status bar
526    #[serde(default = "default_status_error_indicator_bg")]
527    pub status_error_indicator_bg: ColorDef,
528    /// Error indicator text color in status bar
529    #[serde(default = "default_status_error_indicator_fg")]
530    pub status_error_indicator_fg: ColorDef,
531    /// Warning indicator hover background
532    #[serde(default = "default_status_warning_indicator_hover_bg")]
533    pub status_warning_indicator_hover_bg: ColorDef,
534    /// Warning indicator hover text color
535    #[serde(default = "default_status_warning_indicator_hover_fg")]
536    pub status_warning_indicator_hover_fg: ColorDef,
537    /// Error indicator hover background
538    #[serde(default = "default_status_error_indicator_hover_bg")]
539    pub status_error_indicator_hover_bg: ColorDef,
540    /// Error indicator hover text color
541    #[serde(default = "default_status_error_indicator_hover_fg")]
542    pub status_error_indicator_hover_fg: ColorDef,
543    /// Tab drop zone background during drag
544    #[serde(default = "default_tab_drop_zone_bg")]
545    pub tab_drop_zone_bg: ColorDef,
546    /// Tab drop zone border during drag
547    #[serde(default = "default_tab_drop_zone_border")]
548    pub tab_drop_zone_border: ColorDef,
549    /// Settings UI selected item background
550    #[serde(default = "default_settings_selected_bg")]
551    pub settings_selected_bg: ColorDef,
552    /// Settings UI selected item foreground (text on selected background)
553    #[serde(default = "default_settings_selected_fg")]
554    pub settings_selected_fg: ColorDef,
555    /// File status: added file color in file explorer (falls back to diagnostic.info_fg)
556    #[serde(default)]
557    pub file_status_added_fg: Option<ColorDef>,
558    /// File status: modified file color in file explorer (falls back to diagnostic.warning_fg)
559    #[serde(default)]
560    pub file_status_modified_fg: Option<ColorDef>,
561    /// File status: deleted file color in file explorer (falls back to diagnostic.error_fg)
562    #[serde(default)]
563    pub file_status_deleted_fg: Option<ColorDef>,
564    /// File status: renamed file color in file explorer (falls back to diagnostic.info_fg)
565    #[serde(default)]
566    pub file_status_renamed_fg: Option<ColorDef>,
567    /// File status: untracked file color in file explorer (falls back to diagnostic.hint_fg)
568    #[serde(default)]
569    pub file_status_untracked_fg: Option<ColorDef>,
570    /// File status: conflicted file color in file explorer (falls back to diagnostic.error_fg)
571    #[serde(default)]
572    pub file_status_conflicted_fg: Option<ColorDef>,
573}
574
575// Default tab close hover color (for backward compatibility with existing themes)
576// Default tab colors (for minimal themes)
577fn default_tab_active_fg() -> ColorDef {
578    ColorDef::Named("Yellow".to_string())
579}
580fn default_tab_active_bg() -> ColorDef {
581    ColorDef::Named("Blue".to_string())
582}
583fn default_tab_inactive_fg() -> ColorDef {
584    ColorDef::Named("White".to_string())
585}
586fn default_tab_inactive_bg() -> ColorDef {
587    ColorDef::Named("DarkGray".to_string())
588}
589fn default_tab_separator_bg() -> ColorDef {
590    ColorDef::Named("Black".to_string())
591}
592fn default_tab_close_hover_fg() -> ColorDef {
593    ColorDef::Rgb(255, 100, 100) // Red-ish color for close button hover
594}
595fn default_tab_hover_bg() -> ColorDef {
596    ColorDef::Rgb(70, 70, 75) // Slightly lighter than inactive tab bg for hover
597}
598
599// Default menu colors (for backward compatibility with existing themes)
600fn default_menu_bg() -> ColorDef {
601    ColorDef::Rgb(60, 60, 65)
602}
603fn default_menu_fg() -> ColorDef {
604    ColorDef::Rgb(220, 220, 220)
605}
606fn default_menu_active_bg() -> ColorDef {
607    ColorDef::Rgb(60, 60, 60)
608}
609fn default_menu_active_fg() -> ColorDef {
610    ColorDef::Rgb(255, 255, 255)
611}
612fn default_menu_dropdown_bg() -> ColorDef {
613    ColorDef::Rgb(50, 50, 50)
614}
615fn default_menu_dropdown_fg() -> ColorDef {
616    ColorDef::Rgb(220, 220, 220)
617}
618fn default_menu_highlight_bg() -> ColorDef {
619    ColorDef::Rgb(70, 130, 180)
620}
621fn default_menu_highlight_fg() -> ColorDef {
622    ColorDef::Rgb(255, 255, 255)
623}
624fn default_menu_border_fg() -> ColorDef {
625    ColorDef::Rgb(100, 100, 100)
626}
627fn default_menu_separator_fg() -> ColorDef {
628    ColorDef::Rgb(80, 80, 80)
629}
630fn default_menu_hover_bg() -> ColorDef {
631    ColorDef::Rgb(55, 55, 55)
632}
633fn default_menu_hover_fg() -> ColorDef {
634    ColorDef::Rgb(255, 255, 255)
635}
636fn default_menu_disabled_fg() -> ColorDef {
637    ColorDef::Rgb(100, 100, 100) // Gray for disabled items
638}
639fn default_menu_disabled_bg() -> ColorDef {
640    ColorDef::Rgb(50, 50, 50) // Same as dropdown bg
641}
642// Default status bar colors
643fn default_status_bar_fg() -> ColorDef {
644    ColorDef::Named("White".to_string())
645}
646fn default_status_bar_bg() -> ColorDef {
647    ColorDef::Named("DarkGray".to_string())
648}
649
650// Default prompt colors
651fn default_prompt_fg() -> ColorDef {
652    ColorDef::Named("White".to_string())
653}
654fn default_prompt_bg() -> ColorDef {
655    ColorDef::Named("Black".to_string())
656}
657fn default_prompt_selection_fg() -> ColorDef {
658    ColorDef::Named("White".to_string())
659}
660fn default_prompt_selection_bg() -> ColorDef {
661    ColorDef::Rgb(58, 79, 120)
662}
663
664// Default popup colors
665fn default_popup_border_fg() -> ColorDef {
666    ColorDef::Named("Gray".to_string())
667}
668fn default_popup_bg() -> ColorDef {
669    ColorDef::Rgb(30, 30, 30)
670}
671fn default_popup_selection_bg() -> ColorDef {
672    ColorDef::Rgb(58, 79, 120)
673}
674fn default_popup_selection_fg() -> ColorDef {
675    ColorDef::Rgb(255, 255, 255) // White text on selected popup item
676}
677fn default_popup_text_fg() -> ColorDef {
678    ColorDef::Named("White".to_string())
679}
680
681// Default suggestion colors
682fn default_suggestion_bg() -> ColorDef {
683    ColorDef::Rgb(30, 30, 30)
684}
685fn default_suggestion_selected_bg() -> ColorDef {
686    ColorDef::Rgb(58, 79, 120)
687}
688
689// Default help colors
690fn default_help_bg() -> ColorDef {
691    ColorDef::Named("Black".to_string())
692}
693fn default_help_fg() -> ColorDef {
694    ColorDef::Named("White".to_string())
695}
696fn default_help_key_fg() -> ColorDef {
697    ColorDef::Named("Cyan".to_string())
698}
699fn default_help_separator_fg() -> ColorDef {
700    ColorDef::Named("DarkGray".to_string())
701}
702fn default_help_indicator_fg() -> ColorDef {
703    ColorDef::Named("Red".to_string())
704}
705fn default_help_indicator_bg() -> ColorDef {
706    ColorDef::Named("Black".to_string())
707}
708
709fn default_inline_code_bg() -> ColorDef {
710    ColorDef::Named("DarkGray".to_string())
711}
712
713// Default split separator colors
714fn default_split_separator_fg() -> ColorDef {
715    ColorDef::Rgb(100, 100, 100)
716}
717fn default_split_separator_hover_fg() -> ColorDef {
718    ColorDef::Rgb(100, 149, 237) // Cornflower blue for visibility
719}
720fn default_scrollbar_track_fg() -> ColorDef {
721    ColorDef::Named("DarkGray".to_string())
722}
723fn default_scrollbar_thumb_fg() -> ColorDef {
724    ColorDef::Named("Gray".to_string())
725}
726fn default_scrollbar_track_hover_fg() -> ColorDef {
727    ColorDef::Named("Gray".to_string())
728}
729fn default_scrollbar_thumb_hover_fg() -> ColorDef {
730    ColorDef::Named("White".to_string())
731}
732fn default_compose_margin_bg() -> ColorDef {
733    ColorDef::Rgb(18, 18, 18) // Darker than editor_bg for "desk" effect
734}
735fn default_semantic_highlight_bg() -> ColorDef {
736    ColorDef::Rgb(60, 60, 80) // Subtle dark highlight for word occurrences
737}
738fn default_terminal_bg() -> ColorDef {
739    ColorDef::Named("Default".to_string()) // Use terminal's default background (preserves transparency)
740}
741fn default_terminal_fg() -> ColorDef {
742    ColorDef::Named("Default".to_string()) // Use terminal's default foreground
743}
744fn default_status_warning_indicator_bg() -> ColorDef {
745    ColorDef::Rgb(181, 137, 0) // Solarized yellow/amber - noticeable but not harsh
746}
747fn default_status_warning_indicator_fg() -> ColorDef {
748    ColorDef::Rgb(0, 0, 0) // Black text on amber background
749}
750fn default_status_error_indicator_bg() -> ColorDef {
751    ColorDef::Rgb(220, 50, 47) // Solarized red - clearly an error
752}
753fn default_status_error_indicator_fg() -> ColorDef {
754    ColorDef::Rgb(255, 255, 255) // White text on red background
755}
756fn default_status_warning_indicator_hover_bg() -> ColorDef {
757    ColorDef::Rgb(211, 167, 30) // Lighter amber for hover
758}
759fn default_status_warning_indicator_hover_fg() -> ColorDef {
760    ColorDef::Rgb(0, 0, 0) // Black text on hover
761}
762fn default_status_error_indicator_hover_bg() -> ColorDef {
763    ColorDef::Rgb(250, 80, 77) // Lighter red for hover
764}
765fn default_status_error_indicator_hover_fg() -> ColorDef {
766    ColorDef::Rgb(255, 255, 255) // White text on hover
767}
768fn default_tab_drop_zone_bg() -> ColorDef {
769    ColorDef::Rgb(70, 130, 180) // Steel blue with transparency effect
770}
771fn default_tab_drop_zone_border() -> ColorDef {
772    ColorDef::Rgb(100, 149, 237) // Cornflower blue for border
773}
774fn default_settings_selected_bg() -> ColorDef {
775    ColorDef::Rgb(60, 60, 70) // Subtle highlight for selected settings item
776}
777fn default_settings_selected_fg() -> ColorDef {
778    ColorDef::Rgb(255, 255, 255) // White text on selected background
779}
780/// Search result highlighting colors
781#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
782pub struct SearchColors {
783    /// Search match background color
784    #[serde(default = "default_search_match_bg")]
785    pub match_bg: ColorDef,
786    /// Search match text color
787    #[serde(default = "default_search_match_fg")]
788    pub match_fg: ColorDef,
789    /// Background color for jump labels (e.g. flash plugin labels).
790    /// Should be visually distinct from `match_bg` so labels stand
791    /// out against highlighted matches.  Default: bright magenta.
792    #[serde(default = "default_search_label_bg")]
793    pub label_bg: ColorDef,
794    /// Foreground color for jump labels.  Should be high contrast
795    /// against `label_bg` so the single label letter is unambiguous
796    /// even on small terminal cells.  Default: white.
797    #[serde(default = "default_search_label_fg")]
798    pub label_fg: ColorDef,
799}
800
801// Default search colors
802fn default_search_match_bg() -> ColorDef {
803    ColorDef::Rgb(100, 100, 20)
804}
805fn default_search_match_fg() -> ColorDef {
806    ColorDef::Rgb(255, 255, 255)
807}
808// Mirrors flash.nvim's default FlashLabel (links to Substitute, which
809// is a magenta-family colour in most colorschemes).  The pairing is
810// chosen so labels pop visually distinct from `search.match_bg`
811// (typically yellow / orange).
812fn default_search_label_bg() -> ColorDef {
813    ColorDef::Rgb(199, 78, 189)
814}
815fn default_search_label_fg() -> ColorDef {
816    ColorDef::Rgb(255, 255, 255)
817}
818
819/// LSP diagnostic colors (errors, warnings, etc.)
820#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
821pub struct DiagnosticColors {
822    /// Error message text color
823    #[serde(default = "default_diagnostic_error_fg")]
824    pub error_fg: ColorDef,
825    /// Error highlight background
826    #[serde(default = "default_diagnostic_error_bg")]
827    pub error_bg: ColorDef,
828    /// Warning message text color
829    #[serde(default = "default_diagnostic_warning_fg")]
830    pub warning_fg: ColorDef,
831    /// Warning highlight background
832    #[serde(default = "default_diagnostic_warning_bg")]
833    pub warning_bg: ColorDef,
834    /// Info message text color
835    #[serde(default = "default_diagnostic_info_fg")]
836    pub info_fg: ColorDef,
837    /// Info highlight background
838    #[serde(default = "default_diagnostic_info_bg")]
839    pub info_bg: ColorDef,
840    /// Hint message text color
841    #[serde(default = "default_diagnostic_hint_fg")]
842    pub hint_fg: ColorDef,
843    /// Hint highlight background
844    #[serde(default = "default_diagnostic_hint_bg")]
845    pub hint_bg: ColorDef,
846}
847
848// Default diagnostic colors
849fn default_diagnostic_error_fg() -> ColorDef {
850    ColorDef::Named("Red".to_string())
851}
852fn default_diagnostic_error_bg() -> ColorDef {
853    ColorDef::Rgb(60, 20, 20)
854}
855fn default_diagnostic_warning_fg() -> ColorDef {
856    ColorDef::Named("Yellow".to_string())
857}
858fn default_diagnostic_warning_bg() -> ColorDef {
859    ColorDef::Rgb(60, 50, 0)
860}
861fn default_diagnostic_info_fg() -> ColorDef {
862    ColorDef::Named("Blue".to_string())
863}
864fn default_diagnostic_info_bg() -> ColorDef {
865    ColorDef::Rgb(0, 30, 60)
866}
867fn default_diagnostic_hint_fg() -> ColorDef {
868    ColorDef::Named("Gray".to_string())
869}
870fn default_diagnostic_hint_bg() -> ColorDef {
871    ColorDef::Rgb(30, 30, 30)
872}
873
874/// Syntax highlighting colors
875#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
876pub struct SyntaxColors {
877    /// Language keywords (if, for, fn, etc.)
878    #[serde(default = "default_syntax_keyword")]
879    pub keyword: ColorDef,
880    /// String literals
881    #[serde(default = "default_syntax_string")]
882    pub string: ColorDef,
883    /// Code comments
884    #[serde(default = "default_syntax_comment")]
885    pub comment: ColorDef,
886    /// Function names
887    #[serde(default = "default_syntax_function")]
888    pub function: ColorDef,
889    /// Type names
890    #[serde(rename = "type", default = "default_syntax_type")]
891    pub type_: ColorDef,
892    /// Variable names
893    #[serde(default = "default_syntax_variable")]
894    pub variable: ColorDef,
895    /// Constants and literals
896    #[serde(default = "default_syntax_constant")]
897    pub constant: ColorDef,
898    /// Operators (+, -, =, etc.)
899    #[serde(default = "default_syntax_operator")]
900    pub operator: ColorDef,
901    /// Punctuation brackets ({, }, (, ), [, ])
902    #[serde(default = "default_syntax_punctuation_bracket")]
903    pub punctuation_bracket: ColorDef,
904    /// Punctuation delimiters (;, ,, .)
905    #[serde(default = "default_syntax_punctuation_delimiter")]
906    pub punctuation_delimiter: ColorDef,
907}
908
909// Default syntax colors (VSCode Dark+ inspired)
910fn default_syntax_keyword() -> ColorDef {
911    ColorDef::Rgb(86, 156, 214)
912}
913fn default_syntax_string() -> ColorDef {
914    ColorDef::Rgb(206, 145, 120)
915}
916fn default_syntax_comment() -> ColorDef {
917    ColorDef::Rgb(106, 153, 85)
918}
919fn default_syntax_function() -> ColorDef {
920    ColorDef::Rgb(220, 220, 170)
921}
922fn default_syntax_type() -> ColorDef {
923    ColorDef::Rgb(78, 201, 176)
924}
925fn default_syntax_variable() -> ColorDef {
926    ColorDef::Rgb(156, 220, 254)
927}
928fn default_syntax_constant() -> ColorDef {
929    ColorDef::Rgb(79, 193, 255)
930}
931fn default_syntax_operator() -> ColorDef {
932    ColorDef::Rgb(212, 212, 212)
933}
934fn default_syntax_punctuation_bracket() -> ColorDef {
935    ColorDef::Rgb(212, 212, 212) // default foreground — brackets blend with text
936}
937fn default_syntax_punctuation_delimiter() -> ColorDef {
938    ColorDef::Rgb(212, 212, 212) // default foreground — delimiters blend with text
939}
940
941/// Comprehensive theme structure with all UI colors
942#[derive(Debug, Clone)]
943pub struct Theme {
944    /// Theme name (e.g., "dark", "light", "high-contrast")
945    pub name: String,
946
947    // Editor colors
948    pub editor_bg: Color,
949    pub editor_fg: Color,
950    pub cursor: Color,
951    pub inactive_cursor: Color,
952    pub selection_bg: Color,
953    pub current_line_bg: Color,
954    pub line_number_fg: Color,
955    pub line_number_bg: Color,
956
957    /// Background color for rows past end-of-file
958    pub after_eof_bg: Color,
959
960    // Vertical ruler color
961    pub ruler_bg: Color,
962
963    // Whitespace indicator color (tab arrows, space dots)
964    pub whitespace_indicator_fg: Color,
965
966    // Diff highlighting colors
967    pub diff_add_bg: Color,
968    pub diff_remove_bg: Color,
969    pub diff_modify_bg: Color,
970    /// Brighter background for inline diff highlighting on added content
971    pub diff_add_highlight_bg: Color,
972    /// Brighter background for inline diff highlighting on removed content
973    pub diff_remove_highlight_bg: Color,
974
975    // UI element colors
976    pub tab_active_fg: Color,
977    pub tab_active_bg: Color,
978    pub tab_inactive_fg: Color,
979    pub tab_inactive_bg: Color,
980    pub tab_separator_bg: Color,
981    pub tab_close_hover_fg: Color,
982    pub tab_hover_bg: Color,
983
984    // Menu bar colors
985    pub menu_bg: Color,
986    pub menu_fg: Color,
987    pub menu_active_bg: Color,
988    pub menu_active_fg: Color,
989    pub menu_dropdown_bg: Color,
990    pub menu_dropdown_fg: Color,
991    pub menu_highlight_bg: Color,
992    pub menu_highlight_fg: Color,
993    pub menu_border_fg: Color,
994    pub menu_separator_fg: Color,
995    pub menu_hover_bg: Color,
996    pub menu_hover_fg: Color,
997    pub menu_disabled_fg: Color,
998    pub menu_disabled_bg: Color,
999
1000    pub status_bar_fg: Color,
1001    pub status_bar_bg: Color,
1002    pub prompt_fg: Color,
1003    pub prompt_bg: Color,
1004    pub prompt_selection_fg: Color,
1005    pub prompt_selection_bg: Color,
1006
1007    pub popup_border_fg: Color,
1008    pub popup_bg: Color,
1009    pub popup_selection_bg: Color,
1010    pub popup_selection_fg: Color,
1011    pub popup_text_fg: Color,
1012
1013    pub suggestion_bg: Color,
1014    pub suggestion_selected_bg: Color,
1015
1016    pub help_bg: Color,
1017    pub help_fg: Color,
1018    pub help_key_fg: Color,
1019    pub help_separator_fg: Color,
1020
1021    pub help_indicator_fg: Color,
1022    pub help_indicator_bg: Color,
1023
1024    /// Background color for inline code in help popups
1025    pub inline_code_bg: Color,
1026
1027    pub split_separator_fg: Color,
1028    pub split_separator_hover_fg: Color,
1029
1030    // Scrollbar colors
1031    pub scrollbar_track_fg: Color,
1032    pub scrollbar_thumb_fg: Color,
1033    pub scrollbar_track_hover_fg: Color,
1034    pub scrollbar_thumb_hover_fg: Color,
1035
1036    // Compose mode colors
1037    pub compose_margin_bg: Color,
1038
1039    // Semantic highlighting (word under cursor)
1040    pub semantic_highlight_bg: Color,
1041
1042    // Terminal colors (for embedded terminal buffers)
1043    pub terminal_bg: Color,
1044    pub terminal_fg: Color,
1045
1046    // Status bar warning/error indicator colors
1047    pub status_warning_indicator_bg: Color,
1048    pub status_warning_indicator_fg: Color,
1049    pub status_error_indicator_bg: Color,
1050    pub status_error_indicator_fg: Color,
1051    pub status_warning_indicator_hover_bg: Color,
1052    pub status_warning_indicator_hover_fg: Color,
1053    pub status_error_indicator_hover_bg: Color,
1054    pub status_error_indicator_hover_fg: Color,
1055
1056    // Tab drag-and-drop colors
1057    pub tab_drop_zone_bg: Color,
1058    pub tab_drop_zone_border: Color,
1059
1060    // Settings UI colors
1061    pub settings_selected_bg: Color,
1062    pub settings_selected_fg: Color,
1063
1064    // File status colors (git status indicators in file explorer)
1065    pub file_status_added_fg: Color,
1066    pub file_status_modified_fg: Color,
1067    pub file_status_deleted_fg: Color,
1068    pub file_status_renamed_fg: Color,
1069    pub file_status_untracked_fg: Color,
1070    pub file_status_conflicted_fg: Color,
1071
1072    // Search colors
1073    pub search_match_bg: Color,
1074    pub search_match_fg: Color,
1075    pub search_label_bg: Color,
1076    pub search_label_fg: Color,
1077
1078    // Diagnostic colors
1079    pub diagnostic_error_fg: Color,
1080    pub diagnostic_error_bg: Color,
1081    pub diagnostic_warning_fg: Color,
1082    pub diagnostic_warning_bg: Color,
1083    pub diagnostic_info_fg: Color,
1084    pub diagnostic_info_bg: Color,
1085    pub diagnostic_hint_fg: Color,
1086    pub diagnostic_hint_bg: Color,
1087
1088    // Syntax highlighting colors
1089    pub syntax_keyword: Color,
1090    pub syntax_string: Color,
1091    pub syntax_comment: Color,
1092    pub syntax_function: Color,
1093    pub syntax_type: Color,
1094    pub syntax_variable: Color,
1095    pub syntax_constant: Color,
1096    pub syntax_operator: Color,
1097    pub syntax_punctuation_bracket: Color,
1098    pub syntax_punctuation_delimiter: Color,
1099}
1100
1101impl From<ThemeFile> for Theme {
1102    fn from(file: ThemeFile) -> Self {
1103        Self {
1104            name: file.name,
1105            editor_bg: file.editor.bg.clone().into(),
1106            editor_fg: file.editor.fg.into(),
1107            cursor: file.editor.cursor.into(),
1108            inactive_cursor: file.editor.inactive_cursor.into(),
1109            selection_bg: file.editor.selection_bg.into(),
1110            current_line_bg: file.editor.current_line_bg.into(),
1111            line_number_fg: file.editor.line_number_fg.into(),
1112            line_number_bg: file.editor.line_number_bg.into(),
1113            // Use explicit override if provided, otherwise derive a subtle
1114            // contrasting shade from the editor background.
1115            after_eof_bg: file
1116                .editor
1117                .after_eof_bg
1118                .clone()
1119                .map(|c| c.into())
1120                .unwrap_or_else(|| shade_toward_contrast(file.editor.bg.clone().into(), 10)),
1121            ruler_bg: file.editor.ruler_bg.into(),
1122            whitespace_indicator_fg: file.editor.whitespace_indicator_fg.into(),
1123            diff_add_bg: file.editor.diff_add_bg.clone().into(),
1124            diff_remove_bg: file.editor.diff_remove_bg.clone().into(),
1125            diff_modify_bg: file.editor.diff_modify_bg.into(),
1126            // Use explicit override if provided, otherwise brighten from base
1127            diff_add_highlight_bg: file
1128                .editor
1129                .diff_add_highlight_bg
1130                .map(|c| c.into())
1131                .unwrap_or_else(|| brighten_color(file.editor.diff_add_bg.into(), 40)),
1132            diff_remove_highlight_bg: file
1133                .editor
1134                .diff_remove_highlight_bg
1135                .map(|c| c.into())
1136                .unwrap_or_else(|| brighten_color(file.editor.diff_remove_bg.into(), 40)),
1137            tab_active_fg: file.ui.tab_active_fg.into(),
1138            tab_active_bg: file.ui.tab_active_bg.into(),
1139            tab_inactive_fg: file.ui.tab_inactive_fg.into(),
1140            tab_inactive_bg: file.ui.tab_inactive_bg.into(),
1141            tab_separator_bg: file.ui.tab_separator_bg.into(),
1142            tab_close_hover_fg: file.ui.tab_close_hover_fg.into(),
1143            tab_hover_bg: file.ui.tab_hover_bg.into(),
1144            menu_bg: file.ui.menu_bg.into(),
1145            menu_fg: file.ui.menu_fg.into(),
1146            menu_active_bg: file.ui.menu_active_bg.into(),
1147            menu_active_fg: file.ui.menu_active_fg.into(),
1148            menu_dropdown_bg: file.ui.menu_dropdown_bg.into(),
1149            menu_dropdown_fg: file.ui.menu_dropdown_fg.into(),
1150            menu_highlight_bg: file.ui.menu_highlight_bg.into(),
1151            menu_highlight_fg: file.ui.menu_highlight_fg.into(),
1152            menu_border_fg: file.ui.menu_border_fg.into(),
1153            menu_separator_fg: file.ui.menu_separator_fg.into(),
1154            menu_hover_bg: file.ui.menu_hover_bg.into(),
1155            menu_hover_fg: file.ui.menu_hover_fg.into(),
1156            menu_disabled_fg: file.ui.menu_disabled_fg.into(),
1157            menu_disabled_bg: file.ui.menu_disabled_bg.into(),
1158            status_bar_fg: file.ui.status_bar_fg.into(),
1159            status_bar_bg: file.ui.status_bar_bg.into(),
1160            prompt_fg: file.ui.prompt_fg.into(),
1161            prompt_bg: file.ui.prompt_bg.into(),
1162            prompt_selection_fg: file.ui.prompt_selection_fg.into(),
1163            prompt_selection_bg: file.ui.prompt_selection_bg.into(),
1164            popup_border_fg: file.ui.popup_border_fg.into(),
1165            popup_bg: file.ui.popup_bg.into(),
1166            popup_selection_bg: file.ui.popup_selection_bg.into(),
1167            popup_selection_fg: file.ui.popup_selection_fg.into(),
1168            popup_text_fg: file.ui.popup_text_fg.into(),
1169            suggestion_bg: file.ui.suggestion_bg.into(),
1170            suggestion_selected_bg: file.ui.suggestion_selected_bg.into(),
1171            help_bg: file.ui.help_bg.into(),
1172            help_fg: file.ui.help_fg.into(),
1173            help_key_fg: file.ui.help_key_fg.into(),
1174            help_separator_fg: file.ui.help_separator_fg.into(),
1175            help_indicator_fg: file.ui.help_indicator_fg.into(),
1176            help_indicator_bg: file.ui.help_indicator_bg.into(),
1177            inline_code_bg: file.ui.inline_code_bg.into(),
1178            split_separator_fg: file.ui.split_separator_fg.into(),
1179            split_separator_hover_fg: file.ui.split_separator_hover_fg.into(),
1180            scrollbar_track_fg: file.ui.scrollbar_track_fg.into(),
1181            scrollbar_thumb_fg: file.ui.scrollbar_thumb_fg.into(),
1182            scrollbar_track_hover_fg: file.ui.scrollbar_track_hover_fg.into(),
1183            scrollbar_thumb_hover_fg: file.ui.scrollbar_thumb_hover_fg.into(),
1184            compose_margin_bg: file.ui.compose_margin_bg.into(),
1185            semantic_highlight_bg: file.ui.semantic_highlight_bg.into(),
1186            terminal_bg: file.ui.terminal_bg.into(),
1187            terminal_fg: file.ui.terminal_fg.into(),
1188            status_warning_indicator_bg: file.ui.status_warning_indicator_bg.into(),
1189            status_warning_indicator_fg: file.ui.status_warning_indicator_fg.into(),
1190            status_error_indicator_bg: file.ui.status_error_indicator_bg.into(),
1191            status_error_indicator_fg: file.ui.status_error_indicator_fg.into(),
1192            status_warning_indicator_hover_bg: file.ui.status_warning_indicator_hover_bg.into(),
1193            status_warning_indicator_hover_fg: file.ui.status_warning_indicator_hover_fg.into(),
1194            status_error_indicator_hover_bg: file.ui.status_error_indicator_hover_bg.into(),
1195            status_error_indicator_hover_fg: file.ui.status_error_indicator_hover_fg.into(),
1196            tab_drop_zone_bg: file.ui.tab_drop_zone_bg.into(),
1197            tab_drop_zone_border: file.ui.tab_drop_zone_border.into(),
1198            settings_selected_bg: file.ui.settings_selected_bg.into(),
1199            settings_selected_fg: file.ui.settings_selected_fg.into(),
1200            file_status_added_fg: file
1201                .ui
1202                .file_status_added_fg
1203                .map(|c| c.into())
1204                .unwrap_or_else(|| file.diagnostic.info_fg.clone().into()),
1205            file_status_modified_fg: file
1206                .ui
1207                .file_status_modified_fg
1208                .map(|c| c.into())
1209                .unwrap_or_else(|| file.diagnostic.warning_fg.clone().into()),
1210            file_status_deleted_fg: file
1211                .ui
1212                .file_status_deleted_fg
1213                .map(|c| c.into())
1214                .unwrap_or_else(|| file.diagnostic.error_fg.clone().into()),
1215            file_status_renamed_fg: file
1216                .ui
1217                .file_status_renamed_fg
1218                .map(|c| c.into())
1219                .unwrap_or_else(|| file.diagnostic.info_fg.clone().into()),
1220            file_status_untracked_fg: file
1221                .ui
1222                .file_status_untracked_fg
1223                .map(|c| c.into())
1224                .unwrap_or_else(|| file.diagnostic.hint_fg.clone().into()),
1225            file_status_conflicted_fg: file
1226                .ui
1227                .file_status_conflicted_fg
1228                .map(|c| c.into())
1229                .unwrap_or_else(|| file.diagnostic.error_fg.clone().into()),
1230            search_match_bg: file.search.match_bg.into(),
1231            search_match_fg: file.search.match_fg.into(),
1232            search_label_bg: file.search.label_bg.into(),
1233            search_label_fg: file.search.label_fg.into(),
1234            diagnostic_error_fg: file.diagnostic.error_fg.into(),
1235            diagnostic_error_bg: file.diagnostic.error_bg.into(),
1236            diagnostic_warning_fg: file.diagnostic.warning_fg.into(),
1237            diagnostic_warning_bg: file.diagnostic.warning_bg.into(),
1238            diagnostic_info_fg: file.diagnostic.info_fg.into(),
1239            diagnostic_info_bg: file.diagnostic.info_bg.into(),
1240            diagnostic_hint_fg: file.diagnostic.hint_fg.into(),
1241            diagnostic_hint_bg: file.diagnostic.hint_bg.into(),
1242            syntax_keyword: file.syntax.keyword.into(),
1243            syntax_string: file.syntax.string.into(),
1244            syntax_comment: file.syntax.comment.into(),
1245            syntax_function: file.syntax.function.into(),
1246            syntax_type: file.syntax.type_.into(),
1247            syntax_variable: file.syntax.variable.into(),
1248            syntax_constant: file.syntax.constant.into(),
1249            syntax_operator: file.syntax.operator.into(),
1250            syntax_punctuation_bracket: file.syntax.punctuation_bracket.into(),
1251            syntax_punctuation_delimiter: file.syntax.punctuation_delimiter.into(),
1252        }
1253    }
1254}
1255
1256impl From<Theme> for ThemeFile {
1257    fn from(theme: Theme) -> Self {
1258        Self {
1259            name: theme.name,
1260            editor: EditorColors {
1261                bg: theme.editor_bg.into(),
1262                fg: theme.editor_fg.into(),
1263                cursor: theme.cursor.into(),
1264                inactive_cursor: theme.inactive_cursor.into(),
1265                selection_bg: theme.selection_bg.into(),
1266                current_line_bg: theme.current_line_bg.into(),
1267                line_number_fg: theme.line_number_fg.into(),
1268                line_number_bg: theme.line_number_bg.into(),
1269                diff_add_bg: theme.diff_add_bg.into(),
1270                diff_remove_bg: theme.diff_remove_bg.into(),
1271                diff_add_highlight_bg: Some(theme.diff_add_highlight_bg.into()),
1272                diff_remove_highlight_bg: Some(theme.diff_remove_highlight_bg.into()),
1273                diff_modify_bg: theme.diff_modify_bg.into(),
1274                ruler_bg: theme.ruler_bg.into(),
1275                whitespace_indicator_fg: theme.whitespace_indicator_fg.into(),
1276                after_eof_bg: Some(theme.after_eof_bg.into()),
1277            },
1278            ui: UiColors {
1279                tab_active_fg: theme.tab_active_fg.into(),
1280                tab_active_bg: theme.tab_active_bg.into(),
1281                tab_inactive_fg: theme.tab_inactive_fg.into(),
1282                tab_inactive_bg: theme.tab_inactive_bg.into(),
1283                tab_separator_bg: theme.tab_separator_bg.into(),
1284                tab_close_hover_fg: theme.tab_close_hover_fg.into(),
1285                tab_hover_bg: theme.tab_hover_bg.into(),
1286                menu_bg: theme.menu_bg.into(),
1287                menu_fg: theme.menu_fg.into(),
1288                menu_active_bg: theme.menu_active_bg.into(),
1289                menu_active_fg: theme.menu_active_fg.into(),
1290                menu_dropdown_bg: theme.menu_dropdown_bg.into(),
1291                menu_dropdown_fg: theme.menu_dropdown_fg.into(),
1292                menu_highlight_bg: theme.menu_highlight_bg.into(),
1293                menu_highlight_fg: theme.menu_highlight_fg.into(),
1294                menu_border_fg: theme.menu_border_fg.into(),
1295                menu_separator_fg: theme.menu_separator_fg.into(),
1296                menu_hover_bg: theme.menu_hover_bg.into(),
1297                menu_hover_fg: theme.menu_hover_fg.into(),
1298                menu_disabled_fg: theme.menu_disabled_fg.into(),
1299                menu_disabled_bg: theme.menu_disabled_bg.into(),
1300                status_bar_fg: theme.status_bar_fg.into(),
1301                status_bar_bg: theme.status_bar_bg.into(),
1302                prompt_fg: theme.prompt_fg.into(),
1303                prompt_bg: theme.prompt_bg.into(),
1304                prompt_selection_fg: theme.prompt_selection_fg.into(),
1305                prompt_selection_bg: theme.prompt_selection_bg.into(),
1306                popup_border_fg: theme.popup_border_fg.into(),
1307                popup_bg: theme.popup_bg.into(),
1308                popup_selection_bg: theme.popup_selection_bg.into(),
1309                popup_selection_fg: theme.popup_selection_fg.into(),
1310                popup_text_fg: theme.popup_text_fg.into(),
1311                suggestion_bg: theme.suggestion_bg.into(),
1312                suggestion_selected_bg: theme.suggestion_selected_bg.into(),
1313                help_bg: theme.help_bg.into(),
1314                help_fg: theme.help_fg.into(),
1315                help_key_fg: theme.help_key_fg.into(),
1316                help_separator_fg: theme.help_separator_fg.into(),
1317                help_indicator_fg: theme.help_indicator_fg.into(),
1318                help_indicator_bg: theme.help_indicator_bg.into(),
1319                inline_code_bg: theme.inline_code_bg.into(),
1320                split_separator_fg: theme.split_separator_fg.into(),
1321                split_separator_hover_fg: theme.split_separator_hover_fg.into(),
1322                scrollbar_track_fg: theme.scrollbar_track_fg.into(),
1323                scrollbar_thumb_fg: theme.scrollbar_thumb_fg.into(),
1324                scrollbar_track_hover_fg: theme.scrollbar_track_hover_fg.into(),
1325                scrollbar_thumb_hover_fg: theme.scrollbar_thumb_hover_fg.into(),
1326                compose_margin_bg: theme.compose_margin_bg.into(),
1327                semantic_highlight_bg: theme.semantic_highlight_bg.into(),
1328                terminal_bg: theme.terminal_bg.into(),
1329                terminal_fg: theme.terminal_fg.into(),
1330                status_warning_indicator_bg: theme.status_warning_indicator_bg.into(),
1331                status_warning_indicator_fg: theme.status_warning_indicator_fg.into(),
1332                status_error_indicator_bg: theme.status_error_indicator_bg.into(),
1333                status_error_indicator_fg: theme.status_error_indicator_fg.into(),
1334                status_warning_indicator_hover_bg: theme.status_warning_indicator_hover_bg.into(),
1335                status_warning_indicator_hover_fg: theme.status_warning_indicator_hover_fg.into(),
1336                status_error_indicator_hover_bg: theme.status_error_indicator_hover_bg.into(),
1337                status_error_indicator_hover_fg: theme.status_error_indicator_hover_fg.into(),
1338                tab_drop_zone_bg: theme.tab_drop_zone_bg.into(),
1339                tab_drop_zone_border: theme.tab_drop_zone_border.into(),
1340                settings_selected_bg: theme.settings_selected_bg.into(),
1341                settings_selected_fg: theme.settings_selected_fg.into(),
1342                file_status_added_fg: Some(theme.file_status_added_fg.into()),
1343                file_status_modified_fg: Some(theme.file_status_modified_fg.into()),
1344                file_status_deleted_fg: Some(theme.file_status_deleted_fg.into()),
1345                file_status_renamed_fg: Some(theme.file_status_renamed_fg.into()),
1346                file_status_untracked_fg: Some(theme.file_status_untracked_fg.into()),
1347                file_status_conflicted_fg: Some(theme.file_status_conflicted_fg.into()),
1348            },
1349            search: SearchColors {
1350                match_bg: theme.search_match_bg.into(),
1351                match_fg: theme.search_match_fg.into(),
1352                label_bg: theme.search_label_bg.into(),
1353                label_fg: theme.search_label_fg.into(),
1354            },
1355            diagnostic: DiagnosticColors {
1356                error_fg: theme.diagnostic_error_fg.into(),
1357                error_bg: theme.diagnostic_error_bg.into(),
1358                warning_fg: theme.diagnostic_warning_fg.into(),
1359                warning_bg: theme.diagnostic_warning_bg.into(),
1360                info_fg: theme.diagnostic_info_fg.into(),
1361                info_bg: theme.diagnostic_info_bg.into(),
1362                hint_fg: theme.diagnostic_hint_fg.into(),
1363                hint_bg: theme.diagnostic_hint_bg.into(),
1364            },
1365            syntax: SyntaxColors {
1366                keyword: theme.syntax_keyword.into(),
1367                string: theme.syntax_string.into(),
1368                comment: theme.syntax_comment.into(),
1369                function: theme.syntax_function.into(),
1370                type_: theme.syntax_type.into(),
1371                variable: theme.syntax_variable.into(),
1372                constant: theme.syntax_constant.into(),
1373                operator: theme.syntax_operator.into(),
1374                punctuation_bracket: theme.syntax_punctuation_bracket.into(),
1375                punctuation_delimiter: theme.syntax_punctuation_delimiter.into(),
1376            },
1377        }
1378    }
1379}
1380
1381impl Theme {
1382    /// Returns `true` when the theme has a light background.
1383    ///
1384    /// Uses the relative luminance of `editor_bg` (perceived brightness).
1385    /// A threshold of 0.5 separates dark from light; for `Color::Reset` or
1386    /// unresolvable colors, falls back to `false` (dark).
1387    pub fn is_light(&self) -> bool {
1388        if let Some((r, g, b)) = color_to_rgb(self.editor_bg) {
1389            // sRGB relative luminance (ITU-R BT.709)
1390            let lum = 0.2126 * (r as f64 / 255.0)
1391                + 0.7152 * (g as f64 / 255.0)
1392                + 0.0722 * (b as f64 / 255.0);
1393            lum > 0.5
1394        } else {
1395            false
1396        }
1397    }
1398
1399    /// Load a builtin theme by name (no I/O, uses embedded JSON).
1400    pub fn load_builtin(name: &str) -> Option<Self> {
1401        BUILTIN_THEMES
1402            .iter()
1403            .find(|t| t.name == name)
1404            .and_then(|t| serde_json::from_str::<ThemeFile>(t.json).ok())
1405            .map(|tf| tf.into())
1406    }
1407
1408    /// Parse theme from JSON string (no I/O).
1409    pub fn from_json(json: &str) -> Result<Self, String> {
1410        let theme_file: ThemeFile =
1411            serde_json::from_str(json).map_err(|e| format!("Failed to parse theme JSON: {}", e))?;
1412        Ok(theme_file.into())
1413    }
1414
1415    /// Resolve a theme key to a Color.
1416    ///
1417    /// Theme keys use dot notation: "section.field"
1418    /// Examples:
1419    /// - "ui.status_bar_fg" -> status_bar_fg
1420    /// - "editor.selection_bg" -> selection_bg
1421    /// - "syntax.keyword" -> syntax_keyword
1422    /// - "diagnostic.error_fg" -> diagnostic_error_fg
1423    ///
1424    /// Returns None if the key is not recognized.
1425    pub fn resolve_theme_key(&self, key: &str) -> Option<Color> {
1426        // Parse "section.field" format
1427        let parts: Vec<&str> = key.split('.').collect();
1428        if parts.len() != 2 {
1429            return None;
1430        }
1431
1432        let (section, field) = (parts[0], parts[1]);
1433
1434        match section {
1435            "editor" => match field {
1436                "bg" => Some(self.editor_bg),
1437                "fg" => Some(self.editor_fg),
1438                "cursor" => Some(self.cursor),
1439                "inactive_cursor" => Some(self.inactive_cursor),
1440                "selection_bg" => Some(self.selection_bg),
1441                "current_line_bg" => Some(self.current_line_bg),
1442                "line_number_fg" => Some(self.line_number_fg),
1443                "line_number_bg" => Some(self.line_number_bg),
1444                "diff_add_bg" => Some(self.diff_add_bg),
1445                "diff_remove_bg" => Some(self.diff_remove_bg),
1446                "diff_modify_bg" => Some(self.diff_modify_bg),
1447                "ruler_bg" => Some(self.ruler_bg),
1448                "whitespace_indicator_fg" => Some(self.whitespace_indicator_fg),
1449                _ => None,
1450            },
1451            "ui" => match field {
1452                "tab_active_fg" => Some(self.tab_active_fg),
1453                "tab_active_bg" => Some(self.tab_active_bg),
1454                "tab_inactive_fg" => Some(self.tab_inactive_fg),
1455                "tab_inactive_bg" => Some(self.tab_inactive_bg),
1456                "status_bar_fg" => Some(self.status_bar_fg),
1457                "status_bar_bg" => Some(self.status_bar_bg),
1458                "prompt_fg" => Some(self.prompt_fg),
1459                "prompt_bg" => Some(self.prompt_bg),
1460                "prompt_selection_fg" => Some(self.prompt_selection_fg),
1461                "prompt_selection_bg" => Some(self.prompt_selection_bg),
1462                "popup_bg" => Some(self.popup_bg),
1463                "popup_border_fg" => Some(self.popup_border_fg),
1464                "popup_selection_bg" => Some(self.popup_selection_bg),
1465                "popup_selection_fg" => Some(self.popup_selection_fg),
1466                "popup_text_fg" => Some(self.popup_text_fg),
1467                "menu_bg" => Some(self.menu_bg),
1468                "menu_fg" => Some(self.menu_fg),
1469                "menu_active_bg" => Some(self.menu_active_bg),
1470                "menu_active_fg" => Some(self.menu_active_fg),
1471                "help_bg" => Some(self.help_bg),
1472                "help_fg" => Some(self.help_fg),
1473                "help_key_fg" => Some(self.help_key_fg),
1474                "split_separator_fg" => Some(self.split_separator_fg),
1475                "scrollbar_thumb_fg" => Some(self.scrollbar_thumb_fg),
1476                "semantic_highlight_bg" => Some(self.semantic_highlight_bg),
1477                "file_status_added_fg" => Some(self.file_status_added_fg),
1478                "file_status_modified_fg" => Some(self.file_status_modified_fg),
1479                "file_status_deleted_fg" => Some(self.file_status_deleted_fg),
1480                "file_status_renamed_fg" => Some(self.file_status_renamed_fg),
1481                "file_status_untracked_fg" => Some(self.file_status_untracked_fg),
1482                "file_status_conflicted_fg" => Some(self.file_status_conflicted_fg),
1483                _ => None,
1484            },
1485            "syntax" => match field {
1486                "keyword" => Some(self.syntax_keyword),
1487                "string" => Some(self.syntax_string),
1488                "comment" => Some(self.syntax_comment),
1489                "function" => Some(self.syntax_function),
1490                "type" => Some(self.syntax_type),
1491                "variable" => Some(self.syntax_variable),
1492                "constant" => Some(self.syntax_constant),
1493                "operator" => Some(self.syntax_operator),
1494                "punctuation_bracket" => Some(self.syntax_punctuation_bracket),
1495                "punctuation_delimiter" => Some(self.syntax_punctuation_delimiter),
1496                _ => None,
1497            },
1498            "diagnostic" => match field {
1499                "error_fg" => Some(self.diagnostic_error_fg),
1500                "error_bg" => Some(self.diagnostic_error_bg),
1501                "warning_fg" => Some(self.diagnostic_warning_fg),
1502                "warning_bg" => Some(self.diagnostic_warning_bg),
1503                "info_fg" => Some(self.diagnostic_info_fg),
1504                "info_bg" => Some(self.diagnostic_info_bg),
1505                "hint_fg" => Some(self.diagnostic_hint_fg),
1506                "hint_bg" => Some(self.diagnostic_hint_bg),
1507                _ => None,
1508            },
1509            "search" => match field {
1510                "match_bg" => Some(self.search_match_bg),
1511                "match_fg" => Some(self.search_match_fg),
1512                "label_bg" => Some(self.search_label_bg),
1513                "label_fg" => Some(self.search_label_fg),
1514                _ => None,
1515            },
1516            _ => None,
1517        }
1518    }
1519
1520    /// Mutable companion to [`resolve_theme_key`]. Keep the two matches in
1521    /// lock-step: any key readable by `resolve_theme_key` should also be
1522    /// writable here, and vice versa.
1523    pub fn resolve_theme_key_mut(&mut self, key: &str) -> Option<&mut Color> {
1524        let parts: Vec<&str> = key.split('.').collect();
1525        if parts.len() != 2 {
1526            return None;
1527        }
1528        let (section, field) = (parts[0], parts[1]);
1529        match section {
1530            "editor" => match field {
1531                "bg" => Some(&mut self.editor_bg),
1532                "fg" => Some(&mut self.editor_fg),
1533                "cursor" => Some(&mut self.cursor),
1534                "inactive_cursor" => Some(&mut self.inactive_cursor),
1535                "selection_bg" => Some(&mut self.selection_bg),
1536                "current_line_bg" => Some(&mut self.current_line_bg),
1537                "line_number_fg" => Some(&mut self.line_number_fg),
1538                "line_number_bg" => Some(&mut self.line_number_bg),
1539                "diff_add_bg" => Some(&mut self.diff_add_bg),
1540                "diff_remove_bg" => Some(&mut self.diff_remove_bg),
1541                "diff_modify_bg" => Some(&mut self.diff_modify_bg),
1542                "ruler_bg" => Some(&mut self.ruler_bg),
1543                "whitespace_indicator_fg" => Some(&mut self.whitespace_indicator_fg),
1544                _ => None,
1545            },
1546            "ui" => match field {
1547                "tab_active_fg" => Some(&mut self.tab_active_fg),
1548                "tab_active_bg" => Some(&mut self.tab_active_bg),
1549                "tab_inactive_fg" => Some(&mut self.tab_inactive_fg),
1550                "tab_inactive_bg" => Some(&mut self.tab_inactive_bg),
1551                "status_bar_fg" => Some(&mut self.status_bar_fg),
1552                "status_bar_bg" => Some(&mut self.status_bar_bg),
1553                "prompt_fg" => Some(&mut self.prompt_fg),
1554                "prompt_bg" => Some(&mut self.prompt_bg),
1555                "prompt_selection_fg" => Some(&mut self.prompt_selection_fg),
1556                "prompt_selection_bg" => Some(&mut self.prompt_selection_bg),
1557                "popup_bg" => Some(&mut self.popup_bg),
1558                "popup_border_fg" => Some(&mut self.popup_border_fg),
1559                "popup_selection_bg" => Some(&mut self.popup_selection_bg),
1560                "popup_selection_fg" => Some(&mut self.popup_selection_fg),
1561                "popup_text_fg" => Some(&mut self.popup_text_fg),
1562                "menu_bg" => Some(&mut self.menu_bg),
1563                "menu_fg" => Some(&mut self.menu_fg),
1564                "menu_active_bg" => Some(&mut self.menu_active_bg),
1565                "menu_active_fg" => Some(&mut self.menu_active_fg),
1566                "help_bg" => Some(&mut self.help_bg),
1567                "help_fg" => Some(&mut self.help_fg),
1568                "help_key_fg" => Some(&mut self.help_key_fg),
1569                "split_separator_fg" => Some(&mut self.split_separator_fg),
1570                "scrollbar_thumb_fg" => Some(&mut self.scrollbar_thumb_fg),
1571                "semantic_highlight_bg" => Some(&mut self.semantic_highlight_bg),
1572                "file_status_added_fg" => Some(&mut self.file_status_added_fg),
1573                "file_status_modified_fg" => Some(&mut self.file_status_modified_fg),
1574                "file_status_deleted_fg" => Some(&mut self.file_status_deleted_fg),
1575                "file_status_renamed_fg" => Some(&mut self.file_status_renamed_fg),
1576                "file_status_untracked_fg" => Some(&mut self.file_status_untracked_fg),
1577                "file_status_conflicted_fg" => Some(&mut self.file_status_conflicted_fg),
1578                _ => None,
1579            },
1580            "syntax" => match field {
1581                "keyword" => Some(&mut self.syntax_keyword),
1582                "string" => Some(&mut self.syntax_string),
1583                "comment" => Some(&mut self.syntax_comment),
1584                "function" => Some(&mut self.syntax_function),
1585                "type" => Some(&mut self.syntax_type),
1586                "variable" => Some(&mut self.syntax_variable),
1587                "constant" => Some(&mut self.syntax_constant),
1588                "operator" => Some(&mut self.syntax_operator),
1589                "punctuation_bracket" => Some(&mut self.syntax_punctuation_bracket),
1590                "punctuation_delimiter" => Some(&mut self.syntax_punctuation_delimiter),
1591                _ => None,
1592            },
1593            "diagnostic" => match field {
1594                "error_fg" => Some(&mut self.diagnostic_error_fg),
1595                "error_bg" => Some(&mut self.diagnostic_error_bg),
1596                "warning_fg" => Some(&mut self.diagnostic_warning_fg),
1597                "warning_bg" => Some(&mut self.diagnostic_warning_bg),
1598                "info_fg" => Some(&mut self.diagnostic_info_fg),
1599                "info_bg" => Some(&mut self.diagnostic_info_bg),
1600                "hint_fg" => Some(&mut self.diagnostic_hint_fg),
1601                "hint_bg" => Some(&mut self.diagnostic_hint_bg),
1602                _ => None,
1603            },
1604            "search" => match field {
1605                "match_bg" => Some(&mut self.search_match_bg),
1606                "match_fg" => Some(&mut self.search_match_fg),
1607                "label_bg" => Some(&mut self.search_label_bg),
1608                "label_fg" => Some(&mut self.search_label_fg),
1609                _ => None,
1610            },
1611            _ => None,
1612        }
1613    }
1614
1615    /// Apply a map of `"section.field" -> Color` overrides to the running
1616    /// theme in-place. Returns the number of keys that matched a known
1617    /// theme field. Unknown keys are silently dropped so a typo in a fast
1618    /// animation loop doesn't crash the caller.
1619    pub fn override_colors<I, K>(&mut self, overrides: I) -> usize
1620    where
1621        I: IntoIterator<Item = (K, Color)>,
1622        K: AsRef<str>,
1623    {
1624        let mut applied = 0;
1625        for (key, color) in overrides {
1626            if let Some(slot) = self.resolve_theme_key_mut(key.as_ref()) {
1627                *slot = color;
1628                applied += 1;
1629            }
1630        }
1631        applied
1632    }
1633}
1634
1635// =============================================================================
1636// Theme Schema Generation for Plugin API
1637// =============================================================================
1638
1639/// Returns the raw JSON Schema for ThemeFile, generated by schemars.
1640/// The schema uses standard JSON Schema format with $ref for type references.
1641/// Plugins are responsible for parsing and resolving $ref references.
1642pub fn get_theme_schema() -> serde_json::Value {
1643    use schemars::schema_for;
1644    let schema = schema_for!(ThemeFile);
1645    serde_json::to_value(&schema).unwrap_or_default()
1646}
1647
1648/// Returns a map of built-in theme names to their JSON content.
1649pub fn get_builtin_themes() -> serde_json::Value {
1650    let mut map = serde_json::Map::new();
1651    for theme in BUILTIN_THEMES {
1652        map.insert(
1653            theme.name.to_string(),
1654            serde_json::Value::String(theme.json.to_string()),
1655        );
1656    }
1657    serde_json::Value::Object(map)
1658}
1659
1660#[cfg(test)]
1661mod tests {
1662    use super::*;
1663
1664    #[test]
1665    fn test_load_builtin_theme() {
1666        let dark = Theme::load_builtin(THEME_DARK).expect("Dark theme must exist");
1667        assert_eq!(dark.name, THEME_DARK);
1668
1669        let light = Theme::load_builtin(THEME_LIGHT).expect("Light theme must exist");
1670        assert_eq!(light.name, THEME_LIGHT);
1671
1672        let high_contrast =
1673            Theme::load_builtin(THEME_HIGH_CONTRAST).expect("High contrast theme must exist");
1674        assert_eq!(high_contrast.name, THEME_HIGH_CONTRAST);
1675    }
1676
1677    #[test]
1678    fn test_builtin_themes_match_schema() {
1679        for theme in BUILTIN_THEMES {
1680            let _: ThemeFile = serde_json::from_str(theme.json)
1681                .unwrap_or_else(|_| panic!("Theme '{}' does not match schema", theme.name));
1682        }
1683    }
1684
1685    #[test]
1686    fn test_from_json() {
1687        let json = r#"{"name":"test","editor":{},"ui":{},"search":{},"diagnostic":{},"syntax":{}}"#;
1688        let theme = Theme::from_json(json).expect("Should parse minimal theme");
1689        assert_eq!(theme.name, "test");
1690    }
1691
1692    #[test]
1693    fn test_default_reset_color() {
1694        // Test that "Default" maps to Color::Reset
1695        let color: Color = ColorDef::Named("Default".to_string()).into();
1696        assert_eq!(color, Color::Reset);
1697
1698        // Test that "Reset" also maps to Color::Reset
1699        let color: Color = ColorDef::Named("Reset".to_string()).into();
1700        assert_eq!(color, Color::Reset);
1701    }
1702
1703    #[test]
1704    fn test_file_status_colors_fall_back_to_diagnostic_colors() {
1705        // A theme with NO file_status_* keys should inherit from diagnostic colors
1706        let json = r#"{
1707            "name": "test-fallback",
1708            "editor": {},
1709            "ui": {},
1710            "search": {},
1711            "diagnostic": {
1712                "error_fg": [220, 50, 47],
1713                "warning_fg": [181, 137, 0],
1714                "info_fg": [38, 139, 210],
1715                "hint_fg": [101, 123, 131]
1716            },
1717            "syntax": {}
1718        }"#;
1719        let theme = Theme::from_json(json).expect("Should parse theme without file_status keys");
1720
1721        // Verify fallback: added/renamed -> info_fg
1722        assert_eq!(theme.file_status_added_fg, Color::Rgb(38, 139, 210));
1723        assert_eq!(theme.file_status_renamed_fg, Color::Rgb(38, 139, 210));
1724        // modified -> warning_fg
1725        assert_eq!(theme.file_status_modified_fg, Color::Rgb(181, 137, 0));
1726        // deleted/conflicted -> error_fg
1727        assert_eq!(theme.file_status_deleted_fg, Color::Rgb(220, 50, 47));
1728        assert_eq!(theme.file_status_conflicted_fg, Color::Rgb(220, 50, 47));
1729        // untracked -> hint_fg
1730        assert_eq!(theme.file_status_untracked_fg, Color::Rgb(101, 123, 131));
1731    }
1732
1733    #[test]
1734    fn test_file_status_colors_explicit_override() {
1735        // A theme WITH explicit file_status keys should use those, not the fallback
1736        let json = r#"{
1737            "name": "test-override",
1738            "editor": {},
1739            "ui": {
1740                "file_status_added_fg": [80, 250, 123],
1741                "file_status_modified_fg": [255, 184, 108]
1742            },
1743            "search": {},
1744            "diagnostic": {
1745                "info_fg": [38, 139, 210],
1746                "warning_fg": [181, 137, 0]
1747            },
1748            "syntax": {}
1749        }"#;
1750        let theme = Theme::from_json(json).expect("Should parse theme with file_status overrides");
1751
1752        // Explicit overrides should win
1753        assert_eq!(theme.file_status_added_fg, Color::Rgb(80, 250, 123));
1754        assert_eq!(theme.file_status_modified_fg, Color::Rgb(255, 184, 108));
1755        // Non-overridden should still fall back
1756        assert_eq!(theme.file_status_renamed_fg, Color::Rgb(38, 139, 210));
1757    }
1758
1759    #[test]
1760    fn test_file_status_colors_resolve_via_theme_key() {
1761        let json = r#"{
1762            "name": "test-resolve",
1763            "editor": {},
1764            "ui": {
1765                "file_status_added_fg": [80, 250, 123]
1766            },
1767            "search": {},
1768            "diagnostic": {
1769                "warning_fg": [181, 137, 0]
1770            },
1771            "syntax": {}
1772        }"#;
1773        let theme = Theme::from_json(json).expect("Should parse theme");
1774
1775        // Theme key resolution should work for file_status keys
1776        assert_eq!(
1777            theme.resolve_theme_key("ui.file_status_added_fg"),
1778            Some(Color::Rgb(80, 250, 123))
1779        );
1780        assert_eq!(
1781            theme.resolve_theme_key("ui.file_status_modified_fg"),
1782            Some(Color::Rgb(181, 137, 0))
1783        );
1784    }
1785
1786    #[test]
1787    fn override_colors_writes_known_keys_and_drops_unknowns() {
1788        let mut theme = Theme::load_builtin(THEME_DARK).expect("dark builtin");
1789        let applied = theme.override_colors([
1790            ("editor.bg".to_string(), Color::Rgb(10, 20, 30)),
1791            ("ui.status_bar_fg".to_string(), Color::Rgb(1, 2, 3)),
1792            ("does.not_exist".to_string(), Color::Rgb(9, 9, 9)),
1793            ("garbage_no_dot".to_string(), Color::Rgb(9, 9, 9)),
1794        ]);
1795        assert_eq!(applied, 2, "only the two valid keys should be applied");
1796        assert_eq!(
1797            theme.resolve_theme_key("editor.bg"),
1798            Some(Color::Rgb(10, 20, 30))
1799        );
1800        assert_eq!(
1801            theme.resolve_theme_key("ui.status_bar_fg"),
1802            Some(Color::Rgb(1, 2, 3))
1803        );
1804    }
1805
1806    #[test]
1807    fn resolve_theme_key_mut_matches_resolve_theme_key_domain() {
1808        // If a key resolves readably, it must also resolve as a mutable
1809        // slot — the two matches must stay in lock-step.
1810        let mut theme = Theme::load_builtin(THEME_DARK).expect("dark builtin");
1811        let probe = [
1812            "editor.bg",
1813            "editor.fg",
1814            "ui.status_bar_fg",
1815            "ui.tab_active_bg",
1816            "syntax.keyword",
1817            "diagnostic.error_fg",
1818            "search.match_bg",
1819        ];
1820        for key in probe {
1821            assert!(
1822                theme.resolve_theme_key(key).is_some(),
1823                "reader lost key {key}"
1824            );
1825            assert!(
1826                theme.resolve_theme_key_mut(key).is_some(),
1827                "mutator missing key {key}"
1828            );
1829        }
1830    }
1831
1832    #[test]
1833    fn test_all_builtin_themes_have_file_status_colors() {
1834        // Every builtin theme must produce valid file_status colors (via fallback or explicit)
1835        for builtin in BUILTIN_THEMES {
1836            let theme = Theme::from_json(builtin.json)
1837                .unwrap_or_else(|e| panic!("Theme '{}' failed to parse: {}", builtin.name, e));
1838
1839            // All six keys must resolve to Some via resolve_theme_key
1840            for key in &[
1841                "ui.file_status_added_fg",
1842                "ui.file_status_modified_fg",
1843                "ui.file_status_deleted_fg",
1844                "ui.file_status_renamed_fg",
1845                "ui.file_status_untracked_fg",
1846                "ui.file_status_conflicted_fg",
1847            ] {
1848                assert!(
1849                    theme.resolve_theme_key(key).is_some(),
1850                    "Theme '{}' missing resolution for '{}'",
1851                    builtin.name,
1852                    key
1853                );
1854            }
1855        }
1856    }
1857}