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/// Serializable color representation
123#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
124#[serde(untagged)]
125pub enum ColorDef {
126    /// RGB color as [r, g, b]
127    Rgb(u8, u8, u8),
128    /// Named color
129    Named(String),
130}
131
132impl From<ColorDef> for Color {
133    fn from(def: ColorDef) -> Self {
134        match def {
135            ColorDef::Rgb(r, g, b) => Color::Rgb(r, g, b),
136            ColorDef::Named(name) => match name.as_str() {
137                "Black" => Color::Black,
138                "Red" => Color::Red,
139                "Green" => Color::Green,
140                "Yellow" => Color::Yellow,
141                "Blue" => Color::Blue,
142                "Magenta" => Color::Magenta,
143                "Cyan" => Color::Cyan,
144                "Gray" => Color::Gray,
145                "DarkGray" => Color::DarkGray,
146                "LightRed" => Color::LightRed,
147                "LightGreen" => Color::LightGreen,
148                "LightYellow" => Color::LightYellow,
149                "LightBlue" => Color::LightBlue,
150                "LightMagenta" => Color::LightMagenta,
151                "LightCyan" => Color::LightCyan,
152                "White" => Color::White,
153                // Default/Reset uses the terminal's default color (preserves transparency)
154                "Default" | "Reset" => Color::Reset,
155                _ => Color::White, // Default fallback
156            },
157        }
158    }
159}
160
161/// Convert a named color string (e.g. "Yellow", "Red") to a ratatui Color.
162/// Returns None if the string is not a recognized named color.
163pub fn named_color_from_str(name: &str) -> Option<Color> {
164    match name {
165        "Black" => Some(Color::Black),
166        "Red" => Some(Color::Red),
167        "Green" => Some(Color::Green),
168        "Yellow" => Some(Color::Yellow),
169        "Blue" => Some(Color::Blue),
170        "Magenta" => Some(Color::Magenta),
171        "Cyan" => Some(Color::Cyan),
172        "Gray" => Some(Color::Gray),
173        "DarkGray" => Some(Color::DarkGray),
174        "LightRed" => Some(Color::LightRed),
175        "LightGreen" => Some(Color::LightGreen),
176        "LightYellow" => Some(Color::LightYellow),
177        "LightBlue" => Some(Color::LightBlue),
178        "LightMagenta" => Some(Color::LightMagenta),
179        "LightCyan" => Some(Color::LightCyan),
180        "White" => Some(Color::White),
181        "Default" | "Reset" => Some(Color::Reset),
182        _ => None,
183    }
184}
185
186impl From<Color> for ColorDef {
187    fn from(color: Color) -> Self {
188        match color {
189            Color::Rgb(r, g, b) => ColorDef::Rgb(r, g, b),
190            Color::White => ColorDef::Named("White".to_string()),
191            Color::Black => ColorDef::Named("Black".to_string()),
192            Color::Red => ColorDef::Named("Red".to_string()),
193            Color::Green => ColorDef::Named("Green".to_string()),
194            Color::Blue => ColorDef::Named("Blue".to_string()),
195            Color::Yellow => ColorDef::Named("Yellow".to_string()),
196            Color::Magenta => ColorDef::Named("Magenta".to_string()),
197            Color::Cyan => ColorDef::Named("Cyan".to_string()),
198            Color::Gray => ColorDef::Named("Gray".to_string()),
199            Color::DarkGray => ColorDef::Named("DarkGray".to_string()),
200            Color::LightRed => ColorDef::Named("LightRed".to_string()),
201            Color::LightGreen => ColorDef::Named("LightGreen".to_string()),
202            Color::LightBlue => ColorDef::Named("LightBlue".to_string()),
203            Color::LightYellow => ColorDef::Named("LightYellow".to_string()),
204            Color::LightMagenta => ColorDef::Named("LightMagenta".to_string()),
205            Color::LightCyan => ColorDef::Named("LightCyan".to_string()),
206            Color::Reset => ColorDef::Named("Default".to_string()),
207            Color::Indexed(_) => {
208                // Fallback for indexed colors
209                if let Some((r, g, b)) = color_to_rgb(color) {
210                    ColorDef::Rgb(r, g, b)
211                } else {
212                    ColorDef::Named("Default".to_string())
213                }
214            }
215        }
216    }
217}
218
219/// Serializable theme definition (matches JSON structure)
220#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
221pub struct ThemeFile {
222    /// Theme name
223    pub name: String,
224    /// Editor area colors
225    pub editor: EditorColors,
226    /// UI element colors (tabs, menus, status bar, etc.)
227    pub ui: UiColors,
228    /// Search result highlighting colors
229    pub search: SearchColors,
230    /// LSP diagnostic colors (errors, warnings, etc.)
231    pub diagnostic: DiagnosticColors,
232    /// Syntax highlighting colors
233    pub syntax: SyntaxColors,
234}
235
236/// Editor area colors
237#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
238pub struct EditorColors {
239    /// Editor background color
240    #[serde(default = "default_editor_bg")]
241    pub bg: ColorDef,
242    /// Default text color
243    #[serde(default = "default_editor_fg")]
244    pub fg: ColorDef,
245    /// Cursor color
246    #[serde(default = "default_cursor")]
247    pub cursor: ColorDef,
248    /// Cursor color in unfocused splits
249    #[serde(default = "default_inactive_cursor")]
250    pub inactive_cursor: ColorDef,
251    /// Selected text background
252    #[serde(default = "default_selection_bg")]
253    pub selection_bg: ColorDef,
254    /// Background of the line containing cursor
255    #[serde(default = "default_current_line_bg")]
256    pub current_line_bg: ColorDef,
257    /// Line number text color
258    #[serde(default = "default_line_number_fg")]
259    pub line_number_fg: ColorDef,
260    /// Line number gutter background
261    #[serde(default = "default_line_number_bg")]
262    pub line_number_bg: ColorDef,
263    /// Diff added line background
264    #[serde(default = "default_diff_add_bg")]
265    pub diff_add_bg: ColorDef,
266    /// Diff removed line background
267    #[serde(default = "default_diff_remove_bg")]
268    pub diff_remove_bg: ColorDef,
269    /// Diff added word-level highlight background (optional override)
270    /// When not set, computed by brightening diff_add_bg
271    #[serde(default)]
272    pub diff_add_highlight_bg: Option<ColorDef>,
273    /// Diff removed word-level highlight background (optional override)
274    /// When not set, computed by brightening diff_remove_bg
275    #[serde(default)]
276    pub diff_remove_highlight_bg: Option<ColorDef>,
277    /// Diff modified line background
278    #[serde(default = "default_diff_modify_bg")]
279    pub diff_modify_bg: ColorDef,
280    /// Vertical ruler background color
281    #[serde(default = "default_ruler_bg")]
282    pub ruler_bg: ColorDef,
283    /// Whitespace indicator foreground color (for tab arrows and space dots)
284    #[serde(default = "default_whitespace_indicator_fg")]
285    pub whitespace_indicator_fg: ColorDef,
286}
287
288// Default editor colors (for minimal themes)
289fn default_editor_bg() -> ColorDef {
290    ColorDef::Rgb(30, 30, 30)
291}
292fn default_editor_fg() -> ColorDef {
293    ColorDef::Rgb(212, 212, 212)
294}
295fn default_cursor() -> ColorDef {
296    ColorDef::Rgb(255, 255, 255)
297}
298fn default_inactive_cursor() -> ColorDef {
299    ColorDef::Named("DarkGray".to_string())
300}
301fn default_selection_bg() -> ColorDef {
302    ColorDef::Rgb(38, 79, 120)
303}
304fn default_current_line_bg() -> ColorDef {
305    ColorDef::Rgb(40, 40, 40)
306}
307fn default_line_number_fg() -> ColorDef {
308    ColorDef::Rgb(100, 100, 100)
309}
310fn default_line_number_bg() -> ColorDef {
311    ColorDef::Rgb(30, 30, 30)
312}
313fn default_diff_add_bg() -> ColorDef {
314    ColorDef::Rgb(35, 60, 35) // Dark green
315}
316fn default_diff_remove_bg() -> ColorDef {
317    ColorDef::Rgb(70, 35, 35) // Dark red
318}
319fn default_diff_modify_bg() -> ColorDef {
320    ColorDef::Rgb(40, 38, 30) // Very subtle yellow tint, close to dark bg
321}
322fn default_ruler_bg() -> ColorDef {
323    ColorDef::Rgb(50, 50, 50) // Subtle dark gray, slightly lighter than default editor bg
324}
325fn default_whitespace_indicator_fg() -> ColorDef {
326    ColorDef::Rgb(70, 70, 70) // Subdued dark gray, subtle but visible
327}
328
329/// UI element colors (tabs, menus, status bar, etc.)
330#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
331pub struct UiColors {
332    /// Active tab text color
333    #[serde(default = "default_tab_active_fg")]
334    pub tab_active_fg: ColorDef,
335    /// Active tab background color
336    #[serde(default = "default_tab_active_bg")]
337    pub tab_active_bg: ColorDef,
338    /// Inactive tab text color
339    #[serde(default = "default_tab_inactive_fg")]
340    pub tab_inactive_fg: ColorDef,
341    /// Inactive tab background color
342    #[serde(default = "default_tab_inactive_bg")]
343    pub tab_inactive_bg: ColorDef,
344    /// Tab bar separator color
345    #[serde(default = "default_tab_separator_bg")]
346    pub tab_separator_bg: ColorDef,
347    /// Tab close button hover color
348    #[serde(default = "default_tab_close_hover_fg")]
349    pub tab_close_hover_fg: ColorDef,
350    /// Tab hover background color
351    #[serde(default = "default_tab_hover_bg")]
352    pub tab_hover_bg: ColorDef,
353    /// Menu bar background
354    #[serde(default = "default_menu_bg")]
355    pub menu_bg: ColorDef,
356    /// Menu bar text color
357    #[serde(default = "default_menu_fg")]
358    pub menu_fg: ColorDef,
359    /// Active menu item background
360    #[serde(default = "default_menu_active_bg")]
361    pub menu_active_bg: ColorDef,
362    /// Active menu item text color
363    #[serde(default = "default_menu_active_fg")]
364    pub menu_active_fg: ColorDef,
365    /// Dropdown menu background
366    #[serde(default = "default_menu_dropdown_bg")]
367    pub menu_dropdown_bg: ColorDef,
368    /// Dropdown menu text color
369    #[serde(default = "default_menu_dropdown_fg")]
370    pub menu_dropdown_fg: ColorDef,
371    /// Highlighted menu item background
372    #[serde(default = "default_menu_highlight_bg")]
373    pub menu_highlight_bg: ColorDef,
374    /// Highlighted menu item text color
375    #[serde(default = "default_menu_highlight_fg")]
376    pub menu_highlight_fg: ColorDef,
377    /// Menu border color
378    #[serde(default = "default_menu_border_fg")]
379    pub menu_border_fg: ColorDef,
380    /// Menu separator line color
381    #[serde(default = "default_menu_separator_fg")]
382    pub menu_separator_fg: ColorDef,
383    /// Menu item hover background
384    #[serde(default = "default_menu_hover_bg")]
385    pub menu_hover_bg: ColorDef,
386    /// Menu item hover text color
387    #[serde(default = "default_menu_hover_fg")]
388    pub menu_hover_fg: ColorDef,
389    /// Disabled menu item text color
390    #[serde(default = "default_menu_disabled_fg")]
391    pub menu_disabled_fg: ColorDef,
392    /// Disabled menu item background
393    #[serde(default = "default_menu_disabled_bg")]
394    pub menu_disabled_bg: ColorDef,
395    /// Status bar text color
396    #[serde(default = "default_status_bar_fg")]
397    pub status_bar_fg: ColorDef,
398    /// Status bar background color
399    #[serde(default = "default_status_bar_bg")]
400    pub status_bar_bg: ColorDef,
401    /// Command prompt text color
402    #[serde(default = "default_prompt_fg")]
403    pub prompt_fg: ColorDef,
404    /// Command prompt background
405    #[serde(default = "default_prompt_bg")]
406    pub prompt_bg: ColorDef,
407    /// Prompt selected text color
408    #[serde(default = "default_prompt_selection_fg")]
409    pub prompt_selection_fg: ColorDef,
410    /// Prompt selection background
411    #[serde(default = "default_prompt_selection_bg")]
412    pub prompt_selection_bg: ColorDef,
413    /// Popup window border color
414    #[serde(default = "default_popup_border_fg")]
415    pub popup_border_fg: ColorDef,
416    /// Popup window background
417    #[serde(default = "default_popup_bg")]
418    pub popup_bg: ColorDef,
419    /// Popup selected item background
420    #[serde(default = "default_popup_selection_bg")]
421    pub popup_selection_bg: ColorDef,
422    /// Popup selected item text color
423    #[serde(default = "default_popup_selection_fg")]
424    pub popup_selection_fg: ColorDef,
425    /// Popup window text color
426    #[serde(default = "default_popup_text_fg")]
427    pub popup_text_fg: ColorDef,
428    /// Autocomplete suggestion background
429    #[serde(default = "default_suggestion_bg")]
430    pub suggestion_bg: ColorDef,
431    /// Selected suggestion background
432    #[serde(default = "default_suggestion_selected_bg")]
433    pub suggestion_selected_bg: ColorDef,
434    /// Help panel background
435    #[serde(default = "default_help_bg")]
436    pub help_bg: ColorDef,
437    /// Help panel text color
438    #[serde(default = "default_help_fg")]
439    pub help_fg: ColorDef,
440    /// Help keybinding text color
441    #[serde(default = "default_help_key_fg")]
442    pub help_key_fg: ColorDef,
443    /// Help panel separator color
444    #[serde(default = "default_help_separator_fg")]
445    pub help_separator_fg: ColorDef,
446    /// Help indicator text color
447    #[serde(default = "default_help_indicator_fg")]
448    pub help_indicator_fg: ColorDef,
449    /// Help indicator background
450    #[serde(default = "default_help_indicator_bg")]
451    pub help_indicator_bg: ColorDef,
452    /// Inline code block background
453    #[serde(default = "default_inline_code_bg")]
454    pub inline_code_bg: ColorDef,
455    /// Split pane separator color
456    #[serde(default = "default_split_separator_fg")]
457    pub split_separator_fg: ColorDef,
458    /// Split separator hover color
459    #[serde(default = "default_split_separator_hover_fg")]
460    pub split_separator_hover_fg: ColorDef,
461    /// Scrollbar track color
462    #[serde(default = "default_scrollbar_track_fg")]
463    pub scrollbar_track_fg: ColorDef,
464    /// Scrollbar thumb color
465    #[serde(default = "default_scrollbar_thumb_fg")]
466    pub scrollbar_thumb_fg: ColorDef,
467    /// Scrollbar track hover color
468    #[serde(default = "default_scrollbar_track_hover_fg")]
469    pub scrollbar_track_hover_fg: ColorDef,
470    /// Scrollbar thumb hover color
471    #[serde(default = "default_scrollbar_thumb_hover_fg")]
472    pub scrollbar_thumb_hover_fg: ColorDef,
473    /// Compose mode margin background
474    #[serde(default = "default_compose_margin_bg")]
475    pub compose_margin_bg: ColorDef,
476    /// Word under cursor highlight
477    #[serde(default = "default_semantic_highlight_bg")]
478    pub semantic_highlight_bg: ColorDef,
479    /// Embedded terminal background (use Default for transparency)
480    #[serde(default = "default_terminal_bg")]
481    pub terminal_bg: ColorDef,
482    /// Embedded terminal default text color
483    #[serde(default = "default_terminal_fg")]
484    pub terminal_fg: ColorDef,
485    /// Warning indicator background in status bar
486    #[serde(default = "default_status_warning_indicator_bg")]
487    pub status_warning_indicator_bg: ColorDef,
488    /// Warning indicator text color in status bar
489    #[serde(default = "default_status_warning_indicator_fg")]
490    pub status_warning_indicator_fg: ColorDef,
491    /// Error indicator background in status bar
492    #[serde(default = "default_status_error_indicator_bg")]
493    pub status_error_indicator_bg: ColorDef,
494    /// Error indicator text color in status bar
495    #[serde(default = "default_status_error_indicator_fg")]
496    pub status_error_indicator_fg: ColorDef,
497    /// Warning indicator hover background
498    #[serde(default = "default_status_warning_indicator_hover_bg")]
499    pub status_warning_indicator_hover_bg: ColorDef,
500    /// Warning indicator hover text color
501    #[serde(default = "default_status_warning_indicator_hover_fg")]
502    pub status_warning_indicator_hover_fg: ColorDef,
503    /// Error indicator hover background
504    #[serde(default = "default_status_error_indicator_hover_bg")]
505    pub status_error_indicator_hover_bg: ColorDef,
506    /// Error indicator hover text color
507    #[serde(default = "default_status_error_indicator_hover_fg")]
508    pub status_error_indicator_hover_fg: ColorDef,
509    /// Tab drop zone background during drag
510    #[serde(default = "default_tab_drop_zone_bg")]
511    pub tab_drop_zone_bg: ColorDef,
512    /// Tab drop zone border during drag
513    #[serde(default = "default_tab_drop_zone_border")]
514    pub tab_drop_zone_border: ColorDef,
515    /// Settings UI selected item background
516    #[serde(default = "default_settings_selected_bg")]
517    pub settings_selected_bg: ColorDef,
518    /// Settings UI selected item foreground (text on selected background)
519    #[serde(default = "default_settings_selected_fg")]
520    pub settings_selected_fg: ColorDef,
521    /// File status: added file color in file explorer (falls back to diagnostic.info_fg)
522    #[serde(default)]
523    pub file_status_added_fg: Option<ColorDef>,
524    /// File status: modified file color in file explorer (falls back to diagnostic.warning_fg)
525    #[serde(default)]
526    pub file_status_modified_fg: Option<ColorDef>,
527    /// File status: deleted file color in file explorer (falls back to diagnostic.error_fg)
528    #[serde(default)]
529    pub file_status_deleted_fg: Option<ColorDef>,
530    /// File status: renamed file color in file explorer (falls back to diagnostic.info_fg)
531    #[serde(default)]
532    pub file_status_renamed_fg: Option<ColorDef>,
533    /// File status: untracked file color in file explorer (falls back to diagnostic.hint_fg)
534    #[serde(default)]
535    pub file_status_untracked_fg: Option<ColorDef>,
536    /// File status: conflicted file color in file explorer (falls back to diagnostic.error_fg)
537    #[serde(default)]
538    pub file_status_conflicted_fg: Option<ColorDef>,
539}
540
541// Default tab close hover color (for backward compatibility with existing themes)
542// Default tab colors (for minimal themes)
543fn default_tab_active_fg() -> ColorDef {
544    ColorDef::Named("Yellow".to_string())
545}
546fn default_tab_active_bg() -> ColorDef {
547    ColorDef::Named("Blue".to_string())
548}
549fn default_tab_inactive_fg() -> ColorDef {
550    ColorDef::Named("White".to_string())
551}
552fn default_tab_inactive_bg() -> ColorDef {
553    ColorDef::Named("DarkGray".to_string())
554}
555fn default_tab_separator_bg() -> ColorDef {
556    ColorDef::Named("Black".to_string())
557}
558fn default_tab_close_hover_fg() -> ColorDef {
559    ColorDef::Rgb(255, 100, 100) // Red-ish color for close button hover
560}
561fn default_tab_hover_bg() -> ColorDef {
562    ColorDef::Rgb(70, 70, 75) // Slightly lighter than inactive tab bg for hover
563}
564
565// Default menu colors (for backward compatibility with existing themes)
566fn default_menu_bg() -> ColorDef {
567    ColorDef::Rgb(60, 60, 65)
568}
569fn default_menu_fg() -> ColorDef {
570    ColorDef::Rgb(220, 220, 220)
571}
572fn default_menu_active_bg() -> ColorDef {
573    ColorDef::Rgb(60, 60, 60)
574}
575fn default_menu_active_fg() -> ColorDef {
576    ColorDef::Rgb(255, 255, 255)
577}
578fn default_menu_dropdown_bg() -> ColorDef {
579    ColorDef::Rgb(50, 50, 50)
580}
581fn default_menu_dropdown_fg() -> ColorDef {
582    ColorDef::Rgb(220, 220, 220)
583}
584fn default_menu_highlight_bg() -> ColorDef {
585    ColorDef::Rgb(70, 130, 180)
586}
587fn default_menu_highlight_fg() -> ColorDef {
588    ColorDef::Rgb(255, 255, 255)
589}
590fn default_menu_border_fg() -> ColorDef {
591    ColorDef::Rgb(100, 100, 100)
592}
593fn default_menu_separator_fg() -> ColorDef {
594    ColorDef::Rgb(80, 80, 80)
595}
596fn default_menu_hover_bg() -> ColorDef {
597    ColorDef::Rgb(55, 55, 55)
598}
599fn default_menu_hover_fg() -> ColorDef {
600    ColorDef::Rgb(255, 255, 255)
601}
602fn default_menu_disabled_fg() -> ColorDef {
603    ColorDef::Rgb(100, 100, 100) // Gray for disabled items
604}
605fn default_menu_disabled_bg() -> ColorDef {
606    ColorDef::Rgb(50, 50, 50) // Same as dropdown bg
607}
608// Default status bar colors
609fn default_status_bar_fg() -> ColorDef {
610    ColorDef::Named("White".to_string())
611}
612fn default_status_bar_bg() -> ColorDef {
613    ColorDef::Named("DarkGray".to_string())
614}
615
616// Default prompt colors
617fn default_prompt_fg() -> ColorDef {
618    ColorDef::Named("White".to_string())
619}
620fn default_prompt_bg() -> ColorDef {
621    ColorDef::Named("Black".to_string())
622}
623fn default_prompt_selection_fg() -> ColorDef {
624    ColorDef::Named("White".to_string())
625}
626fn default_prompt_selection_bg() -> ColorDef {
627    ColorDef::Rgb(58, 79, 120)
628}
629
630// Default popup colors
631fn default_popup_border_fg() -> ColorDef {
632    ColorDef::Named("Gray".to_string())
633}
634fn default_popup_bg() -> ColorDef {
635    ColorDef::Rgb(30, 30, 30)
636}
637fn default_popup_selection_bg() -> ColorDef {
638    ColorDef::Rgb(58, 79, 120)
639}
640fn default_popup_selection_fg() -> ColorDef {
641    ColorDef::Rgb(255, 255, 255) // White text on selected popup item
642}
643fn default_popup_text_fg() -> ColorDef {
644    ColorDef::Named("White".to_string())
645}
646
647// Default suggestion colors
648fn default_suggestion_bg() -> ColorDef {
649    ColorDef::Rgb(30, 30, 30)
650}
651fn default_suggestion_selected_bg() -> ColorDef {
652    ColorDef::Rgb(58, 79, 120)
653}
654
655// Default help colors
656fn default_help_bg() -> ColorDef {
657    ColorDef::Named("Black".to_string())
658}
659fn default_help_fg() -> ColorDef {
660    ColorDef::Named("White".to_string())
661}
662fn default_help_key_fg() -> ColorDef {
663    ColorDef::Named("Cyan".to_string())
664}
665fn default_help_separator_fg() -> ColorDef {
666    ColorDef::Named("DarkGray".to_string())
667}
668fn default_help_indicator_fg() -> ColorDef {
669    ColorDef::Named("Red".to_string())
670}
671fn default_help_indicator_bg() -> ColorDef {
672    ColorDef::Named("Black".to_string())
673}
674
675fn default_inline_code_bg() -> ColorDef {
676    ColorDef::Named("DarkGray".to_string())
677}
678
679// Default split separator colors
680fn default_split_separator_fg() -> ColorDef {
681    ColorDef::Rgb(100, 100, 100)
682}
683fn default_split_separator_hover_fg() -> ColorDef {
684    ColorDef::Rgb(100, 149, 237) // Cornflower blue for visibility
685}
686fn default_scrollbar_track_fg() -> ColorDef {
687    ColorDef::Named("DarkGray".to_string())
688}
689fn default_scrollbar_thumb_fg() -> ColorDef {
690    ColorDef::Named("Gray".to_string())
691}
692fn default_scrollbar_track_hover_fg() -> ColorDef {
693    ColorDef::Named("Gray".to_string())
694}
695fn default_scrollbar_thumb_hover_fg() -> ColorDef {
696    ColorDef::Named("White".to_string())
697}
698fn default_compose_margin_bg() -> ColorDef {
699    ColorDef::Rgb(18, 18, 18) // Darker than editor_bg for "desk" effect
700}
701fn default_semantic_highlight_bg() -> ColorDef {
702    ColorDef::Rgb(60, 60, 80) // Subtle dark highlight for word occurrences
703}
704fn default_terminal_bg() -> ColorDef {
705    ColorDef::Named("Default".to_string()) // Use terminal's default background (preserves transparency)
706}
707fn default_terminal_fg() -> ColorDef {
708    ColorDef::Named("Default".to_string()) // Use terminal's default foreground
709}
710fn default_status_warning_indicator_bg() -> ColorDef {
711    ColorDef::Rgb(181, 137, 0) // Solarized yellow/amber - noticeable but not harsh
712}
713fn default_status_warning_indicator_fg() -> ColorDef {
714    ColorDef::Rgb(0, 0, 0) // Black text on amber background
715}
716fn default_status_error_indicator_bg() -> ColorDef {
717    ColorDef::Rgb(220, 50, 47) // Solarized red - clearly an error
718}
719fn default_status_error_indicator_fg() -> ColorDef {
720    ColorDef::Rgb(255, 255, 255) // White text on red background
721}
722fn default_status_warning_indicator_hover_bg() -> ColorDef {
723    ColorDef::Rgb(211, 167, 30) // Lighter amber for hover
724}
725fn default_status_warning_indicator_hover_fg() -> ColorDef {
726    ColorDef::Rgb(0, 0, 0) // Black text on hover
727}
728fn default_status_error_indicator_hover_bg() -> ColorDef {
729    ColorDef::Rgb(250, 80, 77) // Lighter red for hover
730}
731fn default_status_error_indicator_hover_fg() -> ColorDef {
732    ColorDef::Rgb(255, 255, 255) // White text on hover
733}
734fn default_tab_drop_zone_bg() -> ColorDef {
735    ColorDef::Rgb(70, 130, 180) // Steel blue with transparency effect
736}
737fn default_tab_drop_zone_border() -> ColorDef {
738    ColorDef::Rgb(100, 149, 237) // Cornflower blue for border
739}
740fn default_settings_selected_bg() -> ColorDef {
741    ColorDef::Rgb(60, 60, 70) // Subtle highlight for selected settings item
742}
743fn default_settings_selected_fg() -> ColorDef {
744    ColorDef::Rgb(255, 255, 255) // White text on selected background
745}
746/// Search result highlighting colors
747#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
748pub struct SearchColors {
749    /// Search match background color
750    #[serde(default = "default_search_match_bg")]
751    pub match_bg: ColorDef,
752    /// Search match text color
753    #[serde(default = "default_search_match_fg")]
754    pub match_fg: ColorDef,
755}
756
757// Default search colors
758fn default_search_match_bg() -> ColorDef {
759    ColorDef::Rgb(100, 100, 20)
760}
761fn default_search_match_fg() -> ColorDef {
762    ColorDef::Rgb(255, 255, 255)
763}
764
765/// LSP diagnostic colors (errors, warnings, etc.)
766#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
767pub struct DiagnosticColors {
768    /// Error message text color
769    #[serde(default = "default_diagnostic_error_fg")]
770    pub error_fg: ColorDef,
771    /// Error highlight background
772    #[serde(default = "default_diagnostic_error_bg")]
773    pub error_bg: ColorDef,
774    /// Warning message text color
775    #[serde(default = "default_diagnostic_warning_fg")]
776    pub warning_fg: ColorDef,
777    /// Warning highlight background
778    #[serde(default = "default_diagnostic_warning_bg")]
779    pub warning_bg: ColorDef,
780    /// Info message text color
781    #[serde(default = "default_diagnostic_info_fg")]
782    pub info_fg: ColorDef,
783    /// Info highlight background
784    #[serde(default = "default_diagnostic_info_bg")]
785    pub info_bg: ColorDef,
786    /// Hint message text color
787    #[serde(default = "default_diagnostic_hint_fg")]
788    pub hint_fg: ColorDef,
789    /// Hint highlight background
790    #[serde(default = "default_diagnostic_hint_bg")]
791    pub hint_bg: ColorDef,
792}
793
794// Default diagnostic colors
795fn default_diagnostic_error_fg() -> ColorDef {
796    ColorDef::Named("Red".to_string())
797}
798fn default_diagnostic_error_bg() -> ColorDef {
799    ColorDef::Rgb(60, 20, 20)
800}
801fn default_diagnostic_warning_fg() -> ColorDef {
802    ColorDef::Named("Yellow".to_string())
803}
804fn default_diagnostic_warning_bg() -> ColorDef {
805    ColorDef::Rgb(60, 50, 0)
806}
807fn default_diagnostic_info_fg() -> ColorDef {
808    ColorDef::Named("Blue".to_string())
809}
810fn default_diagnostic_info_bg() -> ColorDef {
811    ColorDef::Rgb(0, 30, 60)
812}
813fn default_diagnostic_hint_fg() -> ColorDef {
814    ColorDef::Named("Gray".to_string())
815}
816fn default_diagnostic_hint_bg() -> ColorDef {
817    ColorDef::Rgb(30, 30, 30)
818}
819
820/// Syntax highlighting colors
821#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
822pub struct SyntaxColors {
823    /// Language keywords (if, for, fn, etc.)
824    #[serde(default = "default_syntax_keyword")]
825    pub keyword: ColorDef,
826    /// String literals
827    #[serde(default = "default_syntax_string")]
828    pub string: ColorDef,
829    /// Code comments
830    #[serde(default = "default_syntax_comment")]
831    pub comment: ColorDef,
832    /// Function names
833    #[serde(default = "default_syntax_function")]
834    pub function: ColorDef,
835    /// Type names
836    #[serde(rename = "type", default = "default_syntax_type")]
837    pub type_: ColorDef,
838    /// Variable names
839    #[serde(default = "default_syntax_variable")]
840    pub variable: ColorDef,
841    /// Constants and literals
842    #[serde(default = "default_syntax_constant")]
843    pub constant: ColorDef,
844    /// Operators (+, -, =, etc.)
845    #[serde(default = "default_syntax_operator")]
846    pub operator: ColorDef,
847    /// Punctuation brackets ({, }, (, ), [, ])
848    #[serde(default = "default_syntax_punctuation_bracket")]
849    pub punctuation_bracket: ColorDef,
850    /// Punctuation delimiters (;, ,, .)
851    #[serde(default = "default_syntax_punctuation_delimiter")]
852    pub punctuation_delimiter: ColorDef,
853}
854
855// Default syntax colors (VSCode Dark+ inspired)
856fn default_syntax_keyword() -> ColorDef {
857    ColorDef::Rgb(86, 156, 214)
858}
859fn default_syntax_string() -> ColorDef {
860    ColorDef::Rgb(206, 145, 120)
861}
862fn default_syntax_comment() -> ColorDef {
863    ColorDef::Rgb(106, 153, 85)
864}
865fn default_syntax_function() -> ColorDef {
866    ColorDef::Rgb(220, 220, 170)
867}
868fn default_syntax_type() -> ColorDef {
869    ColorDef::Rgb(78, 201, 176)
870}
871fn default_syntax_variable() -> ColorDef {
872    ColorDef::Rgb(156, 220, 254)
873}
874fn default_syntax_constant() -> ColorDef {
875    ColorDef::Rgb(79, 193, 255)
876}
877fn default_syntax_operator() -> ColorDef {
878    ColorDef::Rgb(212, 212, 212)
879}
880fn default_syntax_punctuation_bracket() -> ColorDef {
881    ColorDef::Rgb(212, 212, 212) // default foreground — brackets blend with text
882}
883fn default_syntax_punctuation_delimiter() -> ColorDef {
884    ColorDef::Rgb(212, 212, 212) // default foreground — delimiters blend with text
885}
886
887/// Comprehensive theme structure with all UI colors
888#[derive(Debug, Clone)]
889pub struct Theme {
890    /// Theme name (e.g., "dark", "light", "high-contrast")
891    pub name: String,
892
893    // Editor colors
894    pub editor_bg: Color,
895    pub editor_fg: Color,
896    pub cursor: Color,
897    pub inactive_cursor: Color,
898    pub selection_bg: Color,
899    pub current_line_bg: Color,
900    pub line_number_fg: Color,
901    pub line_number_bg: Color,
902
903    // Vertical ruler color
904    pub ruler_bg: Color,
905
906    // Whitespace indicator color (tab arrows, space dots)
907    pub whitespace_indicator_fg: Color,
908
909    // Diff highlighting colors
910    pub diff_add_bg: Color,
911    pub diff_remove_bg: Color,
912    pub diff_modify_bg: Color,
913    /// Brighter background for inline diff highlighting on added content
914    pub diff_add_highlight_bg: Color,
915    /// Brighter background for inline diff highlighting on removed content
916    pub diff_remove_highlight_bg: Color,
917
918    // UI element colors
919    pub tab_active_fg: Color,
920    pub tab_active_bg: Color,
921    pub tab_inactive_fg: Color,
922    pub tab_inactive_bg: Color,
923    pub tab_separator_bg: Color,
924    pub tab_close_hover_fg: Color,
925    pub tab_hover_bg: Color,
926
927    // Menu bar colors
928    pub menu_bg: Color,
929    pub menu_fg: Color,
930    pub menu_active_bg: Color,
931    pub menu_active_fg: Color,
932    pub menu_dropdown_bg: Color,
933    pub menu_dropdown_fg: Color,
934    pub menu_highlight_bg: Color,
935    pub menu_highlight_fg: Color,
936    pub menu_border_fg: Color,
937    pub menu_separator_fg: Color,
938    pub menu_hover_bg: Color,
939    pub menu_hover_fg: Color,
940    pub menu_disabled_fg: Color,
941    pub menu_disabled_bg: Color,
942
943    pub status_bar_fg: Color,
944    pub status_bar_bg: Color,
945    pub prompt_fg: Color,
946    pub prompt_bg: Color,
947    pub prompt_selection_fg: Color,
948    pub prompt_selection_bg: Color,
949
950    pub popup_border_fg: Color,
951    pub popup_bg: Color,
952    pub popup_selection_bg: Color,
953    pub popup_selection_fg: Color,
954    pub popup_text_fg: Color,
955
956    pub suggestion_bg: Color,
957    pub suggestion_selected_bg: Color,
958
959    pub help_bg: Color,
960    pub help_fg: Color,
961    pub help_key_fg: Color,
962    pub help_separator_fg: Color,
963
964    pub help_indicator_fg: Color,
965    pub help_indicator_bg: Color,
966
967    /// Background color for inline code in help popups
968    pub inline_code_bg: Color,
969
970    pub split_separator_fg: Color,
971    pub split_separator_hover_fg: Color,
972
973    // Scrollbar colors
974    pub scrollbar_track_fg: Color,
975    pub scrollbar_thumb_fg: Color,
976    pub scrollbar_track_hover_fg: Color,
977    pub scrollbar_thumb_hover_fg: Color,
978
979    // Compose mode colors
980    pub compose_margin_bg: Color,
981
982    // Semantic highlighting (word under cursor)
983    pub semantic_highlight_bg: Color,
984
985    // Terminal colors (for embedded terminal buffers)
986    pub terminal_bg: Color,
987    pub terminal_fg: Color,
988
989    // Status bar warning/error indicator colors
990    pub status_warning_indicator_bg: Color,
991    pub status_warning_indicator_fg: Color,
992    pub status_error_indicator_bg: Color,
993    pub status_error_indicator_fg: Color,
994    pub status_warning_indicator_hover_bg: Color,
995    pub status_warning_indicator_hover_fg: Color,
996    pub status_error_indicator_hover_bg: Color,
997    pub status_error_indicator_hover_fg: Color,
998
999    // Tab drag-and-drop colors
1000    pub tab_drop_zone_bg: Color,
1001    pub tab_drop_zone_border: Color,
1002
1003    // Settings UI colors
1004    pub settings_selected_bg: Color,
1005    pub settings_selected_fg: Color,
1006
1007    // File status colors (git status indicators in file explorer)
1008    pub file_status_added_fg: Color,
1009    pub file_status_modified_fg: Color,
1010    pub file_status_deleted_fg: Color,
1011    pub file_status_renamed_fg: Color,
1012    pub file_status_untracked_fg: Color,
1013    pub file_status_conflicted_fg: Color,
1014
1015    // Search colors
1016    pub search_match_bg: Color,
1017    pub search_match_fg: Color,
1018
1019    // Diagnostic colors
1020    pub diagnostic_error_fg: Color,
1021    pub diagnostic_error_bg: Color,
1022    pub diagnostic_warning_fg: Color,
1023    pub diagnostic_warning_bg: Color,
1024    pub diagnostic_info_fg: Color,
1025    pub diagnostic_info_bg: Color,
1026    pub diagnostic_hint_fg: Color,
1027    pub diagnostic_hint_bg: Color,
1028
1029    // Syntax highlighting colors
1030    pub syntax_keyword: Color,
1031    pub syntax_string: Color,
1032    pub syntax_comment: Color,
1033    pub syntax_function: Color,
1034    pub syntax_type: Color,
1035    pub syntax_variable: Color,
1036    pub syntax_constant: Color,
1037    pub syntax_operator: Color,
1038    pub syntax_punctuation_bracket: Color,
1039    pub syntax_punctuation_delimiter: Color,
1040}
1041
1042impl From<ThemeFile> for Theme {
1043    fn from(file: ThemeFile) -> Self {
1044        Self {
1045            name: file.name,
1046            editor_bg: file.editor.bg.into(),
1047            editor_fg: file.editor.fg.into(),
1048            cursor: file.editor.cursor.into(),
1049            inactive_cursor: file.editor.inactive_cursor.into(),
1050            selection_bg: file.editor.selection_bg.into(),
1051            current_line_bg: file.editor.current_line_bg.into(),
1052            line_number_fg: file.editor.line_number_fg.into(),
1053            line_number_bg: file.editor.line_number_bg.into(),
1054            ruler_bg: file.editor.ruler_bg.into(),
1055            whitespace_indicator_fg: file.editor.whitespace_indicator_fg.into(),
1056            diff_add_bg: file.editor.diff_add_bg.clone().into(),
1057            diff_remove_bg: file.editor.diff_remove_bg.clone().into(),
1058            diff_modify_bg: file.editor.diff_modify_bg.into(),
1059            // Use explicit override if provided, otherwise brighten from base
1060            diff_add_highlight_bg: file
1061                .editor
1062                .diff_add_highlight_bg
1063                .map(|c| c.into())
1064                .unwrap_or_else(|| brighten_color(file.editor.diff_add_bg.into(), 40)),
1065            diff_remove_highlight_bg: file
1066                .editor
1067                .diff_remove_highlight_bg
1068                .map(|c| c.into())
1069                .unwrap_or_else(|| brighten_color(file.editor.diff_remove_bg.into(), 40)),
1070            tab_active_fg: file.ui.tab_active_fg.into(),
1071            tab_active_bg: file.ui.tab_active_bg.into(),
1072            tab_inactive_fg: file.ui.tab_inactive_fg.into(),
1073            tab_inactive_bg: file.ui.tab_inactive_bg.into(),
1074            tab_separator_bg: file.ui.tab_separator_bg.into(),
1075            tab_close_hover_fg: file.ui.tab_close_hover_fg.into(),
1076            tab_hover_bg: file.ui.tab_hover_bg.into(),
1077            menu_bg: file.ui.menu_bg.into(),
1078            menu_fg: file.ui.menu_fg.into(),
1079            menu_active_bg: file.ui.menu_active_bg.into(),
1080            menu_active_fg: file.ui.menu_active_fg.into(),
1081            menu_dropdown_bg: file.ui.menu_dropdown_bg.into(),
1082            menu_dropdown_fg: file.ui.menu_dropdown_fg.into(),
1083            menu_highlight_bg: file.ui.menu_highlight_bg.into(),
1084            menu_highlight_fg: file.ui.menu_highlight_fg.into(),
1085            menu_border_fg: file.ui.menu_border_fg.into(),
1086            menu_separator_fg: file.ui.menu_separator_fg.into(),
1087            menu_hover_bg: file.ui.menu_hover_bg.into(),
1088            menu_hover_fg: file.ui.menu_hover_fg.into(),
1089            menu_disabled_fg: file.ui.menu_disabled_fg.into(),
1090            menu_disabled_bg: file.ui.menu_disabled_bg.into(),
1091            status_bar_fg: file.ui.status_bar_fg.into(),
1092            status_bar_bg: file.ui.status_bar_bg.into(),
1093            prompt_fg: file.ui.prompt_fg.into(),
1094            prompt_bg: file.ui.prompt_bg.into(),
1095            prompt_selection_fg: file.ui.prompt_selection_fg.into(),
1096            prompt_selection_bg: file.ui.prompt_selection_bg.into(),
1097            popup_border_fg: file.ui.popup_border_fg.into(),
1098            popup_bg: file.ui.popup_bg.into(),
1099            popup_selection_bg: file.ui.popup_selection_bg.into(),
1100            popup_selection_fg: file.ui.popup_selection_fg.into(),
1101            popup_text_fg: file.ui.popup_text_fg.into(),
1102            suggestion_bg: file.ui.suggestion_bg.into(),
1103            suggestion_selected_bg: file.ui.suggestion_selected_bg.into(),
1104            help_bg: file.ui.help_bg.into(),
1105            help_fg: file.ui.help_fg.into(),
1106            help_key_fg: file.ui.help_key_fg.into(),
1107            help_separator_fg: file.ui.help_separator_fg.into(),
1108            help_indicator_fg: file.ui.help_indicator_fg.into(),
1109            help_indicator_bg: file.ui.help_indicator_bg.into(),
1110            inline_code_bg: file.ui.inline_code_bg.into(),
1111            split_separator_fg: file.ui.split_separator_fg.into(),
1112            split_separator_hover_fg: file.ui.split_separator_hover_fg.into(),
1113            scrollbar_track_fg: file.ui.scrollbar_track_fg.into(),
1114            scrollbar_thumb_fg: file.ui.scrollbar_thumb_fg.into(),
1115            scrollbar_track_hover_fg: file.ui.scrollbar_track_hover_fg.into(),
1116            scrollbar_thumb_hover_fg: file.ui.scrollbar_thumb_hover_fg.into(),
1117            compose_margin_bg: file.ui.compose_margin_bg.into(),
1118            semantic_highlight_bg: file.ui.semantic_highlight_bg.into(),
1119            terminal_bg: file.ui.terminal_bg.into(),
1120            terminal_fg: file.ui.terminal_fg.into(),
1121            status_warning_indicator_bg: file.ui.status_warning_indicator_bg.into(),
1122            status_warning_indicator_fg: file.ui.status_warning_indicator_fg.into(),
1123            status_error_indicator_bg: file.ui.status_error_indicator_bg.into(),
1124            status_error_indicator_fg: file.ui.status_error_indicator_fg.into(),
1125            status_warning_indicator_hover_bg: file.ui.status_warning_indicator_hover_bg.into(),
1126            status_warning_indicator_hover_fg: file.ui.status_warning_indicator_hover_fg.into(),
1127            status_error_indicator_hover_bg: file.ui.status_error_indicator_hover_bg.into(),
1128            status_error_indicator_hover_fg: file.ui.status_error_indicator_hover_fg.into(),
1129            tab_drop_zone_bg: file.ui.tab_drop_zone_bg.into(),
1130            tab_drop_zone_border: file.ui.tab_drop_zone_border.into(),
1131            settings_selected_bg: file.ui.settings_selected_bg.into(),
1132            settings_selected_fg: file.ui.settings_selected_fg.into(),
1133            file_status_added_fg: file
1134                .ui
1135                .file_status_added_fg
1136                .map(|c| c.into())
1137                .unwrap_or_else(|| file.diagnostic.info_fg.clone().into()),
1138            file_status_modified_fg: file
1139                .ui
1140                .file_status_modified_fg
1141                .map(|c| c.into())
1142                .unwrap_or_else(|| file.diagnostic.warning_fg.clone().into()),
1143            file_status_deleted_fg: file
1144                .ui
1145                .file_status_deleted_fg
1146                .map(|c| c.into())
1147                .unwrap_or_else(|| file.diagnostic.error_fg.clone().into()),
1148            file_status_renamed_fg: file
1149                .ui
1150                .file_status_renamed_fg
1151                .map(|c| c.into())
1152                .unwrap_or_else(|| file.diagnostic.info_fg.clone().into()),
1153            file_status_untracked_fg: file
1154                .ui
1155                .file_status_untracked_fg
1156                .map(|c| c.into())
1157                .unwrap_or_else(|| file.diagnostic.hint_fg.clone().into()),
1158            file_status_conflicted_fg: file
1159                .ui
1160                .file_status_conflicted_fg
1161                .map(|c| c.into())
1162                .unwrap_or_else(|| file.diagnostic.error_fg.clone().into()),
1163            search_match_bg: file.search.match_bg.into(),
1164            search_match_fg: file.search.match_fg.into(),
1165            diagnostic_error_fg: file.diagnostic.error_fg.into(),
1166            diagnostic_error_bg: file.diagnostic.error_bg.into(),
1167            diagnostic_warning_fg: file.diagnostic.warning_fg.into(),
1168            diagnostic_warning_bg: file.diagnostic.warning_bg.into(),
1169            diagnostic_info_fg: file.diagnostic.info_fg.into(),
1170            diagnostic_info_bg: file.diagnostic.info_bg.into(),
1171            diagnostic_hint_fg: file.diagnostic.hint_fg.into(),
1172            diagnostic_hint_bg: file.diagnostic.hint_bg.into(),
1173            syntax_keyword: file.syntax.keyword.into(),
1174            syntax_string: file.syntax.string.into(),
1175            syntax_comment: file.syntax.comment.into(),
1176            syntax_function: file.syntax.function.into(),
1177            syntax_type: file.syntax.type_.into(),
1178            syntax_variable: file.syntax.variable.into(),
1179            syntax_constant: file.syntax.constant.into(),
1180            syntax_operator: file.syntax.operator.into(),
1181            syntax_punctuation_bracket: file.syntax.punctuation_bracket.into(),
1182            syntax_punctuation_delimiter: file.syntax.punctuation_delimiter.into(),
1183        }
1184    }
1185}
1186
1187impl From<Theme> for ThemeFile {
1188    fn from(theme: Theme) -> Self {
1189        Self {
1190            name: theme.name,
1191            editor: EditorColors {
1192                bg: theme.editor_bg.into(),
1193                fg: theme.editor_fg.into(),
1194                cursor: theme.cursor.into(),
1195                inactive_cursor: theme.inactive_cursor.into(),
1196                selection_bg: theme.selection_bg.into(),
1197                current_line_bg: theme.current_line_bg.into(),
1198                line_number_fg: theme.line_number_fg.into(),
1199                line_number_bg: theme.line_number_bg.into(),
1200                diff_add_bg: theme.diff_add_bg.into(),
1201                diff_remove_bg: theme.diff_remove_bg.into(),
1202                diff_add_highlight_bg: Some(theme.diff_add_highlight_bg.into()),
1203                diff_remove_highlight_bg: Some(theme.diff_remove_highlight_bg.into()),
1204                diff_modify_bg: theme.diff_modify_bg.into(),
1205                ruler_bg: theme.ruler_bg.into(),
1206                whitespace_indicator_fg: theme.whitespace_indicator_fg.into(),
1207            },
1208            ui: UiColors {
1209                tab_active_fg: theme.tab_active_fg.into(),
1210                tab_active_bg: theme.tab_active_bg.into(),
1211                tab_inactive_fg: theme.tab_inactive_fg.into(),
1212                tab_inactive_bg: theme.tab_inactive_bg.into(),
1213                tab_separator_bg: theme.tab_separator_bg.into(),
1214                tab_close_hover_fg: theme.tab_close_hover_fg.into(),
1215                tab_hover_bg: theme.tab_hover_bg.into(),
1216                menu_bg: theme.menu_bg.into(),
1217                menu_fg: theme.menu_fg.into(),
1218                menu_active_bg: theme.menu_active_bg.into(),
1219                menu_active_fg: theme.menu_active_fg.into(),
1220                menu_dropdown_bg: theme.menu_dropdown_bg.into(),
1221                menu_dropdown_fg: theme.menu_dropdown_fg.into(),
1222                menu_highlight_bg: theme.menu_highlight_bg.into(),
1223                menu_highlight_fg: theme.menu_highlight_fg.into(),
1224                menu_border_fg: theme.menu_border_fg.into(),
1225                menu_separator_fg: theme.menu_separator_fg.into(),
1226                menu_hover_bg: theme.menu_hover_bg.into(),
1227                menu_hover_fg: theme.menu_hover_fg.into(),
1228                menu_disabled_fg: theme.menu_disabled_fg.into(),
1229                menu_disabled_bg: theme.menu_disabled_bg.into(),
1230                status_bar_fg: theme.status_bar_fg.into(),
1231                status_bar_bg: theme.status_bar_bg.into(),
1232                prompt_fg: theme.prompt_fg.into(),
1233                prompt_bg: theme.prompt_bg.into(),
1234                prompt_selection_fg: theme.prompt_selection_fg.into(),
1235                prompt_selection_bg: theme.prompt_selection_bg.into(),
1236                popup_border_fg: theme.popup_border_fg.into(),
1237                popup_bg: theme.popup_bg.into(),
1238                popup_selection_bg: theme.popup_selection_bg.into(),
1239                popup_selection_fg: theme.popup_selection_fg.into(),
1240                popup_text_fg: theme.popup_text_fg.into(),
1241                suggestion_bg: theme.suggestion_bg.into(),
1242                suggestion_selected_bg: theme.suggestion_selected_bg.into(),
1243                help_bg: theme.help_bg.into(),
1244                help_fg: theme.help_fg.into(),
1245                help_key_fg: theme.help_key_fg.into(),
1246                help_separator_fg: theme.help_separator_fg.into(),
1247                help_indicator_fg: theme.help_indicator_fg.into(),
1248                help_indicator_bg: theme.help_indicator_bg.into(),
1249                inline_code_bg: theme.inline_code_bg.into(),
1250                split_separator_fg: theme.split_separator_fg.into(),
1251                split_separator_hover_fg: theme.split_separator_hover_fg.into(),
1252                scrollbar_track_fg: theme.scrollbar_track_fg.into(),
1253                scrollbar_thumb_fg: theme.scrollbar_thumb_fg.into(),
1254                scrollbar_track_hover_fg: theme.scrollbar_track_hover_fg.into(),
1255                scrollbar_thumb_hover_fg: theme.scrollbar_thumb_hover_fg.into(),
1256                compose_margin_bg: theme.compose_margin_bg.into(),
1257                semantic_highlight_bg: theme.semantic_highlight_bg.into(),
1258                terminal_bg: theme.terminal_bg.into(),
1259                terminal_fg: theme.terminal_fg.into(),
1260                status_warning_indicator_bg: theme.status_warning_indicator_bg.into(),
1261                status_warning_indicator_fg: theme.status_warning_indicator_fg.into(),
1262                status_error_indicator_bg: theme.status_error_indicator_bg.into(),
1263                status_error_indicator_fg: theme.status_error_indicator_fg.into(),
1264                status_warning_indicator_hover_bg: theme.status_warning_indicator_hover_bg.into(),
1265                status_warning_indicator_hover_fg: theme.status_warning_indicator_hover_fg.into(),
1266                status_error_indicator_hover_bg: theme.status_error_indicator_hover_bg.into(),
1267                status_error_indicator_hover_fg: theme.status_error_indicator_hover_fg.into(),
1268                tab_drop_zone_bg: theme.tab_drop_zone_bg.into(),
1269                tab_drop_zone_border: theme.tab_drop_zone_border.into(),
1270                settings_selected_bg: theme.settings_selected_bg.into(),
1271                settings_selected_fg: theme.settings_selected_fg.into(),
1272                file_status_added_fg: Some(theme.file_status_added_fg.into()),
1273                file_status_modified_fg: Some(theme.file_status_modified_fg.into()),
1274                file_status_deleted_fg: Some(theme.file_status_deleted_fg.into()),
1275                file_status_renamed_fg: Some(theme.file_status_renamed_fg.into()),
1276                file_status_untracked_fg: Some(theme.file_status_untracked_fg.into()),
1277                file_status_conflicted_fg: Some(theme.file_status_conflicted_fg.into()),
1278            },
1279            search: SearchColors {
1280                match_bg: theme.search_match_bg.into(),
1281                match_fg: theme.search_match_fg.into(),
1282            },
1283            diagnostic: DiagnosticColors {
1284                error_fg: theme.diagnostic_error_fg.into(),
1285                error_bg: theme.diagnostic_error_bg.into(),
1286                warning_fg: theme.diagnostic_warning_fg.into(),
1287                warning_bg: theme.diagnostic_warning_bg.into(),
1288                info_fg: theme.diagnostic_info_fg.into(),
1289                info_bg: theme.diagnostic_info_bg.into(),
1290                hint_fg: theme.diagnostic_hint_fg.into(),
1291                hint_bg: theme.diagnostic_hint_bg.into(),
1292            },
1293            syntax: SyntaxColors {
1294                keyword: theme.syntax_keyword.into(),
1295                string: theme.syntax_string.into(),
1296                comment: theme.syntax_comment.into(),
1297                function: theme.syntax_function.into(),
1298                type_: theme.syntax_type.into(),
1299                variable: theme.syntax_variable.into(),
1300                constant: theme.syntax_constant.into(),
1301                operator: theme.syntax_operator.into(),
1302                punctuation_bracket: theme.syntax_punctuation_bracket.into(),
1303                punctuation_delimiter: theme.syntax_punctuation_delimiter.into(),
1304            },
1305        }
1306    }
1307}
1308
1309impl Theme {
1310    /// Returns `true` when the theme has a light background.
1311    ///
1312    /// Uses the relative luminance of `editor_bg` (perceived brightness).
1313    /// A threshold of 0.5 separates dark from light; for `Color::Reset` or
1314    /// unresolvable colors, falls back to `false` (dark).
1315    pub fn is_light(&self) -> bool {
1316        if let Some((r, g, b)) = color_to_rgb(self.editor_bg) {
1317            // sRGB relative luminance (ITU-R BT.709)
1318            let lum = 0.2126 * (r as f64 / 255.0)
1319                + 0.7152 * (g as f64 / 255.0)
1320                + 0.0722 * (b as f64 / 255.0);
1321            lum > 0.5
1322        } else {
1323            false
1324        }
1325    }
1326
1327    /// Load a builtin theme by name (no I/O, uses embedded JSON).
1328    pub fn load_builtin(name: &str) -> Option<Self> {
1329        BUILTIN_THEMES
1330            .iter()
1331            .find(|t| t.name == name)
1332            .and_then(|t| serde_json::from_str::<ThemeFile>(t.json).ok())
1333            .map(|tf| tf.into())
1334    }
1335
1336    /// Parse theme from JSON string (no I/O).
1337    pub fn from_json(json: &str) -> Result<Self, String> {
1338        let theme_file: ThemeFile =
1339            serde_json::from_str(json).map_err(|e| format!("Failed to parse theme JSON: {}", e))?;
1340        Ok(theme_file.into())
1341    }
1342
1343    /// Resolve a theme key to a Color.
1344    ///
1345    /// Theme keys use dot notation: "section.field"
1346    /// Examples:
1347    /// - "ui.status_bar_fg" -> status_bar_fg
1348    /// - "editor.selection_bg" -> selection_bg
1349    /// - "syntax.keyword" -> syntax_keyword
1350    /// - "diagnostic.error_fg" -> diagnostic_error_fg
1351    ///
1352    /// Returns None if the key is not recognized.
1353    pub fn resolve_theme_key(&self, key: &str) -> Option<Color> {
1354        // Parse "section.field" format
1355        let parts: Vec<&str> = key.split('.').collect();
1356        if parts.len() != 2 {
1357            return None;
1358        }
1359
1360        let (section, field) = (parts[0], parts[1]);
1361
1362        match section {
1363            "editor" => match field {
1364                "bg" => Some(self.editor_bg),
1365                "fg" => Some(self.editor_fg),
1366                "cursor" => Some(self.cursor),
1367                "inactive_cursor" => Some(self.inactive_cursor),
1368                "selection_bg" => Some(self.selection_bg),
1369                "current_line_bg" => Some(self.current_line_bg),
1370                "line_number_fg" => Some(self.line_number_fg),
1371                "line_number_bg" => Some(self.line_number_bg),
1372                "diff_add_bg" => Some(self.diff_add_bg),
1373                "diff_remove_bg" => Some(self.diff_remove_bg),
1374                "diff_modify_bg" => Some(self.diff_modify_bg),
1375                "ruler_bg" => Some(self.ruler_bg),
1376                "whitespace_indicator_fg" => Some(self.whitespace_indicator_fg),
1377                _ => None,
1378            },
1379            "ui" => match field {
1380                "tab_active_fg" => Some(self.tab_active_fg),
1381                "tab_active_bg" => Some(self.tab_active_bg),
1382                "tab_inactive_fg" => Some(self.tab_inactive_fg),
1383                "tab_inactive_bg" => Some(self.tab_inactive_bg),
1384                "status_bar_fg" => Some(self.status_bar_fg),
1385                "status_bar_bg" => Some(self.status_bar_bg),
1386                "prompt_fg" => Some(self.prompt_fg),
1387                "prompt_bg" => Some(self.prompt_bg),
1388                "prompt_selection_fg" => Some(self.prompt_selection_fg),
1389                "prompt_selection_bg" => Some(self.prompt_selection_bg),
1390                "popup_bg" => Some(self.popup_bg),
1391                "popup_border_fg" => Some(self.popup_border_fg),
1392                "popup_selection_bg" => Some(self.popup_selection_bg),
1393                "popup_selection_fg" => Some(self.popup_selection_fg),
1394                "popup_text_fg" => Some(self.popup_text_fg),
1395                "menu_bg" => Some(self.menu_bg),
1396                "menu_fg" => Some(self.menu_fg),
1397                "menu_active_bg" => Some(self.menu_active_bg),
1398                "menu_active_fg" => Some(self.menu_active_fg),
1399                "help_bg" => Some(self.help_bg),
1400                "help_fg" => Some(self.help_fg),
1401                "help_key_fg" => Some(self.help_key_fg),
1402                "split_separator_fg" => Some(self.split_separator_fg),
1403                "scrollbar_thumb_fg" => Some(self.scrollbar_thumb_fg),
1404                "semantic_highlight_bg" => Some(self.semantic_highlight_bg),
1405                "file_status_added_fg" => Some(self.file_status_added_fg),
1406                "file_status_modified_fg" => Some(self.file_status_modified_fg),
1407                "file_status_deleted_fg" => Some(self.file_status_deleted_fg),
1408                "file_status_renamed_fg" => Some(self.file_status_renamed_fg),
1409                "file_status_untracked_fg" => Some(self.file_status_untracked_fg),
1410                "file_status_conflicted_fg" => Some(self.file_status_conflicted_fg),
1411                _ => None,
1412            },
1413            "syntax" => match field {
1414                "keyword" => Some(self.syntax_keyword),
1415                "string" => Some(self.syntax_string),
1416                "comment" => Some(self.syntax_comment),
1417                "function" => Some(self.syntax_function),
1418                "type" => Some(self.syntax_type),
1419                "variable" => Some(self.syntax_variable),
1420                "constant" => Some(self.syntax_constant),
1421                "operator" => Some(self.syntax_operator),
1422                "punctuation_bracket" => Some(self.syntax_punctuation_bracket),
1423                "punctuation_delimiter" => Some(self.syntax_punctuation_delimiter),
1424                _ => None,
1425            },
1426            "diagnostic" => match field {
1427                "error_fg" => Some(self.diagnostic_error_fg),
1428                "error_bg" => Some(self.diagnostic_error_bg),
1429                "warning_fg" => Some(self.diagnostic_warning_fg),
1430                "warning_bg" => Some(self.diagnostic_warning_bg),
1431                "info_fg" => Some(self.diagnostic_info_fg),
1432                "info_bg" => Some(self.diagnostic_info_bg),
1433                "hint_fg" => Some(self.diagnostic_hint_fg),
1434                "hint_bg" => Some(self.diagnostic_hint_bg),
1435                _ => None,
1436            },
1437            "search" => match field {
1438                "match_bg" => Some(self.search_match_bg),
1439                "match_fg" => Some(self.search_match_fg),
1440                _ => None,
1441            },
1442            _ => None,
1443        }
1444    }
1445}
1446
1447// =============================================================================
1448// Theme Schema Generation for Plugin API
1449// =============================================================================
1450
1451/// Returns the raw JSON Schema for ThemeFile, generated by schemars.
1452/// The schema uses standard JSON Schema format with $ref for type references.
1453/// Plugins are responsible for parsing and resolving $ref references.
1454pub fn get_theme_schema() -> serde_json::Value {
1455    use schemars::schema_for;
1456    let schema = schema_for!(ThemeFile);
1457    serde_json::to_value(&schema).unwrap_or_default()
1458}
1459
1460/// Returns a map of built-in theme names to their JSON content.
1461pub fn get_builtin_themes() -> serde_json::Value {
1462    let mut map = serde_json::Map::new();
1463    for theme in BUILTIN_THEMES {
1464        map.insert(
1465            theme.name.to_string(),
1466            serde_json::Value::String(theme.json.to_string()),
1467        );
1468    }
1469    serde_json::Value::Object(map)
1470}
1471
1472#[cfg(test)]
1473mod tests {
1474    use super::*;
1475
1476    #[test]
1477    fn test_load_builtin_theme() {
1478        let dark = Theme::load_builtin(THEME_DARK).expect("Dark theme must exist");
1479        assert_eq!(dark.name, THEME_DARK);
1480
1481        let light = Theme::load_builtin(THEME_LIGHT).expect("Light theme must exist");
1482        assert_eq!(light.name, THEME_LIGHT);
1483
1484        let high_contrast =
1485            Theme::load_builtin(THEME_HIGH_CONTRAST).expect("High contrast theme must exist");
1486        assert_eq!(high_contrast.name, THEME_HIGH_CONTRAST);
1487    }
1488
1489    #[test]
1490    fn test_builtin_themes_match_schema() {
1491        for theme in BUILTIN_THEMES {
1492            let _: ThemeFile = serde_json::from_str(theme.json)
1493                .unwrap_or_else(|_| panic!("Theme '{}' does not match schema", theme.name));
1494        }
1495    }
1496
1497    #[test]
1498    fn test_from_json() {
1499        let json = r#"{"name":"test","editor":{},"ui":{},"search":{},"diagnostic":{},"syntax":{}}"#;
1500        let theme = Theme::from_json(json).expect("Should parse minimal theme");
1501        assert_eq!(theme.name, "test");
1502    }
1503
1504    #[test]
1505    fn test_default_reset_color() {
1506        // Test that "Default" maps to Color::Reset
1507        let color: Color = ColorDef::Named("Default".to_string()).into();
1508        assert_eq!(color, Color::Reset);
1509
1510        // Test that "Reset" also maps to Color::Reset
1511        let color: Color = ColorDef::Named("Reset".to_string()).into();
1512        assert_eq!(color, Color::Reset);
1513    }
1514
1515    #[test]
1516    fn test_file_status_colors_fall_back_to_diagnostic_colors() {
1517        // A theme with NO file_status_* keys should inherit from diagnostic colors
1518        let json = r#"{
1519            "name": "test-fallback",
1520            "editor": {},
1521            "ui": {},
1522            "search": {},
1523            "diagnostic": {
1524                "error_fg": [220, 50, 47],
1525                "warning_fg": [181, 137, 0],
1526                "info_fg": [38, 139, 210],
1527                "hint_fg": [101, 123, 131]
1528            },
1529            "syntax": {}
1530        }"#;
1531        let theme = Theme::from_json(json).expect("Should parse theme without file_status keys");
1532
1533        // Verify fallback: added/renamed -> info_fg
1534        assert_eq!(theme.file_status_added_fg, Color::Rgb(38, 139, 210));
1535        assert_eq!(theme.file_status_renamed_fg, Color::Rgb(38, 139, 210));
1536        // modified -> warning_fg
1537        assert_eq!(theme.file_status_modified_fg, Color::Rgb(181, 137, 0));
1538        // deleted/conflicted -> error_fg
1539        assert_eq!(theme.file_status_deleted_fg, Color::Rgb(220, 50, 47));
1540        assert_eq!(theme.file_status_conflicted_fg, Color::Rgb(220, 50, 47));
1541        // untracked -> hint_fg
1542        assert_eq!(theme.file_status_untracked_fg, Color::Rgb(101, 123, 131));
1543    }
1544
1545    #[test]
1546    fn test_file_status_colors_explicit_override() {
1547        // A theme WITH explicit file_status keys should use those, not the fallback
1548        let json = r#"{
1549            "name": "test-override",
1550            "editor": {},
1551            "ui": {
1552                "file_status_added_fg": [80, 250, 123],
1553                "file_status_modified_fg": [255, 184, 108]
1554            },
1555            "search": {},
1556            "diagnostic": {
1557                "info_fg": [38, 139, 210],
1558                "warning_fg": [181, 137, 0]
1559            },
1560            "syntax": {}
1561        }"#;
1562        let theme = Theme::from_json(json).expect("Should parse theme with file_status overrides");
1563
1564        // Explicit overrides should win
1565        assert_eq!(theme.file_status_added_fg, Color::Rgb(80, 250, 123));
1566        assert_eq!(theme.file_status_modified_fg, Color::Rgb(255, 184, 108));
1567        // Non-overridden should still fall back
1568        assert_eq!(theme.file_status_renamed_fg, Color::Rgb(38, 139, 210));
1569    }
1570
1571    #[test]
1572    fn test_file_status_colors_resolve_via_theme_key() {
1573        let json = r#"{
1574            "name": "test-resolve",
1575            "editor": {},
1576            "ui": {
1577                "file_status_added_fg": [80, 250, 123]
1578            },
1579            "search": {},
1580            "diagnostic": {
1581                "warning_fg": [181, 137, 0]
1582            },
1583            "syntax": {}
1584        }"#;
1585        let theme = Theme::from_json(json).expect("Should parse theme");
1586
1587        // Theme key resolution should work for file_status keys
1588        assert_eq!(
1589            theme.resolve_theme_key("ui.file_status_added_fg"),
1590            Some(Color::Rgb(80, 250, 123))
1591        );
1592        assert_eq!(
1593            theme.resolve_theme_key("ui.file_status_modified_fg"),
1594            Some(Color::Rgb(181, 137, 0))
1595        );
1596    }
1597
1598    #[test]
1599    fn test_all_builtin_themes_have_file_status_colors() {
1600        // Every builtin theme must produce valid file_status colors (via fallback or explicit)
1601        for builtin in BUILTIN_THEMES {
1602            let theme = Theme::from_json(builtin.json)
1603                .unwrap_or_else(|e| panic!("Theme '{}' failed to parse: {}", builtin.name, e));
1604
1605            // All six keys must resolve to Some via resolve_theme_key
1606            for key in &[
1607                "ui.file_status_added_fg",
1608                "ui.file_status_modified_fg",
1609                "ui.file_status_deleted_fg",
1610                "ui.file_status_renamed_fg",
1611                "ui.file_status_untracked_fg",
1612                "ui.file_status_conflicted_fg",
1613            ] {
1614                assert!(
1615                    theme.resolve_theme_key(key).is_some(),
1616                    "Theme '{}' missing resolution for '{}'",
1617                    builtin.name,
1618                    key
1619                );
1620            }
1621        }
1622    }
1623}