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)]
541 pub indentation_guide_fg: Option<ColorDef>,
542 #[serde(default = "default_whitespace_indicator_fg")]
544 pub whitespace_indicator_fg: ColorDef,
545 #[serde(default = "default_bracket_match_fg")]
547 pub bracket_match_fg: ColorDef,
548 #[serde(default = "default_bracket_rainbow_1")]
550 pub bracket_rainbow_1: ColorDef,
551 #[serde(default = "default_bracket_rainbow_2")]
553 pub bracket_rainbow_2: ColorDef,
554 #[serde(default = "default_bracket_rainbow_3")]
556 pub bracket_rainbow_3: ColorDef,
557 #[serde(default = "default_bracket_rainbow_4")]
559 pub bracket_rainbow_4: ColorDef,
560 #[serde(default = "default_bracket_rainbow_5")]
562 pub bracket_rainbow_5: ColorDef,
563 #[serde(default = "default_bracket_rainbow_6")]
565 pub bracket_rainbow_6: ColorDef,
566 #[serde(default)]
571 pub after_eof_bg: Option<ColorDef>,
572}
573
574fn default_editor_bg() -> ColorDef {
576 ColorDef::Rgb(30, 30, 30)
577}
578fn default_editor_fg() -> ColorDef {
579 ColorDef::Rgb(212, 212, 212)
580}
581fn default_cursor() -> ColorDef {
582 ColorDef::Rgb(255, 255, 255)
583}
584fn default_inactive_cursor() -> ColorDef {
585 ColorDef::Named("DarkGray".to_string())
586}
587fn default_selection_bg() -> ColorDef {
588 ColorDef::Rgb(38, 79, 120)
589}
590fn default_current_line_bg() -> ColorDef {
591 ColorDef::Rgb(40, 40, 40)
592}
593fn default_line_number_fg() -> ColorDef {
594 ColorDef::Rgb(100, 100, 100)
595}
596fn default_line_number_bg() -> ColorDef {
597 ColorDef::Rgb(30, 30, 30)
598}
599fn default_diff_add_bg() -> ColorDef {
600 ColorDef::Rgb(35, 60, 35) }
602fn default_diff_remove_bg() -> ColorDef {
603 ColorDef::Rgb(70, 35, 35) }
605fn default_diff_modify_bg() -> ColorDef {
606 ColorDef::Rgb(40, 38, 30) }
608fn default_ruler_bg() -> ColorDef {
609 ColorDef::Rgb(50, 50, 50) }
611fn default_whitespace_indicator_fg() -> ColorDef {
612 ColorDef::Rgb(70, 70, 70) }
614fn default_bracket_match_fg() -> ColorDef {
615 ColorDef::Rgb(255, 215, 0) }
617fn default_bracket_rainbow_1() -> ColorDef {
618 ColorDef::Rgb(255, 215, 0) }
620fn default_bracket_rainbow_2() -> ColorDef {
621 ColorDef::Rgb(218, 112, 214) }
623fn default_bracket_rainbow_3() -> ColorDef {
624 ColorDef::Rgb(50, 205, 50) }
626fn default_bracket_rainbow_4() -> ColorDef {
627 ColorDef::Rgb(30, 144, 255) }
629fn default_bracket_rainbow_5() -> ColorDef {
630 ColorDef::Rgb(255, 127, 80) }
632fn default_bracket_rainbow_6() -> ColorDef {
633 ColorDef::Rgb(147, 112, 219) }
635
636#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
655pub struct UiColors {
656 #[serde(default = "default_tab_active_fg")]
658 pub tab_active_fg: ColorDef,
659 #[serde(default = "default_tab_active_bg")]
661 pub tab_active_bg: ColorDef,
662 #[serde(default = "default_tab_inactive_fg")]
664 pub tab_inactive_fg: ColorDef,
665 #[serde(default = "default_tab_inactive_bg")]
667 pub tab_inactive_bg: ColorDef,
668 #[serde(default = "default_tab_separator_bg")]
670 pub tab_separator_bg: ColorDef,
671 #[serde(default = "default_tab_close_hover_fg")]
673 pub tab_close_hover_fg: ColorDef,
674 #[serde(default = "default_tab_hover_bg")]
676 pub tab_hover_bg: ColorDef,
677 #[serde(default = "default_menu_bg")]
679 pub menu_bg: ColorDef,
680 #[serde(default = "default_menu_fg")]
682 pub menu_fg: ColorDef,
683 #[serde(default = "default_menu_active_bg")]
685 pub menu_active_bg: ColorDef,
686 #[serde(default = "default_menu_active_fg")]
688 pub menu_active_fg: ColorDef,
689 #[serde(default = "default_menu_dropdown_bg")]
691 pub menu_dropdown_bg: ColorDef,
692 #[serde(default = "default_menu_dropdown_fg")]
694 pub menu_dropdown_fg: ColorDef,
695 #[serde(default = "default_menu_highlight_bg")]
697 pub menu_highlight_bg: ColorDef,
698 #[serde(default = "default_menu_highlight_fg")]
700 pub menu_highlight_fg: ColorDef,
701 #[serde(default = "default_menu_border_fg")]
703 pub menu_border_fg: ColorDef,
704 #[serde(default = "default_menu_separator_fg")]
706 pub menu_separator_fg: ColorDef,
707 #[serde(default = "default_menu_hover_bg")]
709 pub menu_hover_bg: ColorDef,
710 #[serde(default = "default_menu_hover_fg")]
712 pub menu_hover_fg: ColorDef,
713 #[serde(default = "default_menu_disabled_fg")]
715 pub menu_disabled_fg: ColorDef,
716 #[serde(default = "default_menu_disabled_bg")]
718 pub menu_disabled_bg: ColorDef,
719 #[serde(default = "default_status_bar_fg")]
721 pub status_bar_fg: ColorDef,
722 #[serde(default = "default_status_bar_bg")]
724 pub status_bar_bg: ColorDef,
725 #[serde(default)]
727 pub status_palette_fg: Option<ColorDef>,
728 #[serde(default)]
730 pub status_palette_bg: Option<ColorDef>,
731 #[serde(default)]
733 pub status_separator_fg: Option<ColorDef>,
734 #[serde(default)]
736 pub status_separator_bg: Option<ColorDef>,
737 #[serde(default)]
739 pub status_lsp_on_fg: Option<ColorDef>,
740 #[serde(default)]
742 pub status_lsp_on_bg: Option<ColorDef>,
743 #[serde(default)]
747 pub status_lsp_actionable_fg: Option<ColorDef>,
748 #[serde(default)]
751 pub status_lsp_actionable_bg: Option<ColorDef>,
752 #[serde(default = "default_prompt_fg")]
754 pub prompt_fg: ColorDef,
755 #[serde(default = "default_prompt_bg")]
757 pub prompt_bg: ColorDef,
758 #[serde(default = "default_prompt_selection_fg")]
760 pub prompt_selection_fg: ColorDef,
761 #[serde(default = "default_prompt_selection_bg")]
763 pub prompt_selection_bg: ColorDef,
764 #[serde(default = "default_popup_border_fg")]
766 pub popup_border_fg: ColorDef,
767 #[serde(default = "default_popup_bg")]
769 pub popup_bg: ColorDef,
770 #[serde(default = "default_popup_selection_bg")]
772 pub popup_selection_bg: ColorDef,
773 #[serde(default = "default_text_input_selection_bg")]
781 pub text_input_selection_bg: ColorDef,
782 #[serde(default = "default_popup_selection_fg")]
784 pub popup_selection_fg: ColorDef,
785 #[serde(default = "default_popup_text_fg", alias = "popup_fg")]
789 pub popup_text_fg: ColorDef,
790 #[serde(default = "default_suggestion_bg")]
792 pub suggestion_bg: ColorDef,
793 #[serde(default)]
797 pub suggestion_fg: Option<ColorDef>,
798 #[serde(default = "default_suggestion_selected_bg")]
800 pub suggestion_selected_bg: ColorDef,
801 #[serde(default = "default_help_bg")]
803 pub help_bg: ColorDef,
804 #[serde(default = "default_help_fg")]
806 pub help_fg: ColorDef,
807 #[serde(default = "default_help_key_fg")]
809 pub help_key_fg: ColorDef,
810 #[serde(default = "default_help_separator_fg")]
812 pub help_separator_fg: ColorDef,
813 #[serde(default = "default_help_indicator_fg")]
815 pub help_indicator_fg: ColorDef,
816 #[serde(default = "default_help_indicator_bg")]
818 pub help_indicator_bg: ColorDef,
819 #[serde(default = "default_inline_code_bg")]
821 pub inline_code_bg: ColorDef,
822 #[serde(default = "default_split_separator_fg")]
824 pub split_separator_fg: ColorDef,
825 #[serde(default = "default_split_separator_hover_fg")]
827 pub split_separator_hover_fg: ColorDef,
828 #[serde(default = "default_scrollbar_track_fg")]
830 pub scrollbar_track_fg: ColorDef,
831 #[serde(default = "default_scrollbar_thumb_fg")]
833 pub scrollbar_thumb_fg: ColorDef,
834 #[serde(default = "default_scrollbar_track_hover_fg")]
836 pub scrollbar_track_hover_fg: ColorDef,
837 #[serde(default = "default_scrollbar_thumb_hover_fg")]
839 pub scrollbar_thumb_hover_fg: ColorDef,
840 #[serde(default = "default_compose_margin_bg")]
842 pub compose_margin_bg: ColorDef,
843 #[serde(default = "default_semantic_highlight_bg")]
845 pub semantic_highlight_bg: ColorDef,
846 #[serde(default)]
853 pub semantic_highlight_modifier: Option<ModifierDef>,
854 #[serde(default = "default_terminal_bg")]
856 pub terminal_bg: ColorDef,
857 #[serde(default = "default_terminal_fg")]
859 pub terminal_fg: ColorDef,
860 #[serde(default = "default_status_warning_indicator_bg")]
862 pub status_warning_indicator_bg: ColorDef,
863 #[serde(default = "default_status_warning_indicator_fg")]
865 pub status_warning_indicator_fg: ColorDef,
866 #[serde(default = "default_status_error_indicator_bg")]
868 pub status_error_indicator_bg: ColorDef,
869 #[serde(default = "default_status_error_indicator_fg")]
871 pub status_error_indicator_fg: ColorDef,
872 #[serde(default = "default_status_warning_indicator_hover_bg")]
874 pub status_warning_indicator_hover_bg: ColorDef,
875 #[serde(default = "default_status_warning_indicator_hover_fg")]
877 pub status_warning_indicator_hover_fg: ColorDef,
878 #[serde(default = "default_status_error_indicator_hover_bg")]
880 pub status_error_indicator_hover_bg: ColorDef,
881 #[serde(default = "default_status_error_indicator_hover_fg")]
883 pub status_error_indicator_hover_fg: ColorDef,
884 #[serde(default = "default_tab_drop_zone_bg")]
886 pub tab_drop_zone_bg: ColorDef,
887 #[serde(default = "default_tab_drop_zone_border")]
889 pub tab_drop_zone_border: ColorDef,
890 #[serde(default = "default_settings_selected_bg")]
892 pub settings_selected_bg: ColorDef,
893 #[serde(default = "default_settings_selected_fg")]
895 pub settings_selected_fg: ColorDef,
896 #[serde(default)]
898 pub file_status_added_fg: Option<ColorDef>,
899 #[serde(default)]
901 pub file_status_modified_fg: Option<ColorDef>,
902 #[serde(default)]
904 pub file_status_deleted_fg: Option<ColorDef>,
905 #[serde(default)]
907 pub file_status_renamed_fg: Option<ColorDef>,
908 #[serde(default)]
910 pub file_status_untracked_fg: Option<ColorDef>,
911 #[serde(default)]
913 pub file_status_conflicted_fg: Option<ColorDef>,
914}
915
916fn default_tab_active_fg() -> ColorDef {
919 ColorDef::Named("Yellow".to_string())
920}
921fn default_tab_active_bg() -> ColorDef {
922 ColorDef::Named("Blue".to_string())
923}
924fn default_tab_inactive_fg() -> ColorDef {
925 ColorDef::Named("White".to_string())
926}
927fn default_tab_inactive_bg() -> ColorDef {
928 ColorDef::Named("DarkGray".to_string())
929}
930fn default_tab_separator_bg() -> ColorDef {
931 ColorDef::Named("Black".to_string())
932}
933fn default_tab_close_hover_fg() -> ColorDef {
934 ColorDef::Rgb(255, 100, 100) }
936fn default_tab_hover_bg() -> ColorDef {
937 ColorDef::Rgb(70, 70, 75) }
939
940fn default_menu_bg() -> ColorDef {
942 ColorDef::Rgb(60, 60, 65)
943}
944fn default_menu_fg() -> ColorDef {
945 ColorDef::Rgb(220, 220, 220)
946}
947fn default_menu_active_bg() -> ColorDef {
948 ColorDef::Rgb(60, 60, 60)
949}
950fn default_menu_active_fg() -> ColorDef {
951 ColorDef::Rgb(255, 255, 255)
952}
953fn default_menu_dropdown_bg() -> ColorDef {
954 ColorDef::Rgb(50, 50, 50)
955}
956fn default_menu_dropdown_fg() -> ColorDef {
957 ColorDef::Rgb(220, 220, 220)
958}
959fn default_menu_highlight_bg() -> ColorDef {
960 ColorDef::Rgb(70, 130, 180)
961}
962fn default_menu_highlight_fg() -> ColorDef {
963 ColorDef::Rgb(255, 255, 255)
964}
965fn default_menu_border_fg() -> ColorDef {
966 ColorDef::Rgb(100, 100, 100)
967}
968fn default_menu_separator_fg() -> ColorDef {
969 ColorDef::Rgb(80, 80, 80)
970}
971fn default_menu_hover_bg() -> ColorDef {
972 ColorDef::Rgb(55, 55, 55)
973}
974fn default_menu_hover_fg() -> ColorDef {
975 ColorDef::Rgb(255, 255, 255)
976}
977fn default_menu_disabled_fg() -> ColorDef {
978 ColorDef::Rgb(100, 100, 100) }
980fn default_menu_disabled_bg() -> ColorDef {
981 ColorDef::Rgb(50, 50, 50) }
983fn default_status_bar_fg() -> ColorDef {
985 ColorDef::Named("White".to_string())
986}
987fn default_status_bar_bg() -> ColorDef {
988 ColorDef::Named("DarkGray".to_string())
989}
990
991fn default_prompt_fg() -> ColorDef {
993 ColorDef::Named("White".to_string())
994}
995fn default_prompt_bg() -> ColorDef {
996 ColorDef::Named("Black".to_string())
997}
998fn default_prompt_selection_fg() -> ColorDef {
999 ColorDef::Named("White".to_string())
1000}
1001fn default_prompt_selection_bg() -> ColorDef {
1002 ColorDef::Rgb(58, 79, 120)
1003}
1004
1005pub(crate) fn default_popup_border_fg() -> ColorDef {
1007 ColorDef::Named("Gray".to_string())
1008}
1009pub(crate) fn default_popup_bg() -> ColorDef {
1010 ColorDef::Rgb(30, 30, 30)
1011}
1012fn default_popup_selection_bg() -> ColorDef {
1013 ColorDef::Rgb(58, 79, 120)
1014}
1015fn default_text_input_selection_bg() -> ColorDef {
1016 ColorDef::Rgb(58, 79, 120)
1020}
1021fn default_popup_selection_fg() -> ColorDef {
1022 ColorDef::Rgb(255, 255, 255) }
1024fn default_popup_text_fg() -> ColorDef {
1025 ColorDef::Named("White".to_string())
1026}
1027
1028fn default_suggestion_bg() -> ColorDef {
1030 ColorDef::Rgb(30, 30, 30)
1031}
1032fn default_suggestion_selected_bg() -> ColorDef {
1033 ColorDef::Rgb(58, 79, 120)
1034}
1035
1036fn default_help_bg() -> ColorDef {
1038 ColorDef::Named("Black".to_string())
1039}
1040fn default_help_fg() -> ColorDef {
1041 ColorDef::Named("White".to_string())
1042}
1043fn default_help_key_fg() -> ColorDef {
1044 ColorDef::Named("Cyan".to_string())
1045}
1046fn default_help_separator_fg() -> ColorDef {
1047 ColorDef::Named("DarkGray".to_string())
1048}
1049fn default_help_indicator_fg() -> ColorDef {
1050 ColorDef::Named("Red".to_string())
1051}
1052fn default_help_indicator_bg() -> ColorDef {
1053 ColorDef::Named("Black".to_string())
1054}
1055
1056fn default_inline_code_bg() -> ColorDef {
1057 ColorDef::Named("DarkGray".to_string())
1058}
1059
1060fn default_split_separator_fg() -> ColorDef {
1062 ColorDef::Rgb(100, 100, 100)
1063}
1064fn default_split_separator_hover_fg() -> ColorDef {
1065 ColorDef::Rgb(100, 149, 237) }
1067fn default_scrollbar_track_fg() -> ColorDef {
1068 ColorDef::Named("DarkGray".to_string())
1069}
1070fn default_scrollbar_thumb_fg() -> ColorDef {
1071 ColorDef::Named("Gray".to_string())
1072}
1073fn default_scrollbar_track_hover_fg() -> ColorDef {
1074 ColorDef::Named("Gray".to_string())
1075}
1076fn default_scrollbar_thumb_hover_fg() -> ColorDef {
1077 ColorDef::Named("White".to_string())
1078}
1079fn default_compose_margin_bg() -> ColorDef {
1080 ColorDef::Rgb(18, 18, 18) }
1082fn default_semantic_highlight_bg() -> ColorDef {
1083 ColorDef::Rgb(60, 60, 80) }
1085fn default_terminal_bg() -> ColorDef {
1086 ColorDef::Named("Default".to_string()) }
1088fn default_terminal_fg() -> ColorDef {
1089 ColorDef::Named("Default".to_string()) }
1091fn default_status_warning_indicator_bg() -> ColorDef {
1092 ColorDef::Rgb(181, 137, 0) }
1094fn default_status_warning_indicator_fg() -> ColorDef {
1095 ColorDef::Rgb(0, 0, 0) }
1097fn default_status_error_indicator_bg() -> ColorDef {
1098 ColorDef::Rgb(220, 50, 47) }
1100fn default_status_error_indicator_fg() -> ColorDef {
1101 ColorDef::Rgb(255, 255, 255) }
1103fn default_status_warning_indicator_hover_bg() -> ColorDef {
1104 ColorDef::Rgb(211, 167, 30) }
1106fn default_status_warning_indicator_hover_fg() -> ColorDef {
1107 ColorDef::Rgb(0, 0, 0) }
1109fn default_status_error_indicator_hover_bg() -> ColorDef {
1110 ColorDef::Rgb(250, 80, 77) }
1112fn default_status_error_indicator_hover_fg() -> ColorDef {
1113 ColorDef::Rgb(255, 255, 255) }
1115fn default_tab_drop_zone_bg() -> ColorDef {
1116 ColorDef::Rgb(70, 130, 180) }
1118fn default_tab_drop_zone_border() -> ColorDef {
1119 ColorDef::Rgb(100, 149, 237) }
1121fn default_settings_selected_bg() -> ColorDef {
1122 ColorDef::Rgb(60, 60, 70) }
1124fn default_settings_selected_fg() -> ColorDef {
1125 ColorDef::Rgb(255, 255, 255) }
1127#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1129pub struct SearchColors {
1130 #[serde(default = "default_search_match_bg")]
1132 pub match_bg: ColorDef,
1133 #[serde(default = "default_search_match_fg")]
1135 pub match_fg: ColorDef,
1136 #[serde(default = "default_search_label_bg")]
1140 pub label_bg: ColorDef,
1141 #[serde(default = "default_search_label_fg")]
1145 pub label_fg: ColorDef,
1146}
1147
1148fn default_search_match_bg() -> ColorDef {
1150 ColorDef::Rgb(100, 100, 20)
1151}
1152fn default_search_match_fg() -> ColorDef {
1153 ColorDef::Rgb(255, 255, 255)
1154}
1155fn default_search_label_bg() -> ColorDef {
1160 ColorDef::Rgb(199, 78, 189)
1161}
1162fn default_search_label_fg() -> ColorDef {
1163 ColorDef::Rgb(255, 255, 255)
1164}
1165
1166#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1168pub struct DiagnosticColors {
1169 #[serde(default = "default_diagnostic_error_fg")]
1171 pub error_fg: ColorDef,
1172 #[serde(default = "default_diagnostic_error_bg")]
1174 pub error_bg: ColorDef,
1175 #[serde(default = "default_diagnostic_warning_fg")]
1177 pub warning_fg: ColorDef,
1178 #[serde(default = "default_diagnostic_warning_bg")]
1180 pub warning_bg: ColorDef,
1181 #[serde(default = "default_diagnostic_info_fg")]
1183 pub info_fg: ColorDef,
1184 #[serde(default = "default_diagnostic_info_bg")]
1186 pub info_bg: ColorDef,
1187 #[serde(default = "default_diagnostic_hint_fg")]
1189 pub hint_fg: ColorDef,
1190 #[serde(default = "default_diagnostic_hint_bg")]
1192 pub hint_bg: ColorDef,
1193}
1194
1195fn default_diagnostic_error_fg() -> ColorDef {
1197 ColorDef::Named("Red".to_string())
1198}
1199fn default_diagnostic_error_bg() -> ColorDef {
1200 ColorDef::Rgb(60, 20, 20)
1201}
1202fn default_diagnostic_warning_fg() -> ColorDef {
1203 ColorDef::Named("Yellow".to_string())
1204}
1205fn default_diagnostic_warning_bg() -> ColorDef {
1206 ColorDef::Rgb(60, 50, 0)
1207}
1208fn default_diagnostic_info_fg() -> ColorDef {
1209 ColorDef::Named("Blue".to_string())
1210}
1211fn default_diagnostic_info_bg() -> ColorDef {
1212 ColorDef::Rgb(0, 30, 60)
1213}
1214fn default_diagnostic_hint_fg() -> ColorDef {
1215 ColorDef::Named("Gray".to_string())
1216}
1217fn default_diagnostic_hint_bg() -> ColorDef {
1218 ColorDef::Rgb(30, 30, 30)
1219}
1220
1221#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1223pub struct SyntaxColors {
1224 #[serde(default = "default_syntax_keyword")]
1226 pub keyword: ColorDef,
1227 #[serde(default = "default_syntax_string")]
1229 pub string: ColorDef,
1230 #[serde(default = "default_syntax_comment")]
1232 pub comment: ColorDef,
1233 #[serde(default = "default_syntax_function")]
1235 pub function: ColorDef,
1236 #[serde(rename = "type", default = "default_syntax_type")]
1238 pub type_: ColorDef,
1239 #[serde(default = "default_syntax_variable")]
1241 pub variable: ColorDef,
1242 #[serde(default = "default_syntax_variable_builtin")]
1244 pub variable_builtin: ColorDef,
1245 #[serde(default = "default_syntax_constant")]
1247 pub constant: ColorDef,
1248 #[serde(default = "default_syntax_operator")]
1250 pub operator: ColorDef,
1251 #[serde(default = "default_syntax_punctuation_bracket")]
1253 pub punctuation_bracket: ColorDef,
1254 #[serde(default = "default_syntax_punctuation_delimiter")]
1256 pub punctuation_delimiter: ColorDef,
1257}
1258
1259fn default_syntax_keyword() -> ColorDef {
1261 ColorDef::Rgb(86, 156, 214)
1262}
1263fn default_syntax_string() -> ColorDef {
1264 ColorDef::Rgb(206, 145, 120)
1265}
1266fn default_syntax_comment() -> ColorDef {
1267 ColorDef::Rgb(106, 153, 85)
1268}
1269fn default_syntax_function() -> ColorDef {
1270 ColorDef::Rgb(220, 220, 170)
1271}
1272fn default_syntax_type() -> ColorDef {
1273 ColorDef::Rgb(78, 201, 176)
1274}
1275fn default_syntax_variable() -> ColorDef {
1276 ColorDef::Rgb(156, 220, 254)
1277}
1278fn default_syntax_variable_builtin() -> ColorDef {
1279 ColorDef::Rgb(86, 156, 214) }
1281fn default_syntax_constant() -> ColorDef {
1282 ColorDef::Rgb(79, 193, 255)
1283}
1284fn default_syntax_operator() -> ColorDef {
1285 ColorDef::Rgb(212, 212, 212)
1286}
1287fn default_syntax_punctuation_bracket() -> ColorDef {
1288 ColorDef::Rgb(212, 212, 212) }
1290fn default_syntax_punctuation_delimiter() -> ColorDef {
1291 ColorDef::Rgb(212, 212, 212) }
1293
1294#[derive(Debug, Clone)]
1296pub struct Theme {
1297 pub name: String,
1299
1300 pub editor_bg: Color,
1302 pub editor_fg: Color,
1303 pub cursor: Color,
1304 pub inactive_cursor: Color,
1305 pub selection_bg: Color,
1306 pub selection_modifier: Modifier,
1311 pub current_line_bg: Color,
1312 pub line_number_fg: Color,
1313 pub line_number_bg: Color,
1314
1315 pub after_eof_bg: Color,
1317
1318 pub ruler_bg: Color,
1320
1321 pub indentation_guide_fg: Color,
1323
1324 pub whitespace_indicator_fg: Color,
1326
1327 pub bracket_match_fg: Color,
1329 pub bracket_rainbow_1: Color,
1330 pub bracket_rainbow_2: Color,
1331 pub bracket_rainbow_3: Color,
1332 pub bracket_rainbow_4: Color,
1333 pub bracket_rainbow_5: Color,
1334 pub bracket_rainbow_6: Color,
1335
1336 pub diff_add_bg: Color,
1338 pub diff_remove_bg: Color,
1339 pub diff_modify_bg: Color,
1340 pub diff_add_highlight_bg: Color,
1342 pub diff_remove_highlight_bg: Color,
1344 pub diff_add_collision_fg: Option<Color>,
1348 pub diff_remove_collision_fg: Option<Color>,
1349 pub diff_modify_collision_fg: Option<Color>,
1350
1351 pub tab_active_fg: Color,
1353 pub tab_active_bg: Color,
1354 pub tab_inactive_fg: Color,
1355 pub tab_inactive_bg: Color,
1356 pub tab_separator_bg: Color,
1357 pub tab_close_hover_fg: Color,
1358 pub tab_hover_bg: Color,
1359
1360 pub menu_bg: Color,
1362 pub menu_fg: Color,
1363 pub menu_active_bg: Color,
1364 pub menu_active_fg: Color,
1365 pub menu_dropdown_bg: Color,
1366 pub menu_dropdown_fg: Color,
1367 pub menu_highlight_bg: Color,
1368 pub menu_highlight_fg: Color,
1369 pub menu_border_fg: Color,
1370 pub menu_separator_fg: Color,
1371 pub menu_hover_bg: Color,
1372 pub menu_hover_fg: Color,
1373 pub menu_disabled_fg: Color,
1374 pub menu_disabled_bg: Color,
1375
1376 pub status_bar_fg: Color,
1377 pub status_bar_bg: Color,
1378 pub status_palette_fg: Color,
1380 pub status_palette_bg: Color,
1381 pub status_separator_fg: Color,
1383 pub status_separator_bg: Color,
1384 pub status_lsp_on_fg: Color,
1386 pub status_lsp_on_bg: Color,
1387 pub status_lsp_actionable_fg: Color,
1390 pub status_lsp_actionable_bg: Color,
1391 pub prompt_fg: Color,
1392 pub prompt_bg: Color,
1393 pub prompt_selection_fg: Color,
1394 pub prompt_selection_bg: Color,
1395
1396 pub popup_border_fg: Color,
1397 pub popup_bg: Color,
1398 pub popup_selection_bg: Color,
1399 pub popup_selection_fg: Color,
1400 pub popup_text_fg: Color,
1401 pub text_input_selection_bg: Color,
1405
1406 pub suggestion_bg: Color,
1407 pub suggestion_fg: Color,
1408 pub suggestion_selected_bg: Color,
1409
1410 pub help_bg: Color,
1411 pub help_fg: Color,
1412 pub help_key_fg: Color,
1413 pub help_separator_fg: Color,
1414
1415 pub help_indicator_fg: Color,
1416 pub help_indicator_bg: Color,
1417
1418 pub inline_code_bg: Color,
1420
1421 pub split_separator_fg: Color,
1422 pub split_separator_hover_fg: Color,
1423
1424 pub scrollbar_track_fg: Color,
1426 pub scrollbar_thumb_fg: Color,
1427 pub scrollbar_track_hover_fg: Color,
1428 pub scrollbar_thumb_hover_fg: Color,
1429
1430 pub compose_margin_bg: Color,
1432
1433 pub semantic_highlight_bg: Color,
1435 pub semantic_highlight_modifier: Modifier,
1440
1441 pub terminal_bg: Color,
1443 pub terminal_fg: Color,
1444
1445 pub status_warning_indicator_bg: Color,
1447 pub status_warning_indicator_fg: Color,
1448 pub status_error_indicator_bg: Color,
1449 pub status_error_indicator_fg: Color,
1450 pub status_warning_indicator_hover_bg: Color,
1451 pub status_warning_indicator_hover_fg: Color,
1452 pub status_error_indicator_hover_bg: Color,
1453 pub status_error_indicator_hover_fg: Color,
1454
1455 pub tab_drop_zone_bg: Color,
1457 pub tab_drop_zone_border: Color,
1458
1459 pub settings_selected_bg: Color,
1461 pub settings_selected_fg: Color,
1462
1463 pub file_status_added_fg: Color,
1465 pub file_status_modified_fg: Color,
1466 pub file_status_deleted_fg: Color,
1467 pub file_status_renamed_fg: Color,
1468 pub file_status_untracked_fg: Color,
1469 pub file_status_conflicted_fg: Color,
1470
1471 pub search_match_bg: Color,
1473 pub search_match_fg: Color,
1474 pub search_label_bg: Color,
1475 pub search_label_fg: Color,
1476
1477 pub diagnostic_error_fg: Color,
1479 pub diagnostic_error_bg: Color,
1480 pub diagnostic_warning_fg: Color,
1481 pub diagnostic_warning_bg: Color,
1482 pub diagnostic_info_fg: Color,
1483 pub diagnostic_info_bg: Color,
1484 pub diagnostic_hint_fg: Color,
1485 pub diagnostic_hint_bg: Color,
1486
1487 pub syntax_keyword: Color,
1489 pub syntax_string: Color,
1490 pub syntax_comment: Color,
1491 pub syntax_function: Color,
1492 pub syntax_type: Color,
1493 pub syntax_variable: Color,
1494 pub syntax_variable_builtin: Color,
1495 pub syntax_constant: Color,
1496 pub syntax_operator: Color,
1497 pub syntax_punctuation_bracket: Color,
1498 pub syntax_punctuation_delimiter: Color,
1499}
1500
1501impl From<ThemeFile> for Theme {
1502 fn from(file: ThemeFile) -> Self {
1503 Self {
1504 name: file.name,
1505 editor_bg: file.editor.bg.clone().into(),
1506 editor_fg: file.editor.fg.into(),
1507 cursor: file.editor.cursor.into(),
1508 inactive_cursor: file.editor.inactive_cursor.into(),
1509 selection_bg: file.editor.selection_bg.into(),
1510 selection_modifier: file
1511 .editor
1512 .selection_modifier
1513 .as_ref()
1514 .map(Modifier::from)
1515 .unwrap_or(Modifier::empty()),
1516 current_line_bg: file.editor.current_line_bg.into(),
1517 line_number_fg: file.editor.line_number_fg.into(),
1518 line_number_bg: file.editor.line_number_bg.into(),
1519 after_eof_bg: file
1522 .editor
1523 .after_eof_bg
1524 .clone()
1525 .map(|c| c.into())
1526 .unwrap_or_else(|| shade_toward_contrast(file.editor.bg.clone().into(), 10)),
1527 ruler_bg: file.editor.ruler_bg.into(),
1528 indentation_guide_fg: file
1529 .editor
1530 .indentation_guide_fg
1531 .clone()
1532 .unwrap_or_else(|| file.editor.whitespace_indicator_fg.clone())
1533 .into(),
1534 whitespace_indicator_fg: file.editor.whitespace_indicator_fg.into(),
1535 bracket_match_fg: file.editor.bracket_match_fg.into(),
1536 bracket_rainbow_1: file.editor.bracket_rainbow_1.into(),
1537 bracket_rainbow_2: file.editor.bracket_rainbow_2.into(),
1538 bracket_rainbow_3: file.editor.bracket_rainbow_3.into(),
1539 bracket_rainbow_4: file.editor.bracket_rainbow_4.into(),
1540 bracket_rainbow_5: file.editor.bracket_rainbow_5.into(),
1541 bracket_rainbow_6: file.editor.bracket_rainbow_6.into(),
1542 diff_add_bg: file.editor.diff_add_bg.clone().into(),
1543 diff_remove_bg: file.editor.diff_remove_bg.clone().into(),
1544 diff_modify_bg: file.editor.diff_modify_bg.into(),
1545 diff_add_highlight_bg: file
1547 .editor
1548 .diff_add_highlight_bg
1549 .map(|c| c.into())
1550 .unwrap_or_else(|| brighten_color(file.editor.diff_add_bg.into(), 40)),
1551 diff_remove_highlight_bg: file
1552 .editor
1553 .diff_remove_highlight_bg
1554 .map(|c| c.into())
1555 .unwrap_or_else(|| brighten_color(file.editor.diff_remove_bg.into(), 40)),
1556 diff_add_collision_fg: file.editor.diff_add_collision_fg.clone().map(|c| c.into()),
1557 diff_remove_collision_fg: file
1558 .editor
1559 .diff_remove_collision_fg
1560 .clone()
1561 .map(|c| c.into()),
1562 diff_modify_collision_fg: file
1563 .editor
1564 .diff_modify_collision_fg
1565 .clone()
1566 .map(|c| c.into()),
1567 tab_active_fg: file.ui.tab_active_fg.into(),
1568 tab_active_bg: file.ui.tab_active_bg.into(),
1569 tab_inactive_fg: file.ui.tab_inactive_fg.into(),
1570 tab_inactive_bg: file.ui.tab_inactive_bg.into(),
1571 tab_separator_bg: file.ui.tab_separator_bg.into(),
1572 tab_close_hover_fg: file.ui.tab_close_hover_fg.into(),
1573 tab_hover_bg: file.ui.tab_hover_bg.into(),
1574 menu_bg: file.ui.menu_bg.into(),
1575 menu_fg: file.ui.menu_fg.into(),
1576 menu_active_bg: file.ui.menu_active_bg.into(),
1577 menu_active_fg: file.ui.menu_active_fg.into(),
1578 menu_dropdown_bg: file.ui.menu_dropdown_bg.into(),
1579 menu_dropdown_fg: file.ui.menu_dropdown_fg.into(),
1580 menu_highlight_bg: file.ui.menu_highlight_bg.into(),
1581 menu_highlight_fg: file.ui.menu_highlight_fg.into(),
1582 menu_border_fg: file.ui.menu_border_fg.into(),
1583 menu_separator_fg: file.ui.menu_separator_fg.into(),
1584 menu_hover_bg: file.ui.menu_hover_bg.into(),
1585 menu_hover_fg: file.ui.menu_hover_fg.into(),
1586 menu_disabled_fg: file.ui.menu_disabled_fg.into(),
1587 menu_disabled_bg: file.ui.menu_disabled_bg.into(),
1588 status_bar_fg: file.ui.status_bar_fg.clone().into(),
1589 status_bar_bg: file.ui.status_bar_bg.clone().into(),
1590 status_palette_fg: file
1591 .ui
1592 .status_palette_fg
1593 .clone()
1594 .map(|c| c.into())
1595 .unwrap_or_else(|| file.ui.status_bar_fg.clone().into()),
1596 status_palette_bg: file
1597 .ui
1598 .status_palette_bg
1599 .clone()
1600 .map(|c| c.into())
1601 .unwrap_or_else(|| file.ui.status_bar_bg.clone().into()),
1602 status_separator_fg: file
1603 .ui
1604 .status_separator_fg
1605 .clone()
1606 .map(|c| c.into())
1607 .unwrap_or_else(|| file.ui.status_bar_fg.clone().into()),
1608 status_separator_bg: file
1609 .ui
1610 .status_separator_bg
1611 .clone()
1612 .map(|c| c.into())
1613 .unwrap_or_else(|| file.ui.status_bar_bg.clone().into()),
1614 status_lsp_on_fg: file
1615 .ui
1616 .status_lsp_on_fg
1617 .clone()
1618 .map(|c| c.into())
1619 .unwrap_or_else(|| file.ui.status_bar_fg.clone().into()),
1620 status_lsp_on_bg: file
1621 .ui
1622 .status_lsp_on_bg
1623 .clone()
1624 .map(|c| c.into())
1625 .unwrap_or_else(|| file.ui.status_bar_bg.clone().into()),
1626 status_lsp_actionable_fg: file
1627 .ui
1628 .status_lsp_actionable_fg
1629 .clone()
1630 .map(|c| c.into())
1631 .unwrap_or_else(|| file.ui.status_warning_indicator_fg.clone().into()),
1632 status_lsp_actionable_bg: file
1633 .ui
1634 .status_lsp_actionable_bg
1635 .clone()
1636 .map(|c| c.into())
1637 .unwrap_or_else(|| file.ui.status_warning_indicator_bg.clone().into()),
1638 prompt_fg: file.ui.prompt_fg.into(),
1639 prompt_bg: file.ui.prompt_bg.into(),
1640 prompt_selection_fg: file.ui.prompt_selection_fg.into(),
1641 prompt_selection_bg: file.ui.prompt_selection_bg.into(),
1642 popup_border_fg: file.ui.popup_border_fg.into(),
1643 popup_bg: file.ui.popup_bg.into(),
1644 popup_selection_bg: file.ui.popup_selection_bg.into(),
1645 popup_selection_fg: file.ui.popup_selection_fg.into(),
1646 popup_text_fg: file.ui.popup_text_fg.clone().into(),
1647 text_input_selection_bg: file.ui.text_input_selection_bg.into(),
1648 suggestion_bg: file.ui.suggestion_bg.into(),
1649 suggestion_fg: file
1650 .ui
1651 .suggestion_fg
1652 .clone()
1653 .map(|c| c.into())
1654 .unwrap_or_else(|| file.ui.popup_text_fg.clone().into()),
1655 suggestion_selected_bg: file.ui.suggestion_selected_bg.into(),
1656 help_bg: file.ui.help_bg.into(),
1657 help_fg: file.ui.help_fg.into(),
1658 help_key_fg: file.ui.help_key_fg.into(),
1659 help_separator_fg: file.ui.help_separator_fg.into(),
1660 help_indicator_fg: file.ui.help_indicator_fg.into(),
1661 help_indicator_bg: file.ui.help_indicator_bg.into(),
1662 inline_code_bg: file.ui.inline_code_bg.into(),
1663 split_separator_fg: file.ui.split_separator_fg.into(),
1664 split_separator_hover_fg: file.ui.split_separator_hover_fg.into(),
1665 scrollbar_track_fg: file.ui.scrollbar_track_fg.into(),
1666 scrollbar_thumb_fg: file.ui.scrollbar_thumb_fg.into(),
1667 scrollbar_track_hover_fg: file.ui.scrollbar_track_hover_fg.into(),
1668 scrollbar_thumb_hover_fg: file.ui.scrollbar_thumb_hover_fg.into(),
1669 compose_margin_bg: file.ui.compose_margin_bg.into(),
1670 semantic_highlight_bg: file.ui.semantic_highlight_bg.into(),
1671 semantic_highlight_modifier: file
1672 .ui
1673 .semantic_highlight_modifier
1674 .as_ref()
1675 .map(Modifier::from)
1676 .unwrap_or(Modifier::empty()),
1677 terminal_bg: file.ui.terminal_bg.into(),
1678 terminal_fg: file.ui.terminal_fg.into(),
1679 status_warning_indicator_bg: file.ui.status_warning_indicator_bg.into(),
1680 status_warning_indicator_fg: file.ui.status_warning_indicator_fg.into(),
1681 status_error_indicator_bg: file.ui.status_error_indicator_bg.into(),
1682 status_error_indicator_fg: file.ui.status_error_indicator_fg.into(),
1683 status_warning_indicator_hover_bg: file.ui.status_warning_indicator_hover_bg.into(),
1684 status_warning_indicator_hover_fg: file.ui.status_warning_indicator_hover_fg.into(),
1685 status_error_indicator_hover_bg: file.ui.status_error_indicator_hover_bg.into(),
1686 status_error_indicator_hover_fg: file.ui.status_error_indicator_hover_fg.into(),
1687 tab_drop_zone_bg: file.ui.tab_drop_zone_bg.into(),
1688 tab_drop_zone_border: file.ui.tab_drop_zone_border.into(),
1689 settings_selected_bg: file.ui.settings_selected_bg.into(),
1690 settings_selected_fg: file.ui.settings_selected_fg.into(),
1691 file_status_added_fg: file
1692 .ui
1693 .file_status_added_fg
1694 .map(|c| c.into())
1695 .unwrap_or_else(|| file.diagnostic.info_fg.clone().into()),
1696 file_status_modified_fg: file
1697 .ui
1698 .file_status_modified_fg
1699 .map(|c| c.into())
1700 .unwrap_or_else(|| file.diagnostic.warning_fg.clone().into()),
1701 file_status_deleted_fg: file
1702 .ui
1703 .file_status_deleted_fg
1704 .map(|c| c.into())
1705 .unwrap_or_else(|| file.diagnostic.error_fg.clone().into()),
1706 file_status_renamed_fg: file
1707 .ui
1708 .file_status_renamed_fg
1709 .map(|c| c.into())
1710 .unwrap_or_else(|| file.diagnostic.info_fg.clone().into()),
1711 file_status_untracked_fg: file
1712 .ui
1713 .file_status_untracked_fg
1714 .map(|c| c.into())
1715 .unwrap_or_else(|| file.diagnostic.hint_fg.clone().into()),
1716 file_status_conflicted_fg: file
1717 .ui
1718 .file_status_conflicted_fg
1719 .map(|c| c.into())
1720 .unwrap_or_else(|| file.diagnostic.error_fg.clone().into()),
1721 search_match_bg: file.search.match_bg.into(),
1722 search_match_fg: file.search.match_fg.into(),
1723 search_label_bg: file.search.label_bg.into(),
1724 search_label_fg: file.search.label_fg.into(),
1725 diagnostic_error_fg: file.diagnostic.error_fg.into(),
1726 diagnostic_error_bg: file.diagnostic.error_bg.into(),
1727 diagnostic_warning_fg: file.diagnostic.warning_fg.into(),
1728 diagnostic_warning_bg: file.diagnostic.warning_bg.into(),
1729 diagnostic_info_fg: file.diagnostic.info_fg.into(),
1730 diagnostic_info_bg: file.diagnostic.info_bg.into(),
1731 diagnostic_hint_fg: file.diagnostic.hint_fg.into(),
1732 diagnostic_hint_bg: file.diagnostic.hint_bg.into(),
1733 syntax_keyword: file.syntax.keyword.into(),
1734 syntax_string: file.syntax.string.into(),
1735 syntax_comment: file.syntax.comment.into(),
1736 syntax_function: file.syntax.function.into(),
1737 syntax_type: file.syntax.type_.into(),
1738 syntax_variable: file.syntax.variable.into(),
1739 syntax_variable_builtin: file.syntax.variable_builtin.into(),
1740 syntax_constant: file.syntax.constant.into(),
1741 syntax_operator: file.syntax.operator.into(),
1742 syntax_punctuation_bracket: file.syntax.punctuation_bracket.into(),
1743 syntax_punctuation_delimiter: file.syntax.punctuation_delimiter.into(),
1744 }
1745 }
1746}
1747
1748impl From<Theme> for ThemeFile {
1749 fn from(theme: Theme) -> Self {
1750 Self {
1751 name: theme.name,
1752 extends: None,
1755 editor: EditorColors {
1756 bg: theme.editor_bg.into(),
1757 fg: theme.editor_fg.into(),
1758 cursor: theme.cursor.into(),
1759 inactive_cursor: theme.inactive_cursor.into(),
1760 selection_bg: theme.selection_bg.into(),
1761 selection_modifier: if theme.selection_modifier.is_empty() {
1762 None
1763 } else {
1764 Some(theme.selection_modifier.into())
1765 },
1766 current_line_bg: theme.current_line_bg.into(),
1767 line_number_fg: theme.line_number_fg.into(),
1768 line_number_bg: theme.line_number_bg.into(),
1769 diff_add_bg: theme.diff_add_bg.into(),
1770 diff_remove_bg: theme.diff_remove_bg.into(),
1771 diff_add_highlight_bg: Some(theme.diff_add_highlight_bg.into()),
1772 diff_remove_highlight_bg: Some(theme.diff_remove_highlight_bg.into()),
1773 diff_modify_bg: theme.diff_modify_bg.into(),
1774 diff_add_collision_fg: theme.diff_add_collision_fg.map(|c| c.into()),
1775 diff_remove_collision_fg: theme.diff_remove_collision_fg.map(|c| c.into()),
1776 diff_modify_collision_fg: theme.diff_modify_collision_fg.map(|c| c.into()),
1777 ruler_bg: theme.ruler_bg.into(),
1778 indentation_guide_fg: Some(theme.indentation_guide_fg.into()),
1779 whitespace_indicator_fg: theme.whitespace_indicator_fg.into(),
1780 bracket_match_fg: theme.bracket_match_fg.into(),
1781 bracket_rainbow_1: theme.bracket_rainbow_1.into(),
1782 bracket_rainbow_2: theme.bracket_rainbow_2.into(),
1783 bracket_rainbow_3: theme.bracket_rainbow_3.into(),
1784 bracket_rainbow_4: theme.bracket_rainbow_4.into(),
1785 bracket_rainbow_5: theme.bracket_rainbow_5.into(),
1786 bracket_rainbow_6: theme.bracket_rainbow_6.into(),
1787 after_eof_bg: Some(theme.after_eof_bg.into()),
1788 },
1789 ui: UiColors {
1790 tab_active_fg: theme.tab_active_fg.into(),
1791 tab_active_bg: theme.tab_active_bg.into(),
1792 tab_inactive_fg: theme.tab_inactive_fg.into(),
1793 tab_inactive_bg: theme.tab_inactive_bg.into(),
1794 tab_separator_bg: theme.tab_separator_bg.into(),
1795 tab_close_hover_fg: theme.tab_close_hover_fg.into(),
1796 tab_hover_bg: theme.tab_hover_bg.into(),
1797 menu_bg: theme.menu_bg.into(),
1798 menu_fg: theme.menu_fg.into(),
1799 menu_active_bg: theme.menu_active_bg.into(),
1800 menu_active_fg: theme.menu_active_fg.into(),
1801 menu_dropdown_bg: theme.menu_dropdown_bg.into(),
1802 menu_dropdown_fg: theme.menu_dropdown_fg.into(),
1803 menu_highlight_bg: theme.menu_highlight_bg.into(),
1804 menu_highlight_fg: theme.menu_highlight_fg.into(),
1805 menu_border_fg: theme.menu_border_fg.into(),
1806 menu_separator_fg: theme.menu_separator_fg.into(),
1807 menu_hover_bg: theme.menu_hover_bg.into(),
1808 menu_hover_fg: theme.menu_hover_fg.into(),
1809 menu_disabled_fg: theme.menu_disabled_fg.into(),
1810 menu_disabled_bg: theme.menu_disabled_bg.into(),
1811 status_bar_fg: theme.status_bar_fg.into(),
1812 status_bar_bg: theme.status_bar_bg.into(),
1813 status_palette_fg: Some(theme.status_palette_fg.into()),
1814 status_palette_bg: Some(theme.status_palette_bg.into()),
1815 status_separator_fg: Some(theme.status_separator_fg.into()),
1816 status_separator_bg: Some(theme.status_separator_bg.into()),
1817 status_lsp_on_fg: Some(theme.status_lsp_on_fg.into()),
1818 status_lsp_on_bg: Some(theme.status_lsp_on_bg.into()),
1819 status_lsp_actionable_fg: Some(theme.status_lsp_actionable_fg.into()),
1820 status_lsp_actionable_bg: Some(theme.status_lsp_actionable_bg.into()),
1821 prompt_fg: theme.prompt_fg.into(),
1822 prompt_bg: theme.prompt_bg.into(),
1823 prompt_selection_fg: theme.prompt_selection_fg.into(),
1824 prompt_selection_bg: theme.prompt_selection_bg.into(),
1825 popup_border_fg: theme.popup_border_fg.into(),
1826 popup_bg: theme.popup_bg.into(),
1827 popup_selection_bg: theme.popup_selection_bg.into(),
1828 popup_selection_fg: theme.popup_selection_fg.into(),
1829 popup_text_fg: theme.popup_text_fg.into(),
1830 text_input_selection_bg: theme.text_input_selection_bg.into(),
1831 suggestion_bg: theme.suggestion_bg.into(),
1832 suggestion_fg: Some(theme.suggestion_fg.into()),
1833 suggestion_selected_bg: theme.suggestion_selected_bg.into(),
1834 help_bg: theme.help_bg.into(),
1835 help_fg: theme.help_fg.into(),
1836 help_key_fg: theme.help_key_fg.into(),
1837 help_separator_fg: theme.help_separator_fg.into(),
1838 help_indicator_fg: theme.help_indicator_fg.into(),
1839 help_indicator_bg: theme.help_indicator_bg.into(),
1840 inline_code_bg: theme.inline_code_bg.into(),
1841 split_separator_fg: theme.split_separator_fg.into(),
1842 split_separator_hover_fg: theme.split_separator_hover_fg.into(),
1843 scrollbar_track_fg: theme.scrollbar_track_fg.into(),
1844 scrollbar_thumb_fg: theme.scrollbar_thumb_fg.into(),
1845 scrollbar_track_hover_fg: theme.scrollbar_track_hover_fg.into(),
1846 scrollbar_thumb_hover_fg: theme.scrollbar_thumb_hover_fg.into(),
1847 compose_margin_bg: theme.compose_margin_bg.into(),
1848 semantic_highlight_bg: theme.semantic_highlight_bg.into(),
1849 semantic_highlight_modifier: if theme.semantic_highlight_modifier.is_empty() {
1850 None
1851 } else {
1852 Some(theme.semantic_highlight_modifier.into())
1853 },
1854 terminal_bg: theme.terminal_bg.into(),
1855 terminal_fg: theme.terminal_fg.into(),
1856 status_warning_indicator_bg: theme.status_warning_indicator_bg.into(),
1857 status_warning_indicator_fg: theme.status_warning_indicator_fg.into(),
1858 status_error_indicator_bg: theme.status_error_indicator_bg.into(),
1859 status_error_indicator_fg: theme.status_error_indicator_fg.into(),
1860 status_warning_indicator_hover_bg: theme.status_warning_indicator_hover_bg.into(),
1861 status_warning_indicator_hover_fg: theme.status_warning_indicator_hover_fg.into(),
1862 status_error_indicator_hover_bg: theme.status_error_indicator_hover_bg.into(),
1863 status_error_indicator_hover_fg: theme.status_error_indicator_hover_fg.into(),
1864 tab_drop_zone_bg: theme.tab_drop_zone_bg.into(),
1865 tab_drop_zone_border: theme.tab_drop_zone_border.into(),
1866 settings_selected_bg: theme.settings_selected_bg.into(),
1867 settings_selected_fg: theme.settings_selected_fg.into(),
1868 file_status_added_fg: Some(theme.file_status_added_fg.into()),
1869 file_status_modified_fg: Some(theme.file_status_modified_fg.into()),
1870 file_status_deleted_fg: Some(theme.file_status_deleted_fg.into()),
1871 file_status_renamed_fg: Some(theme.file_status_renamed_fg.into()),
1872 file_status_untracked_fg: Some(theme.file_status_untracked_fg.into()),
1873 file_status_conflicted_fg: Some(theme.file_status_conflicted_fg.into()),
1874 },
1875 search: SearchColors {
1876 match_bg: theme.search_match_bg.into(),
1877 match_fg: theme.search_match_fg.into(),
1878 label_bg: theme.search_label_bg.into(),
1879 label_fg: theme.search_label_fg.into(),
1880 },
1881 diagnostic: DiagnosticColors {
1882 error_fg: theme.diagnostic_error_fg.into(),
1883 error_bg: theme.diagnostic_error_bg.into(),
1884 warning_fg: theme.diagnostic_warning_fg.into(),
1885 warning_bg: theme.diagnostic_warning_bg.into(),
1886 info_fg: theme.diagnostic_info_fg.into(),
1887 info_bg: theme.diagnostic_info_bg.into(),
1888 hint_fg: theme.diagnostic_hint_fg.into(),
1889 hint_bg: theme.diagnostic_hint_bg.into(),
1890 },
1891 syntax: SyntaxColors {
1892 keyword: theme.syntax_keyword.into(),
1893 string: theme.syntax_string.into(),
1894 comment: theme.syntax_comment.into(),
1895 function: theme.syntax_function.into(),
1896 type_: theme.syntax_type.into(),
1897 variable: theme.syntax_variable.into(),
1898 variable_builtin: theme.syntax_variable_builtin.into(),
1899 constant: theme.syntax_constant.into(),
1900 operator: theme.syntax_operator.into(),
1901 punctuation_bracket: theme.syntax_punctuation_bracket.into(),
1902 punctuation_delimiter: theme.syntax_punctuation_delimiter.into(),
1903 },
1904 }
1905 }
1906}
1907
1908fn resolve_base_theme(theme_file: &ThemeFile, raw: &serde_json::Value) -> Result<Theme, String> {
1915 if let Some(extends) = theme_file.extends.as_deref() {
1917 let name = extends.strip_prefix("builtin://").unwrap_or(extends);
1918 return Theme::load_builtin(name).ok_or_else(|| {
1919 let available: Vec<&str> = BUILTIN_THEMES.iter().map(|t| t.name).collect();
1920 format!(
1921 "theme `extends: {:?}` does not match any built-in theme. \
1922 Available: {}. \
1923 Inheriting from other user themes is not yet supported.",
1924 extends,
1925 available.join(", ")
1926 )
1927 });
1928 }
1929
1930 if let Some(bg) = raw
1936 .get("editor")
1937 .and_then(|e| e.get("bg"))
1938 .cloned()
1939 .and_then(|v| serde_json::from_value::<ColorDef>(v).ok())
1940 {
1941 let color: Color = bg.into();
1942 if let Some((r, g, b)) = color_to_rgb(color) {
1943 let lum = relative_luminance(r, g, b);
1944 let base_name = if lum > 0.5 { THEME_LIGHT } else { THEME_DARK };
1945 if let Some(base) = Theme::load_builtin(base_name) {
1946 return Ok(base);
1947 }
1948 }
1949 }
1950
1951 Ok(theme_file.clone().into())
1953}
1954
1955fn relative_luminance(r: u8, g: u8, b: u8) -> f64 {
1958 0.2126 * (r as f64 / 255.0) + 0.7152 * (g as f64 / 255.0) + 0.0722 * (b as f64 / 255.0)
1959}
1960
1961fn apply_theme_overrides(theme: &mut Theme, theme_file: &ThemeFile, raw: &serde_json::Value) {
1966 theme.name = theme_file.name.clone();
1968
1969 for section in ["editor", "ui", "search", "diagnostic", "syntax"] {
1970 let Some(obj) = raw.get(section).and_then(|v| v.as_object()) else {
1971 continue;
1972 };
1973 for (field, value) in obj {
1974 if value.is_null() {
1977 continue;
1978 }
1979 let key = format!("{}.{}", section, field);
1980 if let Ok(color_def) = serde_json::from_value::<ColorDef>(value.clone()) {
1981 if let Some(slot) = theme.resolve_theme_key_mut(&key) {
1982 *slot = color_def.into();
1983 }
1984 }
1985 }
1986 }
1987
1988 if raw
1989 .get("editor")
1990 .and_then(|v| v.as_object())
1991 .is_some_and(|editor| {
1992 editor.contains_key("whitespace_indicator_fg")
1993 && !editor.contains_key("indentation_guide_fg")
1994 })
1995 {
1996 theme.indentation_guide_fg = theme.whitespace_indicator_fg;
1997 }
1998}
1999
2000impl Theme {
2001 pub fn is_light(&self) -> bool {
2007 color_to_rgb(self.editor_bg)
2008 .map(|(r, g, b)| relative_luminance(r, g, b) > 0.5)
2009 .unwrap_or(false)
2010 }
2011
2012 pub fn load_builtin(name: &str) -> Option<Self> {
2014 BUILTIN_THEMES
2015 .iter()
2016 .find(|t| t.name == name)
2017 .and_then(|t| serde_json::from_str::<ThemeFile>(t.json).ok())
2018 .map(|tf| tf.into())
2019 }
2020
2021 pub fn from_json(json: &str) -> Result<Self, String> {
2031 let raw: serde_json::Value =
2036 serde_json::from_str(json).map_err(|e| format!("Failed to parse theme JSON: {}", e))?;
2037 let theme_file: ThemeFile = serde_json::from_value(raw.clone())
2038 .map_err(|e| format!("Failed to parse theme: {}", e))?;
2039
2040 let mut theme = resolve_base_theme(&theme_file, &raw)?;
2041 apply_theme_overrides(&mut theme, &theme_file, &raw);
2042 Ok(theme)
2043 }
2044
2045 pub fn modifier_for_bg_key(&self, key: &str) -> Modifier {
2054 match key {
2055 "editor.selection_bg" => self.selection_modifier,
2056 "ui.semantic_highlight_bg" => self.semantic_highlight_modifier,
2057 _ => Modifier::empty(),
2058 }
2059 }
2060}
2061
2062fn split_theme_key(key: &str) -> Option<(&str, &str)> {
2065 let (section, field) = key.split_once('.')?;
2066 if field.contains('.') {
2067 return None;
2068 }
2069 Some((section, field))
2070}
2071
2072macro_rules! theme_color_keys {
2084 (
2085 $(
2086 $section:literal => {
2087 $( $field_key:literal => $kind:tt $field:ident ),* $(,)?
2088 }
2089 ),* $(,)?
2090 ) => {
2091 impl Theme {
2092 pub fn resolve_theme_key(&self, key: &str) -> Option<Color> {
2097 let (section, field) = split_theme_key(key)?;
2098 match section {
2099 $(
2100 $section => match field {
2101 $( $field_key => theme_color_keys!(@get self, $kind $field), )*
2102 _ => None,
2103 },
2104 )*
2105 _ => None,
2106 }
2107 }
2108
2109 pub fn resolve_theme_key_mut(&mut self, key: &str) -> Option<&mut Color> {
2113 let (section, field) = split_theme_key(key)?;
2114 match section {
2115 $(
2116 $section => match field {
2117 $( $field_key => theme_color_keys!(@get_mut self, $kind $field), )*
2118 _ => None,
2119 },
2120 )*
2121 _ => None,
2122 }
2123 }
2124 }
2125 };
2126
2127 (@get $self:ident, color $field:ident) => { Some($self.$field) };
2130 (@get $self:ident, opt $field:ident) => { $self.$field };
2131 (@get_mut $self:ident, color $field:ident) => { Some(&mut $self.$field) };
2132 (@get_mut $self:ident, opt $field:ident) => { $self.$field.as_mut() };
2133}
2134
2135theme_color_keys! {
2136 "editor" => {
2137 "after_eof_bg" => color after_eof_bg,
2138 "bg" => color editor_bg,
2139 "current_line_bg" => color current_line_bg,
2140 "cursor" => color cursor,
2141 "diff_add_bg" => color diff_add_bg,
2142 "diff_add_collision_fg" => opt diff_add_collision_fg,
2143 "diff_add_highlight_bg" => color diff_add_highlight_bg,
2144 "diff_modify_bg" => color diff_modify_bg,
2145 "diff_modify_collision_fg" => opt diff_modify_collision_fg,
2146 "diff_remove_bg" => color diff_remove_bg,
2147 "diff_remove_collision_fg" => opt diff_remove_collision_fg,
2148 "diff_remove_highlight_bg" => color diff_remove_highlight_bg,
2149 "fg" => color editor_fg,
2150 "inactive_cursor" => color inactive_cursor,
2151 "indentation_guide_fg" => color indentation_guide_fg,
2152 "line_number_bg" => color line_number_bg,
2153 "line_number_fg" => color line_number_fg,
2154 "ruler_bg" => color ruler_bg,
2155 "selection_bg" => color selection_bg,
2156 "whitespace_indicator_fg" => color whitespace_indicator_fg,
2157 "bracket_match_fg" => color bracket_match_fg,
2158 "bracket_rainbow_1" => color bracket_rainbow_1,
2159 "bracket_rainbow_2" => color bracket_rainbow_2,
2160 "bracket_rainbow_3" => color bracket_rainbow_3,
2161 "bracket_rainbow_4" => color bracket_rainbow_4,
2162 "bracket_rainbow_5" => color bracket_rainbow_5,
2163 "bracket_rainbow_6" => color bracket_rainbow_6,
2164 },
2165 "ui" => {
2166 "compose_margin_bg" => color compose_margin_bg,
2167 "file_status_added_fg" => color file_status_added_fg,
2168 "file_status_conflicted_fg" => color file_status_conflicted_fg,
2169 "file_status_deleted_fg" => color file_status_deleted_fg,
2170 "file_status_modified_fg" => color file_status_modified_fg,
2171 "file_status_renamed_fg" => color file_status_renamed_fg,
2172 "file_status_untracked_fg" => color file_status_untracked_fg,
2173 "help_bg" => color help_bg,
2174 "help_fg" => color help_fg,
2175 "help_indicator_bg" => color help_indicator_bg,
2176 "help_indicator_fg" => color help_indicator_fg,
2177 "help_key_fg" => color help_key_fg,
2178 "help_separator_fg" => color help_separator_fg,
2179 "inline_code_bg" => color inline_code_bg,
2180 "menu_active_bg" => color menu_active_bg,
2181 "menu_active_fg" => color menu_active_fg,
2182 "menu_bg" => color menu_bg,
2183 "menu_border_fg" => color menu_border_fg,
2184 "menu_disabled_bg" => color menu_disabled_bg,
2185 "menu_disabled_fg" => color menu_disabled_fg,
2186 "menu_dropdown_bg" => color menu_dropdown_bg,
2187 "menu_dropdown_fg" => color menu_dropdown_fg,
2188 "menu_fg" => color menu_fg,
2189 "menu_highlight_bg" => color menu_highlight_bg,
2190 "menu_highlight_fg" => color menu_highlight_fg,
2191 "menu_hover_bg" => color menu_hover_bg,
2192 "menu_hover_fg" => color menu_hover_fg,
2193 "menu_separator_fg" => color menu_separator_fg,
2194 "popup_bg" => color popup_bg,
2195 "popup_border_fg" => color popup_border_fg,
2196 "popup_selection_bg" => color popup_selection_bg,
2197 "popup_selection_fg" => color popup_selection_fg,
2198 "popup_text_fg" => color popup_text_fg,
2199 "prompt_bg" => color prompt_bg,
2200 "prompt_fg" => color prompt_fg,
2201 "prompt_selection_bg" => color prompt_selection_bg,
2202 "prompt_selection_fg" => color prompt_selection_fg,
2203 "scrollbar_thumb_fg" => color scrollbar_thumb_fg,
2204 "scrollbar_thumb_hover_fg" => color scrollbar_thumb_hover_fg,
2205 "scrollbar_track_fg" => color scrollbar_track_fg,
2206 "scrollbar_track_hover_fg" => color scrollbar_track_hover_fg,
2207 "semantic_highlight_bg" => color semantic_highlight_bg,
2208 "settings_selected_bg" => color settings_selected_bg,
2209 "settings_selected_fg" => color settings_selected_fg,
2210 "split_separator_fg" => color split_separator_fg,
2211 "split_separator_hover_fg" => color split_separator_hover_fg,
2212 "status_bar_bg" => color status_bar_bg,
2213 "status_bar_fg" => color status_bar_fg,
2214 "status_error_indicator_bg" => color status_error_indicator_bg,
2215 "status_error_indicator_fg" => color status_error_indicator_fg,
2216 "status_error_indicator_hover_bg" => color status_error_indicator_hover_bg,
2217 "status_error_indicator_hover_fg" => color status_error_indicator_hover_fg,
2218 "status_lsp_actionable_bg" => color status_lsp_actionable_bg,
2219 "status_lsp_actionable_fg" => color status_lsp_actionable_fg,
2220 "status_lsp_on_bg" => color status_lsp_on_bg,
2221 "status_lsp_on_fg" => color status_lsp_on_fg,
2222 "status_palette_bg" => color status_palette_bg,
2223 "status_palette_fg" => color status_palette_fg,
2224 "status_separator_bg" => color status_separator_bg,
2225 "status_separator_fg" => color status_separator_fg,
2226 "status_warning_indicator_bg" => color status_warning_indicator_bg,
2227 "status_warning_indicator_fg" => color status_warning_indicator_fg,
2228 "status_warning_indicator_hover_bg" => color status_warning_indicator_hover_bg,
2229 "status_warning_indicator_hover_fg" => color status_warning_indicator_hover_fg,
2230 "suggestion_bg" => color suggestion_bg,
2231 "suggestion_fg" => color suggestion_fg,
2232 "suggestion_selected_bg" => color suggestion_selected_bg,
2233 "tab_active_bg" => color tab_active_bg,
2234 "tab_active_fg" => color tab_active_fg,
2235 "tab_close_hover_fg" => color tab_close_hover_fg,
2236 "tab_drop_zone_bg" => color tab_drop_zone_bg,
2237 "tab_drop_zone_border" => color tab_drop_zone_border,
2238 "tab_hover_bg" => color tab_hover_bg,
2239 "tab_inactive_bg" => color tab_inactive_bg,
2240 "tab_inactive_fg" => color tab_inactive_fg,
2241 "tab_separator_bg" => color tab_separator_bg,
2242 "terminal_bg" => color terminal_bg,
2243 "terminal_fg" => color terminal_fg,
2244 "text_input_selection_bg" => color text_input_selection_bg,
2245 },
2246 "syntax" => {
2247 "comment" => color syntax_comment,
2248 "constant" => color syntax_constant,
2249 "function" => color syntax_function,
2250 "keyword" => color syntax_keyword,
2251 "operator" => color syntax_operator,
2252 "punctuation_bracket" => color syntax_punctuation_bracket,
2253 "punctuation_delimiter" => color syntax_punctuation_delimiter,
2254 "string" => color syntax_string,
2255 "type" => color syntax_type,
2256 "variable" => color syntax_variable,
2257 "variable_builtin" => color syntax_variable_builtin,
2258 },
2259 "diagnostic" => {
2260 "error_bg" => color diagnostic_error_bg,
2261 "error_fg" => color diagnostic_error_fg,
2262 "hint_bg" => color diagnostic_hint_bg,
2263 "hint_fg" => color diagnostic_hint_fg,
2264 "info_bg" => color diagnostic_info_bg,
2265 "info_fg" => color diagnostic_info_fg,
2266 "warning_bg" => color diagnostic_warning_bg,
2267 "warning_fg" => color diagnostic_warning_fg,
2268 },
2269 "search" => {
2270 "label_bg" => color search_label_bg,
2271 "label_fg" => color search_label_fg,
2272 "match_bg" => color search_match_bg,
2273 "match_fg" => color search_match_fg,
2274 },
2275}
2276
2277impl Theme {
2278 pub fn override_colors<I, K>(&mut self, overrides: I) -> usize
2283 where
2284 I: IntoIterator<Item = (K, Color)>,
2285 K: AsRef<str>,
2286 {
2287 let mut applied = 0;
2288 for (key, color) in overrides {
2289 if let Some(slot) = self.resolve_theme_key_mut(key.as_ref()) {
2290 *slot = color;
2291 applied += 1;
2292 }
2293 }
2294 applied
2295 }
2296}
2297
2298pub fn get_theme_schema() -> serde_json::Value {
2306 use schemars::schema_for;
2307 let schema = schema_for!(ThemeFile);
2308 serde_json::to_value(&schema).unwrap_or_default()
2309}
2310
2311pub fn get_builtin_themes() -> serde_json::Value {
2313 let mut map = serde_json::Map::new();
2314 for theme in BUILTIN_THEMES {
2315 map.insert(
2316 theme.name.to_string(),
2317 serde_json::Value::String(theme.json.to_string()),
2318 );
2319 }
2320 serde_json::Value::Object(map)
2321}
2322
2323#[cfg(test)]
2324mod tests {
2325 use super::*;
2326
2327 #[test]
2328 fn test_load_builtin_theme() {
2329 let dark = Theme::load_builtin(THEME_DARK).expect("Dark theme must exist");
2330 assert_eq!(dark.name, THEME_DARK);
2331
2332 let light = Theme::load_builtin(THEME_LIGHT).expect("Light theme must exist");
2333 assert_eq!(light.name, THEME_LIGHT);
2334
2335 let high_contrast =
2336 Theme::load_builtin(THEME_HIGH_CONTRAST).expect("High contrast theme must exist");
2337 assert_eq!(high_contrast.name, THEME_HIGH_CONTRAST);
2338
2339 let terminal = Theme::load_builtin(THEME_TERMINAL).expect("Terminal theme must exist");
2340 assert_eq!(terminal.name, THEME_TERMINAL);
2341 assert_eq!(terminal.editor_bg, Color::Reset);
2345 assert_eq!(terminal.editor_fg, Color::Reset);
2346 assert_eq!(terminal.terminal_bg, Color::Reset);
2347 assert!(terminal.selection_modifier.contains(Modifier::REVERSED));
2350 assert!(terminal
2351 .semantic_highlight_modifier
2352 .contains(Modifier::BOLD));
2353 }
2354
2355 #[test]
2356 fn test_suggestion_fg_falls_back_and_contrasts() {
2357 let dracula = Theme::load_builtin(THEME_DRACULA).expect("Dracula theme must exist");
2364 assert_eq!(
2365 dracula.suggestion_fg, dracula.popup_text_fg,
2366 "suggestion_fg should fall back to popup_text_fg when unset"
2367 );
2368 assert_ne!(
2369 dracula.suggestion_fg, dracula.suggestion_bg,
2370 "suggestion_fg must contrast with suggestion_bg, not vanish into it"
2371 );
2372 }
2373
2374 #[test]
2375 fn test_modifier_def_round_trip() {
2376 let cases = [
2377 (vec!["reversed"], Modifier::REVERSED),
2378 (
2379 vec!["bold", "underlined"],
2380 Modifier::BOLD | Modifier::UNDERLINED,
2381 ),
2382 (vec!["italic", "dim"], Modifier::ITALIC | Modifier::DIM),
2383 (vec!["reverse"], Modifier::REVERSED), (vec!["underline"], Modifier::UNDERLINED), ];
2386 for (strs, expected) in cases {
2387 let def = ModifierDef(strs.iter().map(|s| s.to_string()).collect());
2388 let m: Modifier = (&def).into();
2389 assert_eq!(m, expected, "ModifierDef({:?}) -> Modifier", strs);
2390 }
2391 }
2392
2393 #[test]
2394 fn test_modifier_def_unknown_strings_are_dropped() {
2395 let def = ModifierDef(vec!["reversed".into(), "wibble".into(), "bold".into()]);
2398 let m: Modifier = (&def).into();
2399 assert_eq!(m, Modifier::REVERSED | Modifier::BOLD);
2400 }
2401
2402 #[test]
2403 fn test_themes_without_modifier_default_to_empty() {
2404 let dark = Theme::load_builtin(THEME_DARK).expect("Dark theme must exist");
2409 assert!(dark.selection_modifier.is_empty());
2410 assert!(dark.semantic_highlight_modifier.is_empty());
2411 }
2412
2413 #[test]
2414 fn test_modifier_for_bg_key_lookup() {
2415 let terminal = Theme::load_builtin(THEME_TERMINAL).expect("Terminal theme must exist");
2416 assert!(terminal
2419 .modifier_for_bg_key("editor.selection_bg")
2420 .contains(Modifier::REVERSED));
2421 assert!(terminal
2422 .modifier_for_bg_key("ui.semantic_highlight_bg")
2423 .contains(Modifier::BOLD));
2424 assert!(terminal
2427 .modifier_for_bg_key("ui.popup_selection_bg")
2428 .is_empty());
2429 assert!(terminal.modifier_for_bg_key("nonsense").is_empty());
2430 }
2431
2432 #[test]
2433 fn test_modifier_round_trip_via_theme_file() {
2434 let original = Theme::load_builtin(THEME_TERMINAL).expect("Terminal theme must exist");
2436 let file: ThemeFile = original.clone().into();
2437 let json = serde_json::to_string(&file).expect("serialize");
2438 let parsed: ThemeFile = serde_json::from_str(&json).expect("parse");
2439 let round_tripped: Theme = parsed.into();
2440 assert_eq!(
2441 round_tripped.selection_modifier,
2442 original.selection_modifier
2443 );
2444 assert_eq!(
2445 round_tripped.semantic_highlight_modifier,
2446 original.semantic_highlight_modifier
2447 );
2448 }
2449
2450 #[test]
2451 fn test_builtin_themes_match_schema() {
2452 for theme in BUILTIN_THEMES {
2453 let _: ThemeFile = serde_json::from_str(theme.json)
2454 .unwrap_or_else(|_| panic!("Theme '{}' does not match schema", theme.name));
2455 }
2456 }
2457
2458 #[test]
2459 fn test_from_json() {
2460 let json = r#"{"name":"test","editor":{},"ui":{},"search":{},"diagnostic":{},"syntax":{}}"#;
2461 let theme = Theme::from_json(json).expect("Should parse minimal theme");
2462 assert_eq!(theme.name, "test");
2463 }
2464
2465 #[test]
2477 fn test_minimal_user_theme_from_issue_1281_loads() {
2478 let json = r#"{
2480 "name": "gruvbox-light-orange",
2481 "editor": {
2482 "bg": [251, 241, 199],
2483 "fg": [60, 56, 54],
2484 "cursor": [254, 128, 25],
2485 "selection_bg": [213, 196, 161]
2486 },
2487 "syntax": {
2488 "keyword": [175, 58, 3],
2489 "string": [152, 151, 26],
2490 "comment": [146, 131, 116]
2491 }
2492}"#;
2493 let theme = Theme::from_json(json)
2494 .expect("Theme from issue #1281 should parse without `ui`/`search`/`diagnostic`");
2495 assert_eq!(theme.name, "gruvbox-light-orange");
2496
2497 assert_eq!(theme.editor_bg, Color::Rgb(251, 241, 199));
2499 assert_eq!(theme.editor_fg, Color::Rgb(60, 56, 54));
2500 assert_eq!(theme.cursor, Color::Rgb(254, 128, 25));
2501 assert_eq!(theme.selection_bg, Color::Rgb(213, 196, 161));
2502 assert_eq!(theme.syntax_keyword, Color::Rgb(175, 58, 3));
2503 assert_eq!(theme.syntax_string, Color::Rgb(152, 151, 26));
2504 assert_eq!(theme.syntax_comment, Color::Rgb(146, 131, 116));
2505
2506 let light = Theme::load_builtin(THEME_LIGHT).expect("light builtin");
2510 assert_eq!(
2511 theme.status_bar_fg, light.status_bar_fg,
2512 "ui.status_bar_fg should inherit from builtin://light when bg is bright"
2513 );
2514 assert_eq!(
2515 theme.diagnostic_error_fg, light.diagnostic_error_fg,
2516 "diagnostic.error_fg should inherit from builtin://light when bg is bright"
2517 );
2518 assert_eq!(
2519 theme.menu_bg, light.menu_bg,
2520 "ui.menu_bg should inherit from builtin://light when bg is bright"
2521 );
2522 }
2523
2524 #[test]
2527 fn test_extends_explicit_builtin_wins_over_auto_infer() {
2528 let json = r#"{
2531 "name": "explicit-light",
2532 "extends": "builtin://light",
2533 "editor": { "bg": [0, 0, 0] }
2534 }"#;
2535 let theme = Theme::from_json(json).expect("extends should resolve");
2536 let light = Theme::load_builtin(THEME_LIGHT).expect("light builtin");
2537
2538 assert_eq!(theme.editor_bg, Color::Rgb(0, 0, 0));
2540 assert_eq!(theme.menu_bg, light.menu_bg);
2542 assert_eq!(theme.tab_active_bg, light.tab_active_bg);
2543 assert_eq!(theme.diagnostic_warning_fg, light.diagnostic_warning_fg);
2544 }
2545
2546 #[test]
2551 fn test_extends_bare_builtin_name_works() {
2552 let json = r#"{ "name": "x", "extends": "high-contrast" }"#;
2553 let theme = Theme::from_json(json).expect("bare-name extends should resolve");
2554 let hc = Theme::load_builtin("high-contrast").expect("hc builtin");
2555 assert_eq!(theme.menu_bg, hc.menu_bg);
2556 }
2557
2558 #[test]
2563 fn test_extends_unknown_builtin_errors_with_helpful_message() {
2564 let json = r#"{ "name": "x", "extends": "builtin://no-such-theme" }"#;
2565 let err = Theme::from_json(json).expect_err("unknown extends must error");
2566 assert!(
2567 err.contains("no-such-theme"),
2568 "error should quote the bad value, got: {}",
2569 err
2570 );
2571 assert!(
2572 err.contains("dark") && err.contains("light"),
2573 "error should list available builtins, got: {}",
2574 err
2575 );
2576 }
2577
2578 #[test]
2582 fn test_auto_infer_dark_base_from_dark_bg() {
2583 let json = r#"{ "name": "x", "editor": { "bg": [20, 20, 30] } }"#;
2584 let theme = Theme::from_json(json).expect("should parse");
2585 let dark = Theme::load_builtin(THEME_DARK).expect("dark builtin");
2586 assert_eq!(theme.menu_bg, dark.menu_bg);
2587 assert_eq!(theme.diagnostic_error_fg, dark.diagnostic_error_fg);
2588 }
2589
2590 #[test]
2594 fn test_no_inheritance_signal_uses_hardcoded_defaults() {
2595 let json = r#"{ "name": "x" }"#;
2596 let theme = Theme::from_json(json).expect("should parse");
2597 assert_eq!(theme.editor_bg, Color::Rgb(30, 30, 30));
2600 }
2601
2602 #[test]
2603 fn test_indentation_guide_fg_inherits_whitespace_indicator_fg_when_omitted() {
2604 let json = r#"{
2605 "name": "x",
2606 "extends": "builtin://dark",
2607 "editor": { "whitespace_indicator_fg": [12, 34, 56] }
2608 }"#;
2609 let theme = Theme::from_json(json).expect("should parse");
2610
2611 assert_eq!(theme.whitespace_indicator_fg, Color::Rgb(12, 34, 56));
2612 assert_eq!(theme.indentation_guide_fg, Color::Rgb(12, 34, 56));
2613 }
2614
2615 #[test]
2616 fn test_explicit_indentation_guide_fg_overrides_whitespace_fallback() {
2617 let json = r#"{
2618 "name": "x",
2619 "editor": {
2620 "whitespace_indicator_fg": [12, 34, 56],
2621 "indentation_guide_fg": [65, 67, 69]
2622 }
2623 }"#;
2624 let theme = Theme::from_json(json).expect("should parse");
2625
2626 assert_eq!(theme.whitespace_indicator_fg, Color::Rgb(12, 34, 56));
2627 assert_eq!(theme.indentation_guide_fg, Color::Rgb(65, 67, 69));
2628 }
2629
2630 #[test]
2634 fn test_theme_without_name_still_errors() {
2635 let json = r#"{ "editor": {} }"#;
2636 let err = Theme::from_json(json).expect_err("missing `name` must be an error");
2637 assert!(
2638 err.contains("name"),
2639 "error should mention the missing `name` field, got: {}",
2640 err
2641 );
2642 }
2643
2644 #[test]
2649 fn test_extends_overrides_compose_field_by_field() {
2650 let json = r#"{
2651 "name": "dark-with-pink-cursor",
2652 "extends": "builtin://dark",
2653 "editor": { "cursor": [255, 105, 180] }
2654 }"#;
2655 let theme = Theme::from_json(json).expect("should parse");
2656 let dark = Theme::load_builtin(THEME_DARK).expect("dark builtin");
2657
2658 assert_eq!(theme.cursor, Color::Rgb(255, 105, 180));
2660 assert_eq!(theme.editor_bg, dark.editor_bg);
2662 assert_eq!(theme.editor_fg, dark.editor_fg);
2663 assert_eq!(theme.selection_bg, dark.selection_bg);
2664 assert_eq!(theme.menu_bg, dark.menu_bg);
2666 assert_eq!(theme.syntax_keyword, dark.syntax_keyword);
2667 }
2668
2669 #[test]
2670 fn test_default_reset_color() {
2671 let color: Color = ColorDef::Named("Default".to_string()).into();
2673 assert_eq!(color, Color::Reset);
2674
2675 let color: Color = ColorDef::Named("Reset".to_string()).into();
2677 assert_eq!(color, Color::Reset);
2678 }
2679
2680 #[test]
2681 fn test_file_status_colors_fall_back_to_diagnostic_colors() {
2682 let json = r#"{
2684 "name": "test-fallback",
2685 "editor": {},
2686 "ui": {},
2687 "search": {},
2688 "diagnostic": {
2689 "error_fg": [220, 50, 47],
2690 "warning_fg": [181, 137, 0],
2691 "info_fg": [38, 139, 210],
2692 "hint_fg": [101, 123, 131]
2693 },
2694 "syntax": {}
2695 }"#;
2696 let theme = Theme::from_json(json).expect("Should parse theme without file_status keys");
2697
2698 assert_eq!(theme.file_status_added_fg, Color::Rgb(38, 139, 210));
2700 assert_eq!(theme.file_status_renamed_fg, Color::Rgb(38, 139, 210));
2701 assert_eq!(theme.file_status_modified_fg, Color::Rgb(181, 137, 0));
2703 assert_eq!(theme.file_status_deleted_fg, Color::Rgb(220, 50, 47));
2705 assert_eq!(theme.file_status_conflicted_fg, Color::Rgb(220, 50, 47));
2706 assert_eq!(theme.file_status_untracked_fg, Color::Rgb(101, 123, 131));
2708 }
2709
2710 #[test]
2711 fn test_file_status_colors_explicit_override() {
2712 let json = r#"{
2714 "name": "test-override",
2715 "editor": {},
2716 "ui": {
2717 "file_status_added_fg": [80, 250, 123],
2718 "file_status_modified_fg": [255, 184, 108]
2719 },
2720 "search": {},
2721 "diagnostic": {
2722 "info_fg": [38, 139, 210],
2723 "warning_fg": [181, 137, 0]
2724 },
2725 "syntax": {}
2726 }"#;
2727 let theme = Theme::from_json(json).expect("Should parse theme with file_status overrides");
2728
2729 assert_eq!(theme.file_status_added_fg, Color::Rgb(80, 250, 123));
2731 assert_eq!(theme.file_status_modified_fg, Color::Rgb(255, 184, 108));
2732 assert_eq!(theme.file_status_renamed_fg, Color::Rgb(38, 139, 210));
2734 }
2735
2736 #[test]
2737 fn test_file_status_colors_resolve_via_theme_key() {
2738 let json = r#"{
2739 "name": "test-resolve",
2740 "editor": {},
2741 "ui": {
2742 "file_status_added_fg": [80, 250, 123]
2743 },
2744 "search": {},
2745 "diagnostic": {
2746 "warning_fg": [181, 137, 0]
2747 },
2748 "syntax": {}
2749 }"#;
2750 let theme = Theme::from_json(json).expect("Should parse theme");
2751
2752 assert_eq!(
2754 theme.resolve_theme_key("ui.file_status_added_fg"),
2755 Some(Color::Rgb(80, 250, 123))
2756 );
2757 assert_eq!(
2758 theme.resolve_theme_key("ui.file_status_modified_fg"),
2759 Some(Color::Rgb(181, 137, 0))
2760 );
2761 }
2762
2763 #[test]
2764 fn override_colors_writes_known_keys_and_drops_unknowns() {
2765 let mut theme = Theme::load_builtin(THEME_DARK).expect("dark builtin");
2766 let applied = theme.override_colors([
2767 ("editor.bg".to_string(), Color::Rgb(10, 20, 30)),
2768 ("ui.status_bar_fg".to_string(), Color::Rgb(1, 2, 3)),
2769 ("does.not_exist".to_string(), Color::Rgb(9, 9, 9)),
2770 ("garbage_no_dot".to_string(), Color::Rgb(9, 9, 9)),
2771 ]);
2772 assert_eq!(applied, 2, "only the two valid keys should be applied");
2773 assert_eq!(
2774 theme.resolve_theme_key("editor.bg"),
2775 Some(Color::Rgb(10, 20, 30))
2776 );
2777 assert_eq!(
2778 theme.resolve_theme_key("ui.status_bar_fg"),
2779 Some(Color::Rgb(1, 2, 3))
2780 );
2781 }
2782
2783 #[test]
2784 fn resolve_theme_key_mut_matches_resolve_theme_key_domain() {
2785 let mut theme = Theme::load_builtin(THEME_DARK).expect("dark builtin");
2788 let probe = [
2789 "editor.bg",
2790 "editor.fg",
2791 "ui.status_bar_fg",
2792 "ui.tab_active_bg",
2793 "syntax.keyword",
2794 "diagnostic.error_fg",
2795 "search.match_bg",
2796 ];
2797 for key in probe {
2798 assert!(
2799 theme.resolve_theme_key(key).is_some(),
2800 "reader lost key {key}"
2801 );
2802 assert!(
2803 theme.resolve_theme_key_mut(key).is_some(),
2804 "mutator missing key {key}"
2805 );
2806 }
2807 }
2808
2809 fn schema_color_keys() -> Vec<(String, String)> {
2818 let theme = Theme::load_builtin(THEME_DARK).expect("dark builtin");
2819 let file: ThemeFile = theme.into();
2820 let value = serde_json::to_value(&file).expect("ThemeFile serializes");
2821 let obj = value.as_object().expect("ThemeFile is a JSON object");
2822
2823 let mut keys = Vec::new();
2824 for section in ["editor", "ui", "search", "diagnostic", "syntax"] {
2825 let fields = obj
2826 .get(section)
2827 .and_then(|v| v.as_object())
2828 .unwrap_or_else(|| panic!("section `{section}` missing from serialized ThemeFile"));
2829 for (field, val) in fields {
2830 if is_color_leaf(val) {
2831 keys.push((section.to_string(), field.clone()));
2832 }
2833 }
2834 }
2835 assert!(
2836 keys.len() >= 100,
2837 "expected the theme to expose at least ~100 color keys, found {} — \
2838 has the serialization shape changed?",
2839 keys.len()
2840 );
2841 keys
2842 }
2843
2844 fn is_color_leaf(v: &serde_json::Value) -> bool {
2848 v.is_string()
2849 || v.as_array()
2850 .is_some_and(|a| a.len() == 3 && a.iter().all(serde_json::Value::is_number))
2851 }
2852
2853 fn sentinel(i: usize) -> Color {
2857 Color::Rgb((i >> 8) as u8, (i & 0xff) as u8, 0x5a)
2858 }
2859
2860 #[test]
2861 fn every_exposed_color_key_resolves_in_both_directions() {
2862 let mut theme = Theme::load_builtin(THEME_DARK).expect("dark builtin");
2869 let mut missing_reader = Vec::new();
2870 let mut missing_mutator = Vec::new();
2871 for (section, field) in schema_color_keys() {
2872 let key = format!("{section}.{field}");
2873 if theme.resolve_theme_key(&key).is_none() {
2874 missing_reader.push(key.clone());
2875 }
2876 if theme.resolve_theme_key_mut(&key).is_none() {
2877 missing_mutator.push(key);
2878 }
2879 }
2880 assert!(
2881 missing_reader.is_empty() && missing_mutator.is_empty(),
2882 "theme color keys exposed by the JSON schema but dropped by a resolver:\n \
2883 resolve_theme_key: {missing_reader:?}\n \
2884 resolve_theme_key_mut: {missing_mutator:?}"
2885 );
2886 }
2887
2888 #[test]
2889 fn color_keys_round_trip_through_the_same_field_and_section() {
2890 let keys = schema_color_keys();
2901 let mut theme = Theme::load_builtin(THEME_DARK).expect("dark builtin");
2902
2903 let pairs: Vec<(String, Color)> = keys
2904 .iter()
2905 .enumerate()
2906 .map(|(i, (s, f))| (format!("{s}.{f}"), sentinel(i)))
2907 .collect();
2908 let applied = theme.override_colors(pairs.iter().map(|(k, c)| (k.as_str(), *c)));
2909 assert_eq!(
2910 applied,
2911 keys.len(),
2912 "override_colors should write every exposed key via resolve_theme_key_mut"
2913 );
2914
2915 for (i, (s, f)) in keys.iter().enumerate() {
2917 let key = format!("{s}.{f}");
2918 assert_eq!(
2919 theme.resolve_theme_key(&key),
2920 Some(sentinel(i)),
2921 "reader and mutator disagree on the field `{key}` addresses"
2922 );
2923 }
2924
2925 let file: ThemeFile = theme.into();
2928 let value = serde_json::to_value(&file).expect("ThemeFile serializes");
2929 let obj = value.as_object().expect("ThemeFile is a JSON object");
2930 for (i, (s, f)) in keys.iter().enumerate() {
2931 let leaf = obj
2932 .get(s)
2933 .and_then(|sec| sec.get(f))
2934 .unwrap_or_else(|| panic!("`{s}.{f}` vanished from serialized ThemeFile"));
2935 let color: Color = serde_json::from_value::<ColorDef>(leaf.clone())
2936 .expect("color leaf parses as ColorDef")
2937 .into();
2938 assert_eq!(
2939 color,
2940 sentinel(i),
2941 "`{s}.{f}` serialized back to the wrong field or section"
2942 );
2943 }
2944
2945 let reloaded = Theme::from_json(&value.to_string()).expect("from_json round-trips");
2948 for (i, (s, f)) in keys.iter().enumerate() {
2949 let key = format!("{s}.{f}");
2950 assert_eq!(
2951 reloaded.resolve_theme_key(&key),
2952 Some(sentinel(i)),
2953 "`{key}` did not survive ThemeFile -> JSON -> from_json"
2954 );
2955 }
2956 }
2957
2958 #[test]
2959 fn test_all_builtin_themes_set_prominent_palette_indicator() {
2960 for builtin in BUILTIN_THEMES {
2967 let theme = Theme::from_json(builtin.json)
2968 .unwrap_or_else(|e| panic!("Theme '{}' failed to parse: {}", builtin.name, e));
2969 assert!(
2970 theme.status_palette_fg != theme.status_bar_fg
2971 || theme.status_palette_bg != theme.status_bar_bg,
2972 "Theme '{}' must set status_palette_fg/bg to a prominent \
2973 accent distinct from status_bar_fg/bg",
2974 builtin.name
2975 );
2976 }
2977 }
2978
2979 #[test]
2980 fn test_all_builtin_themes_have_file_status_colors() {
2981 for builtin in BUILTIN_THEMES {
2983 let theme = Theme::from_json(builtin.json)
2984 .unwrap_or_else(|e| panic!("Theme '{}' failed to parse: {}", builtin.name, e));
2985
2986 for key in &[
2988 "ui.file_status_added_fg",
2989 "ui.file_status_modified_fg",
2990 "ui.file_status_deleted_fg",
2991 "ui.file_status_renamed_fg",
2992 "ui.file_status_untracked_fg",
2993 "ui.file_status_conflicted_fg",
2994 ] {
2995 assert!(
2996 theme.resolve_theme_key(key).is_some(),
2997 "Theme '{}' missing resolution for '{}'",
2998 builtin.name,
2999 key
3000 );
3001 }
3002 }
3003 }
3004
3005 #[test]
3014 fn test_all_builtin_themes_define_visible_occurrence_highlight() {
3015 for builtin in BUILTIN_THEMES {
3016 let raw: serde_json::Value = serde_json::from_str(builtin.json)
3017 .unwrap_or_else(|e| panic!("Theme '{}' is not valid JSON: {}", builtin.name, e));
3018 let ui = raw.get("ui").and_then(|u| u.as_object());
3019 let has_bg = ui.is_some_and(|u| u.contains_key("semantic_highlight_bg"));
3020 let has_modifier = ui.is_some_and(|u| u.contains_key("semantic_highlight_modifier"));
3021 assert!(
3022 has_bg || has_modifier,
3023 "Theme '{}' must explicitly define `ui.semantic_highlight_bg` (or a \
3024 `semantic_highlight_modifier`) so occurrence highlighting is theme-appropriate \
3025 instead of falling back to the hard-wired global default (#2312)",
3026 builtin.name
3027 );
3028
3029 let theme = Theme::from_json(builtin.json)
3030 .unwrap_or_else(|e| panic!("Theme '{}' failed to parse: {}", builtin.name, e));
3031 if theme
3035 .modifier_for_bg_key("ui.semantic_highlight_bg")
3036 .is_empty()
3037 {
3038 assert_ne!(
3039 theme.semantic_highlight_bg, theme.editor_bg,
3040 "Theme '{}': occurrence-highlight background equals the editor background \
3041 (highlight would be invisible) and no modifier compensates (#2312)",
3042 builtin.name
3043 );
3044 }
3045 }
3046 }
3047}