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}
790
791// Default search colors
792fn default_search_match_bg() -> ColorDef {
793    ColorDef::Rgb(100, 100, 20)
794}
795fn default_search_match_fg() -> ColorDef {
796    ColorDef::Rgb(255, 255, 255)
797}
798
799/// LSP diagnostic colors (errors, warnings, etc.)
800#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
801pub struct DiagnosticColors {
802    /// Error message text color
803    #[serde(default = "default_diagnostic_error_fg")]
804    pub error_fg: ColorDef,
805    /// Error highlight background
806    #[serde(default = "default_diagnostic_error_bg")]
807    pub error_bg: ColorDef,
808    /// Warning message text color
809    #[serde(default = "default_diagnostic_warning_fg")]
810    pub warning_fg: ColorDef,
811    /// Warning highlight background
812    #[serde(default = "default_diagnostic_warning_bg")]
813    pub warning_bg: ColorDef,
814    /// Info message text color
815    #[serde(default = "default_diagnostic_info_fg")]
816    pub info_fg: ColorDef,
817    /// Info highlight background
818    #[serde(default = "default_diagnostic_info_bg")]
819    pub info_bg: ColorDef,
820    /// Hint message text color
821    #[serde(default = "default_diagnostic_hint_fg")]
822    pub hint_fg: ColorDef,
823    /// Hint highlight background
824    #[serde(default = "default_diagnostic_hint_bg")]
825    pub hint_bg: ColorDef,
826}
827
828// Default diagnostic colors
829fn default_diagnostic_error_fg() -> ColorDef {
830    ColorDef::Named("Red".to_string())
831}
832fn default_diagnostic_error_bg() -> ColorDef {
833    ColorDef::Rgb(60, 20, 20)
834}
835fn default_diagnostic_warning_fg() -> ColorDef {
836    ColorDef::Named("Yellow".to_string())
837}
838fn default_diagnostic_warning_bg() -> ColorDef {
839    ColorDef::Rgb(60, 50, 0)
840}
841fn default_diagnostic_info_fg() -> ColorDef {
842    ColorDef::Named("Blue".to_string())
843}
844fn default_diagnostic_info_bg() -> ColorDef {
845    ColorDef::Rgb(0, 30, 60)
846}
847fn default_diagnostic_hint_fg() -> ColorDef {
848    ColorDef::Named("Gray".to_string())
849}
850fn default_diagnostic_hint_bg() -> ColorDef {
851    ColorDef::Rgb(30, 30, 30)
852}
853
854/// Syntax highlighting colors
855#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
856pub struct SyntaxColors {
857    /// Language keywords (if, for, fn, etc.)
858    #[serde(default = "default_syntax_keyword")]
859    pub keyword: ColorDef,
860    /// String literals
861    #[serde(default = "default_syntax_string")]
862    pub string: ColorDef,
863    /// Code comments
864    #[serde(default = "default_syntax_comment")]
865    pub comment: ColorDef,
866    /// Function names
867    #[serde(default = "default_syntax_function")]
868    pub function: ColorDef,
869    /// Type names
870    #[serde(rename = "type", default = "default_syntax_type")]
871    pub type_: ColorDef,
872    /// Variable names
873    #[serde(default = "default_syntax_variable")]
874    pub variable: ColorDef,
875    /// Constants and literals
876    #[serde(default = "default_syntax_constant")]
877    pub constant: ColorDef,
878    /// Operators (+, -, =, etc.)
879    #[serde(default = "default_syntax_operator")]
880    pub operator: ColorDef,
881    /// Punctuation brackets ({, }, (, ), [, ])
882    #[serde(default = "default_syntax_punctuation_bracket")]
883    pub punctuation_bracket: ColorDef,
884    /// Punctuation delimiters (;, ,, .)
885    #[serde(default = "default_syntax_punctuation_delimiter")]
886    pub punctuation_delimiter: ColorDef,
887}
888
889// Default syntax colors (VSCode Dark+ inspired)
890fn default_syntax_keyword() -> ColorDef {
891    ColorDef::Rgb(86, 156, 214)
892}
893fn default_syntax_string() -> ColorDef {
894    ColorDef::Rgb(206, 145, 120)
895}
896fn default_syntax_comment() -> ColorDef {
897    ColorDef::Rgb(106, 153, 85)
898}
899fn default_syntax_function() -> ColorDef {
900    ColorDef::Rgb(220, 220, 170)
901}
902fn default_syntax_type() -> ColorDef {
903    ColorDef::Rgb(78, 201, 176)
904}
905fn default_syntax_variable() -> ColorDef {
906    ColorDef::Rgb(156, 220, 254)
907}
908fn default_syntax_constant() -> ColorDef {
909    ColorDef::Rgb(79, 193, 255)
910}
911fn default_syntax_operator() -> ColorDef {
912    ColorDef::Rgb(212, 212, 212)
913}
914fn default_syntax_punctuation_bracket() -> ColorDef {
915    ColorDef::Rgb(212, 212, 212) // default foreground — brackets blend with text
916}
917fn default_syntax_punctuation_delimiter() -> ColorDef {
918    ColorDef::Rgb(212, 212, 212) // default foreground — delimiters blend with text
919}
920
921/// Comprehensive theme structure with all UI colors
922#[derive(Debug, Clone)]
923pub struct Theme {
924    /// Theme name (e.g., "dark", "light", "high-contrast")
925    pub name: String,
926
927    // Editor colors
928    pub editor_bg: Color,
929    pub editor_fg: Color,
930    pub cursor: Color,
931    pub inactive_cursor: Color,
932    pub selection_bg: Color,
933    pub current_line_bg: Color,
934    pub line_number_fg: Color,
935    pub line_number_bg: Color,
936
937    /// Background color for rows past end-of-file
938    pub after_eof_bg: Color,
939
940    // Vertical ruler color
941    pub ruler_bg: Color,
942
943    // Whitespace indicator color (tab arrows, space dots)
944    pub whitespace_indicator_fg: Color,
945
946    // Diff highlighting colors
947    pub diff_add_bg: Color,
948    pub diff_remove_bg: Color,
949    pub diff_modify_bg: Color,
950    /// Brighter background for inline diff highlighting on added content
951    pub diff_add_highlight_bg: Color,
952    /// Brighter background for inline diff highlighting on removed content
953    pub diff_remove_highlight_bg: Color,
954
955    // UI element colors
956    pub tab_active_fg: Color,
957    pub tab_active_bg: Color,
958    pub tab_inactive_fg: Color,
959    pub tab_inactive_bg: Color,
960    pub tab_separator_bg: Color,
961    pub tab_close_hover_fg: Color,
962    pub tab_hover_bg: Color,
963
964    // Menu bar colors
965    pub menu_bg: Color,
966    pub menu_fg: Color,
967    pub menu_active_bg: Color,
968    pub menu_active_fg: Color,
969    pub menu_dropdown_bg: Color,
970    pub menu_dropdown_fg: Color,
971    pub menu_highlight_bg: Color,
972    pub menu_highlight_fg: Color,
973    pub menu_border_fg: Color,
974    pub menu_separator_fg: Color,
975    pub menu_hover_bg: Color,
976    pub menu_hover_fg: Color,
977    pub menu_disabled_fg: Color,
978    pub menu_disabled_bg: Color,
979
980    pub status_bar_fg: Color,
981    pub status_bar_bg: Color,
982    pub prompt_fg: Color,
983    pub prompt_bg: Color,
984    pub prompt_selection_fg: Color,
985    pub prompt_selection_bg: Color,
986
987    pub popup_border_fg: Color,
988    pub popup_bg: Color,
989    pub popup_selection_bg: Color,
990    pub popup_selection_fg: Color,
991    pub popup_text_fg: Color,
992
993    pub suggestion_bg: Color,
994    pub suggestion_selected_bg: Color,
995
996    pub help_bg: Color,
997    pub help_fg: Color,
998    pub help_key_fg: Color,
999    pub help_separator_fg: Color,
1000
1001    pub help_indicator_fg: Color,
1002    pub help_indicator_bg: Color,
1003
1004    /// Background color for inline code in help popups
1005    pub inline_code_bg: Color,
1006
1007    pub split_separator_fg: Color,
1008    pub split_separator_hover_fg: Color,
1009
1010    // Scrollbar colors
1011    pub scrollbar_track_fg: Color,
1012    pub scrollbar_thumb_fg: Color,
1013    pub scrollbar_track_hover_fg: Color,
1014    pub scrollbar_thumb_hover_fg: Color,
1015
1016    // Compose mode colors
1017    pub compose_margin_bg: Color,
1018
1019    // Semantic highlighting (word under cursor)
1020    pub semantic_highlight_bg: Color,
1021
1022    // Terminal colors (for embedded terminal buffers)
1023    pub terminal_bg: Color,
1024    pub terminal_fg: Color,
1025
1026    // Status bar warning/error indicator colors
1027    pub status_warning_indicator_bg: Color,
1028    pub status_warning_indicator_fg: Color,
1029    pub status_error_indicator_bg: Color,
1030    pub status_error_indicator_fg: Color,
1031    pub status_warning_indicator_hover_bg: Color,
1032    pub status_warning_indicator_hover_fg: Color,
1033    pub status_error_indicator_hover_bg: Color,
1034    pub status_error_indicator_hover_fg: Color,
1035
1036    // Tab drag-and-drop colors
1037    pub tab_drop_zone_bg: Color,
1038    pub tab_drop_zone_border: Color,
1039
1040    // Settings UI colors
1041    pub settings_selected_bg: Color,
1042    pub settings_selected_fg: Color,
1043
1044    // File status colors (git status indicators in file explorer)
1045    pub file_status_added_fg: Color,
1046    pub file_status_modified_fg: Color,
1047    pub file_status_deleted_fg: Color,
1048    pub file_status_renamed_fg: Color,
1049    pub file_status_untracked_fg: Color,
1050    pub file_status_conflicted_fg: Color,
1051
1052    // Search colors
1053    pub search_match_bg: Color,
1054    pub search_match_fg: Color,
1055
1056    // Diagnostic colors
1057    pub diagnostic_error_fg: Color,
1058    pub diagnostic_error_bg: Color,
1059    pub diagnostic_warning_fg: Color,
1060    pub diagnostic_warning_bg: Color,
1061    pub diagnostic_info_fg: Color,
1062    pub diagnostic_info_bg: Color,
1063    pub diagnostic_hint_fg: Color,
1064    pub diagnostic_hint_bg: Color,
1065
1066    // Syntax highlighting colors
1067    pub syntax_keyword: Color,
1068    pub syntax_string: Color,
1069    pub syntax_comment: Color,
1070    pub syntax_function: Color,
1071    pub syntax_type: Color,
1072    pub syntax_variable: Color,
1073    pub syntax_constant: Color,
1074    pub syntax_operator: Color,
1075    pub syntax_punctuation_bracket: Color,
1076    pub syntax_punctuation_delimiter: Color,
1077}
1078
1079impl From<ThemeFile> for Theme {
1080    fn from(file: ThemeFile) -> Self {
1081        Self {
1082            name: file.name,
1083            editor_bg: file.editor.bg.clone().into(),
1084            editor_fg: file.editor.fg.into(),
1085            cursor: file.editor.cursor.into(),
1086            inactive_cursor: file.editor.inactive_cursor.into(),
1087            selection_bg: file.editor.selection_bg.into(),
1088            current_line_bg: file.editor.current_line_bg.into(),
1089            line_number_fg: file.editor.line_number_fg.into(),
1090            line_number_bg: file.editor.line_number_bg.into(),
1091            // Use explicit override if provided, otherwise derive a subtle
1092            // contrasting shade from the editor background.
1093            after_eof_bg: file
1094                .editor
1095                .after_eof_bg
1096                .clone()
1097                .map(|c| c.into())
1098                .unwrap_or_else(|| shade_toward_contrast(file.editor.bg.clone().into(), 10)),
1099            ruler_bg: file.editor.ruler_bg.into(),
1100            whitespace_indicator_fg: file.editor.whitespace_indicator_fg.into(),
1101            diff_add_bg: file.editor.diff_add_bg.clone().into(),
1102            diff_remove_bg: file.editor.diff_remove_bg.clone().into(),
1103            diff_modify_bg: file.editor.diff_modify_bg.into(),
1104            // Use explicit override if provided, otherwise brighten from base
1105            diff_add_highlight_bg: file
1106                .editor
1107                .diff_add_highlight_bg
1108                .map(|c| c.into())
1109                .unwrap_or_else(|| brighten_color(file.editor.diff_add_bg.into(), 40)),
1110            diff_remove_highlight_bg: file
1111                .editor
1112                .diff_remove_highlight_bg
1113                .map(|c| c.into())
1114                .unwrap_or_else(|| brighten_color(file.editor.diff_remove_bg.into(), 40)),
1115            tab_active_fg: file.ui.tab_active_fg.into(),
1116            tab_active_bg: file.ui.tab_active_bg.into(),
1117            tab_inactive_fg: file.ui.tab_inactive_fg.into(),
1118            tab_inactive_bg: file.ui.tab_inactive_bg.into(),
1119            tab_separator_bg: file.ui.tab_separator_bg.into(),
1120            tab_close_hover_fg: file.ui.tab_close_hover_fg.into(),
1121            tab_hover_bg: file.ui.tab_hover_bg.into(),
1122            menu_bg: file.ui.menu_bg.into(),
1123            menu_fg: file.ui.menu_fg.into(),
1124            menu_active_bg: file.ui.menu_active_bg.into(),
1125            menu_active_fg: file.ui.menu_active_fg.into(),
1126            menu_dropdown_bg: file.ui.menu_dropdown_bg.into(),
1127            menu_dropdown_fg: file.ui.menu_dropdown_fg.into(),
1128            menu_highlight_bg: file.ui.menu_highlight_bg.into(),
1129            menu_highlight_fg: file.ui.menu_highlight_fg.into(),
1130            menu_border_fg: file.ui.menu_border_fg.into(),
1131            menu_separator_fg: file.ui.menu_separator_fg.into(),
1132            menu_hover_bg: file.ui.menu_hover_bg.into(),
1133            menu_hover_fg: file.ui.menu_hover_fg.into(),
1134            menu_disabled_fg: file.ui.menu_disabled_fg.into(),
1135            menu_disabled_bg: file.ui.menu_disabled_bg.into(),
1136            status_bar_fg: file.ui.status_bar_fg.into(),
1137            status_bar_bg: file.ui.status_bar_bg.into(),
1138            prompt_fg: file.ui.prompt_fg.into(),
1139            prompt_bg: file.ui.prompt_bg.into(),
1140            prompt_selection_fg: file.ui.prompt_selection_fg.into(),
1141            prompt_selection_bg: file.ui.prompt_selection_bg.into(),
1142            popup_border_fg: file.ui.popup_border_fg.into(),
1143            popup_bg: file.ui.popup_bg.into(),
1144            popup_selection_bg: file.ui.popup_selection_bg.into(),
1145            popup_selection_fg: file.ui.popup_selection_fg.into(),
1146            popup_text_fg: file.ui.popup_text_fg.into(),
1147            suggestion_bg: file.ui.suggestion_bg.into(),
1148            suggestion_selected_bg: file.ui.suggestion_selected_bg.into(),
1149            help_bg: file.ui.help_bg.into(),
1150            help_fg: file.ui.help_fg.into(),
1151            help_key_fg: file.ui.help_key_fg.into(),
1152            help_separator_fg: file.ui.help_separator_fg.into(),
1153            help_indicator_fg: file.ui.help_indicator_fg.into(),
1154            help_indicator_bg: file.ui.help_indicator_bg.into(),
1155            inline_code_bg: file.ui.inline_code_bg.into(),
1156            split_separator_fg: file.ui.split_separator_fg.into(),
1157            split_separator_hover_fg: file.ui.split_separator_hover_fg.into(),
1158            scrollbar_track_fg: file.ui.scrollbar_track_fg.into(),
1159            scrollbar_thumb_fg: file.ui.scrollbar_thumb_fg.into(),
1160            scrollbar_track_hover_fg: file.ui.scrollbar_track_hover_fg.into(),
1161            scrollbar_thumb_hover_fg: file.ui.scrollbar_thumb_hover_fg.into(),
1162            compose_margin_bg: file.ui.compose_margin_bg.into(),
1163            semantic_highlight_bg: file.ui.semantic_highlight_bg.into(),
1164            terminal_bg: file.ui.terminal_bg.into(),
1165            terminal_fg: file.ui.terminal_fg.into(),
1166            status_warning_indicator_bg: file.ui.status_warning_indicator_bg.into(),
1167            status_warning_indicator_fg: file.ui.status_warning_indicator_fg.into(),
1168            status_error_indicator_bg: file.ui.status_error_indicator_bg.into(),
1169            status_error_indicator_fg: file.ui.status_error_indicator_fg.into(),
1170            status_warning_indicator_hover_bg: file.ui.status_warning_indicator_hover_bg.into(),
1171            status_warning_indicator_hover_fg: file.ui.status_warning_indicator_hover_fg.into(),
1172            status_error_indicator_hover_bg: file.ui.status_error_indicator_hover_bg.into(),
1173            status_error_indicator_hover_fg: file.ui.status_error_indicator_hover_fg.into(),
1174            tab_drop_zone_bg: file.ui.tab_drop_zone_bg.into(),
1175            tab_drop_zone_border: file.ui.tab_drop_zone_border.into(),
1176            settings_selected_bg: file.ui.settings_selected_bg.into(),
1177            settings_selected_fg: file.ui.settings_selected_fg.into(),
1178            file_status_added_fg: file
1179                .ui
1180                .file_status_added_fg
1181                .map(|c| c.into())
1182                .unwrap_or_else(|| file.diagnostic.info_fg.clone().into()),
1183            file_status_modified_fg: file
1184                .ui
1185                .file_status_modified_fg
1186                .map(|c| c.into())
1187                .unwrap_or_else(|| file.diagnostic.warning_fg.clone().into()),
1188            file_status_deleted_fg: file
1189                .ui
1190                .file_status_deleted_fg
1191                .map(|c| c.into())
1192                .unwrap_or_else(|| file.diagnostic.error_fg.clone().into()),
1193            file_status_renamed_fg: file
1194                .ui
1195                .file_status_renamed_fg
1196                .map(|c| c.into())
1197                .unwrap_or_else(|| file.diagnostic.info_fg.clone().into()),
1198            file_status_untracked_fg: file
1199                .ui
1200                .file_status_untracked_fg
1201                .map(|c| c.into())
1202                .unwrap_or_else(|| file.diagnostic.hint_fg.clone().into()),
1203            file_status_conflicted_fg: file
1204                .ui
1205                .file_status_conflicted_fg
1206                .map(|c| c.into())
1207                .unwrap_or_else(|| file.diagnostic.error_fg.clone().into()),
1208            search_match_bg: file.search.match_bg.into(),
1209            search_match_fg: file.search.match_fg.into(),
1210            diagnostic_error_fg: file.diagnostic.error_fg.into(),
1211            diagnostic_error_bg: file.diagnostic.error_bg.into(),
1212            diagnostic_warning_fg: file.diagnostic.warning_fg.into(),
1213            diagnostic_warning_bg: file.diagnostic.warning_bg.into(),
1214            diagnostic_info_fg: file.diagnostic.info_fg.into(),
1215            diagnostic_info_bg: file.diagnostic.info_bg.into(),
1216            diagnostic_hint_fg: file.diagnostic.hint_fg.into(),
1217            diagnostic_hint_bg: file.diagnostic.hint_bg.into(),
1218            syntax_keyword: file.syntax.keyword.into(),
1219            syntax_string: file.syntax.string.into(),
1220            syntax_comment: file.syntax.comment.into(),
1221            syntax_function: file.syntax.function.into(),
1222            syntax_type: file.syntax.type_.into(),
1223            syntax_variable: file.syntax.variable.into(),
1224            syntax_constant: file.syntax.constant.into(),
1225            syntax_operator: file.syntax.operator.into(),
1226            syntax_punctuation_bracket: file.syntax.punctuation_bracket.into(),
1227            syntax_punctuation_delimiter: file.syntax.punctuation_delimiter.into(),
1228        }
1229    }
1230}
1231
1232impl From<Theme> for ThemeFile {
1233    fn from(theme: Theme) -> Self {
1234        Self {
1235            name: theme.name,
1236            editor: EditorColors {
1237                bg: theme.editor_bg.into(),
1238                fg: theme.editor_fg.into(),
1239                cursor: theme.cursor.into(),
1240                inactive_cursor: theme.inactive_cursor.into(),
1241                selection_bg: theme.selection_bg.into(),
1242                current_line_bg: theme.current_line_bg.into(),
1243                line_number_fg: theme.line_number_fg.into(),
1244                line_number_bg: theme.line_number_bg.into(),
1245                diff_add_bg: theme.diff_add_bg.into(),
1246                diff_remove_bg: theme.diff_remove_bg.into(),
1247                diff_add_highlight_bg: Some(theme.diff_add_highlight_bg.into()),
1248                diff_remove_highlight_bg: Some(theme.diff_remove_highlight_bg.into()),
1249                diff_modify_bg: theme.diff_modify_bg.into(),
1250                ruler_bg: theme.ruler_bg.into(),
1251                whitespace_indicator_fg: theme.whitespace_indicator_fg.into(),
1252                after_eof_bg: Some(theme.after_eof_bg.into()),
1253            },
1254            ui: UiColors {
1255                tab_active_fg: theme.tab_active_fg.into(),
1256                tab_active_bg: theme.tab_active_bg.into(),
1257                tab_inactive_fg: theme.tab_inactive_fg.into(),
1258                tab_inactive_bg: theme.tab_inactive_bg.into(),
1259                tab_separator_bg: theme.tab_separator_bg.into(),
1260                tab_close_hover_fg: theme.tab_close_hover_fg.into(),
1261                tab_hover_bg: theme.tab_hover_bg.into(),
1262                menu_bg: theme.menu_bg.into(),
1263                menu_fg: theme.menu_fg.into(),
1264                menu_active_bg: theme.menu_active_bg.into(),
1265                menu_active_fg: theme.menu_active_fg.into(),
1266                menu_dropdown_bg: theme.menu_dropdown_bg.into(),
1267                menu_dropdown_fg: theme.menu_dropdown_fg.into(),
1268                menu_highlight_bg: theme.menu_highlight_bg.into(),
1269                menu_highlight_fg: theme.menu_highlight_fg.into(),
1270                menu_border_fg: theme.menu_border_fg.into(),
1271                menu_separator_fg: theme.menu_separator_fg.into(),
1272                menu_hover_bg: theme.menu_hover_bg.into(),
1273                menu_hover_fg: theme.menu_hover_fg.into(),
1274                menu_disabled_fg: theme.menu_disabled_fg.into(),
1275                menu_disabled_bg: theme.menu_disabled_bg.into(),
1276                status_bar_fg: theme.status_bar_fg.into(),
1277                status_bar_bg: theme.status_bar_bg.into(),
1278                prompt_fg: theme.prompt_fg.into(),
1279                prompt_bg: theme.prompt_bg.into(),
1280                prompt_selection_fg: theme.prompt_selection_fg.into(),
1281                prompt_selection_bg: theme.prompt_selection_bg.into(),
1282                popup_border_fg: theme.popup_border_fg.into(),
1283                popup_bg: theme.popup_bg.into(),
1284                popup_selection_bg: theme.popup_selection_bg.into(),
1285                popup_selection_fg: theme.popup_selection_fg.into(),
1286                popup_text_fg: theme.popup_text_fg.into(),
1287                suggestion_bg: theme.suggestion_bg.into(),
1288                suggestion_selected_bg: theme.suggestion_selected_bg.into(),
1289                help_bg: theme.help_bg.into(),
1290                help_fg: theme.help_fg.into(),
1291                help_key_fg: theme.help_key_fg.into(),
1292                help_separator_fg: theme.help_separator_fg.into(),
1293                help_indicator_fg: theme.help_indicator_fg.into(),
1294                help_indicator_bg: theme.help_indicator_bg.into(),
1295                inline_code_bg: theme.inline_code_bg.into(),
1296                split_separator_fg: theme.split_separator_fg.into(),
1297                split_separator_hover_fg: theme.split_separator_hover_fg.into(),
1298                scrollbar_track_fg: theme.scrollbar_track_fg.into(),
1299                scrollbar_thumb_fg: theme.scrollbar_thumb_fg.into(),
1300                scrollbar_track_hover_fg: theme.scrollbar_track_hover_fg.into(),
1301                scrollbar_thumb_hover_fg: theme.scrollbar_thumb_hover_fg.into(),
1302                compose_margin_bg: theme.compose_margin_bg.into(),
1303                semantic_highlight_bg: theme.semantic_highlight_bg.into(),
1304                terminal_bg: theme.terminal_bg.into(),
1305                terminal_fg: theme.terminal_fg.into(),
1306                status_warning_indicator_bg: theme.status_warning_indicator_bg.into(),
1307                status_warning_indicator_fg: theme.status_warning_indicator_fg.into(),
1308                status_error_indicator_bg: theme.status_error_indicator_bg.into(),
1309                status_error_indicator_fg: theme.status_error_indicator_fg.into(),
1310                status_warning_indicator_hover_bg: theme.status_warning_indicator_hover_bg.into(),
1311                status_warning_indicator_hover_fg: theme.status_warning_indicator_hover_fg.into(),
1312                status_error_indicator_hover_bg: theme.status_error_indicator_hover_bg.into(),
1313                status_error_indicator_hover_fg: theme.status_error_indicator_hover_fg.into(),
1314                tab_drop_zone_bg: theme.tab_drop_zone_bg.into(),
1315                tab_drop_zone_border: theme.tab_drop_zone_border.into(),
1316                settings_selected_bg: theme.settings_selected_bg.into(),
1317                settings_selected_fg: theme.settings_selected_fg.into(),
1318                file_status_added_fg: Some(theme.file_status_added_fg.into()),
1319                file_status_modified_fg: Some(theme.file_status_modified_fg.into()),
1320                file_status_deleted_fg: Some(theme.file_status_deleted_fg.into()),
1321                file_status_renamed_fg: Some(theme.file_status_renamed_fg.into()),
1322                file_status_untracked_fg: Some(theme.file_status_untracked_fg.into()),
1323                file_status_conflicted_fg: Some(theme.file_status_conflicted_fg.into()),
1324            },
1325            search: SearchColors {
1326                match_bg: theme.search_match_bg.into(),
1327                match_fg: theme.search_match_fg.into(),
1328            },
1329            diagnostic: DiagnosticColors {
1330                error_fg: theme.diagnostic_error_fg.into(),
1331                error_bg: theme.diagnostic_error_bg.into(),
1332                warning_fg: theme.diagnostic_warning_fg.into(),
1333                warning_bg: theme.diagnostic_warning_bg.into(),
1334                info_fg: theme.diagnostic_info_fg.into(),
1335                info_bg: theme.diagnostic_info_bg.into(),
1336                hint_fg: theme.diagnostic_hint_fg.into(),
1337                hint_bg: theme.diagnostic_hint_bg.into(),
1338            },
1339            syntax: SyntaxColors {
1340                keyword: theme.syntax_keyword.into(),
1341                string: theme.syntax_string.into(),
1342                comment: theme.syntax_comment.into(),
1343                function: theme.syntax_function.into(),
1344                type_: theme.syntax_type.into(),
1345                variable: theme.syntax_variable.into(),
1346                constant: theme.syntax_constant.into(),
1347                operator: theme.syntax_operator.into(),
1348                punctuation_bracket: theme.syntax_punctuation_bracket.into(),
1349                punctuation_delimiter: theme.syntax_punctuation_delimiter.into(),
1350            },
1351        }
1352    }
1353}
1354
1355impl Theme {
1356    /// Returns `true` when the theme has a light background.
1357    ///
1358    /// Uses the relative luminance of `editor_bg` (perceived brightness).
1359    /// A threshold of 0.5 separates dark from light; for `Color::Reset` or
1360    /// unresolvable colors, falls back to `false` (dark).
1361    pub fn is_light(&self) -> bool {
1362        if let Some((r, g, b)) = color_to_rgb(self.editor_bg) {
1363            // sRGB relative luminance (ITU-R BT.709)
1364            let lum = 0.2126 * (r as f64 / 255.0)
1365                + 0.7152 * (g as f64 / 255.0)
1366                + 0.0722 * (b as f64 / 255.0);
1367            lum > 0.5
1368        } else {
1369            false
1370        }
1371    }
1372
1373    /// Load a builtin theme by name (no I/O, uses embedded JSON).
1374    pub fn load_builtin(name: &str) -> Option<Self> {
1375        BUILTIN_THEMES
1376            .iter()
1377            .find(|t| t.name == name)
1378            .and_then(|t| serde_json::from_str::<ThemeFile>(t.json).ok())
1379            .map(|tf| tf.into())
1380    }
1381
1382    /// Parse theme from JSON string (no I/O).
1383    pub fn from_json(json: &str) -> Result<Self, String> {
1384        let theme_file: ThemeFile =
1385            serde_json::from_str(json).map_err(|e| format!("Failed to parse theme JSON: {}", e))?;
1386        Ok(theme_file.into())
1387    }
1388
1389    /// Resolve a theme key to a Color.
1390    ///
1391    /// Theme keys use dot notation: "section.field"
1392    /// Examples:
1393    /// - "ui.status_bar_fg" -> status_bar_fg
1394    /// - "editor.selection_bg" -> selection_bg
1395    /// - "syntax.keyword" -> syntax_keyword
1396    /// - "diagnostic.error_fg" -> diagnostic_error_fg
1397    ///
1398    /// Returns None if the key is not recognized.
1399    pub fn resolve_theme_key(&self, key: &str) -> Option<Color> {
1400        // Parse "section.field" format
1401        let parts: Vec<&str> = key.split('.').collect();
1402        if parts.len() != 2 {
1403            return None;
1404        }
1405
1406        let (section, field) = (parts[0], parts[1]);
1407
1408        match section {
1409            "editor" => match field {
1410                "bg" => Some(self.editor_bg),
1411                "fg" => Some(self.editor_fg),
1412                "cursor" => Some(self.cursor),
1413                "inactive_cursor" => Some(self.inactive_cursor),
1414                "selection_bg" => Some(self.selection_bg),
1415                "current_line_bg" => Some(self.current_line_bg),
1416                "line_number_fg" => Some(self.line_number_fg),
1417                "line_number_bg" => Some(self.line_number_bg),
1418                "diff_add_bg" => Some(self.diff_add_bg),
1419                "diff_remove_bg" => Some(self.diff_remove_bg),
1420                "diff_modify_bg" => Some(self.diff_modify_bg),
1421                "ruler_bg" => Some(self.ruler_bg),
1422                "whitespace_indicator_fg" => Some(self.whitespace_indicator_fg),
1423                _ => None,
1424            },
1425            "ui" => match field {
1426                "tab_active_fg" => Some(self.tab_active_fg),
1427                "tab_active_bg" => Some(self.tab_active_bg),
1428                "tab_inactive_fg" => Some(self.tab_inactive_fg),
1429                "tab_inactive_bg" => Some(self.tab_inactive_bg),
1430                "status_bar_fg" => Some(self.status_bar_fg),
1431                "status_bar_bg" => Some(self.status_bar_bg),
1432                "prompt_fg" => Some(self.prompt_fg),
1433                "prompt_bg" => Some(self.prompt_bg),
1434                "prompt_selection_fg" => Some(self.prompt_selection_fg),
1435                "prompt_selection_bg" => Some(self.prompt_selection_bg),
1436                "popup_bg" => Some(self.popup_bg),
1437                "popup_border_fg" => Some(self.popup_border_fg),
1438                "popup_selection_bg" => Some(self.popup_selection_bg),
1439                "popup_selection_fg" => Some(self.popup_selection_fg),
1440                "popup_text_fg" => Some(self.popup_text_fg),
1441                "menu_bg" => Some(self.menu_bg),
1442                "menu_fg" => Some(self.menu_fg),
1443                "menu_active_bg" => Some(self.menu_active_bg),
1444                "menu_active_fg" => Some(self.menu_active_fg),
1445                "help_bg" => Some(self.help_bg),
1446                "help_fg" => Some(self.help_fg),
1447                "help_key_fg" => Some(self.help_key_fg),
1448                "split_separator_fg" => Some(self.split_separator_fg),
1449                "scrollbar_thumb_fg" => Some(self.scrollbar_thumb_fg),
1450                "semantic_highlight_bg" => Some(self.semantic_highlight_bg),
1451                "file_status_added_fg" => Some(self.file_status_added_fg),
1452                "file_status_modified_fg" => Some(self.file_status_modified_fg),
1453                "file_status_deleted_fg" => Some(self.file_status_deleted_fg),
1454                "file_status_renamed_fg" => Some(self.file_status_renamed_fg),
1455                "file_status_untracked_fg" => Some(self.file_status_untracked_fg),
1456                "file_status_conflicted_fg" => Some(self.file_status_conflicted_fg),
1457                _ => None,
1458            },
1459            "syntax" => match field {
1460                "keyword" => Some(self.syntax_keyword),
1461                "string" => Some(self.syntax_string),
1462                "comment" => Some(self.syntax_comment),
1463                "function" => Some(self.syntax_function),
1464                "type" => Some(self.syntax_type),
1465                "variable" => Some(self.syntax_variable),
1466                "constant" => Some(self.syntax_constant),
1467                "operator" => Some(self.syntax_operator),
1468                "punctuation_bracket" => Some(self.syntax_punctuation_bracket),
1469                "punctuation_delimiter" => Some(self.syntax_punctuation_delimiter),
1470                _ => None,
1471            },
1472            "diagnostic" => match field {
1473                "error_fg" => Some(self.diagnostic_error_fg),
1474                "error_bg" => Some(self.diagnostic_error_bg),
1475                "warning_fg" => Some(self.diagnostic_warning_fg),
1476                "warning_bg" => Some(self.diagnostic_warning_bg),
1477                "info_fg" => Some(self.diagnostic_info_fg),
1478                "info_bg" => Some(self.diagnostic_info_bg),
1479                "hint_fg" => Some(self.diagnostic_hint_fg),
1480                "hint_bg" => Some(self.diagnostic_hint_bg),
1481                _ => None,
1482            },
1483            "search" => match field {
1484                "match_bg" => Some(self.search_match_bg),
1485                "match_fg" => Some(self.search_match_fg),
1486                _ => None,
1487            },
1488            _ => None,
1489        }
1490    }
1491
1492    /// Mutable companion to [`resolve_theme_key`]. Keep the two matches in
1493    /// lock-step: any key readable by `resolve_theme_key` should also be
1494    /// writable here, and vice versa.
1495    pub fn resolve_theme_key_mut(&mut self, key: &str) -> Option<&mut Color> {
1496        let parts: Vec<&str> = key.split('.').collect();
1497        if parts.len() != 2 {
1498            return None;
1499        }
1500        let (section, field) = (parts[0], parts[1]);
1501        match section {
1502            "editor" => match field {
1503                "bg" => Some(&mut self.editor_bg),
1504                "fg" => Some(&mut self.editor_fg),
1505                "cursor" => Some(&mut self.cursor),
1506                "inactive_cursor" => Some(&mut self.inactive_cursor),
1507                "selection_bg" => Some(&mut self.selection_bg),
1508                "current_line_bg" => Some(&mut self.current_line_bg),
1509                "line_number_fg" => Some(&mut self.line_number_fg),
1510                "line_number_bg" => Some(&mut self.line_number_bg),
1511                "diff_add_bg" => Some(&mut self.diff_add_bg),
1512                "diff_remove_bg" => Some(&mut self.diff_remove_bg),
1513                "diff_modify_bg" => Some(&mut self.diff_modify_bg),
1514                "ruler_bg" => Some(&mut self.ruler_bg),
1515                "whitespace_indicator_fg" => Some(&mut self.whitespace_indicator_fg),
1516                _ => None,
1517            },
1518            "ui" => match field {
1519                "tab_active_fg" => Some(&mut self.tab_active_fg),
1520                "tab_active_bg" => Some(&mut self.tab_active_bg),
1521                "tab_inactive_fg" => Some(&mut self.tab_inactive_fg),
1522                "tab_inactive_bg" => Some(&mut self.tab_inactive_bg),
1523                "status_bar_fg" => Some(&mut self.status_bar_fg),
1524                "status_bar_bg" => Some(&mut self.status_bar_bg),
1525                "prompt_fg" => Some(&mut self.prompt_fg),
1526                "prompt_bg" => Some(&mut self.prompt_bg),
1527                "prompt_selection_fg" => Some(&mut self.prompt_selection_fg),
1528                "prompt_selection_bg" => Some(&mut self.prompt_selection_bg),
1529                "popup_bg" => Some(&mut self.popup_bg),
1530                "popup_border_fg" => Some(&mut self.popup_border_fg),
1531                "popup_selection_bg" => Some(&mut self.popup_selection_bg),
1532                "popup_selection_fg" => Some(&mut self.popup_selection_fg),
1533                "popup_text_fg" => Some(&mut self.popup_text_fg),
1534                "menu_bg" => Some(&mut self.menu_bg),
1535                "menu_fg" => Some(&mut self.menu_fg),
1536                "menu_active_bg" => Some(&mut self.menu_active_bg),
1537                "menu_active_fg" => Some(&mut self.menu_active_fg),
1538                "help_bg" => Some(&mut self.help_bg),
1539                "help_fg" => Some(&mut self.help_fg),
1540                "help_key_fg" => Some(&mut self.help_key_fg),
1541                "split_separator_fg" => Some(&mut self.split_separator_fg),
1542                "scrollbar_thumb_fg" => Some(&mut self.scrollbar_thumb_fg),
1543                "semantic_highlight_bg" => Some(&mut self.semantic_highlight_bg),
1544                "file_status_added_fg" => Some(&mut self.file_status_added_fg),
1545                "file_status_modified_fg" => Some(&mut self.file_status_modified_fg),
1546                "file_status_deleted_fg" => Some(&mut self.file_status_deleted_fg),
1547                "file_status_renamed_fg" => Some(&mut self.file_status_renamed_fg),
1548                "file_status_untracked_fg" => Some(&mut self.file_status_untracked_fg),
1549                "file_status_conflicted_fg" => Some(&mut self.file_status_conflicted_fg),
1550                _ => None,
1551            },
1552            "syntax" => match field {
1553                "keyword" => Some(&mut self.syntax_keyword),
1554                "string" => Some(&mut self.syntax_string),
1555                "comment" => Some(&mut self.syntax_comment),
1556                "function" => Some(&mut self.syntax_function),
1557                "type" => Some(&mut self.syntax_type),
1558                "variable" => Some(&mut self.syntax_variable),
1559                "constant" => Some(&mut self.syntax_constant),
1560                "operator" => Some(&mut self.syntax_operator),
1561                "punctuation_bracket" => Some(&mut self.syntax_punctuation_bracket),
1562                "punctuation_delimiter" => Some(&mut self.syntax_punctuation_delimiter),
1563                _ => None,
1564            },
1565            "diagnostic" => match field {
1566                "error_fg" => Some(&mut self.diagnostic_error_fg),
1567                "error_bg" => Some(&mut self.diagnostic_error_bg),
1568                "warning_fg" => Some(&mut self.diagnostic_warning_fg),
1569                "warning_bg" => Some(&mut self.diagnostic_warning_bg),
1570                "info_fg" => Some(&mut self.diagnostic_info_fg),
1571                "info_bg" => Some(&mut self.diagnostic_info_bg),
1572                "hint_fg" => Some(&mut self.diagnostic_hint_fg),
1573                "hint_bg" => Some(&mut self.diagnostic_hint_bg),
1574                _ => None,
1575            },
1576            "search" => match field {
1577                "match_bg" => Some(&mut self.search_match_bg),
1578                "match_fg" => Some(&mut self.search_match_fg),
1579                _ => None,
1580            },
1581            _ => None,
1582        }
1583    }
1584
1585    /// Apply a map of `"section.field" -> Color` overrides to the running
1586    /// theme in-place. Returns the number of keys that matched a known
1587    /// theme field. Unknown keys are silently dropped so a typo in a fast
1588    /// animation loop doesn't crash the caller.
1589    pub fn override_colors<I, K>(&mut self, overrides: I) -> usize
1590    where
1591        I: IntoIterator<Item = (K, Color)>,
1592        K: AsRef<str>,
1593    {
1594        let mut applied = 0;
1595        for (key, color) in overrides {
1596            if let Some(slot) = self.resolve_theme_key_mut(key.as_ref()) {
1597                *slot = color;
1598                applied += 1;
1599            }
1600        }
1601        applied
1602    }
1603}
1604
1605// =============================================================================
1606// Theme Schema Generation for Plugin API
1607// =============================================================================
1608
1609/// Returns the raw JSON Schema for ThemeFile, generated by schemars.
1610/// The schema uses standard JSON Schema format with $ref for type references.
1611/// Plugins are responsible for parsing and resolving $ref references.
1612pub fn get_theme_schema() -> serde_json::Value {
1613    use schemars::schema_for;
1614    let schema = schema_for!(ThemeFile);
1615    serde_json::to_value(&schema).unwrap_or_default()
1616}
1617
1618/// Returns a map of built-in theme names to their JSON content.
1619pub fn get_builtin_themes() -> serde_json::Value {
1620    let mut map = serde_json::Map::new();
1621    for theme in BUILTIN_THEMES {
1622        map.insert(
1623            theme.name.to_string(),
1624            serde_json::Value::String(theme.json.to_string()),
1625        );
1626    }
1627    serde_json::Value::Object(map)
1628}
1629
1630#[cfg(test)]
1631mod tests {
1632    use super::*;
1633
1634    #[test]
1635    fn test_load_builtin_theme() {
1636        let dark = Theme::load_builtin(THEME_DARK).expect("Dark theme must exist");
1637        assert_eq!(dark.name, THEME_DARK);
1638
1639        let light = Theme::load_builtin(THEME_LIGHT).expect("Light theme must exist");
1640        assert_eq!(light.name, THEME_LIGHT);
1641
1642        let high_contrast =
1643            Theme::load_builtin(THEME_HIGH_CONTRAST).expect("High contrast theme must exist");
1644        assert_eq!(high_contrast.name, THEME_HIGH_CONTRAST);
1645    }
1646
1647    #[test]
1648    fn test_builtin_themes_match_schema() {
1649        for theme in BUILTIN_THEMES {
1650            let _: ThemeFile = serde_json::from_str(theme.json)
1651                .unwrap_or_else(|_| panic!("Theme '{}' does not match schema", theme.name));
1652        }
1653    }
1654
1655    #[test]
1656    fn test_from_json() {
1657        let json = r#"{"name":"test","editor":{},"ui":{},"search":{},"diagnostic":{},"syntax":{}}"#;
1658        let theme = Theme::from_json(json).expect("Should parse minimal theme");
1659        assert_eq!(theme.name, "test");
1660    }
1661
1662    #[test]
1663    fn test_default_reset_color() {
1664        // Test that "Default" maps to Color::Reset
1665        let color: Color = ColorDef::Named("Default".to_string()).into();
1666        assert_eq!(color, Color::Reset);
1667
1668        // Test that "Reset" also maps to Color::Reset
1669        let color: Color = ColorDef::Named("Reset".to_string()).into();
1670        assert_eq!(color, Color::Reset);
1671    }
1672
1673    #[test]
1674    fn test_file_status_colors_fall_back_to_diagnostic_colors() {
1675        // A theme with NO file_status_* keys should inherit from diagnostic colors
1676        let json = r#"{
1677            "name": "test-fallback",
1678            "editor": {},
1679            "ui": {},
1680            "search": {},
1681            "diagnostic": {
1682                "error_fg": [220, 50, 47],
1683                "warning_fg": [181, 137, 0],
1684                "info_fg": [38, 139, 210],
1685                "hint_fg": [101, 123, 131]
1686            },
1687            "syntax": {}
1688        }"#;
1689        let theme = Theme::from_json(json).expect("Should parse theme without file_status keys");
1690
1691        // Verify fallback: added/renamed -> info_fg
1692        assert_eq!(theme.file_status_added_fg, Color::Rgb(38, 139, 210));
1693        assert_eq!(theme.file_status_renamed_fg, Color::Rgb(38, 139, 210));
1694        // modified -> warning_fg
1695        assert_eq!(theme.file_status_modified_fg, Color::Rgb(181, 137, 0));
1696        // deleted/conflicted -> error_fg
1697        assert_eq!(theme.file_status_deleted_fg, Color::Rgb(220, 50, 47));
1698        assert_eq!(theme.file_status_conflicted_fg, Color::Rgb(220, 50, 47));
1699        // untracked -> hint_fg
1700        assert_eq!(theme.file_status_untracked_fg, Color::Rgb(101, 123, 131));
1701    }
1702
1703    #[test]
1704    fn test_file_status_colors_explicit_override() {
1705        // A theme WITH explicit file_status keys should use those, not the fallback
1706        let json = r#"{
1707            "name": "test-override",
1708            "editor": {},
1709            "ui": {
1710                "file_status_added_fg": [80, 250, 123],
1711                "file_status_modified_fg": [255, 184, 108]
1712            },
1713            "search": {},
1714            "diagnostic": {
1715                "info_fg": [38, 139, 210],
1716                "warning_fg": [181, 137, 0]
1717            },
1718            "syntax": {}
1719        }"#;
1720        let theme = Theme::from_json(json).expect("Should parse theme with file_status overrides");
1721
1722        // Explicit overrides should win
1723        assert_eq!(theme.file_status_added_fg, Color::Rgb(80, 250, 123));
1724        assert_eq!(theme.file_status_modified_fg, Color::Rgb(255, 184, 108));
1725        // Non-overridden should still fall back
1726        assert_eq!(theme.file_status_renamed_fg, Color::Rgb(38, 139, 210));
1727    }
1728
1729    #[test]
1730    fn test_file_status_colors_resolve_via_theme_key() {
1731        let json = r#"{
1732            "name": "test-resolve",
1733            "editor": {},
1734            "ui": {
1735                "file_status_added_fg": [80, 250, 123]
1736            },
1737            "search": {},
1738            "diagnostic": {
1739                "warning_fg": [181, 137, 0]
1740            },
1741            "syntax": {}
1742        }"#;
1743        let theme = Theme::from_json(json).expect("Should parse theme");
1744
1745        // Theme key resolution should work for file_status keys
1746        assert_eq!(
1747            theme.resolve_theme_key("ui.file_status_added_fg"),
1748            Some(Color::Rgb(80, 250, 123))
1749        );
1750        assert_eq!(
1751            theme.resolve_theme_key("ui.file_status_modified_fg"),
1752            Some(Color::Rgb(181, 137, 0))
1753        );
1754    }
1755
1756    #[test]
1757    fn override_colors_writes_known_keys_and_drops_unknowns() {
1758        let mut theme = Theme::load_builtin(THEME_DARK).expect("dark builtin");
1759        let applied = theme.override_colors([
1760            ("editor.bg".to_string(), Color::Rgb(10, 20, 30)),
1761            ("ui.status_bar_fg".to_string(), Color::Rgb(1, 2, 3)),
1762            ("does.not_exist".to_string(), Color::Rgb(9, 9, 9)),
1763            ("garbage_no_dot".to_string(), Color::Rgb(9, 9, 9)),
1764        ]);
1765        assert_eq!(applied, 2, "only the two valid keys should be applied");
1766        assert_eq!(
1767            theme.resolve_theme_key("editor.bg"),
1768            Some(Color::Rgb(10, 20, 30))
1769        );
1770        assert_eq!(
1771            theme.resolve_theme_key("ui.status_bar_fg"),
1772            Some(Color::Rgb(1, 2, 3))
1773        );
1774    }
1775
1776    #[test]
1777    fn resolve_theme_key_mut_matches_resolve_theme_key_domain() {
1778        // If a key resolves readably, it must also resolve as a mutable
1779        // slot — the two matches must stay in lock-step.
1780        let mut theme = Theme::load_builtin(THEME_DARK).expect("dark builtin");
1781        let probe = [
1782            "editor.bg",
1783            "editor.fg",
1784            "ui.status_bar_fg",
1785            "ui.tab_active_bg",
1786            "syntax.keyword",
1787            "diagnostic.error_fg",
1788            "search.match_bg",
1789        ];
1790        for key in probe {
1791            assert!(
1792                theme.resolve_theme_key(key).is_some(),
1793                "reader lost key {key}"
1794            );
1795            assert!(
1796                theme.resolve_theme_key_mut(key).is_some(),
1797                "mutator missing key {key}"
1798            );
1799        }
1800    }
1801
1802    #[test]
1803    fn test_all_builtin_themes_have_file_status_colors() {
1804        // Every builtin theme must produce valid file_status colors (via fallback or explicit)
1805        for builtin in BUILTIN_THEMES {
1806            let theme = Theme::from_json(builtin.json)
1807                .unwrap_or_else(|e| panic!("Theme '{}' failed to parse: {}", builtin.name, e));
1808
1809            // All six keys must resolve to Some via resolve_theme_key
1810            for key in &[
1811                "ui.file_status_added_fg",
1812                "ui.file_status_modified_fg",
1813                "ui.file_status_deleted_fg",
1814                "ui.file_status_renamed_fg",
1815                "ui.file_status_untracked_fg",
1816                "ui.file_status_conflicted_fg",
1817            ] {
1818                assert!(
1819                    theme.resolve_theme_key(key).is_some(),
1820                    "Theme '{}' missing resolution for '{}'",
1821                    builtin.name,
1822                    key
1823                );
1824            }
1825        }
1826    }
1827}