1use ratatui::style::{Color, Modifier};
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";
17pub const THEME_TERMINAL: &str = "terminal";
21
22pub struct BuiltinTheme {
24 pub name: &'static str,
25 pub pack: &'static str,
27 pub json: &'static str,
28}
29
30include!(concat!(env!("OUT_DIR"), "/builtin_themes.rs"));
32
33#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct ThemeInfo {
36 pub name: String,
38 pub pack: String,
40 pub key: String,
48}
49
50impl ThemeInfo {
51 pub fn new(name: impl Into<String>, pack: impl Into<String>) -> Self {
54 let name = name.into();
55 let pack = pack.into();
56 let key = if pack.is_empty() {
57 name.clone()
58 } else {
59 format!("{}/{}", pack, name)
60 };
61 Self { name, pack, key }
62 }
63
64 pub fn with_key(
66 name: impl Into<String>,
67 pack: impl Into<String>,
68 key: impl Into<String>,
69 ) -> Self {
70 Self {
71 name: name.into(),
72 pack: pack.into(),
73 key: key.into(),
74 }
75 }
76
77 pub fn display_name(&self) -> String {
79 if self.pack.is_empty() {
80 self.name.clone()
81 } else {
82 format!("{} ({})", self.name, self.pack)
83 }
84 }
85}
86
87pub fn color_to_rgb(color: Color) -> Option<(u8, u8, u8)> {
90 match color {
91 Color::Rgb(r, g, b) => Some((r, g, b)),
92 Color::White => Some((255, 255, 255)),
93 Color::Black => Some((0, 0, 0)),
94 Color::Red => Some((205, 0, 0)),
95 Color::Green => Some((0, 205, 0)),
96 Color::Blue => Some((0, 0, 238)),
97 Color::Yellow => Some((205, 205, 0)),
98 Color::Magenta => Some((205, 0, 205)),
99 Color::Cyan => Some((0, 205, 205)),
100 Color::Gray => Some((229, 229, 229)),
101 Color::DarkGray => Some((127, 127, 127)),
102 Color::LightRed => Some((255, 0, 0)),
103 Color::LightGreen => Some((0, 255, 0)),
104 Color::LightBlue => Some((92, 92, 255)),
105 Color::LightYellow => Some((255, 255, 0)),
106 Color::LightMagenta => Some((255, 0, 255)),
107 Color::LightCyan => Some((0, 255, 255)),
108 Color::Reset | Color::Indexed(_) => None,
109 }
110}
111
112pub fn brighten_color(color: Color, amount: u8) -> Color {
115 if let Some((r, g, b)) = color_to_rgb(color) {
116 Color::Rgb(
117 r.saturating_add(amount),
118 g.saturating_add(amount),
119 b.saturating_add(amount),
120 )
121 } else {
122 color
123 }
124}
125
126pub fn shade_toward_contrast(color: Color, amount: u8) -> Color {
134 if let Some((r, g, b)) = color_to_rgb(color) {
135 let avg = (u16::from(r) + u16::from(g) + u16::from(b)) / 3;
136 if avg < 128 {
137 Color::Rgb(
138 r.saturating_add(amount),
139 g.saturating_add(amount),
140 b.saturating_add(amount),
141 )
142 } else {
143 Color::Rgb(
144 r.saturating_sub(amount),
145 g.saturating_sub(amount),
146 b.saturating_sub(amount),
147 )
148 }
149 } else {
150 color
151 }
152}
153
154#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
156#[serde(untagged)]
157pub enum ColorDef {
158 Rgb(u8, u8, u8),
160 Named(String),
162}
163
164impl From<ColorDef> for Color {
165 fn from(def: ColorDef) -> Self {
166 match def {
167 ColorDef::Rgb(r, g, b) => Color::Rgb(r, g, b),
168 ColorDef::Named(name) => match name.as_str() {
169 "Black" => Color::Black,
170 "Red" => Color::Red,
171 "Green" => Color::Green,
172 "Yellow" => Color::Yellow,
173 "Blue" => Color::Blue,
174 "Magenta" => Color::Magenta,
175 "Cyan" => Color::Cyan,
176 "Gray" => Color::Gray,
177 "DarkGray" => Color::DarkGray,
178 "LightRed" => Color::LightRed,
179 "LightGreen" => Color::LightGreen,
180 "LightYellow" => Color::LightYellow,
181 "LightBlue" => Color::LightBlue,
182 "LightMagenta" => Color::LightMagenta,
183 "LightCyan" => Color::LightCyan,
184 "White" => Color::White,
185 "Default" | "Reset" => Color::Reset,
187 _ => Color::White, },
189 }
190 }
191}
192
193#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
207#[serde(transparent)]
208pub struct ModifierDef(pub Vec<String>);
209
210impl From<&ModifierDef> for Modifier {
211 fn from(def: &ModifierDef) -> Self {
212 let mut m = Modifier::empty();
213 for s in &def.0 {
214 match s.as_str() {
215 "reversed" | "reverse" => m |= Modifier::REVERSED,
216 "bold" => m |= Modifier::BOLD,
217 "italic" => m |= Modifier::ITALIC,
218 "underlined" | "underline" => m |= Modifier::UNDERLINED,
219 "dim" => m |= Modifier::DIM,
220 _ => {}
221 }
222 }
223 m
224 }
225}
226
227impl From<ModifierDef> for Modifier {
228 fn from(def: ModifierDef) -> Self {
229 Modifier::from(&def)
230 }
231}
232
233impl From<Modifier> for ModifierDef {
234 fn from(m: Modifier) -> Self {
235 let mut out = Vec::new();
238 if m.contains(Modifier::REVERSED) {
239 out.push("reversed".to_string());
240 }
241 if m.contains(Modifier::BOLD) {
242 out.push("bold".to_string());
243 }
244 if m.contains(Modifier::ITALIC) {
245 out.push("italic".to_string());
246 }
247 if m.contains(Modifier::UNDERLINED) {
248 out.push("underlined".to_string());
249 }
250 if m.contains(Modifier::DIM) {
251 out.push("dim".to_string());
252 }
253 ModifierDef(out)
254 }
255}
256
257pub fn named_color_from_str(name: &str) -> Option<Color> {
260 match name {
261 "Black" => Some(Color::Black),
262 "Red" => Some(Color::Red),
263 "Green" => Some(Color::Green),
264 "Yellow" => Some(Color::Yellow),
265 "Blue" => Some(Color::Blue),
266 "Magenta" => Some(Color::Magenta),
267 "Cyan" => Some(Color::Cyan),
268 "Gray" => Some(Color::Gray),
269 "DarkGray" => Some(Color::DarkGray),
270 "LightRed" => Some(Color::LightRed),
271 "LightGreen" => Some(Color::LightGreen),
272 "LightYellow" => Some(Color::LightYellow),
273 "LightBlue" => Some(Color::LightBlue),
274 "LightMagenta" => Some(Color::LightMagenta),
275 "LightCyan" => Some(Color::LightCyan),
276 "White" => Some(Color::White),
277 "Default" | "Reset" => Some(Color::Reset),
278 _ => None,
279 }
280}
281
282fn token_color_named_from_ratatui(color: Color) -> &'static str {
287 match color {
288 Color::Black => "Black",
289 Color::Red => "Red",
290 Color::Green => "Green",
291 Color::Yellow => "Yellow",
292 Color::Blue => "Blue",
293 Color::Magenta => "Magenta",
294 Color::Cyan => "Cyan",
295 Color::Gray => "Gray",
296 Color::DarkGray => "DarkGray",
297 Color::LightRed => "LightRed",
298 Color::LightGreen => "LightGreen",
299 Color::LightYellow => "LightYellow",
300 Color::LightBlue => "LightBlue",
301 Color::LightMagenta => "LightMagenta",
302 Color::LightCyan => "LightCyan",
303 Color::White => "White",
304 Color::Reset => "Default",
305 _ => "Default",
308 }
309}
310
311pub trait TokenColorExt {
318 fn to_ratatui(&self, theme: &Theme) -> Color;
319 fn from_ratatui(color: Color) -> Option<fresh_core::api::TokenColor>;
320}
321
322impl TokenColorExt for fresh_core::api::TokenColor {
323 fn to_ratatui(&self, theme: &Theme) -> Color {
324 use fresh_core::api::TokenColor;
325 match self {
326 TokenColor::Rgb(r, g, b) => Color::Rgb(*r, *g, *b),
327 TokenColor::Named(name) => {
328 if let Some(c) = named_color_from_str(name) {
329 return c;
330 }
331 if let Some(rest) = name.strip_prefix("Indexed:") {
332 if let Ok(n) = rest.parse::<u8>() {
333 return Color::Indexed(n);
334 }
335 }
336 theme.resolve_theme_key(name).unwrap_or(Color::Reset)
337 }
338 }
339 }
340
341 fn from_ratatui(color: Color) -> Option<fresh_core::api::TokenColor> {
342 use fresh_core::api::TokenColor;
343 match color {
344 Color::Rgb(r, g, b) => Some(TokenColor::Rgb(r, g, b)),
345 Color::Indexed(n) => Some(TokenColor::Named(format!("Indexed:{n}"))),
346 other => Some(TokenColor::Named(
347 token_color_named_from_ratatui(other).to_string(),
348 )),
349 }
350 }
351}
352
353impl From<Color> for ColorDef {
354 fn from(color: Color) -> Self {
355 match color {
356 Color::Rgb(r, g, b) => ColorDef::Rgb(r, g, b),
357 Color::White => ColorDef::Named("White".to_string()),
358 Color::Black => ColorDef::Named("Black".to_string()),
359 Color::Red => ColorDef::Named("Red".to_string()),
360 Color::Green => ColorDef::Named("Green".to_string()),
361 Color::Blue => ColorDef::Named("Blue".to_string()),
362 Color::Yellow => ColorDef::Named("Yellow".to_string()),
363 Color::Magenta => ColorDef::Named("Magenta".to_string()),
364 Color::Cyan => ColorDef::Named("Cyan".to_string()),
365 Color::Gray => ColorDef::Named("Gray".to_string()),
366 Color::DarkGray => ColorDef::Named("DarkGray".to_string()),
367 Color::LightRed => ColorDef::Named("LightRed".to_string()),
368 Color::LightGreen => ColorDef::Named("LightGreen".to_string()),
369 Color::LightBlue => ColorDef::Named("LightBlue".to_string()),
370 Color::LightYellow => ColorDef::Named("LightYellow".to_string()),
371 Color::LightMagenta => ColorDef::Named("LightMagenta".to_string()),
372 Color::LightCyan => ColorDef::Named("LightCyan".to_string()),
373 Color::Reset => ColorDef::Named("Default".to_string()),
374 Color::Indexed(_) => {
375 if let Some((r, g, b)) = color_to_rgb(color) {
377 ColorDef::Rgb(r, g, b)
378 } else {
379 ColorDef::Named("Default".to_string())
380 }
381 }
382 }
383 }
384}
385
386#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
409pub struct ThemeFile {
410 pub name: String,
412 #[serde(default, skip_serializing_if = "Option::is_none")]
418 pub extends: Option<String>,
419 #[serde(default = "default_editor_colors")]
421 pub editor: EditorColors,
422 #[serde(default = "default_ui_colors")]
424 pub ui: UiColors,
425 #[serde(default = "default_search_colors")]
427 pub search: SearchColors,
428 #[serde(default = "default_diagnostic_colors")]
430 pub diagnostic: DiagnosticColors,
431 #[serde(default = "default_syntax_colors")]
433 pub syntax: SyntaxColors,
434}
435
436fn default_section<T: serde::de::DeserializeOwned>(section: &'static str) -> T {
441 serde_json::from_str("{}").unwrap_or_else(|e| {
442 panic!(
443 "theme section `{}` must be default-constructible from `{{}}` \
444 (every field needs `#[serde(default = ...)]`): {}",
445 section, e
446 )
447 })
448}
449
450fn default_editor_colors() -> EditorColors {
451 default_section("editor")
452}
453
454fn default_ui_colors() -> UiColors {
455 default_section("ui")
456}
457
458fn default_search_colors() -> SearchColors {
459 default_section("search")
460}
461
462fn default_diagnostic_colors() -> DiagnosticColors {
463 default_section("diagnostic")
464}
465
466fn default_syntax_colors() -> SyntaxColors {
467 default_section("syntax")
468}
469
470#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
472pub struct EditorColors {
473 #[serde(default = "default_editor_bg")]
475 pub bg: ColorDef,
476 #[serde(default = "default_editor_fg")]
478 pub fg: ColorDef,
479 #[serde(default = "default_cursor")]
481 pub cursor: ColorDef,
482 #[serde(default = "default_inactive_cursor")]
484 pub inactive_cursor: ColorDef,
485 #[serde(default = "default_selection_bg")]
487 pub selection_bg: ColorDef,
488 #[serde(default)]
496 pub selection_modifier: Option<ModifierDef>,
497 #[serde(default = "default_current_line_bg")]
499 pub current_line_bg: ColorDef,
500 #[serde(default = "default_line_number_fg")]
502 pub line_number_fg: ColorDef,
503 #[serde(default = "default_line_number_bg")]
505 pub line_number_bg: ColorDef,
506 #[serde(default = "default_diff_add_bg")]
508 pub diff_add_bg: ColorDef,
509 #[serde(default = "default_diff_remove_bg")]
511 pub diff_remove_bg: ColorDef,
512 #[serde(default)]
515 pub diff_add_highlight_bg: Option<ColorDef>,
516 #[serde(default)]
519 pub diff_remove_highlight_bg: Option<ColorDef>,
520 #[serde(default = "default_diff_modify_bg")]
522 pub diff_modify_bg: ColorDef,
523 #[serde(default)]
527 pub diff_add_collision_fg: Option<ColorDef>,
528 #[serde(default)]
530 pub diff_remove_collision_fg: Option<ColorDef>,
531 #[serde(default)]
533 pub diff_modify_collision_fg: Option<ColorDef>,
534 #[serde(default = "default_ruler_bg")]
536 pub ruler_bg: ColorDef,
537 #[serde(default = "default_whitespace_indicator_fg")]
539 pub whitespace_indicator_fg: ColorDef,
540 #[serde(default = "default_bracket_match_fg")]
542 pub bracket_match_fg: ColorDef,
543 #[serde(default = "default_bracket_rainbow_1")]
545 pub bracket_rainbow_1: ColorDef,
546 #[serde(default = "default_bracket_rainbow_2")]
548 pub bracket_rainbow_2: ColorDef,
549 #[serde(default = "default_bracket_rainbow_3")]
551 pub bracket_rainbow_3: ColorDef,
552 #[serde(default = "default_bracket_rainbow_4")]
554 pub bracket_rainbow_4: ColorDef,
555 #[serde(default = "default_bracket_rainbow_5")]
557 pub bracket_rainbow_5: ColorDef,
558 #[serde(default = "default_bracket_rainbow_6")]
560 pub bracket_rainbow_6: ColorDef,
561 #[serde(default)]
566 pub after_eof_bg: Option<ColorDef>,
567}
568
569fn default_editor_bg() -> ColorDef {
571 ColorDef::Rgb(30, 30, 30)
572}
573fn default_editor_fg() -> ColorDef {
574 ColorDef::Rgb(212, 212, 212)
575}
576fn default_cursor() -> ColorDef {
577 ColorDef::Rgb(255, 255, 255)
578}
579fn default_inactive_cursor() -> ColorDef {
580 ColorDef::Named("DarkGray".to_string())
581}
582fn default_selection_bg() -> ColorDef {
583 ColorDef::Rgb(38, 79, 120)
584}
585fn default_current_line_bg() -> ColorDef {
586 ColorDef::Rgb(40, 40, 40)
587}
588fn default_line_number_fg() -> ColorDef {
589 ColorDef::Rgb(100, 100, 100)
590}
591fn default_line_number_bg() -> ColorDef {
592 ColorDef::Rgb(30, 30, 30)
593}
594fn default_diff_add_bg() -> ColorDef {
595 ColorDef::Rgb(35, 60, 35) }
597fn default_diff_remove_bg() -> ColorDef {
598 ColorDef::Rgb(70, 35, 35) }
600fn default_diff_modify_bg() -> ColorDef {
601 ColorDef::Rgb(40, 38, 30) }
603fn default_ruler_bg() -> ColorDef {
604 ColorDef::Rgb(50, 50, 50) }
606fn default_whitespace_indicator_fg() -> ColorDef {
607 ColorDef::Rgb(70, 70, 70) }
609fn default_bracket_match_fg() -> ColorDef {
610 ColorDef::Rgb(255, 215, 0) }
612fn default_bracket_rainbow_1() -> ColorDef {
613 ColorDef::Rgb(255, 215, 0) }
615fn default_bracket_rainbow_2() -> ColorDef {
616 ColorDef::Rgb(218, 112, 214) }
618fn default_bracket_rainbow_3() -> ColorDef {
619 ColorDef::Rgb(50, 205, 50) }
621fn default_bracket_rainbow_4() -> ColorDef {
622 ColorDef::Rgb(30, 144, 255) }
624fn default_bracket_rainbow_5() -> ColorDef {
625 ColorDef::Rgb(255, 127, 80) }
627fn default_bracket_rainbow_6() -> ColorDef {
628 ColorDef::Rgb(147, 112, 219) }
630
631#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
650pub struct UiColors {
651 #[serde(default = "default_tab_active_fg")]
653 pub tab_active_fg: ColorDef,
654 #[serde(default = "default_tab_active_bg")]
656 pub tab_active_bg: ColorDef,
657 #[serde(default = "default_tab_inactive_fg")]
659 pub tab_inactive_fg: ColorDef,
660 #[serde(default = "default_tab_inactive_bg")]
662 pub tab_inactive_bg: ColorDef,
663 #[serde(default = "default_tab_separator_bg")]
665 pub tab_separator_bg: ColorDef,
666 #[serde(default = "default_tab_close_hover_fg")]
668 pub tab_close_hover_fg: ColorDef,
669 #[serde(default = "default_tab_hover_bg")]
671 pub tab_hover_bg: ColorDef,
672 #[serde(default = "default_menu_bg")]
674 pub menu_bg: ColorDef,
675 #[serde(default = "default_menu_fg")]
677 pub menu_fg: ColorDef,
678 #[serde(default = "default_menu_active_bg")]
680 pub menu_active_bg: ColorDef,
681 #[serde(default = "default_menu_active_fg")]
683 pub menu_active_fg: ColorDef,
684 #[serde(default = "default_menu_dropdown_bg")]
686 pub menu_dropdown_bg: ColorDef,
687 #[serde(default = "default_menu_dropdown_fg")]
689 pub menu_dropdown_fg: ColorDef,
690 #[serde(default = "default_menu_highlight_bg")]
692 pub menu_highlight_bg: ColorDef,
693 #[serde(default = "default_menu_highlight_fg")]
695 pub menu_highlight_fg: ColorDef,
696 #[serde(default = "default_menu_border_fg")]
698 pub menu_border_fg: ColorDef,
699 #[serde(default = "default_menu_separator_fg")]
701 pub menu_separator_fg: ColorDef,
702 #[serde(default = "default_menu_hover_bg")]
704 pub menu_hover_bg: ColorDef,
705 #[serde(default = "default_menu_hover_fg")]
707 pub menu_hover_fg: ColorDef,
708 #[serde(default = "default_menu_disabled_fg")]
710 pub menu_disabled_fg: ColorDef,
711 #[serde(default = "default_menu_disabled_bg")]
713 pub menu_disabled_bg: ColorDef,
714 #[serde(default = "default_status_bar_fg")]
716 pub status_bar_fg: ColorDef,
717 #[serde(default = "default_status_bar_bg")]
719 pub status_bar_bg: ColorDef,
720 #[serde(default)]
722 pub status_palette_fg: Option<ColorDef>,
723 #[serde(default)]
725 pub status_palette_bg: Option<ColorDef>,
726 #[serde(default)]
728 pub status_separator_fg: Option<ColorDef>,
729 #[serde(default)]
731 pub status_separator_bg: Option<ColorDef>,
732 #[serde(default)]
734 pub status_lsp_on_fg: Option<ColorDef>,
735 #[serde(default)]
737 pub status_lsp_on_bg: Option<ColorDef>,
738 #[serde(default)]
742 pub status_lsp_actionable_fg: Option<ColorDef>,
743 #[serde(default)]
746 pub status_lsp_actionable_bg: Option<ColorDef>,
747 #[serde(default = "default_prompt_fg")]
749 pub prompt_fg: ColorDef,
750 #[serde(default = "default_prompt_bg")]
752 pub prompt_bg: ColorDef,
753 #[serde(default = "default_prompt_selection_fg")]
755 pub prompt_selection_fg: ColorDef,
756 #[serde(default = "default_prompt_selection_bg")]
758 pub prompt_selection_bg: ColorDef,
759 #[serde(default = "default_popup_border_fg")]
761 pub popup_border_fg: ColorDef,
762 #[serde(default = "default_popup_bg")]
764 pub popup_bg: ColorDef,
765 #[serde(default = "default_popup_selection_bg")]
767 pub popup_selection_bg: ColorDef,
768 #[serde(default = "default_text_input_selection_bg")]
776 pub text_input_selection_bg: ColorDef,
777 #[serde(default = "default_popup_selection_fg")]
779 pub popup_selection_fg: ColorDef,
780 #[serde(default = "default_popup_text_fg", alias = "popup_fg")]
784 pub popup_text_fg: ColorDef,
785 #[serde(default = "default_suggestion_bg")]
787 pub suggestion_bg: ColorDef,
788 #[serde(default)]
792 pub suggestion_fg: Option<ColorDef>,
793 #[serde(default = "default_suggestion_selected_bg")]
795 pub suggestion_selected_bg: ColorDef,
796 #[serde(default = "default_help_bg")]
798 pub help_bg: ColorDef,
799 #[serde(default = "default_help_fg")]
801 pub help_fg: ColorDef,
802 #[serde(default = "default_help_key_fg")]
804 pub help_key_fg: ColorDef,
805 #[serde(default = "default_help_separator_fg")]
807 pub help_separator_fg: ColorDef,
808 #[serde(default = "default_help_indicator_fg")]
810 pub help_indicator_fg: ColorDef,
811 #[serde(default = "default_help_indicator_bg")]
813 pub help_indicator_bg: ColorDef,
814 #[serde(default = "default_inline_code_bg")]
816 pub inline_code_bg: ColorDef,
817 #[serde(default = "default_split_separator_fg")]
819 pub split_separator_fg: ColorDef,
820 #[serde(default = "default_split_separator_hover_fg")]
822 pub split_separator_hover_fg: ColorDef,
823 #[serde(default = "default_scrollbar_track_fg")]
825 pub scrollbar_track_fg: ColorDef,
826 #[serde(default = "default_scrollbar_thumb_fg")]
828 pub scrollbar_thumb_fg: ColorDef,
829 #[serde(default = "default_scrollbar_track_hover_fg")]
831 pub scrollbar_track_hover_fg: ColorDef,
832 #[serde(default = "default_scrollbar_thumb_hover_fg")]
834 pub scrollbar_thumb_hover_fg: ColorDef,
835 #[serde(default = "default_compose_margin_bg")]
837 pub compose_margin_bg: ColorDef,
838 #[serde(default = "default_semantic_highlight_bg")]
840 pub semantic_highlight_bg: ColorDef,
841 #[serde(default)]
848 pub semantic_highlight_modifier: Option<ModifierDef>,
849 #[serde(default = "default_terminal_bg")]
851 pub terminal_bg: ColorDef,
852 #[serde(default = "default_terminal_fg")]
854 pub terminal_fg: ColorDef,
855 #[serde(default = "default_status_warning_indicator_bg")]
857 pub status_warning_indicator_bg: ColorDef,
858 #[serde(default = "default_status_warning_indicator_fg")]
860 pub status_warning_indicator_fg: ColorDef,
861 #[serde(default = "default_status_error_indicator_bg")]
863 pub status_error_indicator_bg: ColorDef,
864 #[serde(default = "default_status_error_indicator_fg")]
866 pub status_error_indicator_fg: ColorDef,
867 #[serde(default = "default_status_warning_indicator_hover_bg")]
869 pub status_warning_indicator_hover_bg: ColorDef,
870 #[serde(default = "default_status_warning_indicator_hover_fg")]
872 pub status_warning_indicator_hover_fg: ColorDef,
873 #[serde(default = "default_status_error_indicator_hover_bg")]
875 pub status_error_indicator_hover_bg: ColorDef,
876 #[serde(default = "default_status_error_indicator_hover_fg")]
878 pub status_error_indicator_hover_fg: ColorDef,
879 #[serde(default = "default_tab_drop_zone_bg")]
881 pub tab_drop_zone_bg: ColorDef,
882 #[serde(default = "default_tab_drop_zone_border")]
884 pub tab_drop_zone_border: ColorDef,
885 #[serde(default = "default_settings_selected_bg")]
887 pub settings_selected_bg: ColorDef,
888 #[serde(default = "default_settings_selected_fg")]
890 pub settings_selected_fg: ColorDef,
891 #[serde(default)]
893 pub file_status_added_fg: Option<ColorDef>,
894 #[serde(default)]
896 pub file_status_modified_fg: Option<ColorDef>,
897 #[serde(default)]
899 pub file_status_deleted_fg: Option<ColorDef>,
900 #[serde(default)]
902 pub file_status_renamed_fg: Option<ColorDef>,
903 #[serde(default)]
905 pub file_status_untracked_fg: Option<ColorDef>,
906 #[serde(default)]
908 pub file_status_conflicted_fg: Option<ColorDef>,
909}
910
911fn default_tab_active_fg() -> ColorDef {
914 ColorDef::Named("Yellow".to_string())
915}
916fn default_tab_active_bg() -> ColorDef {
917 ColorDef::Named("Blue".to_string())
918}
919fn default_tab_inactive_fg() -> ColorDef {
920 ColorDef::Named("White".to_string())
921}
922fn default_tab_inactive_bg() -> ColorDef {
923 ColorDef::Named("DarkGray".to_string())
924}
925fn default_tab_separator_bg() -> ColorDef {
926 ColorDef::Named("Black".to_string())
927}
928fn default_tab_close_hover_fg() -> ColorDef {
929 ColorDef::Rgb(255, 100, 100) }
931fn default_tab_hover_bg() -> ColorDef {
932 ColorDef::Rgb(70, 70, 75) }
934
935fn default_menu_bg() -> ColorDef {
937 ColorDef::Rgb(60, 60, 65)
938}
939fn default_menu_fg() -> ColorDef {
940 ColorDef::Rgb(220, 220, 220)
941}
942fn default_menu_active_bg() -> ColorDef {
943 ColorDef::Rgb(60, 60, 60)
944}
945fn default_menu_active_fg() -> ColorDef {
946 ColorDef::Rgb(255, 255, 255)
947}
948fn default_menu_dropdown_bg() -> ColorDef {
949 ColorDef::Rgb(50, 50, 50)
950}
951fn default_menu_dropdown_fg() -> ColorDef {
952 ColorDef::Rgb(220, 220, 220)
953}
954fn default_menu_highlight_bg() -> ColorDef {
955 ColorDef::Rgb(70, 130, 180)
956}
957fn default_menu_highlight_fg() -> ColorDef {
958 ColorDef::Rgb(255, 255, 255)
959}
960fn default_menu_border_fg() -> ColorDef {
961 ColorDef::Rgb(100, 100, 100)
962}
963fn default_menu_separator_fg() -> ColorDef {
964 ColorDef::Rgb(80, 80, 80)
965}
966fn default_menu_hover_bg() -> ColorDef {
967 ColorDef::Rgb(55, 55, 55)
968}
969fn default_menu_hover_fg() -> ColorDef {
970 ColorDef::Rgb(255, 255, 255)
971}
972fn default_menu_disabled_fg() -> ColorDef {
973 ColorDef::Rgb(100, 100, 100) }
975fn default_menu_disabled_bg() -> ColorDef {
976 ColorDef::Rgb(50, 50, 50) }
978fn default_status_bar_fg() -> ColorDef {
980 ColorDef::Named("White".to_string())
981}
982fn default_status_bar_bg() -> ColorDef {
983 ColorDef::Named("DarkGray".to_string())
984}
985
986fn default_prompt_fg() -> ColorDef {
988 ColorDef::Named("White".to_string())
989}
990fn default_prompt_bg() -> ColorDef {
991 ColorDef::Named("Black".to_string())
992}
993fn default_prompt_selection_fg() -> ColorDef {
994 ColorDef::Named("White".to_string())
995}
996fn default_prompt_selection_bg() -> ColorDef {
997 ColorDef::Rgb(58, 79, 120)
998}
999
1000fn default_popup_border_fg() -> ColorDef {
1002 ColorDef::Named("Gray".to_string())
1003}
1004fn default_popup_bg() -> ColorDef {
1005 ColorDef::Rgb(30, 30, 30)
1006}
1007fn default_popup_selection_bg() -> ColorDef {
1008 ColorDef::Rgb(58, 79, 120)
1009}
1010fn default_text_input_selection_bg() -> ColorDef {
1011 ColorDef::Rgb(58, 79, 120)
1015}
1016fn default_popup_selection_fg() -> ColorDef {
1017 ColorDef::Rgb(255, 255, 255) }
1019fn default_popup_text_fg() -> ColorDef {
1020 ColorDef::Named("White".to_string())
1021}
1022
1023fn default_suggestion_bg() -> ColorDef {
1025 ColorDef::Rgb(30, 30, 30)
1026}
1027fn default_suggestion_selected_bg() -> ColorDef {
1028 ColorDef::Rgb(58, 79, 120)
1029}
1030
1031fn default_help_bg() -> ColorDef {
1033 ColorDef::Named("Black".to_string())
1034}
1035fn default_help_fg() -> ColorDef {
1036 ColorDef::Named("White".to_string())
1037}
1038fn default_help_key_fg() -> ColorDef {
1039 ColorDef::Named("Cyan".to_string())
1040}
1041fn default_help_separator_fg() -> ColorDef {
1042 ColorDef::Named("DarkGray".to_string())
1043}
1044fn default_help_indicator_fg() -> ColorDef {
1045 ColorDef::Named("Red".to_string())
1046}
1047fn default_help_indicator_bg() -> ColorDef {
1048 ColorDef::Named("Black".to_string())
1049}
1050
1051fn default_inline_code_bg() -> ColorDef {
1052 ColorDef::Named("DarkGray".to_string())
1053}
1054
1055fn default_split_separator_fg() -> ColorDef {
1057 ColorDef::Rgb(100, 100, 100)
1058}
1059fn default_split_separator_hover_fg() -> ColorDef {
1060 ColorDef::Rgb(100, 149, 237) }
1062fn default_scrollbar_track_fg() -> ColorDef {
1063 ColorDef::Named("DarkGray".to_string())
1064}
1065fn default_scrollbar_thumb_fg() -> ColorDef {
1066 ColorDef::Named("Gray".to_string())
1067}
1068fn default_scrollbar_track_hover_fg() -> ColorDef {
1069 ColorDef::Named("Gray".to_string())
1070}
1071fn default_scrollbar_thumb_hover_fg() -> ColorDef {
1072 ColorDef::Named("White".to_string())
1073}
1074fn default_compose_margin_bg() -> ColorDef {
1075 ColorDef::Rgb(18, 18, 18) }
1077fn default_semantic_highlight_bg() -> ColorDef {
1078 ColorDef::Rgb(60, 60, 80) }
1080fn default_terminal_bg() -> ColorDef {
1081 ColorDef::Named("Default".to_string()) }
1083fn default_terminal_fg() -> ColorDef {
1084 ColorDef::Named("Default".to_string()) }
1086fn default_status_warning_indicator_bg() -> ColorDef {
1087 ColorDef::Rgb(181, 137, 0) }
1089fn default_status_warning_indicator_fg() -> ColorDef {
1090 ColorDef::Rgb(0, 0, 0) }
1092fn default_status_error_indicator_bg() -> ColorDef {
1093 ColorDef::Rgb(220, 50, 47) }
1095fn default_status_error_indicator_fg() -> ColorDef {
1096 ColorDef::Rgb(255, 255, 255) }
1098fn default_status_warning_indicator_hover_bg() -> ColorDef {
1099 ColorDef::Rgb(211, 167, 30) }
1101fn default_status_warning_indicator_hover_fg() -> ColorDef {
1102 ColorDef::Rgb(0, 0, 0) }
1104fn default_status_error_indicator_hover_bg() -> ColorDef {
1105 ColorDef::Rgb(250, 80, 77) }
1107fn default_status_error_indicator_hover_fg() -> ColorDef {
1108 ColorDef::Rgb(255, 255, 255) }
1110fn default_tab_drop_zone_bg() -> ColorDef {
1111 ColorDef::Rgb(70, 130, 180) }
1113fn default_tab_drop_zone_border() -> ColorDef {
1114 ColorDef::Rgb(100, 149, 237) }
1116fn default_settings_selected_bg() -> ColorDef {
1117 ColorDef::Rgb(60, 60, 70) }
1119fn default_settings_selected_fg() -> ColorDef {
1120 ColorDef::Rgb(255, 255, 255) }
1122#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1124pub struct SearchColors {
1125 #[serde(default = "default_search_match_bg")]
1127 pub match_bg: ColorDef,
1128 #[serde(default = "default_search_match_fg")]
1130 pub match_fg: ColorDef,
1131 #[serde(default = "default_search_label_bg")]
1135 pub label_bg: ColorDef,
1136 #[serde(default = "default_search_label_fg")]
1140 pub label_fg: ColorDef,
1141}
1142
1143fn default_search_match_bg() -> ColorDef {
1145 ColorDef::Rgb(100, 100, 20)
1146}
1147fn default_search_match_fg() -> ColorDef {
1148 ColorDef::Rgb(255, 255, 255)
1149}
1150fn default_search_label_bg() -> ColorDef {
1155 ColorDef::Rgb(199, 78, 189)
1156}
1157fn default_search_label_fg() -> ColorDef {
1158 ColorDef::Rgb(255, 255, 255)
1159}
1160
1161#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1163pub struct DiagnosticColors {
1164 #[serde(default = "default_diagnostic_error_fg")]
1166 pub error_fg: ColorDef,
1167 #[serde(default = "default_diagnostic_error_bg")]
1169 pub error_bg: ColorDef,
1170 #[serde(default = "default_diagnostic_warning_fg")]
1172 pub warning_fg: ColorDef,
1173 #[serde(default = "default_diagnostic_warning_bg")]
1175 pub warning_bg: ColorDef,
1176 #[serde(default = "default_diagnostic_info_fg")]
1178 pub info_fg: ColorDef,
1179 #[serde(default = "default_diagnostic_info_bg")]
1181 pub info_bg: ColorDef,
1182 #[serde(default = "default_diagnostic_hint_fg")]
1184 pub hint_fg: ColorDef,
1185 #[serde(default = "default_diagnostic_hint_bg")]
1187 pub hint_bg: ColorDef,
1188}
1189
1190fn default_diagnostic_error_fg() -> ColorDef {
1192 ColorDef::Named("Red".to_string())
1193}
1194fn default_diagnostic_error_bg() -> ColorDef {
1195 ColorDef::Rgb(60, 20, 20)
1196}
1197fn default_diagnostic_warning_fg() -> ColorDef {
1198 ColorDef::Named("Yellow".to_string())
1199}
1200fn default_diagnostic_warning_bg() -> ColorDef {
1201 ColorDef::Rgb(60, 50, 0)
1202}
1203fn default_diagnostic_info_fg() -> ColorDef {
1204 ColorDef::Named("Blue".to_string())
1205}
1206fn default_diagnostic_info_bg() -> ColorDef {
1207 ColorDef::Rgb(0, 30, 60)
1208}
1209fn default_diagnostic_hint_fg() -> ColorDef {
1210 ColorDef::Named("Gray".to_string())
1211}
1212fn default_diagnostic_hint_bg() -> ColorDef {
1213 ColorDef::Rgb(30, 30, 30)
1214}
1215
1216#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1218pub struct SyntaxColors {
1219 #[serde(default = "default_syntax_keyword")]
1221 pub keyword: ColorDef,
1222 #[serde(default = "default_syntax_string")]
1224 pub string: ColorDef,
1225 #[serde(default = "default_syntax_comment")]
1227 pub comment: ColorDef,
1228 #[serde(default = "default_syntax_function")]
1230 pub function: ColorDef,
1231 #[serde(rename = "type", default = "default_syntax_type")]
1233 pub type_: ColorDef,
1234 #[serde(default = "default_syntax_variable")]
1236 pub variable: ColorDef,
1237 #[serde(default = "default_syntax_variable_builtin")]
1239 pub variable_builtin: ColorDef,
1240 #[serde(default = "default_syntax_constant")]
1242 pub constant: ColorDef,
1243 #[serde(default = "default_syntax_operator")]
1245 pub operator: ColorDef,
1246 #[serde(default = "default_syntax_punctuation_bracket")]
1248 pub punctuation_bracket: ColorDef,
1249 #[serde(default = "default_syntax_punctuation_delimiter")]
1251 pub punctuation_delimiter: ColorDef,
1252}
1253
1254fn default_syntax_keyword() -> ColorDef {
1256 ColorDef::Rgb(86, 156, 214)
1257}
1258fn default_syntax_string() -> ColorDef {
1259 ColorDef::Rgb(206, 145, 120)
1260}
1261fn default_syntax_comment() -> ColorDef {
1262 ColorDef::Rgb(106, 153, 85)
1263}
1264fn default_syntax_function() -> ColorDef {
1265 ColorDef::Rgb(220, 220, 170)
1266}
1267fn default_syntax_type() -> ColorDef {
1268 ColorDef::Rgb(78, 201, 176)
1269}
1270fn default_syntax_variable() -> ColorDef {
1271 ColorDef::Rgb(156, 220, 254)
1272}
1273fn default_syntax_variable_builtin() -> ColorDef {
1274 ColorDef::Rgb(86, 156, 214) }
1276fn default_syntax_constant() -> ColorDef {
1277 ColorDef::Rgb(79, 193, 255)
1278}
1279fn default_syntax_operator() -> ColorDef {
1280 ColorDef::Rgb(212, 212, 212)
1281}
1282fn default_syntax_punctuation_bracket() -> ColorDef {
1283 ColorDef::Rgb(212, 212, 212) }
1285fn default_syntax_punctuation_delimiter() -> ColorDef {
1286 ColorDef::Rgb(212, 212, 212) }
1288
1289#[derive(Debug, Clone)]
1291pub struct Theme {
1292 pub name: String,
1294
1295 pub editor_bg: Color,
1297 pub editor_fg: Color,
1298 pub cursor: Color,
1299 pub inactive_cursor: Color,
1300 pub selection_bg: Color,
1301 pub selection_modifier: Modifier,
1306 pub current_line_bg: Color,
1307 pub line_number_fg: Color,
1308 pub line_number_bg: Color,
1309
1310 pub after_eof_bg: Color,
1312
1313 pub ruler_bg: Color,
1315
1316 pub whitespace_indicator_fg: Color,
1318
1319 pub bracket_match_fg: Color,
1321 pub bracket_rainbow_1: Color,
1322 pub bracket_rainbow_2: Color,
1323 pub bracket_rainbow_3: Color,
1324 pub bracket_rainbow_4: Color,
1325 pub bracket_rainbow_5: Color,
1326 pub bracket_rainbow_6: Color,
1327
1328 pub diff_add_bg: Color,
1330 pub diff_remove_bg: Color,
1331 pub diff_modify_bg: Color,
1332 pub diff_add_highlight_bg: Color,
1334 pub diff_remove_highlight_bg: Color,
1336 pub diff_add_collision_fg: Option<Color>,
1340 pub diff_remove_collision_fg: Option<Color>,
1341 pub diff_modify_collision_fg: Option<Color>,
1342
1343 pub tab_active_fg: Color,
1345 pub tab_active_bg: Color,
1346 pub tab_inactive_fg: Color,
1347 pub tab_inactive_bg: Color,
1348 pub tab_separator_bg: Color,
1349 pub tab_close_hover_fg: Color,
1350 pub tab_hover_bg: Color,
1351
1352 pub menu_bg: Color,
1354 pub menu_fg: Color,
1355 pub menu_active_bg: Color,
1356 pub menu_active_fg: Color,
1357 pub menu_dropdown_bg: Color,
1358 pub menu_dropdown_fg: Color,
1359 pub menu_highlight_bg: Color,
1360 pub menu_highlight_fg: Color,
1361 pub menu_border_fg: Color,
1362 pub menu_separator_fg: Color,
1363 pub menu_hover_bg: Color,
1364 pub menu_hover_fg: Color,
1365 pub menu_disabled_fg: Color,
1366 pub menu_disabled_bg: Color,
1367
1368 pub status_bar_fg: Color,
1369 pub status_bar_bg: Color,
1370 pub status_palette_fg: Color,
1372 pub status_palette_bg: Color,
1373 pub status_separator_fg: Color,
1375 pub status_separator_bg: Color,
1376 pub status_lsp_on_fg: Color,
1378 pub status_lsp_on_bg: Color,
1379 pub status_lsp_actionable_fg: Color,
1382 pub status_lsp_actionable_bg: Color,
1383 pub prompt_fg: Color,
1384 pub prompt_bg: Color,
1385 pub prompt_selection_fg: Color,
1386 pub prompt_selection_bg: Color,
1387
1388 pub popup_border_fg: Color,
1389 pub popup_bg: Color,
1390 pub popup_selection_bg: Color,
1391 pub popup_selection_fg: Color,
1392 pub popup_text_fg: Color,
1393 pub text_input_selection_bg: Color,
1397
1398 pub suggestion_bg: Color,
1399 pub suggestion_fg: Color,
1400 pub suggestion_selected_bg: Color,
1401
1402 pub help_bg: Color,
1403 pub help_fg: Color,
1404 pub help_key_fg: Color,
1405 pub help_separator_fg: Color,
1406
1407 pub help_indicator_fg: Color,
1408 pub help_indicator_bg: Color,
1409
1410 pub inline_code_bg: Color,
1412
1413 pub split_separator_fg: Color,
1414 pub split_separator_hover_fg: Color,
1415
1416 pub scrollbar_track_fg: Color,
1418 pub scrollbar_thumb_fg: Color,
1419 pub scrollbar_track_hover_fg: Color,
1420 pub scrollbar_thumb_hover_fg: Color,
1421
1422 pub compose_margin_bg: Color,
1424
1425 pub semantic_highlight_bg: Color,
1427 pub semantic_highlight_modifier: Modifier,
1432
1433 pub terminal_bg: Color,
1435 pub terminal_fg: Color,
1436
1437 pub status_warning_indicator_bg: Color,
1439 pub status_warning_indicator_fg: Color,
1440 pub status_error_indicator_bg: Color,
1441 pub status_error_indicator_fg: Color,
1442 pub status_warning_indicator_hover_bg: Color,
1443 pub status_warning_indicator_hover_fg: Color,
1444 pub status_error_indicator_hover_bg: Color,
1445 pub status_error_indicator_hover_fg: Color,
1446
1447 pub tab_drop_zone_bg: Color,
1449 pub tab_drop_zone_border: Color,
1450
1451 pub settings_selected_bg: Color,
1453 pub settings_selected_fg: Color,
1454
1455 pub file_status_added_fg: Color,
1457 pub file_status_modified_fg: Color,
1458 pub file_status_deleted_fg: Color,
1459 pub file_status_renamed_fg: Color,
1460 pub file_status_untracked_fg: Color,
1461 pub file_status_conflicted_fg: Color,
1462
1463 pub search_match_bg: Color,
1465 pub search_match_fg: Color,
1466 pub search_label_bg: Color,
1467 pub search_label_fg: Color,
1468
1469 pub diagnostic_error_fg: Color,
1471 pub diagnostic_error_bg: Color,
1472 pub diagnostic_warning_fg: Color,
1473 pub diagnostic_warning_bg: Color,
1474 pub diagnostic_info_fg: Color,
1475 pub diagnostic_info_bg: Color,
1476 pub diagnostic_hint_fg: Color,
1477 pub diagnostic_hint_bg: Color,
1478
1479 pub syntax_keyword: Color,
1481 pub syntax_string: Color,
1482 pub syntax_comment: Color,
1483 pub syntax_function: Color,
1484 pub syntax_type: Color,
1485 pub syntax_variable: Color,
1486 pub syntax_variable_builtin: Color,
1487 pub syntax_constant: Color,
1488 pub syntax_operator: Color,
1489 pub syntax_punctuation_bracket: Color,
1490 pub syntax_punctuation_delimiter: Color,
1491}
1492
1493impl From<ThemeFile> for Theme {
1494 fn from(file: ThemeFile) -> Self {
1495 Self {
1496 name: file.name,
1497 editor_bg: file.editor.bg.clone().into(),
1498 editor_fg: file.editor.fg.into(),
1499 cursor: file.editor.cursor.into(),
1500 inactive_cursor: file.editor.inactive_cursor.into(),
1501 selection_bg: file.editor.selection_bg.into(),
1502 selection_modifier: file
1503 .editor
1504 .selection_modifier
1505 .as_ref()
1506 .map(Modifier::from)
1507 .unwrap_or(Modifier::empty()),
1508 current_line_bg: file.editor.current_line_bg.into(),
1509 line_number_fg: file.editor.line_number_fg.into(),
1510 line_number_bg: file.editor.line_number_bg.into(),
1511 after_eof_bg: file
1514 .editor
1515 .after_eof_bg
1516 .clone()
1517 .map(|c| c.into())
1518 .unwrap_or_else(|| shade_toward_contrast(file.editor.bg.clone().into(), 10)),
1519 ruler_bg: file.editor.ruler_bg.into(),
1520 whitespace_indicator_fg: file.editor.whitespace_indicator_fg.into(),
1521 bracket_match_fg: file.editor.bracket_match_fg.into(),
1522 bracket_rainbow_1: file.editor.bracket_rainbow_1.into(),
1523 bracket_rainbow_2: file.editor.bracket_rainbow_2.into(),
1524 bracket_rainbow_3: file.editor.bracket_rainbow_3.into(),
1525 bracket_rainbow_4: file.editor.bracket_rainbow_4.into(),
1526 bracket_rainbow_5: file.editor.bracket_rainbow_5.into(),
1527 bracket_rainbow_6: file.editor.bracket_rainbow_6.into(),
1528 diff_add_bg: file.editor.diff_add_bg.clone().into(),
1529 diff_remove_bg: file.editor.diff_remove_bg.clone().into(),
1530 diff_modify_bg: file.editor.diff_modify_bg.into(),
1531 diff_add_highlight_bg: file
1533 .editor
1534 .diff_add_highlight_bg
1535 .map(|c| c.into())
1536 .unwrap_or_else(|| brighten_color(file.editor.diff_add_bg.into(), 40)),
1537 diff_remove_highlight_bg: file
1538 .editor
1539 .diff_remove_highlight_bg
1540 .map(|c| c.into())
1541 .unwrap_or_else(|| brighten_color(file.editor.diff_remove_bg.into(), 40)),
1542 diff_add_collision_fg: file.editor.diff_add_collision_fg.clone().map(|c| c.into()),
1543 diff_remove_collision_fg: file
1544 .editor
1545 .diff_remove_collision_fg
1546 .clone()
1547 .map(|c| c.into()),
1548 diff_modify_collision_fg: file
1549 .editor
1550 .diff_modify_collision_fg
1551 .clone()
1552 .map(|c| c.into()),
1553 tab_active_fg: file.ui.tab_active_fg.into(),
1554 tab_active_bg: file.ui.tab_active_bg.into(),
1555 tab_inactive_fg: file.ui.tab_inactive_fg.into(),
1556 tab_inactive_bg: file.ui.tab_inactive_bg.into(),
1557 tab_separator_bg: file.ui.tab_separator_bg.into(),
1558 tab_close_hover_fg: file.ui.tab_close_hover_fg.into(),
1559 tab_hover_bg: file.ui.tab_hover_bg.into(),
1560 menu_bg: file.ui.menu_bg.into(),
1561 menu_fg: file.ui.menu_fg.into(),
1562 menu_active_bg: file.ui.menu_active_bg.into(),
1563 menu_active_fg: file.ui.menu_active_fg.into(),
1564 menu_dropdown_bg: file.ui.menu_dropdown_bg.into(),
1565 menu_dropdown_fg: file.ui.menu_dropdown_fg.into(),
1566 menu_highlight_bg: file.ui.menu_highlight_bg.into(),
1567 menu_highlight_fg: file.ui.menu_highlight_fg.into(),
1568 menu_border_fg: file.ui.menu_border_fg.into(),
1569 menu_separator_fg: file.ui.menu_separator_fg.into(),
1570 menu_hover_bg: file.ui.menu_hover_bg.into(),
1571 menu_hover_fg: file.ui.menu_hover_fg.into(),
1572 menu_disabled_fg: file.ui.menu_disabled_fg.into(),
1573 menu_disabled_bg: file.ui.menu_disabled_bg.into(),
1574 status_bar_fg: file.ui.status_bar_fg.clone().into(),
1575 status_bar_bg: file.ui.status_bar_bg.clone().into(),
1576 status_palette_fg: file
1577 .ui
1578 .status_palette_fg
1579 .clone()
1580 .map(|c| c.into())
1581 .unwrap_or_else(|| file.ui.status_bar_fg.clone().into()),
1582 status_palette_bg: file
1583 .ui
1584 .status_palette_bg
1585 .clone()
1586 .map(|c| c.into())
1587 .unwrap_or_else(|| file.ui.status_bar_bg.clone().into()),
1588 status_separator_fg: file
1589 .ui
1590 .status_separator_fg
1591 .clone()
1592 .map(|c| c.into())
1593 .unwrap_or_else(|| file.ui.status_bar_fg.clone().into()),
1594 status_separator_bg: file
1595 .ui
1596 .status_separator_bg
1597 .clone()
1598 .map(|c| c.into())
1599 .unwrap_or_else(|| file.ui.status_bar_bg.clone().into()),
1600 status_lsp_on_fg: file
1601 .ui
1602 .status_lsp_on_fg
1603 .clone()
1604 .map(|c| c.into())
1605 .unwrap_or_else(|| file.ui.status_bar_fg.clone().into()),
1606 status_lsp_on_bg: file
1607 .ui
1608 .status_lsp_on_bg
1609 .clone()
1610 .map(|c| c.into())
1611 .unwrap_or_else(|| file.ui.status_bar_bg.clone().into()),
1612 status_lsp_actionable_fg: file
1613 .ui
1614 .status_lsp_actionable_fg
1615 .clone()
1616 .map(|c| c.into())
1617 .unwrap_or_else(|| file.ui.status_warning_indicator_fg.clone().into()),
1618 status_lsp_actionable_bg: file
1619 .ui
1620 .status_lsp_actionable_bg
1621 .clone()
1622 .map(|c| c.into())
1623 .unwrap_or_else(|| file.ui.status_warning_indicator_bg.clone().into()),
1624 prompt_fg: file.ui.prompt_fg.into(),
1625 prompt_bg: file.ui.prompt_bg.into(),
1626 prompt_selection_fg: file.ui.prompt_selection_fg.into(),
1627 prompt_selection_bg: file.ui.prompt_selection_bg.into(),
1628 popup_border_fg: file.ui.popup_border_fg.into(),
1629 popup_bg: file.ui.popup_bg.into(),
1630 popup_selection_bg: file.ui.popup_selection_bg.into(),
1631 popup_selection_fg: file.ui.popup_selection_fg.into(),
1632 popup_text_fg: file.ui.popup_text_fg.clone().into(),
1633 text_input_selection_bg: file.ui.text_input_selection_bg.into(),
1634 suggestion_bg: file.ui.suggestion_bg.into(),
1635 suggestion_fg: file
1636 .ui
1637 .suggestion_fg
1638 .clone()
1639 .map(|c| c.into())
1640 .unwrap_or_else(|| file.ui.popup_text_fg.clone().into()),
1641 suggestion_selected_bg: file.ui.suggestion_selected_bg.into(),
1642 help_bg: file.ui.help_bg.into(),
1643 help_fg: file.ui.help_fg.into(),
1644 help_key_fg: file.ui.help_key_fg.into(),
1645 help_separator_fg: file.ui.help_separator_fg.into(),
1646 help_indicator_fg: file.ui.help_indicator_fg.into(),
1647 help_indicator_bg: file.ui.help_indicator_bg.into(),
1648 inline_code_bg: file.ui.inline_code_bg.into(),
1649 split_separator_fg: file.ui.split_separator_fg.into(),
1650 split_separator_hover_fg: file.ui.split_separator_hover_fg.into(),
1651 scrollbar_track_fg: file.ui.scrollbar_track_fg.into(),
1652 scrollbar_thumb_fg: file.ui.scrollbar_thumb_fg.into(),
1653 scrollbar_track_hover_fg: file.ui.scrollbar_track_hover_fg.into(),
1654 scrollbar_thumb_hover_fg: file.ui.scrollbar_thumb_hover_fg.into(),
1655 compose_margin_bg: file.ui.compose_margin_bg.into(),
1656 semantic_highlight_bg: file.ui.semantic_highlight_bg.into(),
1657 semantic_highlight_modifier: file
1658 .ui
1659 .semantic_highlight_modifier
1660 .as_ref()
1661 .map(Modifier::from)
1662 .unwrap_or(Modifier::empty()),
1663 terminal_bg: file.ui.terminal_bg.into(),
1664 terminal_fg: file.ui.terminal_fg.into(),
1665 status_warning_indicator_bg: file.ui.status_warning_indicator_bg.into(),
1666 status_warning_indicator_fg: file.ui.status_warning_indicator_fg.into(),
1667 status_error_indicator_bg: file.ui.status_error_indicator_bg.into(),
1668 status_error_indicator_fg: file.ui.status_error_indicator_fg.into(),
1669 status_warning_indicator_hover_bg: file.ui.status_warning_indicator_hover_bg.into(),
1670 status_warning_indicator_hover_fg: file.ui.status_warning_indicator_hover_fg.into(),
1671 status_error_indicator_hover_bg: file.ui.status_error_indicator_hover_bg.into(),
1672 status_error_indicator_hover_fg: file.ui.status_error_indicator_hover_fg.into(),
1673 tab_drop_zone_bg: file.ui.tab_drop_zone_bg.into(),
1674 tab_drop_zone_border: file.ui.tab_drop_zone_border.into(),
1675 settings_selected_bg: file.ui.settings_selected_bg.into(),
1676 settings_selected_fg: file.ui.settings_selected_fg.into(),
1677 file_status_added_fg: file
1678 .ui
1679 .file_status_added_fg
1680 .map(|c| c.into())
1681 .unwrap_or_else(|| file.diagnostic.info_fg.clone().into()),
1682 file_status_modified_fg: file
1683 .ui
1684 .file_status_modified_fg
1685 .map(|c| c.into())
1686 .unwrap_or_else(|| file.diagnostic.warning_fg.clone().into()),
1687 file_status_deleted_fg: file
1688 .ui
1689 .file_status_deleted_fg
1690 .map(|c| c.into())
1691 .unwrap_or_else(|| file.diagnostic.error_fg.clone().into()),
1692 file_status_renamed_fg: file
1693 .ui
1694 .file_status_renamed_fg
1695 .map(|c| c.into())
1696 .unwrap_or_else(|| file.diagnostic.info_fg.clone().into()),
1697 file_status_untracked_fg: file
1698 .ui
1699 .file_status_untracked_fg
1700 .map(|c| c.into())
1701 .unwrap_or_else(|| file.diagnostic.hint_fg.clone().into()),
1702 file_status_conflicted_fg: file
1703 .ui
1704 .file_status_conflicted_fg
1705 .map(|c| c.into())
1706 .unwrap_or_else(|| file.diagnostic.error_fg.clone().into()),
1707 search_match_bg: file.search.match_bg.into(),
1708 search_match_fg: file.search.match_fg.into(),
1709 search_label_bg: file.search.label_bg.into(),
1710 search_label_fg: file.search.label_fg.into(),
1711 diagnostic_error_fg: file.diagnostic.error_fg.into(),
1712 diagnostic_error_bg: file.diagnostic.error_bg.into(),
1713 diagnostic_warning_fg: file.diagnostic.warning_fg.into(),
1714 diagnostic_warning_bg: file.diagnostic.warning_bg.into(),
1715 diagnostic_info_fg: file.diagnostic.info_fg.into(),
1716 diagnostic_info_bg: file.diagnostic.info_bg.into(),
1717 diagnostic_hint_fg: file.diagnostic.hint_fg.into(),
1718 diagnostic_hint_bg: file.diagnostic.hint_bg.into(),
1719 syntax_keyword: file.syntax.keyword.into(),
1720 syntax_string: file.syntax.string.into(),
1721 syntax_comment: file.syntax.comment.into(),
1722 syntax_function: file.syntax.function.into(),
1723 syntax_type: file.syntax.type_.into(),
1724 syntax_variable: file.syntax.variable.into(),
1725 syntax_variable_builtin: file.syntax.variable_builtin.into(),
1726 syntax_constant: file.syntax.constant.into(),
1727 syntax_operator: file.syntax.operator.into(),
1728 syntax_punctuation_bracket: file.syntax.punctuation_bracket.into(),
1729 syntax_punctuation_delimiter: file.syntax.punctuation_delimiter.into(),
1730 }
1731 }
1732}
1733
1734impl From<Theme> for ThemeFile {
1735 fn from(theme: Theme) -> Self {
1736 Self {
1737 name: theme.name,
1738 extends: None,
1741 editor: EditorColors {
1742 bg: theme.editor_bg.into(),
1743 fg: theme.editor_fg.into(),
1744 cursor: theme.cursor.into(),
1745 inactive_cursor: theme.inactive_cursor.into(),
1746 selection_bg: theme.selection_bg.into(),
1747 selection_modifier: if theme.selection_modifier.is_empty() {
1748 None
1749 } else {
1750 Some(theme.selection_modifier.into())
1751 },
1752 current_line_bg: theme.current_line_bg.into(),
1753 line_number_fg: theme.line_number_fg.into(),
1754 line_number_bg: theme.line_number_bg.into(),
1755 diff_add_bg: theme.diff_add_bg.into(),
1756 diff_remove_bg: theme.diff_remove_bg.into(),
1757 diff_add_highlight_bg: Some(theme.diff_add_highlight_bg.into()),
1758 diff_remove_highlight_bg: Some(theme.diff_remove_highlight_bg.into()),
1759 diff_modify_bg: theme.diff_modify_bg.into(),
1760 diff_add_collision_fg: theme.diff_add_collision_fg.map(|c| c.into()),
1761 diff_remove_collision_fg: theme.diff_remove_collision_fg.map(|c| c.into()),
1762 diff_modify_collision_fg: theme.diff_modify_collision_fg.map(|c| c.into()),
1763 ruler_bg: theme.ruler_bg.into(),
1764 whitespace_indicator_fg: theme.whitespace_indicator_fg.into(),
1765 bracket_match_fg: theme.bracket_match_fg.into(),
1766 bracket_rainbow_1: theme.bracket_rainbow_1.into(),
1767 bracket_rainbow_2: theme.bracket_rainbow_2.into(),
1768 bracket_rainbow_3: theme.bracket_rainbow_3.into(),
1769 bracket_rainbow_4: theme.bracket_rainbow_4.into(),
1770 bracket_rainbow_5: theme.bracket_rainbow_5.into(),
1771 bracket_rainbow_6: theme.bracket_rainbow_6.into(),
1772 after_eof_bg: Some(theme.after_eof_bg.into()),
1773 },
1774 ui: UiColors {
1775 tab_active_fg: theme.tab_active_fg.into(),
1776 tab_active_bg: theme.tab_active_bg.into(),
1777 tab_inactive_fg: theme.tab_inactive_fg.into(),
1778 tab_inactive_bg: theme.tab_inactive_bg.into(),
1779 tab_separator_bg: theme.tab_separator_bg.into(),
1780 tab_close_hover_fg: theme.tab_close_hover_fg.into(),
1781 tab_hover_bg: theme.tab_hover_bg.into(),
1782 menu_bg: theme.menu_bg.into(),
1783 menu_fg: theme.menu_fg.into(),
1784 menu_active_bg: theme.menu_active_bg.into(),
1785 menu_active_fg: theme.menu_active_fg.into(),
1786 menu_dropdown_bg: theme.menu_dropdown_bg.into(),
1787 menu_dropdown_fg: theme.menu_dropdown_fg.into(),
1788 menu_highlight_bg: theme.menu_highlight_bg.into(),
1789 menu_highlight_fg: theme.menu_highlight_fg.into(),
1790 menu_border_fg: theme.menu_border_fg.into(),
1791 menu_separator_fg: theme.menu_separator_fg.into(),
1792 menu_hover_bg: theme.menu_hover_bg.into(),
1793 menu_hover_fg: theme.menu_hover_fg.into(),
1794 menu_disabled_fg: theme.menu_disabled_fg.into(),
1795 menu_disabled_bg: theme.menu_disabled_bg.into(),
1796 status_bar_fg: theme.status_bar_fg.into(),
1797 status_bar_bg: theme.status_bar_bg.into(),
1798 status_palette_fg: Some(theme.status_palette_fg.into()),
1799 status_palette_bg: Some(theme.status_palette_bg.into()),
1800 status_separator_fg: Some(theme.status_separator_fg.into()),
1801 status_separator_bg: Some(theme.status_separator_bg.into()),
1802 status_lsp_on_fg: Some(theme.status_lsp_on_fg.into()),
1803 status_lsp_on_bg: Some(theme.status_lsp_on_bg.into()),
1804 status_lsp_actionable_fg: Some(theme.status_lsp_actionable_fg.into()),
1805 status_lsp_actionable_bg: Some(theme.status_lsp_actionable_bg.into()),
1806 prompt_fg: theme.prompt_fg.into(),
1807 prompt_bg: theme.prompt_bg.into(),
1808 prompt_selection_fg: theme.prompt_selection_fg.into(),
1809 prompt_selection_bg: theme.prompt_selection_bg.into(),
1810 popup_border_fg: theme.popup_border_fg.into(),
1811 popup_bg: theme.popup_bg.into(),
1812 popup_selection_bg: theme.popup_selection_bg.into(),
1813 popup_selection_fg: theme.popup_selection_fg.into(),
1814 popup_text_fg: theme.popup_text_fg.into(),
1815 text_input_selection_bg: theme.text_input_selection_bg.into(),
1816 suggestion_bg: theme.suggestion_bg.into(),
1817 suggestion_fg: Some(theme.suggestion_fg.into()),
1818 suggestion_selected_bg: theme.suggestion_selected_bg.into(),
1819 help_bg: theme.help_bg.into(),
1820 help_fg: theme.help_fg.into(),
1821 help_key_fg: theme.help_key_fg.into(),
1822 help_separator_fg: theme.help_separator_fg.into(),
1823 help_indicator_fg: theme.help_indicator_fg.into(),
1824 help_indicator_bg: theme.help_indicator_bg.into(),
1825 inline_code_bg: theme.inline_code_bg.into(),
1826 split_separator_fg: theme.split_separator_fg.into(),
1827 split_separator_hover_fg: theme.split_separator_hover_fg.into(),
1828 scrollbar_track_fg: theme.scrollbar_track_fg.into(),
1829 scrollbar_thumb_fg: theme.scrollbar_thumb_fg.into(),
1830 scrollbar_track_hover_fg: theme.scrollbar_track_hover_fg.into(),
1831 scrollbar_thumb_hover_fg: theme.scrollbar_thumb_hover_fg.into(),
1832 compose_margin_bg: theme.compose_margin_bg.into(),
1833 semantic_highlight_bg: theme.semantic_highlight_bg.into(),
1834 semantic_highlight_modifier: if theme.semantic_highlight_modifier.is_empty() {
1835 None
1836 } else {
1837 Some(theme.semantic_highlight_modifier.into())
1838 },
1839 terminal_bg: theme.terminal_bg.into(),
1840 terminal_fg: theme.terminal_fg.into(),
1841 status_warning_indicator_bg: theme.status_warning_indicator_bg.into(),
1842 status_warning_indicator_fg: theme.status_warning_indicator_fg.into(),
1843 status_error_indicator_bg: theme.status_error_indicator_bg.into(),
1844 status_error_indicator_fg: theme.status_error_indicator_fg.into(),
1845 status_warning_indicator_hover_bg: theme.status_warning_indicator_hover_bg.into(),
1846 status_warning_indicator_hover_fg: theme.status_warning_indicator_hover_fg.into(),
1847 status_error_indicator_hover_bg: theme.status_error_indicator_hover_bg.into(),
1848 status_error_indicator_hover_fg: theme.status_error_indicator_hover_fg.into(),
1849 tab_drop_zone_bg: theme.tab_drop_zone_bg.into(),
1850 tab_drop_zone_border: theme.tab_drop_zone_border.into(),
1851 settings_selected_bg: theme.settings_selected_bg.into(),
1852 settings_selected_fg: theme.settings_selected_fg.into(),
1853 file_status_added_fg: Some(theme.file_status_added_fg.into()),
1854 file_status_modified_fg: Some(theme.file_status_modified_fg.into()),
1855 file_status_deleted_fg: Some(theme.file_status_deleted_fg.into()),
1856 file_status_renamed_fg: Some(theme.file_status_renamed_fg.into()),
1857 file_status_untracked_fg: Some(theme.file_status_untracked_fg.into()),
1858 file_status_conflicted_fg: Some(theme.file_status_conflicted_fg.into()),
1859 },
1860 search: SearchColors {
1861 match_bg: theme.search_match_bg.into(),
1862 match_fg: theme.search_match_fg.into(),
1863 label_bg: theme.search_label_bg.into(),
1864 label_fg: theme.search_label_fg.into(),
1865 },
1866 diagnostic: DiagnosticColors {
1867 error_fg: theme.diagnostic_error_fg.into(),
1868 error_bg: theme.diagnostic_error_bg.into(),
1869 warning_fg: theme.diagnostic_warning_fg.into(),
1870 warning_bg: theme.diagnostic_warning_bg.into(),
1871 info_fg: theme.diagnostic_info_fg.into(),
1872 info_bg: theme.diagnostic_info_bg.into(),
1873 hint_fg: theme.diagnostic_hint_fg.into(),
1874 hint_bg: theme.diagnostic_hint_bg.into(),
1875 },
1876 syntax: SyntaxColors {
1877 keyword: theme.syntax_keyword.into(),
1878 string: theme.syntax_string.into(),
1879 comment: theme.syntax_comment.into(),
1880 function: theme.syntax_function.into(),
1881 type_: theme.syntax_type.into(),
1882 variable: theme.syntax_variable.into(),
1883 variable_builtin: theme.syntax_variable_builtin.into(),
1884 constant: theme.syntax_constant.into(),
1885 operator: theme.syntax_operator.into(),
1886 punctuation_bracket: theme.syntax_punctuation_bracket.into(),
1887 punctuation_delimiter: theme.syntax_punctuation_delimiter.into(),
1888 },
1889 }
1890 }
1891}
1892
1893fn resolve_base_theme(theme_file: &ThemeFile, raw: &serde_json::Value) -> Result<Theme, String> {
1900 if let Some(extends) = theme_file.extends.as_deref() {
1902 let name = extends.strip_prefix("builtin://").unwrap_or(extends);
1903 return Theme::load_builtin(name).ok_or_else(|| {
1904 let available: Vec<&str> = BUILTIN_THEMES.iter().map(|t| t.name).collect();
1905 format!(
1906 "theme `extends: {:?}` does not match any built-in theme. \
1907 Available: {}. \
1908 Inheriting from other user themes is not yet supported.",
1909 extends,
1910 available.join(", ")
1911 )
1912 });
1913 }
1914
1915 if let Some(bg) = raw
1921 .get("editor")
1922 .and_then(|e| e.get("bg"))
1923 .cloned()
1924 .and_then(|v| serde_json::from_value::<ColorDef>(v).ok())
1925 {
1926 let color: Color = bg.into();
1927 if let Some((r, g, b)) = color_to_rgb(color) {
1928 let lum = relative_luminance(r, g, b);
1929 let base_name = if lum > 0.5 { THEME_LIGHT } else { THEME_DARK };
1930 if let Some(base) = Theme::load_builtin(base_name) {
1931 return Ok(base);
1932 }
1933 }
1934 }
1935
1936 Ok(theme_file.clone().into())
1938}
1939
1940fn relative_luminance(r: u8, g: u8, b: u8) -> f64 {
1943 0.2126 * (r as f64 / 255.0) + 0.7152 * (g as f64 / 255.0) + 0.0722 * (b as f64 / 255.0)
1944}
1945
1946fn apply_theme_overrides(theme: &mut Theme, theme_file: &ThemeFile, raw: &serde_json::Value) {
1951 theme.name = theme_file.name.clone();
1953
1954 for section in ["editor", "ui", "search", "diagnostic", "syntax"] {
1955 let Some(obj) = raw.get(section).and_then(|v| v.as_object()) else {
1956 continue;
1957 };
1958 for (field, value) in obj {
1959 if value.is_null() {
1962 continue;
1963 }
1964 let key = format!("{}.{}", section, field);
1965 if let Ok(color_def) = serde_json::from_value::<ColorDef>(value.clone()) {
1966 if let Some(slot) = theme.resolve_theme_key_mut(&key) {
1967 *slot = color_def.into();
1968 }
1969 }
1970 }
1971 }
1972}
1973
1974impl Theme {
1975 pub fn is_light(&self) -> bool {
1981 color_to_rgb(self.editor_bg)
1982 .map(|(r, g, b)| relative_luminance(r, g, b) > 0.5)
1983 .unwrap_or(false)
1984 }
1985
1986 pub fn load_builtin(name: &str) -> Option<Self> {
1988 BUILTIN_THEMES
1989 .iter()
1990 .find(|t| t.name == name)
1991 .and_then(|t| serde_json::from_str::<ThemeFile>(t.json).ok())
1992 .map(|tf| tf.into())
1993 }
1994
1995 pub fn from_json(json: &str) -> Result<Self, String> {
2005 let raw: serde_json::Value =
2010 serde_json::from_str(json).map_err(|e| format!("Failed to parse theme JSON: {}", e))?;
2011 let theme_file: ThemeFile = serde_json::from_value(raw.clone())
2012 .map_err(|e| format!("Failed to parse theme: {}", e))?;
2013
2014 let mut theme = resolve_base_theme(&theme_file, &raw)?;
2015 apply_theme_overrides(&mut theme, &theme_file, &raw);
2016 Ok(theme)
2017 }
2018
2019 pub fn modifier_for_bg_key(&self, key: &str) -> Modifier {
2028 match key {
2029 "editor.selection_bg" => self.selection_modifier,
2030 "ui.semantic_highlight_bg" => self.semantic_highlight_modifier,
2031 _ => Modifier::empty(),
2032 }
2033 }
2034
2035 pub fn resolve_theme_key(&self, key: &str) -> Option<Color> {
2046 let parts: Vec<&str> = key.split('.').collect();
2048 if parts.len() != 2 {
2049 return None;
2050 }
2051
2052 let (section, field) = (parts[0], parts[1]);
2053
2054 match section {
2055 "editor" => match field {
2056 "after_eof_bg" => Some(self.after_eof_bg),
2057 "bg" => Some(self.editor_bg),
2058 "current_line_bg" => Some(self.current_line_bg),
2059 "cursor" => Some(self.cursor),
2060 "diff_add_bg" => Some(self.diff_add_bg),
2061 "diff_add_collision_fg" => self.diff_add_collision_fg,
2062 "diff_add_highlight_bg" => Some(self.diff_add_highlight_bg),
2063 "diff_modify_bg" => Some(self.diff_modify_bg),
2064 "diff_modify_collision_fg" => self.diff_modify_collision_fg,
2065 "diff_remove_bg" => Some(self.diff_remove_bg),
2066 "diff_remove_collision_fg" => self.diff_remove_collision_fg,
2067 "diff_remove_highlight_bg" => Some(self.diff_remove_highlight_bg),
2068 "fg" => Some(self.editor_fg),
2069 "inactive_cursor" => Some(self.inactive_cursor),
2070 "line_number_bg" => Some(self.line_number_bg),
2071 "line_number_fg" => Some(self.line_number_fg),
2072 "ruler_bg" => Some(self.ruler_bg),
2073 "selection_bg" => Some(self.selection_bg),
2074 "whitespace_indicator_fg" => Some(self.whitespace_indicator_fg),
2075 "bracket_match_fg" => Some(self.bracket_match_fg),
2076 "bracket_rainbow_1" => Some(self.bracket_rainbow_1),
2077 "bracket_rainbow_2" => Some(self.bracket_rainbow_2),
2078 "bracket_rainbow_3" => Some(self.bracket_rainbow_3),
2079 "bracket_rainbow_4" => Some(self.bracket_rainbow_4),
2080 "bracket_rainbow_5" => Some(self.bracket_rainbow_5),
2081 "bracket_rainbow_6" => Some(self.bracket_rainbow_6),
2082 _ => None,
2083 },
2084 "ui" => match field {
2085 "compose_margin_bg" => Some(self.compose_margin_bg),
2086 "file_status_added_fg" => Some(self.file_status_added_fg),
2087 "file_status_conflicted_fg" => Some(self.file_status_conflicted_fg),
2088 "file_status_deleted_fg" => Some(self.file_status_deleted_fg),
2089 "file_status_modified_fg" => Some(self.file_status_modified_fg),
2090 "file_status_renamed_fg" => Some(self.file_status_renamed_fg),
2091 "file_status_untracked_fg" => Some(self.file_status_untracked_fg),
2092 "help_bg" => Some(self.help_bg),
2093 "help_fg" => Some(self.help_fg),
2094 "help_indicator_bg" => Some(self.help_indicator_bg),
2095 "help_indicator_fg" => Some(self.help_indicator_fg),
2096 "help_key_fg" => Some(self.help_key_fg),
2097 "help_separator_fg" => Some(self.help_separator_fg),
2098 "inline_code_bg" => Some(self.inline_code_bg),
2099 "menu_active_bg" => Some(self.menu_active_bg),
2100 "menu_active_fg" => Some(self.menu_active_fg),
2101 "menu_bg" => Some(self.menu_bg),
2102 "menu_border_fg" => Some(self.menu_border_fg),
2103 "menu_disabled_bg" => Some(self.menu_disabled_bg),
2104 "menu_disabled_fg" => Some(self.menu_disabled_fg),
2105 "menu_dropdown_bg" => Some(self.menu_dropdown_bg),
2106 "menu_dropdown_fg" => Some(self.menu_dropdown_fg),
2107 "menu_fg" => Some(self.menu_fg),
2108 "menu_highlight_bg" => Some(self.menu_highlight_bg),
2109 "menu_highlight_fg" => Some(self.menu_highlight_fg),
2110 "menu_hover_bg" => Some(self.menu_hover_bg),
2111 "menu_hover_fg" => Some(self.menu_hover_fg),
2112 "menu_separator_fg" => Some(self.menu_separator_fg),
2113 "popup_bg" => Some(self.popup_bg),
2114 "popup_border_fg" => Some(self.popup_border_fg),
2115 "popup_selection_bg" => Some(self.popup_selection_bg),
2116 "popup_selection_fg" => Some(self.popup_selection_fg),
2117 "popup_text_fg" => Some(self.popup_text_fg),
2118 "prompt_bg" => Some(self.prompt_bg),
2119 "prompt_fg" => Some(self.prompt_fg),
2120 "prompt_selection_bg" => Some(self.prompt_selection_bg),
2121 "prompt_selection_fg" => Some(self.prompt_selection_fg),
2122 "scrollbar_thumb_fg" => Some(self.scrollbar_thumb_fg),
2123 "scrollbar_thumb_hover_fg" => Some(self.scrollbar_thumb_hover_fg),
2124 "scrollbar_track_fg" => Some(self.scrollbar_track_fg),
2125 "scrollbar_track_hover_fg" => Some(self.scrollbar_track_hover_fg),
2126 "semantic_highlight_bg" => Some(self.semantic_highlight_bg),
2127 "settings_selected_bg" => Some(self.settings_selected_bg),
2128 "settings_selected_fg" => Some(self.settings_selected_fg),
2129 "split_separator_fg" => Some(self.split_separator_fg),
2130 "split_separator_hover_fg" => Some(self.split_separator_hover_fg),
2131 "status_bar_bg" => Some(self.status_bar_bg),
2132 "status_bar_fg" => Some(self.status_bar_fg),
2133 "status_error_indicator_bg" => Some(self.status_error_indicator_bg),
2134 "status_error_indicator_fg" => Some(self.status_error_indicator_fg),
2135 "status_error_indicator_hover_bg" => Some(self.status_error_indicator_hover_bg),
2136 "status_error_indicator_hover_fg" => Some(self.status_error_indicator_hover_fg),
2137 "status_lsp_actionable_bg" => Some(self.status_lsp_actionable_bg),
2138 "status_lsp_actionable_fg" => Some(self.status_lsp_actionable_fg),
2139 "status_lsp_on_bg" => Some(self.status_lsp_on_bg),
2140 "status_lsp_on_fg" => Some(self.status_lsp_on_fg),
2141 "status_palette_bg" => Some(self.status_palette_bg),
2142 "status_palette_fg" => Some(self.status_palette_fg),
2143 "status_separator_bg" => Some(self.status_separator_bg),
2144 "status_separator_fg" => Some(self.status_separator_fg),
2145 "status_warning_indicator_bg" => Some(self.status_warning_indicator_bg),
2146 "status_warning_indicator_fg" => Some(self.status_warning_indicator_fg),
2147 "status_warning_indicator_hover_bg" => Some(self.status_warning_indicator_hover_bg),
2148 "status_warning_indicator_hover_fg" => Some(self.status_warning_indicator_hover_fg),
2149 "suggestion_bg" => Some(self.suggestion_bg),
2150 "suggestion_fg" => Some(self.suggestion_fg),
2151 "suggestion_selected_bg" => Some(self.suggestion_selected_bg),
2152 "tab_active_bg" => Some(self.tab_active_bg),
2153 "tab_active_fg" => Some(self.tab_active_fg),
2154 "tab_close_hover_fg" => Some(self.tab_close_hover_fg),
2155 "tab_drop_zone_bg" => Some(self.tab_drop_zone_bg),
2156 "tab_drop_zone_border" => Some(self.tab_drop_zone_border),
2157 "tab_hover_bg" => Some(self.tab_hover_bg),
2158 "tab_inactive_bg" => Some(self.tab_inactive_bg),
2159 "tab_inactive_fg" => Some(self.tab_inactive_fg),
2160 "tab_separator_bg" => Some(self.tab_separator_bg),
2161 "terminal_bg" => Some(self.terminal_bg),
2162 "terminal_fg" => Some(self.terminal_fg),
2163 "text_input_selection_bg" => Some(self.text_input_selection_bg),
2164 _ => None,
2165 },
2166 "syntax" => match field {
2167 "comment" => Some(self.syntax_comment),
2168 "constant" => Some(self.syntax_constant),
2169 "function" => Some(self.syntax_function),
2170 "keyword" => Some(self.syntax_keyword),
2171 "operator" => Some(self.syntax_operator),
2172 "punctuation_bracket" => Some(self.syntax_punctuation_bracket),
2173 "punctuation_delimiter" => Some(self.syntax_punctuation_delimiter),
2174 "string" => Some(self.syntax_string),
2175 "type" => Some(self.syntax_type),
2176 "variable" => Some(self.syntax_variable),
2177 "variable_builtin" => Some(self.syntax_variable_builtin),
2178 _ => None,
2179 },
2180 "diagnostic" => match field {
2181 "error_bg" => Some(self.diagnostic_error_bg),
2182 "error_fg" => Some(self.diagnostic_error_fg),
2183 "hint_bg" => Some(self.diagnostic_hint_bg),
2184 "hint_fg" => Some(self.diagnostic_hint_fg),
2185 "info_bg" => Some(self.diagnostic_info_bg),
2186 "info_fg" => Some(self.diagnostic_info_fg),
2187 "warning_bg" => Some(self.diagnostic_warning_bg),
2188 "warning_fg" => Some(self.diagnostic_warning_fg),
2189 _ => None,
2190 },
2191 "search" => match field {
2192 "label_bg" => Some(self.search_label_bg),
2193 "label_fg" => Some(self.search_label_fg),
2194 "match_bg" => Some(self.search_match_bg),
2195 "match_fg" => Some(self.search_match_fg),
2196 _ => None,
2197 },
2198 _ => None,
2199 }
2200 }
2201
2202 pub fn resolve_theme_key_mut(&mut self, key: &str) -> Option<&mut Color> {
2206 let parts: Vec<&str> = key.split('.').collect();
2207 if parts.len() != 2 {
2208 return None;
2209 }
2210 let (section, field) = (parts[0], parts[1]);
2211 match section {
2212 "editor" => match field {
2213 "bg" => Some(&mut self.editor_bg),
2214 "fg" => Some(&mut self.editor_fg),
2215 "cursor" => Some(&mut self.cursor),
2216 "inactive_cursor" => Some(&mut self.inactive_cursor),
2217 "selection_bg" => Some(&mut self.selection_bg),
2218 "current_line_bg" => Some(&mut self.current_line_bg),
2219 "line_number_fg" => Some(&mut self.line_number_fg),
2220 "line_number_bg" => Some(&mut self.line_number_bg),
2221 "diff_add_bg" => Some(&mut self.diff_add_bg),
2222 "diff_remove_bg" => Some(&mut self.diff_remove_bg),
2223 "diff_modify_bg" => Some(&mut self.diff_modify_bg),
2224 "diff_add_collision_fg" => self.diff_add_collision_fg.as_mut(),
2228 "diff_remove_collision_fg" => self.diff_remove_collision_fg.as_mut(),
2229 "diff_modify_collision_fg" => self.diff_modify_collision_fg.as_mut(),
2230 "ruler_bg" => Some(&mut self.ruler_bg),
2231 "whitespace_indicator_fg" => Some(&mut self.whitespace_indicator_fg),
2232 "bracket_match_fg" => Some(&mut self.bracket_match_fg),
2233 "bracket_rainbow_1" => Some(&mut self.bracket_rainbow_1),
2234 "bracket_rainbow_2" => Some(&mut self.bracket_rainbow_2),
2235 "bracket_rainbow_3" => Some(&mut self.bracket_rainbow_3),
2236 "bracket_rainbow_4" => Some(&mut self.bracket_rainbow_4),
2237 "bracket_rainbow_5" => Some(&mut self.bracket_rainbow_5),
2238 "bracket_rainbow_6" => Some(&mut self.bracket_rainbow_6),
2239 "diff_add_highlight_bg" => Some(&mut self.diff_add_highlight_bg),
2240 "diff_remove_highlight_bg" => Some(&mut self.diff_remove_highlight_bg),
2241 "after_eof_bg" => Some(&mut self.after_eof_bg),
2242 _ => None,
2243 },
2244 "ui" => match field {
2245 "tab_active_fg" => Some(&mut self.tab_active_fg),
2246 "tab_active_bg" => Some(&mut self.tab_active_bg),
2247 "tab_inactive_fg" => Some(&mut self.tab_inactive_fg),
2248 "tab_inactive_bg" => Some(&mut self.tab_inactive_bg),
2249 "status_bar_fg" => Some(&mut self.status_bar_fg),
2250 "status_bar_bg" => Some(&mut self.status_bar_bg),
2251 "status_palette_fg" => Some(&mut self.status_palette_fg),
2252 "status_palette_bg" => Some(&mut self.status_palette_bg),
2253 "status_separator_fg" => Some(&mut self.status_separator_fg),
2254 "status_separator_bg" => Some(&mut self.status_separator_bg),
2255 "status_lsp_on_fg" => Some(&mut self.status_lsp_on_fg),
2256 "status_lsp_on_bg" => Some(&mut self.status_lsp_on_bg),
2257 "status_lsp_actionable_fg" => Some(&mut self.status_lsp_actionable_fg),
2258 "status_lsp_actionable_bg" => Some(&mut self.status_lsp_actionable_bg),
2259 "prompt_fg" => Some(&mut self.prompt_fg),
2260 "prompt_bg" => Some(&mut self.prompt_bg),
2261 "prompt_selection_fg" => Some(&mut self.prompt_selection_fg),
2262 "prompt_selection_bg" => Some(&mut self.prompt_selection_bg),
2263 "popup_bg" => Some(&mut self.popup_bg),
2264 "popup_border_fg" => Some(&mut self.popup_border_fg),
2265 "popup_selection_bg" => Some(&mut self.popup_selection_bg),
2266 "popup_selection_fg" => Some(&mut self.popup_selection_fg),
2267 "popup_text_fg" => Some(&mut self.popup_text_fg),
2268 "text_input_selection_bg" => Some(&mut self.text_input_selection_bg),
2269 "menu_bg" => Some(&mut self.menu_bg),
2270 "menu_fg" => Some(&mut self.menu_fg),
2271 "menu_active_bg" => Some(&mut self.menu_active_bg),
2272 "menu_active_fg" => Some(&mut self.menu_active_fg),
2273 "menu_disabled_fg" => Some(&mut self.menu_disabled_fg),
2274 "menu_disabled_bg" => Some(&mut self.menu_disabled_bg),
2275 "help_bg" => Some(&mut self.help_bg),
2276 "help_fg" => Some(&mut self.help_fg),
2277 "help_key_fg" => Some(&mut self.help_key_fg),
2278 "split_separator_fg" => Some(&mut self.split_separator_fg),
2279 "scrollbar_track_fg" => Some(&mut self.scrollbar_track_fg),
2280 "scrollbar_thumb_fg" => Some(&mut self.scrollbar_thumb_fg),
2281 "scrollbar_track_hover_fg" => Some(&mut self.scrollbar_track_hover_fg),
2282 "scrollbar_thumb_hover_fg" => Some(&mut self.scrollbar_thumb_hover_fg),
2283 "semantic_highlight_bg" => Some(&mut self.semantic_highlight_bg),
2284 "file_status_added_fg" => Some(&mut self.file_status_added_fg),
2285 "file_status_modified_fg" => Some(&mut self.file_status_modified_fg),
2286 "file_status_deleted_fg" => Some(&mut self.file_status_deleted_fg),
2287 "file_status_renamed_fg" => Some(&mut self.file_status_renamed_fg),
2288 "file_status_untracked_fg" => Some(&mut self.file_status_untracked_fg),
2289 "file_status_conflicted_fg" => Some(&mut self.file_status_conflicted_fg),
2290 "menu_dropdown_bg" => Some(&mut self.menu_dropdown_bg),
2291 "menu_dropdown_fg" => Some(&mut self.menu_dropdown_fg),
2292 "menu_highlight_bg" => Some(&mut self.menu_highlight_bg),
2293 "menu_highlight_fg" => Some(&mut self.menu_highlight_fg),
2294 "menu_border_fg" => Some(&mut self.menu_border_fg),
2295 "menu_separator_fg" => Some(&mut self.menu_separator_fg),
2296 "menu_hover_bg" => Some(&mut self.menu_hover_bg),
2297 "menu_hover_fg" => Some(&mut self.menu_hover_fg),
2298 "tab_separator_bg" => Some(&mut self.tab_separator_bg),
2299 "tab_close_hover_fg" => Some(&mut self.tab_close_hover_fg),
2300 "tab_hover_bg" => Some(&mut self.tab_hover_bg),
2301 "inline_code_bg" => Some(&mut self.inline_code_bg),
2302 "split_separator_hover_fg" => Some(&mut self.split_separator_hover_fg),
2303 "compose_margin_bg" => Some(&mut self.compose_margin_bg),
2304 "terminal_bg" => Some(&mut self.terminal_bg),
2305 "terminal_fg" => Some(&mut self.terminal_fg),
2306 "status_warning_indicator_bg" => Some(&mut self.status_warning_indicator_bg),
2307 "status_warning_indicator_fg" => Some(&mut self.status_warning_indicator_fg),
2308 "status_error_indicator_bg" => Some(&mut self.status_error_indicator_bg),
2309 "status_error_indicator_fg" => Some(&mut self.status_error_indicator_fg),
2310 "status_warning_indicator_hover_bg" => {
2311 Some(&mut self.status_warning_indicator_hover_bg)
2312 }
2313 "status_warning_indicator_hover_fg" => {
2314 Some(&mut self.status_warning_indicator_hover_fg)
2315 }
2316 "status_error_indicator_hover_bg" => {
2317 Some(&mut self.status_error_indicator_hover_bg)
2318 }
2319 "status_error_indicator_hover_fg" => {
2320 Some(&mut self.status_error_indicator_hover_fg)
2321 }
2322 "tab_drop_zone_bg" => Some(&mut self.tab_drop_zone_bg),
2323 "tab_drop_zone_border" => Some(&mut self.tab_drop_zone_border),
2324 "settings_selected_bg" => Some(&mut self.settings_selected_bg),
2325 "settings_selected_fg" => Some(&mut self.settings_selected_fg),
2326 "suggestion_bg" => Some(&mut self.suggestion_bg),
2327 "suggestion_fg" => Some(&mut self.suggestion_fg),
2328 "suggestion_selected_bg" => Some(&mut self.suggestion_selected_bg),
2329 "help_separator_fg" => Some(&mut self.help_separator_fg),
2330 "help_indicator_fg" => Some(&mut self.help_indicator_fg),
2331 "help_indicator_bg" => Some(&mut self.help_indicator_bg),
2332 _ => None,
2333 },
2334 "syntax" => match field {
2335 "keyword" => Some(&mut self.syntax_keyword),
2336 "string" => Some(&mut self.syntax_string),
2337 "comment" => Some(&mut self.syntax_comment),
2338 "function" => Some(&mut self.syntax_function),
2339 "type" => Some(&mut self.syntax_type),
2340 "variable" => Some(&mut self.syntax_variable),
2341 "variable_builtin" => Some(&mut self.syntax_variable_builtin),
2342 "constant" => Some(&mut self.syntax_constant),
2343 "operator" => Some(&mut self.syntax_operator),
2344 "punctuation_bracket" => Some(&mut self.syntax_punctuation_bracket),
2345 "punctuation_delimiter" => Some(&mut self.syntax_punctuation_delimiter),
2346 _ => None,
2347 },
2348 "diagnostic" => match field {
2349 "error_fg" => Some(&mut self.diagnostic_error_fg),
2350 "error_bg" => Some(&mut self.diagnostic_error_bg),
2351 "warning_fg" => Some(&mut self.diagnostic_warning_fg),
2352 "warning_bg" => Some(&mut self.diagnostic_warning_bg),
2353 "info_fg" => Some(&mut self.diagnostic_info_fg),
2354 "info_bg" => Some(&mut self.diagnostic_info_bg),
2355 "hint_fg" => Some(&mut self.diagnostic_hint_fg),
2356 "hint_bg" => Some(&mut self.diagnostic_hint_bg),
2357 _ => None,
2358 },
2359 "search" => match field {
2360 "match_bg" => Some(&mut self.search_match_bg),
2361 "match_fg" => Some(&mut self.search_match_fg),
2362 "label_bg" => Some(&mut self.search_label_bg),
2363 "label_fg" => Some(&mut self.search_label_fg),
2364 _ => None,
2365 },
2366 _ => None,
2367 }
2368 }
2369
2370 pub fn override_colors<I, K>(&mut self, overrides: I) -> usize
2375 where
2376 I: IntoIterator<Item = (K, Color)>,
2377 K: AsRef<str>,
2378 {
2379 let mut applied = 0;
2380 for (key, color) in overrides {
2381 if let Some(slot) = self.resolve_theme_key_mut(key.as_ref()) {
2382 *slot = color;
2383 applied += 1;
2384 }
2385 }
2386 applied
2387 }
2388}
2389
2390pub fn get_theme_schema() -> serde_json::Value {
2398 use schemars::schema_for;
2399 let schema = schema_for!(ThemeFile);
2400 serde_json::to_value(&schema).unwrap_or_default()
2401}
2402
2403pub fn get_builtin_themes() -> serde_json::Value {
2405 let mut map = serde_json::Map::new();
2406 for theme in BUILTIN_THEMES {
2407 map.insert(
2408 theme.name.to_string(),
2409 serde_json::Value::String(theme.json.to_string()),
2410 );
2411 }
2412 serde_json::Value::Object(map)
2413}
2414
2415#[cfg(test)]
2416mod tests {
2417 use super::*;
2418
2419 #[test]
2420 fn test_load_builtin_theme() {
2421 let dark = Theme::load_builtin(THEME_DARK).expect("Dark theme must exist");
2422 assert_eq!(dark.name, THEME_DARK);
2423
2424 let light = Theme::load_builtin(THEME_LIGHT).expect("Light theme must exist");
2425 assert_eq!(light.name, THEME_LIGHT);
2426
2427 let high_contrast =
2428 Theme::load_builtin(THEME_HIGH_CONTRAST).expect("High contrast theme must exist");
2429 assert_eq!(high_contrast.name, THEME_HIGH_CONTRAST);
2430
2431 let terminal = Theme::load_builtin(THEME_TERMINAL).expect("Terminal theme must exist");
2432 assert_eq!(terminal.name, THEME_TERMINAL);
2433 assert_eq!(terminal.editor_bg, Color::Reset);
2437 assert_eq!(terminal.editor_fg, Color::Reset);
2438 assert_eq!(terminal.terminal_bg, Color::Reset);
2439 assert!(terminal.selection_modifier.contains(Modifier::REVERSED));
2442 assert!(terminal
2443 .semantic_highlight_modifier
2444 .contains(Modifier::BOLD));
2445 }
2446
2447 #[test]
2448 fn test_suggestion_fg_falls_back_and_contrasts() {
2449 let dracula = Theme::load_builtin(THEME_DRACULA).expect("Dracula theme must exist");
2456 assert_eq!(
2457 dracula.suggestion_fg, dracula.popup_text_fg,
2458 "suggestion_fg should fall back to popup_text_fg when unset"
2459 );
2460 assert_ne!(
2461 dracula.suggestion_fg, dracula.suggestion_bg,
2462 "suggestion_fg must contrast with suggestion_bg, not vanish into it"
2463 );
2464 }
2465
2466 #[test]
2467 fn test_modifier_def_round_trip() {
2468 let cases = [
2469 (vec!["reversed"], Modifier::REVERSED),
2470 (
2471 vec!["bold", "underlined"],
2472 Modifier::BOLD | Modifier::UNDERLINED,
2473 ),
2474 (vec!["italic", "dim"], Modifier::ITALIC | Modifier::DIM),
2475 (vec!["reverse"], Modifier::REVERSED), (vec!["underline"], Modifier::UNDERLINED), ];
2478 for (strs, expected) in cases {
2479 let def = ModifierDef(strs.iter().map(|s| s.to_string()).collect());
2480 let m: Modifier = (&def).into();
2481 assert_eq!(m, expected, "ModifierDef({:?}) -> Modifier", strs);
2482 }
2483 }
2484
2485 #[test]
2486 fn test_modifier_def_unknown_strings_are_dropped() {
2487 let def = ModifierDef(vec!["reversed".into(), "wibble".into(), "bold".into()]);
2490 let m: Modifier = (&def).into();
2491 assert_eq!(m, Modifier::REVERSED | Modifier::BOLD);
2492 }
2493
2494 #[test]
2495 fn test_themes_without_modifier_default_to_empty() {
2496 let dark = Theme::load_builtin(THEME_DARK).expect("Dark theme must exist");
2501 assert!(dark.selection_modifier.is_empty());
2502 assert!(dark.semantic_highlight_modifier.is_empty());
2503 }
2504
2505 #[test]
2506 fn test_modifier_for_bg_key_lookup() {
2507 let terminal = Theme::load_builtin(THEME_TERMINAL).expect("Terminal theme must exist");
2508 assert!(terminal
2511 .modifier_for_bg_key("editor.selection_bg")
2512 .contains(Modifier::REVERSED));
2513 assert!(terminal
2514 .modifier_for_bg_key("ui.semantic_highlight_bg")
2515 .contains(Modifier::BOLD));
2516 assert!(terminal
2519 .modifier_for_bg_key("ui.popup_selection_bg")
2520 .is_empty());
2521 assert!(terminal.modifier_for_bg_key("nonsense").is_empty());
2522 }
2523
2524 #[test]
2525 fn test_modifier_round_trip_via_theme_file() {
2526 let original = Theme::load_builtin(THEME_TERMINAL).expect("Terminal theme must exist");
2528 let file: ThemeFile = original.clone().into();
2529 let json = serde_json::to_string(&file).expect("serialize");
2530 let parsed: ThemeFile = serde_json::from_str(&json).expect("parse");
2531 let round_tripped: Theme = parsed.into();
2532 assert_eq!(
2533 round_tripped.selection_modifier,
2534 original.selection_modifier
2535 );
2536 assert_eq!(
2537 round_tripped.semantic_highlight_modifier,
2538 original.semantic_highlight_modifier
2539 );
2540 }
2541
2542 #[test]
2543 fn test_builtin_themes_match_schema() {
2544 for theme in BUILTIN_THEMES {
2545 let _: ThemeFile = serde_json::from_str(theme.json)
2546 .unwrap_or_else(|_| panic!("Theme '{}' does not match schema", theme.name));
2547 }
2548 }
2549
2550 #[test]
2551 fn test_from_json() {
2552 let json = r#"{"name":"test","editor":{},"ui":{},"search":{},"diagnostic":{},"syntax":{}}"#;
2553 let theme = Theme::from_json(json).expect("Should parse minimal theme");
2554 assert_eq!(theme.name, "test");
2555 }
2556
2557 #[test]
2569 fn test_minimal_user_theme_from_issue_1281_loads() {
2570 let json = r#"{
2572 "name": "gruvbox-light-orange",
2573 "editor": {
2574 "bg": [251, 241, 199],
2575 "fg": [60, 56, 54],
2576 "cursor": [254, 128, 25],
2577 "selection_bg": [213, 196, 161]
2578 },
2579 "syntax": {
2580 "keyword": [175, 58, 3],
2581 "string": [152, 151, 26],
2582 "comment": [146, 131, 116]
2583 }
2584}"#;
2585 let theme = Theme::from_json(json)
2586 .expect("Theme from issue #1281 should parse without `ui`/`search`/`diagnostic`");
2587 assert_eq!(theme.name, "gruvbox-light-orange");
2588
2589 assert_eq!(theme.editor_bg, Color::Rgb(251, 241, 199));
2591 assert_eq!(theme.editor_fg, Color::Rgb(60, 56, 54));
2592 assert_eq!(theme.cursor, Color::Rgb(254, 128, 25));
2593 assert_eq!(theme.selection_bg, Color::Rgb(213, 196, 161));
2594 assert_eq!(theme.syntax_keyword, Color::Rgb(175, 58, 3));
2595 assert_eq!(theme.syntax_string, Color::Rgb(152, 151, 26));
2596 assert_eq!(theme.syntax_comment, Color::Rgb(146, 131, 116));
2597
2598 let light = Theme::load_builtin(THEME_LIGHT).expect("light builtin");
2602 assert_eq!(
2603 theme.status_bar_fg, light.status_bar_fg,
2604 "ui.status_bar_fg should inherit from builtin://light when bg is bright"
2605 );
2606 assert_eq!(
2607 theme.diagnostic_error_fg, light.diagnostic_error_fg,
2608 "diagnostic.error_fg should inherit from builtin://light when bg is bright"
2609 );
2610 assert_eq!(
2611 theme.menu_bg, light.menu_bg,
2612 "ui.menu_bg should inherit from builtin://light when bg is bright"
2613 );
2614 }
2615
2616 #[test]
2619 fn test_extends_explicit_builtin_wins_over_auto_infer() {
2620 let json = r#"{
2623 "name": "explicit-light",
2624 "extends": "builtin://light",
2625 "editor": { "bg": [0, 0, 0] }
2626 }"#;
2627 let theme = Theme::from_json(json).expect("extends should resolve");
2628 let light = Theme::load_builtin(THEME_LIGHT).expect("light builtin");
2629
2630 assert_eq!(theme.editor_bg, Color::Rgb(0, 0, 0));
2632 assert_eq!(theme.menu_bg, light.menu_bg);
2634 assert_eq!(theme.tab_active_bg, light.tab_active_bg);
2635 assert_eq!(theme.diagnostic_warning_fg, light.diagnostic_warning_fg);
2636 }
2637
2638 #[test]
2643 fn test_extends_bare_builtin_name_works() {
2644 let json = r#"{ "name": "x", "extends": "high-contrast" }"#;
2645 let theme = Theme::from_json(json).expect("bare-name extends should resolve");
2646 let hc = Theme::load_builtin("high-contrast").expect("hc builtin");
2647 assert_eq!(theme.menu_bg, hc.menu_bg);
2648 }
2649
2650 #[test]
2655 fn test_extends_unknown_builtin_errors_with_helpful_message() {
2656 let json = r#"{ "name": "x", "extends": "builtin://no-such-theme" }"#;
2657 let err = Theme::from_json(json).expect_err("unknown extends must error");
2658 assert!(
2659 err.contains("no-such-theme"),
2660 "error should quote the bad value, got: {}",
2661 err
2662 );
2663 assert!(
2664 err.contains("dark") && err.contains("light"),
2665 "error should list available builtins, got: {}",
2666 err
2667 );
2668 }
2669
2670 #[test]
2674 fn test_auto_infer_dark_base_from_dark_bg() {
2675 let json = r#"{ "name": "x", "editor": { "bg": [20, 20, 30] } }"#;
2676 let theme = Theme::from_json(json).expect("should parse");
2677 let dark = Theme::load_builtin(THEME_DARK).expect("dark builtin");
2678 assert_eq!(theme.menu_bg, dark.menu_bg);
2679 assert_eq!(theme.diagnostic_error_fg, dark.diagnostic_error_fg);
2680 }
2681
2682 #[test]
2686 fn test_no_inheritance_signal_uses_hardcoded_defaults() {
2687 let json = r#"{ "name": "x" }"#;
2688 let theme = Theme::from_json(json).expect("should parse");
2689 assert_eq!(theme.editor_bg, Color::Rgb(30, 30, 30));
2692 }
2693
2694 #[test]
2698 fn test_theme_without_name_still_errors() {
2699 let json = r#"{ "editor": {} }"#;
2700 let err = Theme::from_json(json).expect_err("missing `name` must be an error");
2701 assert!(
2702 err.contains("name"),
2703 "error should mention the missing `name` field, got: {}",
2704 err
2705 );
2706 }
2707
2708 #[test]
2713 fn test_extends_overrides_compose_field_by_field() {
2714 let json = r#"{
2715 "name": "dark-with-pink-cursor",
2716 "extends": "builtin://dark",
2717 "editor": { "cursor": [255, 105, 180] }
2718 }"#;
2719 let theme = Theme::from_json(json).expect("should parse");
2720 let dark = Theme::load_builtin(THEME_DARK).expect("dark builtin");
2721
2722 assert_eq!(theme.cursor, Color::Rgb(255, 105, 180));
2724 assert_eq!(theme.editor_bg, dark.editor_bg);
2726 assert_eq!(theme.editor_fg, dark.editor_fg);
2727 assert_eq!(theme.selection_bg, dark.selection_bg);
2728 assert_eq!(theme.menu_bg, dark.menu_bg);
2730 assert_eq!(theme.syntax_keyword, dark.syntax_keyword);
2731 }
2732
2733 #[test]
2734 fn test_default_reset_color() {
2735 let color: Color = ColorDef::Named("Default".to_string()).into();
2737 assert_eq!(color, Color::Reset);
2738
2739 let color: Color = ColorDef::Named("Reset".to_string()).into();
2741 assert_eq!(color, Color::Reset);
2742 }
2743
2744 #[test]
2745 fn test_file_status_colors_fall_back_to_diagnostic_colors() {
2746 let json = r#"{
2748 "name": "test-fallback",
2749 "editor": {},
2750 "ui": {},
2751 "search": {},
2752 "diagnostic": {
2753 "error_fg": [220, 50, 47],
2754 "warning_fg": [181, 137, 0],
2755 "info_fg": [38, 139, 210],
2756 "hint_fg": [101, 123, 131]
2757 },
2758 "syntax": {}
2759 }"#;
2760 let theme = Theme::from_json(json).expect("Should parse theme without file_status keys");
2761
2762 assert_eq!(theme.file_status_added_fg, Color::Rgb(38, 139, 210));
2764 assert_eq!(theme.file_status_renamed_fg, Color::Rgb(38, 139, 210));
2765 assert_eq!(theme.file_status_modified_fg, Color::Rgb(181, 137, 0));
2767 assert_eq!(theme.file_status_deleted_fg, Color::Rgb(220, 50, 47));
2769 assert_eq!(theme.file_status_conflicted_fg, Color::Rgb(220, 50, 47));
2770 assert_eq!(theme.file_status_untracked_fg, Color::Rgb(101, 123, 131));
2772 }
2773
2774 #[test]
2775 fn test_file_status_colors_explicit_override() {
2776 let json = r#"{
2778 "name": "test-override",
2779 "editor": {},
2780 "ui": {
2781 "file_status_added_fg": [80, 250, 123],
2782 "file_status_modified_fg": [255, 184, 108]
2783 },
2784 "search": {},
2785 "diagnostic": {
2786 "info_fg": [38, 139, 210],
2787 "warning_fg": [181, 137, 0]
2788 },
2789 "syntax": {}
2790 }"#;
2791 let theme = Theme::from_json(json).expect("Should parse theme with file_status overrides");
2792
2793 assert_eq!(theme.file_status_added_fg, Color::Rgb(80, 250, 123));
2795 assert_eq!(theme.file_status_modified_fg, Color::Rgb(255, 184, 108));
2796 assert_eq!(theme.file_status_renamed_fg, Color::Rgb(38, 139, 210));
2798 }
2799
2800 #[test]
2801 fn test_file_status_colors_resolve_via_theme_key() {
2802 let json = r#"{
2803 "name": "test-resolve",
2804 "editor": {},
2805 "ui": {
2806 "file_status_added_fg": [80, 250, 123]
2807 },
2808 "search": {},
2809 "diagnostic": {
2810 "warning_fg": [181, 137, 0]
2811 },
2812 "syntax": {}
2813 }"#;
2814 let theme = Theme::from_json(json).expect("Should parse theme");
2815
2816 assert_eq!(
2818 theme.resolve_theme_key("ui.file_status_added_fg"),
2819 Some(Color::Rgb(80, 250, 123))
2820 );
2821 assert_eq!(
2822 theme.resolve_theme_key("ui.file_status_modified_fg"),
2823 Some(Color::Rgb(181, 137, 0))
2824 );
2825 }
2826
2827 #[test]
2828 fn override_colors_writes_known_keys_and_drops_unknowns() {
2829 let mut theme = Theme::load_builtin(THEME_DARK).expect("dark builtin");
2830 let applied = theme.override_colors([
2831 ("editor.bg".to_string(), Color::Rgb(10, 20, 30)),
2832 ("ui.status_bar_fg".to_string(), Color::Rgb(1, 2, 3)),
2833 ("does.not_exist".to_string(), Color::Rgb(9, 9, 9)),
2834 ("garbage_no_dot".to_string(), Color::Rgb(9, 9, 9)),
2835 ]);
2836 assert_eq!(applied, 2, "only the two valid keys should be applied");
2837 assert_eq!(
2838 theme.resolve_theme_key("editor.bg"),
2839 Some(Color::Rgb(10, 20, 30))
2840 );
2841 assert_eq!(
2842 theme.resolve_theme_key("ui.status_bar_fg"),
2843 Some(Color::Rgb(1, 2, 3))
2844 );
2845 }
2846
2847 #[test]
2848 fn resolve_theme_key_mut_matches_resolve_theme_key_domain() {
2849 let mut theme = Theme::load_builtin(THEME_DARK).expect("dark builtin");
2852 let probe = [
2853 "editor.bg",
2854 "editor.fg",
2855 "ui.status_bar_fg",
2856 "ui.tab_active_bg",
2857 "syntax.keyword",
2858 "diagnostic.error_fg",
2859 "search.match_bg",
2860 ];
2861 for key in probe {
2862 assert!(
2863 theme.resolve_theme_key(key).is_some(),
2864 "reader lost key {key}"
2865 );
2866 assert!(
2867 theme.resolve_theme_key_mut(key).is_some(),
2868 "mutator missing key {key}"
2869 );
2870 }
2871 }
2872
2873 fn schema_color_keys() -> Vec<(String, String)> {
2882 let theme = Theme::load_builtin(THEME_DARK).expect("dark builtin");
2883 let file: ThemeFile = theme.into();
2884 let value = serde_json::to_value(&file).expect("ThemeFile serializes");
2885 let obj = value.as_object().expect("ThemeFile is a JSON object");
2886
2887 let mut keys = Vec::new();
2888 for section in ["editor", "ui", "search", "diagnostic", "syntax"] {
2889 let fields = obj
2890 .get(section)
2891 .and_then(|v| v.as_object())
2892 .unwrap_or_else(|| panic!("section `{section}` missing from serialized ThemeFile"));
2893 for (field, val) in fields {
2894 if is_color_leaf(val) {
2895 keys.push((section.to_string(), field.clone()));
2896 }
2897 }
2898 }
2899 assert!(
2900 keys.len() >= 100,
2901 "expected the theme to expose at least ~100 color keys, found {} — \
2902 has the serialization shape changed?",
2903 keys.len()
2904 );
2905 keys
2906 }
2907
2908 fn is_color_leaf(v: &serde_json::Value) -> bool {
2912 v.is_string()
2913 || v.as_array()
2914 .is_some_and(|a| a.len() == 3 && a.iter().all(serde_json::Value::is_number))
2915 }
2916
2917 fn sentinel(i: usize) -> Color {
2921 Color::Rgb((i >> 8) as u8, (i & 0xff) as u8, 0x5a)
2922 }
2923
2924 #[test]
2925 fn every_exposed_color_key_resolves_in_both_directions() {
2926 let mut theme = Theme::load_builtin(THEME_DARK).expect("dark builtin");
2933 let mut missing_reader = Vec::new();
2934 let mut missing_mutator = Vec::new();
2935 for (section, field) in schema_color_keys() {
2936 let key = format!("{section}.{field}");
2937 if theme.resolve_theme_key(&key).is_none() {
2938 missing_reader.push(key.clone());
2939 }
2940 if theme.resolve_theme_key_mut(&key).is_none() {
2941 missing_mutator.push(key);
2942 }
2943 }
2944 assert!(
2945 missing_reader.is_empty() && missing_mutator.is_empty(),
2946 "theme color keys exposed by the JSON schema but dropped by a resolver:\n \
2947 resolve_theme_key: {missing_reader:?}\n \
2948 resolve_theme_key_mut: {missing_mutator:?}"
2949 );
2950 }
2951
2952 #[test]
2953 fn color_keys_round_trip_through_the_same_field_and_section() {
2954 let keys = schema_color_keys();
2965 let mut theme = Theme::load_builtin(THEME_DARK).expect("dark builtin");
2966
2967 let pairs: Vec<(String, Color)> = keys
2968 .iter()
2969 .enumerate()
2970 .map(|(i, (s, f))| (format!("{s}.{f}"), sentinel(i)))
2971 .collect();
2972 let applied = theme.override_colors(pairs.iter().map(|(k, c)| (k.as_str(), *c)));
2973 assert_eq!(
2974 applied,
2975 keys.len(),
2976 "override_colors should write every exposed key via resolve_theme_key_mut"
2977 );
2978
2979 for (i, (s, f)) in keys.iter().enumerate() {
2981 let key = format!("{s}.{f}");
2982 assert_eq!(
2983 theme.resolve_theme_key(&key),
2984 Some(sentinel(i)),
2985 "reader and mutator disagree on the field `{key}` addresses"
2986 );
2987 }
2988
2989 let file: ThemeFile = theme.into();
2992 let value = serde_json::to_value(&file).expect("ThemeFile serializes");
2993 let obj = value.as_object().expect("ThemeFile is a JSON object");
2994 for (i, (s, f)) in keys.iter().enumerate() {
2995 let leaf = obj
2996 .get(s)
2997 .and_then(|sec| sec.get(f))
2998 .unwrap_or_else(|| panic!("`{s}.{f}` vanished from serialized ThemeFile"));
2999 let color: Color = serde_json::from_value::<ColorDef>(leaf.clone())
3000 .expect("color leaf parses as ColorDef")
3001 .into();
3002 assert_eq!(
3003 color,
3004 sentinel(i),
3005 "`{s}.{f}` serialized back to the wrong field or section"
3006 );
3007 }
3008
3009 let reloaded = Theme::from_json(&value.to_string()).expect("from_json round-trips");
3012 for (i, (s, f)) in keys.iter().enumerate() {
3013 let key = format!("{s}.{f}");
3014 assert_eq!(
3015 reloaded.resolve_theme_key(&key),
3016 Some(sentinel(i)),
3017 "`{key}` did not survive ThemeFile -> JSON -> from_json"
3018 );
3019 }
3020 }
3021
3022 #[test]
3023 fn test_all_builtin_themes_set_prominent_palette_indicator() {
3024 for builtin in BUILTIN_THEMES {
3031 let theme = Theme::from_json(builtin.json)
3032 .unwrap_or_else(|e| panic!("Theme '{}' failed to parse: {}", builtin.name, e));
3033 assert!(
3034 theme.status_palette_fg != theme.status_bar_fg
3035 || theme.status_palette_bg != theme.status_bar_bg,
3036 "Theme '{}' must set status_palette_fg/bg to a prominent \
3037 accent distinct from status_bar_fg/bg",
3038 builtin.name
3039 );
3040 }
3041 }
3042
3043 #[test]
3044 fn test_all_builtin_themes_have_file_status_colors() {
3045 for builtin in BUILTIN_THEMES {
3047 let theme = Theme::from_json(builtin.json)
3048 .unwrap_or_else(|e| panic!("Theme '{}' failed to parse: {}", builtin.name, e));
3049
3050 for key in &[
3052 "ui.file_status_added_fg",
3053 "ui.file_status_modified_fg",
3054 "ui.file_status_deleted_fg",
3055 "ui.file_status_renamed_fg",
3056 "ui.file_status_untracked_fg",
3057 "ui.file_status_conflicted_fg",
3058 ] {
3059 assert!(
3060 theme.resolve_theme_key(key).is_some(),
3061 "Theme '{}' missing resolution for '{}'",
3062 builtin.name,
3063 key
3064 );
3065 }
3066 }
3067 }
3068}