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)]
545 pub after_eof_bg: Option<ColorDef>,
546}
547
548fn default_editor_bg() -> ColorDef {
550 ColorDef::Rgb(30, 30, 30)
551}
552fn default_editor_fg() -> ColorDef {
553 ColorDef::Rgb(212, 212, 212)
554}
555fn default_cursor() -> ColorDef {
556 ColorDef::Rgb(255, 255, 255)
557}
558fn default_inactive_cursor() -> ColorDef {
559 ColorDef::Named("DarkGray".to_string())
560}
561fn default_selection_bg() -> ColorDef {
562 ColorDef::Rgb(38, 79, 120)
563}
564fn default_current_line_bg() -> ColorDef {
565 ColorDef::Rgb(40, 40, 40)
566}
567fn default_line_number_fg() -> ColorDef {
568 ColorDef::Rgb(100, 100, 100)
569}
570fn default_line_number_bg() -> ColorDef {
571 ColorDef::Rgb(30, 30, 30)
572}
573fn default_diff_add_bg() -> ColorDef {
574 ColorDef::Rgb(35, 60, 35) }
576fn default_diff_remove_bg() -> ColorDef {
577 ColorDef::Rgb(70, 35, 35) }
579fn default_diff_modify_bg() -> ColorDef {
580 ColorDef::Rgb(40, 38, 30) }
582fn default_ruler_bg() -> ColorDef {
583 ColorDef::Rgb(50, 50, 50) }
585fn default_whitespace_indicator_fg() -> ColorDef {
586 ColorDef::Rgb(70, 70, 70) }
588
589#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
608pub struct UiColors {
609 #[serde(default = "default_tab_active_fg")]
611 pub tab_active_fg: ColorDef,
612 #[serde(default = "default_tab_active_bg")]
614 pub tab_active_bg: ColorDef,
615 #[serde(default = "default_tab_inactive_fg")]
617 pub tab_inactive_fg: ColorDef,
618 #[serde(default = "default_tab_inactive_bg")]
620 pub tab_inactive_bg: ColorDef,
621 #[serde(default = "default_tab_separator_bg")]
623 pub tab_separator_bg: ColorDef,
624 #[serde(default = "default_tab_close_hover_fg")]
626 pub tab_close_hover_fg: ColorDef,
627 #[serde(default = "default_tab_hover_bg")]
629 pub tab_hover_bg: ColorDef,
630 #[serde(default = "default_menu_bg")]
632 pub menu_bg: ColorDef,
633 #[serde(default = "default_menu_fg")]
635 pub menu_fg: ColorDef,
636 #[serde(default = "default_menu_active_bg")]
638 pub menu_active_bg: ColorDef,
639 #[serde(default = "default_menu_active_fg")]
641 pub menu_active_fg: ColorDef,
642 #[serde(default = "default_menu_dropdown_bg")]
644 pub menu_dropdown_bg: ColorDef,
645 #[serde(default = "default_menu_dropdown_fg")]
647 pub menu_dropdown_fg: ColorDef,
648 #[serde(default = "default_menu_highlight_bg")]
650 pub menu_highlight_bg: ColorDef,
651 #[serde(default = "default_menu_highlight_fg")]
653 pub menu_highlight_fg: ColorDef,
654 #[serde(default = "default_menu_border_fg")]
656 pub menu_border_fg: ColorDef,
657 #[serde(default = "default_menu_separator_fg")]
659 pub menu_separator_fg: ColorDef,
660 #[serde(default = "default_menu_hover_bg")]
662 pub menu_hover_bg: ColorDef,
663 #[serde(default = "default_menu_hover_fg")]
665 pub menu_hover_fg: ColorDef,
666 #[serde(default = "default_menu_disabled_fg")]
668 pub menu_disabled_fg: ColorDef,
669 #[serde(default = "default_menu_disabled_bg")]
671 pub menu_disabled_bg: ColorDef,
672 #[serde(default = "default_status_bar_fg")]
674 pub status_bar_fg: ColorDef,
675 #[serde(default = "default_status_bar_bg")]
677 pub status_bar_bg: ColorDef,
678 #[serde(default)]
680 pub status_palette_fg: Option<ColorDef>,
681 #[serde(default)]
683 pub status_palette_bg: Option<ColorDef>,
684 #[serde(default)]
686 pub status_lsp_on_fg: Option<ColorDef>,
687 #[serde(default)]
689 pub status_lsp_on_bg: Option<ColorDef>,
690 #[serde(default)]
694 pub status_lsp_actionable_fg: Option<ColorDef>,
695 #[serde(default)]
698 pub status_lsp_actionable_bg: Option<ColorDef>,
699 #[serde(default = "default_prompt_fg")]
701 pub prompt_fg: ColorDef,
702 #[serde(default = "default_prompt_bg")]
704 pub prompt_bg: ColorDef,
705 #[serde(default = "default_prompt_selection_fg")]
707 pub prompt_selection_fg: ColorDef,
708 #[serde(default = "default_prompt_selection_bg")]
710 pub prompt_selection_bg: ColorDef,
711 #[serde(default = "default_popup_border_fg")]
713 pub popup_border_fg: ColorDef,
714 #[serde(default = "default_popup_bg")]
716 pub popup_bg: ColorDef,
717 #[serde(default = "default_popup_selection_bg")]
719 pub popup_selection_bg: ColorDef,
720 #[serde(default = "default_text_input_selection_bg")]
728 pub text_input_selection_bg: ColorDef,
729 #[serde(default = "default_popup_selection_fg")]
731 pub popup_selection_fg: ColorDef,
732 #[serde(default = "default_popup_text_fg", alias = "popup_fg")]
736 pub popup_text_fg: ColorDef,
737 #[serde(default = "default_suggestion_bg")]
739 pub suggestion_bg: ColorDef,
740 #[serde(default)]
744 pub suggestion_fg: Option<ColorDef>,
745 #[serde(default = "default_suggestion_selected_bg")]
747 pub suggestion_selected_bg: ColorDef,
748 #[serde(default = "default_help_bg")]
750 pub help_bg: ColorDef,
751 #[serde(default = "default_help_fg")]
753 pub help_fg: ColorDef,
754 #[serde(default = "default_help_key_fg")]
756 pub help_key_fg: ColorDef,
757 #[serde(default = "default_help_separator_fg")]
759 pub help_separator_fg: ColorDef,
760 #[serde(default = "default_help_indicator_fg")]
762 pub help_indicator_fg: ColorDef,
763 #[serde(default = "default_help_indicator_bg")]
765 pub help_indicator_bg: ColorDef,
766 #[serde(default = "default_inline_code_bg")]
768 pub inline_code_bg: ColorDef,
769 #[serde(default = "default_split_separator_fg")]
771 pub split_separator_fg: ColorDef,
772 #[serde(default = "default_split_separator_hover_fg")]
774 pub split_separator_hover_fg: ColorDef,
775 #[serde(default = "default_scrollbar_track_fg")]
777 pub scrollbar_track_fg: ColorDef,
778 #[serde(default = "default_scrollbar_thumb_fg")]
780 pub scrollbar_thumb_fg: ColorDef,
781 #[serde(default = "default_scrollbar_track_hover_fg")]
783 pub scrollbar_track_hover_fg: ColorDef,
784 #[serde(default = "default_scrollbar_thumb_hover_fg")]
786 pub scrollbar_thumb_hover_fg: ColorDef,
787 #[serde(default = "default_compose_margin_bg")]
789 pub compose_margin_bg: ColorDef,
790 #[serde(default = "default_semantic_highlight_bg")]
792 pub semantic_highlight_bg: ColorDef,
793 #[serde(default)]
800 pub semantic_highlight_modifier: Option<ModifierDef>,
801 #[serde(default = "default_terminal_bg")]
803 pub terminal_bg: ColorDef,
804 #[serde(default = "default_terminal_fg")]
806 pub terminal_fg: ColorDef,
807 #[serde(default = "default_status_warning_indicator_bg")]
809 pub status_warning_indicator_bg: ColorDef,
810 #[serde(default = "default_status_warning_indicator_fg")]
812 pub status_warning_indicator_fg: ColorDef,
813 #[serde(default = "default_status_error_indicator_bg")]
815 pub status_error_indicator_bg: ColorDef,
816 #[serde(default = "default_status_error_indicator_fg")]
818 pub status_error_indicator_fg: ColorDef,
819 #[serde(default = "default_status_warning_indicator_hover_bg")]
821 pub status_warning_indicator_hover_bg: ColorDef,
822 #[serde(default = "default_status_warning_indicator_hover_fg")]
824 pub status_warning_indicator_hover_fg: ColorDef,
825 #[serde(default = "default_status_error_indicator_hover_bg")]
827 pub status_error_indicator_hover_bg: ColorDef,
828 #[serde(default = "default_status_error_indicator_hover_fg")]
830 pub status_error_indicator_hover_fg: ColorDef,
831 #[serde(default = "default_tab_drop_zone_bg")]
833 pub tab_drop_zone_bg: ColorDef,
834 #[serde(default = "default_tab_drop_zone_border")]
836 pub tab_drop_zone_border: ColorDef,
837 #[serde(default = "default_settings_selected_bg")]
839 pub settings_selected_bg: ColorDef,
840 #[serde(default = "default_settings_selected_fg")]
842 pub settings_selected_fg: ColorDef,
843 #[serde(default)]
845 pub file_status_added_fg: Option<ColorDef>,
846 #[serde(default)]
848 pub file_status_modified_fg: Option<ColorDef>,
849 #[serde(default)]
851 pub file_status_deleted_fg: Option<ColorDef>,
852 #[serde(default)]
854 pub file_status_renamed_fg: Option<ColorDef>,
855 #[serde(default)]
857 pub file_status_untracked_fg: Option<ColorDef>,
858 #[serde(default)]
860 pub file_status_conflicted_fg: Option<ColorDef>,
861}
862
863fn default_tab_active_fg() -> ColorDef {
866 ColorDef::Named("Yellow".to_string())
867}
868fn default_tab_active_bg() -> ColorDef {
869 ColorDef::Named("Blue".to_string())
870}
871fn default_tab_inactive_fg() -> ColorDef {
872 ColorDef::Named("White".to_string())
873}
874fn default_tab_inactive_bg() -> ColorDef {
875 ColorDef::Named("DarkGray".to_string())
876}
877fn default_tab_separator_bg() -> ColorDef {
878 ColorDef::Named("Black".to_string())
879}
880fn default_tab_close_hover_fg() -> ColorDef {
881 ColorDef::Rgb(255, 100, 100) }
883fn default_tab_hover_bg() -> ColorDef {
884 ColorDef::Rgb(70, 70, 75) }
886
887fn default_menu_bg() -> ColorDef {
889 ColorDef::Rgb(60, 60, 65)
890}
891fn default_menu_fg() -> ColorDef {
892 ColorDef::Rgb(220, 220, 220)
893}
894fn default_menu_active_bg() -> ColorDef {
895 ColorDef::Rgb(60, 60, 60)
896}
897fn default_menu_active_fg() -> ColorDef {
898 ColorDef::Rgb(255, 255, 255)
899}
900fn default_menu_dropdown_bg() -> ColorDef {
901 ColorDef::Rgb(50, 50, 50)
902}
903fn default_menu_dropdown_fg() -> ColorDef {
904 ColorDef::Rgb(220, 220, 220)
905}
906fn default_menu_highlight_bg() -> ColorDef {
907 ColorDef::Rgb(70, 130, 180)
908}
909fn default_menu_highlight_fg() -> ColorDef {
910 ColorDef::Rgb(255, 255, 255)
911}
912fn default_menu_border_fg() -> ColorDef {
913 ColorDef::Rgb(100, 100, 100)
914}
915fn default_menu_separator_fg() -> ColorDef {
916 ColorDef::Rgb(80, 80, 80)
917}
918fn default_menu_hover_bg() -> ColorDef {
919 ColorDef::Rgb(55, 55, 55)
920}
921fn default_menu_hover_fg() -> ColorDef {
922 ColorDef::Rgb(255, 255, 255)
923}
924fn default_menu_disabled_fg() -> ColorDef {
925 ColorDef::Rgb(100, 100, 100) }
927fn default_menu_disabled_bg() -> ColorDef {
928 ColorDef::Rgb(50, 50, 50) }
930fn default_status_bar_fg() -> ColorDef {
932 ColorDef::Named("White".to_string())
933}
934fn default_status_bar_bg() -> ColorDef {
935 ColorDef::Named("DarkGray".to_string())
936}
937
938fn default_prompt_fg() -> ColorDef {
940 ColorDef::Named("White".to_string())
941}
942fn default_prompt_bg() -> ColorDef {
943 ColorDef::Named("Black".to_string())
944}
945fn default_prompt_selection_fg() -> ColorDef {
946 ColorDef::Named("White".to_string())
947}
948fn default_prompt_selection_bg() -> ColorDef {
949 ColorDef::Rgb(58, 79, 120)
950}
951
952fn default_popup_border_fg() -> ColorDef {
954 ColorDef::Named("Gray".to_string())
955}
956fn default_popup_bg() -> ColorDef {
957 ColorDef::Rgb(30, 30, 30)
958}
959fn default_popup_selection_bg() -> ColorDef {
960 ColorDef::Rgb(58, 79, 120)
961}
962fn default_text_input_selection_bg() -> ColorDef {
963 ColorDef::Rgb(58, 79, 120)
967}
968fn default_popup_selection_fg() -> ColorDef {
969 ColorDef::Rgb(255, 255, 255) }
971fn default_popup_text_fg() -> ColorDef {
972 ColorDef::Named("White".to_string())
973}
974
975fn default_suggestion_bg() -> ColorDef {
977 ColorDef::Rgb(30, 30, 30)
978}
979fn default_suggestion_selected_bg() -> ColorDef {
980 ColorDef::Rgb(58, 79, 120)
981}
982
983fn default_help_bg() -> ColorDef {
985 ColorDef::Named("Black".to_string())
986}
987fn default_help_fg() -> ColorDef {
988 ColorDef::Named("White".to_string())
989}
990fn default_help_key_fg() -> ColorDef {
991 ColorDef::Named("Cyan".to_string())
992}
993fn default_help_separator_fg() -> ColorDef {
994 ColorDef::Named("DarkGray".to_string())
995}
996fn default_help_indicator_fg() -> ColorDef {
997 ColorDef::Named("Red".to_string())
998}
999fn default_help_indicator_bg() -> ColorDef {
1000 ColorDef::Named("Black".to_string())
1001}
1002
1003fn default_inline_code_bg() -> ColorDef {
1004 ColorDef::Named("DarkGray".to_string())
1005}
1006
1007fn default_split_separator_fg() -> ColorDef {
1009 ColorDef::Rgb(100, 100, 100)
1010}
1011fn default_split_separator_hover_fg() -> ColorDef {
1012 ColorDef::Rgb(100, 149, 237) }
1014fn default_scrollbar_track_fg() -> ColorDef {
1015 ColorDef::Named("DarkGray".to_string())
1016}
1017fn default_scrollbar_thumb_fg() -> ColorDef {
1018 ColorDef::Named("Gray".to_string())
1019}
1020fn default_scrollbar_track_hover_fg() -> ColorDef {
1021 ColorDef::Named("Gray".to_string())
1022}
1023fn default_scrollbar_thumb_hover_fg() -> ColorDef {
1024 ColorDef::Named("White".to_string())
1025}
1026fn default_compose_margin_bg() -> ColorDef {
1027 ColorDef::Rgb(18, 18, 18) }
1029fn default_semantic_highlight_bg() -> ColorDef {
1030 ColorDef::Rgb(60, 60, 80) }
1032fn default_terminal_bg() -> ColorDef {
1033 ColorDef::Named("Default".to_string()) }
1035fn default_terminal_fg() -> ColorDef {
1036 ColorDef::Named("Default".to_string()) }
1038fn default_status_warning_indicator_bg() -> ColorDef {
1039 ColorDef::Rgb(181, 137, 0) }
1041fn default_status_warning_indicator_fg() -> ColorDef {
1042 ColorDef::Rgb(0, 0, 0) }
1044fn default_status_error_indicator_bg() -> ColorDef {
1045 ColorDef::Rgb(220, 50, 47) }
1047fn default_status_error_indicator_fg() -> ColorDef {
1048 ColorDef::Rgb(255, 255, 255) }
1050fn default_status_warning_indicator_hover_bg() -> ColorDef {
1051 ColorDef::Rgb(211, 167, 30) }
1053fn default_status_warning_indicator_hover_fg() -> ColorDef {
1054 ColorDef::Rgb(0, 0, 0) }
1056fn default_status_error_indicator_hover_bg() -> ColorDef {
1057 ColorDef::Rgb(250, 80, 77) }
1059fn default_status_error_indicator_hover_fg() -> ColorDef {
1060 ColorDef::Rgb(255, 255, 255) }
1062fn default_tab_drop_zone_bg() -> ColorDef {
1063 ColorDef::Rgb(70, 130, 180) }
1065fn default_tab_drop_zone_border() -> ColorDef {
1066 ColorDef::Rgb(100, 149, 237) }
1068fn default_settings_selected_bg() -> ColorDef {
1069 ColorDef::Rgb(60, 60, 70) }
1071fn default_settings_selected_fg() -> ColorDef {
1072 ColorDef::Rgb(255, 255, 255) }
1074#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1076pub struct SearchColors {
1077 #[serde(default = "default_search_match_bg")]
1079 pub match_bg: ColorDef,
1080 #[serde(default = "default_search_match_fg")]
1082 pub match_fg: ColorDef,
1083 #[serde(default = "default_search_label_bg")]
1087 pub label_bg: ColorDef,
1088 #[serde(default = "default_search_label_fg")]
1092 pub label_fg: ColorDef,
1093}
1094
1095fn default_search_match_bg() -> ColorDef {
1097 ColorDef::Rgb(100, 100, 20)
1098}
1099fn default_search_match_fg() -> ColorDef {
1100 ColorDef::Rgb(255, 255, 255)
1101}
1102fn default_search_label_bg() -> ColorDef {
1107 ColorDef::Rgb(199, 78, 189)
1108}
1109fn default_search_label_fg() -> ColorDef {
1110 ColorDef::Rgb(255, 255, 255)
1111}
1112
1113#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1115pub struct DiagnosticColors {
1116 #[serde(default = "default_diagnostic_error_fg")]
1118 pub error_fg: ColorDef,
1119 #[serde(default = "default_diagnostic_error_bg")]
1121 pub error_bg: ColorDef,
1122 #[serde(default = "default_diagnostic_warning_fg")]
1124 pub warning_fg: ColorDef,
1125 #[serde(default = "default_diagnostic_warning_bg")]
1127 pub warning_bg: ColorDef,
1128 #[serde(default = "default_diagnostic_info_fg")]
1130 pub info_fg: ColorDef,
1131 #[serde(default = "default_diagnostic_info_bg")]
1133 pub info_bg: ColorDef,
1134 #[serde(default = "default_diagnostic_hint_fg")]
1136 pub hint_fg: ColorDef,
1137 #[serde(default = "default_diagnostic_hint_bg")]
1139 pub hint_bg: ColorDef,
1140}
1141
1142fn default_diagnostic_error_fg() -> ColorDef {
1144 ColorDef::Named("Red".to_string())
1145}
1146fn default_diagnostic_error_bg() -> ColorDef {
1147 ColorDef::Rgb(60, 20, 20)
1148}
1149fn default_diagnostic_warning_fg() -> ColorDef {
1150 ColorDef::Named("Yellow".to_string())
1151}
1152fn default_diagnostic_warning_bg() -> ColorDef {
1153 ColorDef::Rgb(60, 50, 0)
1154}
1155fn default_diagnostic_info_fg() -> ColorDef {
1156 ColorDef::Named("Blue".to_string())
1157}
1158fn default_diagnostic_info_bg() -> ColorDef {
1159 ColorDef::Rgb(0, 30, 60)
1160}
1161fn default_diagnostic_hint_fg() -> ColorDef {
1162 ColorDef::Named("Gray".to_string())
1163}
1164fn default_diagnostic_hint_bg() -> ColorDef {
1165 ColorDef::Rgb(30, 30, 30)
1166}
1167
1168#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1170pub struct SyntaxColors {
1171 #[serde(default = "default_syntax_keyword")]
1173 pub keyword: ColorDef,
1174 #[serde(default = "default_syntax_string")]
1176 pub string: ColorDef,
1177 #[serde(default = "default_syntax_comment")]
1179 pub comment: ColorDef,
1180 #[serde(default = "default_syntax_function")]
1182 pub function: ColorDef,
1183 #[serde(rename = "type", default = "default_syntax_type")]
1185 pub type_: ColorDef,
1186 #[serde(default = "default_syntax_variable")]
1188 pub variable: ColorDef,
1189 #[serde(default = "default_syntax_constant")]
1191 pub constant: ColorDef,
1192 #[serde(default = "default_syntax_operator")]
1194 pub operator: ColorDef,
1195 #[serde(default = "default_syntax_punctuation_bracket")]
1197 pub punctuation_bracket: ColorDef,
1198 #[serde(default = "default_syntax_punctuation_delimiter")]
1200 pub punctuation_delimiter: ColorDef,
1201}
1202
1203fn default_syntax_keyword() -> ColorDef {
1205 ColorDef::Rgb(86, 156, 214)
1206}
1207fn default_syntax_string() -> ColorDef {
1208 ColorDef::Rgb(206, 145, 120)
1209}
1210fn default_syntax_comment() -> ColorDef {
1211 ColorDef::Rgb(106, 153, 85)
1212}
1213fn default_syntax_function() -> ColorDef {
1214 ColorDef::Rgb(220, 220, 170)
1215}
1216fn default_syntax_type() -> ColorDef {
1217 ColorDef::Rgb(78, 201, 176)
1218}
1219fn default_syntax_variable() -> ColorDef {
1220 ColorDef::Rgb(156, 220, 254)
1221}
1222fn default_syntax_constant() -> ColorDef {
1223 ColorDef::Rgb(79, 193, 255)
1224}
1225fn default_syntax_operator() -> ColorDef {
1226 ColorDef::Rgb(212, 212, 212)
1227}
1228fn default_syntax_punctuation_bracket() -> ColorDef {
1229 ColorDef::Rgb(212, 212, 212) }
1231fn default_syntax_punctuation_delimiter() -> ColorDef {
1232 ColorDef::Rgb(212, 212, 212) }
1234
1235#[derive(Debug, Clone)]
1237pub struct Theme {
1238 pub name: String,
1240
1241 pub editor_bg: Color,
1243 pub editor_fg: Color,
1244 pub cursor: Color,
1245 pub inactive_cursor: Color,
1246 pub selection_bg: Color,
1247 pub selection_modifier: Modifier,
1252 pub current_line_bg: Color,
1253 pub line_number_fg: Color,
1254 pub line_number_bg: Color,
1255
1256 pub after_eof_bg: Color,
1258
1259 pub ruler_bg: Color,
1261
1262 pub whitespace_indicator_fg: Color,
1264
1265 pub diff_add_bg: Color,
1267 pub diff_remove_bg: Color,
1268 pub diff_modify_bg: Color,
1269 pub diff_add_highlight_bg: Color,
1271 pub diff_remove_highlight_bg: Color,
1273 pub diff_add_collision_fg: Option<Color>,
1277 pub diff_remove_collision_fg: Option<Color>,
1278 pub diff_modify_collision_fg: Option<Color>,
1279
1280 pub tab_active_fg: Color,
1282 pub tab_active_bg: Color,
1283 pub tab_inactive_fg: Color,
1284 pub tab_inactive_bg: Color,
1285 pub tab_separator_bg: Color,
1286 pub tab_close_hover_fg: Color,
1287 pub tab_hover_bg: Color,
1288
1289 pub menu_bg: Color,
1291 pub menu_fg: Color,
1292 pub menu_active_bg: Color,
1293 pub menu_active_fg: Color,
1294 pub menu_dropdown_bg: Color,
1295 pub menu_dropdown_fg: Color,
1296 pub menu_highlight_bg: Color,
1297 pub menu_highlight_fg: Color,
1298 pub menu_border_fg: Color,
1299 pub menu_separator_fg: Color,
1300 pub menu_hover_bg: Color,
1301 pub menu_hover_fg: Color,
1302 pub menu_disabled_fg: Color,
1303 pub menu_disabled_bg: Color,
1304
1305 pub status_bar_fg: Color,
1306 pub status_bar_bg: Color,
1307 pub status_palette_fg: Color,
1309 pub status_palette_bg: Color,
1310 pub status_lsp_on_fg: Color,
1312 pub status_lsp_on_bg: Color,
1313 pub status_lsp_actionable_fg: Color,
1316 pub status_lsp_actionable_bg: Color,
1317 pub prompt_fg: Color,
1318 pub prompt_bg: Color,
1319 pub prompt_selection_fg: Color,
1320 pub prompt_selection_bg: Color,
1321
1322 pub popup_border_fg: Color,
1323 pub popup_bg: Color,
1324 pub popup_selection_bg: Color,
1325 pub popup_selection_fg: Color,
1326 pub popup_text_fg: Color,
1327 pub text_input_selection_bg: Color,
1331
1332 pub suggestion_bg: Color,
1333 pub suggestion_fg: Color,
1334 pub suggestion_selected_bg: Color,
1335
1336 pub help_bg: Color,
1337 pub help_fg: Color,
1338 pub help_key_fg: Color,
1339 pub help_separator_fg: Color,
1340
1341 pub help_indicator_fg: Color,
1342 pub help_indicator_bg: Color,
1343
1344 pub inline_code_bg: Color,
1346
1347 pub split_separator_fg: Color,
1348 pub split_separator_hover_fg: Color,
1349
1350 pub scrollbar_track_fg: Color,
1352 pub scrollbar_thumb_fg: Color,
1353 pub scrollbar_track_hover_fg: Color,
1354 pub scrollbar_thumb_hover_fg: Color,
1355
1356 pub compose_margin_bg: Color,
1358
1359 pub semantic_highlight_bg: Color,
1361 pub semantic_highlight_modifier: Modifier,
1366
1367 pub terminal_bg: Color,
1369 pub terminal_fg: Color,
1370
1371 pub status_warning_indicator_bg: Color,
1373 pub status_warning_indicator_fg: Color,
1374 pub status_error_indicator_bg: Color,
1375 pub status_error_indicator_fg: Color,
1376 pub status_warning_indicator_hover_bg: Color,
1377 pub status_warning_indicator_hover_fg: Color,
1378 pub status_error_indicator_hover_bg: Color,
1379 pub status_error_indicator_hover_fg: Color,
1380
1381 pub tab_drop_zone_bg: Color,
1383 pub tab_drop_zone_border: Color,
1384
1385 pub settings_selected_bg: Color,
1387 pub settings_selected_fg: Color,
1388
1389 pub file_status_added_fg: Color,
1391 pub file_status_modified_fg: Color,
1392 pub file_status_deleted_fg: Color,
1393 pub file_status_renamed_fg: Color,
1394 pub file_status_untracked_fg: Color,
1395 pub file_status_conflicted_fg: Color,
1396
1397 pub search_match_bg: Color,
1399 pub search_match_fg: Color,
1400 pub search_label_bg: Color,
1401 pub search_label_fg: Color,
1402
1403 pub diagnostic_error_fg: Color,
1405 pub diagnostic_error_bg: Color,
1406 pub diagnostic_warning_fg: Color,
1407 pub diagnostic_warning_bg: Color,
1408 pub diagnostic_info_fg: Color,
1409 pub diagnostic_info_bg: Color,
1410 pub diagnostic_hint_fg: Color,
1411 pub diagnostic_hint_bg: Color,
1412
1413 pub syntax_keyword: Color,
1415 pub syntax_string: Color,
1416 pub syntax_comment: Color,
1417 pub syntax_function: Color,
1418 pub syntax_type: Color,
1419 pub syntax_variable: Color,
1420 pub syntax_constant: Color,
1421 pub syntax_operator: Color,
1422 pub syntax_punctuation_bracket: Color,
1423 pub syntax_punctuation_delimiter: Color,
1424}
1425
1426impl From<ThemeFile> for Theme {
1427 fn from(file: ThemeFile) -> Self {
1428 Self {
1429 name: file.name,
1430 editor_bg: file.editor.bg.clone().into(),
1431 editor_fg: file.editor.fg.into(),
1432 cursor: file.editor.cursor.into(),
1433 inactive_cursor: file.editor.inactive_cursor.into(),
1434 selection_bg: file.editor.selection_bg.into(),
1435 selection_modifier: file
1436 .editor
1437 .selection_modifier
1438 .as_ref()
1439 .map(Modifier::from)
1440 .unwrap_or(Modifier::empty()),
1441 current_line_bg: file.editor.current_line_bg.into(),
1442 line_number_fg: file.editor.line_number_fg.into(),
1443 line_number_bg: file.editor.line_number_bg.into(),
1444 after_eof_bg: file
1447 .editor
1448 .after_eof_bg
1449 .clone()
1450 .map(|c| c.into())
1451 .unwrap_or_else(|| shade_toward_contrast(file.editor.bg.clone().into(), 10)),
1452 ruler_bg: file.editor.ruler_bg.into(),
1453 whitespace_indicator_fg: file.editor.whitespace_indicator_fg.into(),
1454 diff_add_bg: file.editor.diff_add_bg.clone().into(),
1455 diff_remove_bg: file.editor.diff_remove_bg.clone().into(),
1456 diff_modify_bg: file.editor.diff_modify_bg.into(),
1457 diff_add_highlight_bg: file
1459 .editor
1460 .diff_add_highlight_bg
1461 .map(|c| c.into())
1462 .unwrap_or_else(|| brighten_color(file.editor.diff_add_bg.into(), 40)),
1463 diff_remove_highlight_bg: file
1464 .editor
1465 .diff_remove_highlight_bg
1466 .map(|c| c.into())
1467 .unwrap_or_else(|| brighten_color(file.editor.diff_remove_bg.into(), 40)),
1468 diff_add_collision_fg: file.editor.diff_add_collision_fg.clone().map(|c| c.into()),
1469 diff_remove_collision_fg: file
1470 .editor
1471 .diff_remove_collision_fg
1472 .clone()
1473 .map(|c| c.into()),
1474 diff_modify_collision_fg: file
1475 .editor
1476 .diff_modify_collision_fg
1477 .clone()
1478 .map(|c| c.into()),
1479 tab_active_fg: file.ui.tab_active_fg.into(),
1480 tab_active_bg: file.ui.tab_active_bg.into(),
1481 tab_inactive_fg: file.ui.tab_inactive_fg.into(),
1482 tab_inactive_bg: file.ui.tab_inactive_bg.into(),
1483 tab_separator_bg: file.ui.tab_separator_bg.into(),
1484 tab_close_hover_fg: file.ui.tab_close_hover_fg.into(),
1485 tab_hover_bg: file.ui.tab_hover_bg.into(),
1486 menu_bg: file.ui.menu_bg.into(),
1487 menu_fg: file.ui.menu_fg.into(),
1488 menu_active_bg: file.ui.menu_active_bg.into(),
1489 menu_active_fg: file.ui.menu_active_fg.into(),
1490 menu_dropdown_bg: file.ui.menu_dropdown_bg.into(),
1491 menu_dropdown_fg: file.ui.menu_dropdown_fg.into(),
1492 menu_highlight_bg: file.ui.menu_highlight_bg.into(),
1493 menu_highlight_fg: file.ui.menu_highlight_fg.into(),
1494 menu_border_fg: file.ui.menu_border_fg.into(),
1495 menu_separator_fg: file.ui.menu_separator_fg.into(),
1496 menu_hover_bg: file.ui.menu_hover_bg.into(),
1497 menu_hover_fg: file.ui.menu_hover_fg.into(),
1498 menu_disabled_fg: file.ui.menu_disabled_fg.into(),
1499 menu_disabled_bg: file.ui.menu_disabled_bg.into(),
1500 status_bar_fg: file.ui.status_bar_fg.clone().into(),
1501 status_bar_bg: file.ui.status_bar_bg.clone().into(),
1502 status_palette_fg: file
1503 .ui
1504 .status_palette_fg
1505 .clone()
1506 .map(|c| c.into())
1507 .unwrap_or_else(|| file.ui.status_bar_fg.clone().into()),
1508 status_palette_bg: file
1509 .ui
1510 .status_palette_bg
1511 .clone()
1512 .map(|c| c.into())
1513 .unwrap_or_else(|| file.ui.status_bar_bg.clone().into()),
1514 status_lsp_on_fg: file
1515 .ui
1516 .status_lsp_on_fg
1517 .clone()
1518 .map(|c| c.into())
1519 .unwrap_or_else(|| file.ui.status_bar_fg.clone().into()),
1520 status_lsp_on_bg: file
1521 .ui
1522 .status_lsp_on_bg
1523 .clone()
1524 .map(|c| c.into())
1525 .unwrap_or_else(|| file.ui.status_bar_bg.clone().into()),
1526 status_lsp_actionable_fg: file
1527 .ui
1528 .status_lsp_actionable_fg
1529 .clone()
1530 .map(|c| c.into())
1531 .unwrap_or_else(|| file.ui.status_warning_indicator_fg.clone().into()),
1532 status_lsp_actionable_bg: file
1533 .ui
1534 .status_lsp_actionable_bg
1535 .clone()
1536 .map(|c| c.into())
1537 .unwrap_or_else(|| file.ui.status_warning_indicator_bg.clone().into()),
1538 prompt_fg: file.ui.prompt_fg.into(),
1539 prompt_bg: file.ui.prompt_bg.into(),
1540 prompt_selection_fg: file.ui.prompt_selection_fg.into(),
1541 prompt_selection_bg: file.ui.prompt_selection_bg.into(),
1542 popup_border_fg: file.ui.popup_border_fg.into(),
1543 popup_bg: file.ui.popup_bg.into(),
1544 popup_selection_bg: file.ui.popup_selection_bg.into(),
1545 popup_selection_fg: file.ui.popup_selection_fg.into(),
1546 popup_text_fg: file.ui.popup_text_fg.clone().into(),
1547 text_input_selection_bg: file.ui.text_input_selection_bg.into(),
1548 suggestion_bg: file.ui.suggestion_bg.into(),
1549 suggestion_fg: file
1550 .ui
1551 .suggestion_fg
1552 .clone()
1553 .map(|c| c.into())
1554 .unwrap_or_else(|| file.ui.popup_text_fg.clone().into()),
1555 suggestion_selected_bg: file.ui.suggestion_selected_bg.into(),
1556 help_bg: file.ui.help_bg.into(),
1557 help_fg: file.ui.help_fg.into(),
1558 help_key_fg: file.ui.help_key_fg.into(),
1559 help_separator_fg: file.ui.help_separator_fg.into(),
1560 help_indicator_fg: file.ui.help_indicator_fg.into(),
1561 help_indicator_bg: file.ui.help_indicator_bg.into(),
1562 inline_code_bg: file.ui.inline_code_bg.into(),
1563 split_separator_fg: file.ui.split_separator_fg.into(),
1564 split_separator_hover_fg: file.ui.split_separator_hover_fg.into(),
1565 scrollbar_track_fg: file.ui.scrollbar_track_fg.into(),
1566 scrollbar_thumb_fg: file.ui.scrollbar_thumb_fg.into(),
1567 scrollbar_track_hover_fg: file.ui.scrollbar_track_hover_fg.into(),
1568 scrollbar_thumb_hover_fg: file.ui.scrollbar_thumb_hover_fg.into(),
1569 compose_margin_bg: file.ui.compose_margin_bg.into(),
1570 semantic_highlight_bg: file.ui.semantic_highlight_bg.into(),
1571 semantic_highlight_modifier: file
1572 .ui
1573 .semantic_highlight_modifier
1574 .as_ref()
1575 .map(Modifier::from)
1576 .unwrap_or(Modifier::empty()),
1577 terminal_bg: file.ui.terminal_bg.into(),
1578 terminal_fg: file.ui.terminal_fg.into(),
1579 status_warning_indicator_bg: file.ui.status_warning_indicator_bg.into(),
1580 status_warning_indicator_fg: file.ui.status_warning_indicator_fg.into(),
1581 status_error_indicator_bg: file.ui.status_error_indicator_bg.into(),
1582 status_error_indicator_fg: file.ui.status_error_indicator_fg.into(),
1583 status_warning_indicator_hover_bg: file.ui.status_warning_indicator_hover_bg.into(),
1584 status_warning_indicator_hover_fg: file.ui.status_warning_indicator_hover_fg.into(),
1585 status_error_indicator_hover_bg: file.ui.status_error_indicator_hover_bg.into(),
1586 status_error_indicator_hover_fg: file.ui.status_error_indicator_hover_fg.into(),
1587 tab_drop_zone_bg: file.ui.tab_drop_zone_bg.into(),
1588 tab_drop_zone_border: file.ui.tab_drop_zone_border.into(),
1589 settings_selected_bg: file.ui.settings_selected_bg.into(),
1590 settings_selected_fg: file.ui.settings_selected_fg.into(),
1591 file_status_added_fg: file
1592 .ui
1593 .file_status_added_fg
1594 .map(|c| c.into())
1595 .unwrap_or_else(|| file.diagnostic.info_fg.clone().into()),
1596 file_status_modified_fg: file
1597 .ui
1598 .file_status_modified_fg
1599 .map(|c| c.into())
1600 .unwrap_or_else(|| file.diagnostic.warning_fg.clone().into()),
1601 file_status_deleted_fg: file
1602 .ui
1603 .file_status_deleted_fg
1604 .map(|c| c.into())
1605 .unwrap_or_else(|| file.diagnostic.error_fg.clone().into()),
1606 file_status_renamed_fg: file
1607 .ui
1608 .file_status_renamed_fg
1609 .map(|c| c.into())
1610 .unwrap_or_else(|| file.diagnostic.info_fg.clone().into()),
1611 file_status_untracked_fg: file
1612 .ui
1613 .file_status_untracked_fg
1614 .map(|c| c.into())
1615 .unwrap_or_else(|| file.diagnostic.hint_fg.clone().into()),
1616 file_status_conflicted_fg: file
1617 .ui
1618 .file_status_conflicted_fg
1619 .map(|c| c.into())
1620 .unwrap_or_else(|| file.diagnostic.error_fg.clone().into()),
1621 search_match_bg: file.search.match_bg.into(),
1622 search_match_fg: file.search.match_fg.into(),
1623 search_label_bg: file.search.label_bg.into(),
1624 search_label_fg: file.search.label_fg.into(),
1625 diagnostic_error_fg: file.diagnostic.error_fg.into(),
1626 diagnostic_error_bg: file.diagnostic.error_bg.into(),
1627 diagnostic_warning_fg: file.diagnostic.warning_fg.into(),
1628 diagnostic_warning_bg: file.diagnostic.warning_bg.into(),
1629 diagnostic_info_fg: file.diagnostic.info_fg.into(),
1630 diagnostic_info_bg: file.diagnostic.info_bg.into(),
1631 diagnostic_hint_fg: file.diagnostic.hint_fg.into(),
1632 diagnostic_hint_bg: file.diagnostic.hint_bg.into(),
1633 syntax_keyword: file.syntax.keyword.into(),
1634 syntax_string: file.syntax.string.into(),
1635 syntax_comment: file.syntax.comment.into(),
1636 syntax_function: file.syntax.function.into(),
1637 syntax_type: file.syntax.type_.into(),
1638 syntax_variable: file.syntax.variable.into(),
1639 syntax_constant: file.syntax.constant.into(),
1640 syntax_operator: file.syntax.operator.into(),
1641 syntax_punctuation_bracket: file.syntax.punctuation_bracket.into(),
1642 syntax_punctuation_delimiter: file.syntax.punctuation_delimiter.into(),
1643 }
1644 }
1645}
1646
1647impl From<Theme> for ThemeFile {
1648 fn from(theme: Theme) -> Self {
1649 Self {
1650 name: theme.name,
1651 extends: None,
1654 editor: EditorColors {
1655 bg: theme.editor_bg.into(),
1656 fg: theme.editor_fg.into(),
1657 cursor: theme.cursor.into(),
1658 inactive_cursor: theme.inactive_cursor.into(),
1659 selection_bg: theme.selection_bg.into(),
1660 selection_modifier: if theme.selection_modifier.is_empty() {
1661 None
1662 } else {
1663 Some(theme.selection_modifier.into())
1664 },
1665 current_line_bg: theme.current_line_bg.into(),
1666 line_number_fg: theme.line_number_fg.into(),
1667 line_number_bg: theme.line_number_bg.into(),
1668 diff_add_bg: theme.diff_add_bg.into(),
1669 diff_remove_bg: theme.diff_remove_bg.into(),
1670 diff_add_highlight_bg: Some(theme.diff_add_highlight_bg.into()),
1671 diff_remove_highlight_bg: Some(theme.diff_remove_highlight_bg.into()),
1672 diff_modify_bg: theme.diff_modify_bg.into(),
1673 diff_add_collision_fg: theme.diff_add_collision_fg.map(|c| c.into()),
1674 diff_remove_collision_fg: theme.diff_remove_collision_fg.map(|c| c.into()),
1675 diff_modify_collision_fg: theme.diff_modify_collision_fg.map(|c| c.into()),
1676 ruler_bg: theme.ruler_bg.into(),
1677 whitespace_indicator_fg: theme.whitespace_indicator_fg.into(),
1678 after_eof_bg: Some(theme.after_eof_bg.into()),
1679 },
1680 ui: UiColors {
1681 tab_active_fg: theme.tab_active_fg.into(),
1682 tab_active_bg: theme.tab_active_bg.into(),
1683 tab_inactive_fg: theme.tab_inactive_fg.into(),
1684 tab_inactive_bg: theme.tab_inactive_bg.into(),
1685 tab_separator_bg: theme.tab_separator_bg.into(),
1686 tab_close_hover_fg: theme.tab_close_hover_fg.into(),
1687 tab_hover_bg: theme.tab_hover_bg.into(),
1688 menu_bg: theme.menu_bg.into(),
1689 menu_fg: theme.menu_fg.into(),
1690 menu_active_bg: theme.menu_active_bg.into(),
1691 menu_active_fg: theme.menu_active_fg.into(),
1692 menu_dropdown_bg: theme.menu_dropdown_bg.into(),
1693 menu_dropdown_fg: theme.menu_dropdown_fg.into(),
1694 menu_highlight_bg: theme.menu_highlight_bg.into(),
1695 menu_highlight_fg: theme.menu_highlight_fg.into(),
1696 menu_border_fg: theme.menu_border_fg.into(),
1697 menu_separator_fg: theme.menu_separator_fg.into(),
1698 menu_hover_bg: theme.menu_hover_bg.into(),
1699 menu_hover_fg: theme.menu_hover_fg.into(),
1700 menu_disabled_fg: theme.menu_disabled_fg.into(),
1701 menu_disabled_bg: theme.menu_disabled_bg.into(),
1702 status_bar_fg: theme.status_bar_fg.into(),
1703 status_bar_bg: theme.status_bar_bg.into(),
1704 status_palette_fg: Some(theme.status_palette_fg.into()),
1705 status_palette_bg: Some(theme.status_palette_bg.into()),
1706 status_lsp_on_fg: Some(theme.status_lsp_on_fg.into()),
1707 status_lsp_on_bg: Some(theme.status_lsp_on_bg.into()),
1708 status_lsp_actionable_fg: Some(theme.status_lsp_actionable_fg.into()),
1709 status_lsp_actionable_bg: Some(theme.status_lsp_actionable_bg.into()),
1710 prompt_fg: theme.prompt_fg.into(),
1711 prompt_bg: theme.prompt_bg.into(),
1712 prompt_selection_fg: theme.prompt_selection_fg.into(),
1713 prompt_selection_bg: theme.prompt_selection_bg.into(),
1714 popup_border_fg: theme.popup_border_fg.into(),
1715 popup_bg: theme.popup_bg.into(),
1716 popup_selection_bg: theme.popup_selection_bg.into(),
1717 popup_selection_fg: theme.popup_selection_fg.into(),
1718 popup_text_fg: theme.popup_text_fg.into(),
1719 text_input_selection_bg: theme.text_input_selection_bg.into(),
1720 suggestion_bg: theme.suggestion_bg.into(),
1721 suggestion_fg: Some(theme.suggestion_fg.into()),
1722 suggestion_selected_bg: theme.suggestion_selected_bg.into(),
1723 help_bg: theme.help_bg.into(),
1724 help_fg: theme.help_fg.into(),
1725 help_key_fg: theme.help_key_fg.into(),
1726 help_separator_fg: theme.help_separator_fg.into(),
1727 help_indicator_fg: theme.help_indicator_fg.into(),
1728 help_indicator_bg: theme.help_indicator_bg.into(),
1729 inline_code_bg: theme.inline_code_bg.into(),
1730 split_separator_fg: theme.split_separator_fg.into(),
1731 split_separator_hover_fg: theme.split_separator_hover_fg.into(),
1732 scrollbar_track_fg: theme.scrollbar_track_fg.into(),
1733 scrollbar_thumb_fg: theme.scrollbar_thumb_fg.into(),
1734 scrollbar_track_hover_fg: theme.scrollbar_track_hover_fg.into(),
1735 scrollbar_thumb_hover_fg: theme.scrollbar_thumb_hover_fg.into(),
1736 compose_margin_bg: theme.compose_margin_bg.into(),
1737 semantic_highlight_bg: theme.semantic_highlight_bg.into(),
1738 semantic_highlight_modifier: if theme.semantic_highlight_modifier.is_empty() {
1739 None
1740 } else {
1741 Some(theme.semantic_highlight_modifier.into())
1742 },
1743 terminal_bg: theme.terminal_bg.into(),
1744 terminal_fg: theme.terminal_fg.into(),
1745 status_warning_indicator_bg: theme.status_warning_indicator_bg.into(),
1746 status_warning_indicator_fg: theme.status_warning_indicator_fg.into(),
1747 status_error_indicator_bg: theme.status_error_indicator_bg.into(),
1748 status_error_indicator_fg: theme.status_error_indicator_fg.into(),
1749 status_warning_indicator_hover_bg: theme.status_warning_indicator_hover_bg.into(),
1750 status_warning_indicator_hover_fg: theme.status_warning_indicator_hover_fg.into(),
1751 status_error_indicator_hover_bg: theme.status_error_indicator_hover_bg.into(),
1752 status_error_indicator_hover_fg: theme.status_error_indicator_hover_fg.into(),
1753 tab_drop_zone_bg: theme.tab_drop_zone_bg.into(),
1754 tab_drop_zone_border: theme.tab_drop_zone_border.into(),
1755 settings_selected_bg: theme.settings_selected_bg.into(),
1756 settings_selected_fg: theme.settings_selected_fg.into(),
1757 file_status_added_fg: Some(theme.file_status_added_fg.into()),
1758 file_status_modified_fg: Some(theme.file_status_modified_fg.into()),
1759 file_status_deleted_fg: Some(theme.file_status_deleted_fg.into()),
1760 file_status_renamed_fg: Some(theme.file_status_renamed_fg.into()),
1761 file_status_untracked_fg: Some(theme.file_status_untracked_fg.into()),
1762 file_status_conflicted_fg: Some(theme.file_status_conflicted_fg.into()),
1763 },
1764 search: SearchColors {
1765 match_bg: theme.search_match_bg.into(),
1766 match_fg: theme.search_match_fg.into(),
1767 label_bg: theme.search_label_bg.into(),
1768 label_fg: theme.search_label_fg.into(),
1769 },
1770 diagnostic: DiagnosticColors {
1771 error_fg: theme.diagnostic_error_fg.into(),
1772 error_bg: theme.diagnostic_error_bg.into(),
1773 warning_fg: theme.diagnostic_warning_fg.into(),
1774 warning_bg: theme.diagnostic_warning_bg.into(),
1775 info_fg: theme.diagnostic_info_fg.into(),
1776 info_bg: theme.diagnostic_info_bg.into(),
1777 hint_fg: theme.diagnostic_hint_fg.into(),
1778 hint_bg: theme.diagnostic_hint_bg.into(),
1779 },
1780 syntax: SyntaxColors {
1781 keyword: theme.syntax_keyword.into(),
1782 string: theme.syntax_string.into(),
1783 comment: theme.syntax_comment.into(),
1784 function: theme.syntax_function.into(),
1785 type_: theme.syntax_type.into(),
1786 variable: theme.syntax_variable.into(),
1787 constant: theme.syntax_constant.into(),
1788 operator: theme.syntax_operator.into(),
1789 punctuation_bracket: theme.syntax_punctuation_bracket.into(),
1790 punctuation_delimiter: theme.syntax_punctuation_delimiter.into(),
1791 },
1792 }
1793 }
1794}
1795
1796fn resolve_base_theme(theme_file: &ThemeFile, raw: &serde_json::Value) -> Result<Theme, String> {
1803 if let Some(extends) = theme_file.extends.as_deref() {
1805 let name = extends.strip_prefix("builtin://").unwrap_or(extends);
1806 return Theme::load_builtin(name).ok_or_else(|| {
1807 let available: Vec<&str> = BUILTIN_THEMES.iter().map(|t| t.name).collect();
1808 format!(
1809 "theme `extends: {:?}` does not match any built-in theme. \
1810 Available: {}. \
1811 Inheriting from other user themes is not yet supported.",
1812 extends,
1813 available.join(", ")
1814 )
1815 });
1816 }
1817
1818 if let Some(bg) = raw
1824 .get("editor")
1825 .and_then(|e| e.get("bg"))
1826 .cloned()
1827 .and_then(|v| serde_json::from_value::<ColorDef>(v).ok())
1828 {
1829 let color: Color = bg.into();
1830 if let Some((r, g, b)) = color_to_rgb(color) {
1831 let lum = relative_luminance(r, g, b);
1832 let base_name = if lum > 0.5 { THEME_LIGHT } else { THEME_DARK };
1833 if let Some(base) = Theme::load_builtin(base_name) {
1834 return Ok(base);
1835 }
1836 }
1837 }
1838
1839 Ok(theme_file.clone().into())
1841}
1842
1843fn relative_luminance(r: u8, g: u8, b: u8) -> f64 {
1846 0.2126 * (r as f64 / 255.0) + 0.7152 * (g as f64 / 255.0) + 0.0722 * (b as f64 / 255.0)
1847}
1848
1849fn apply_theme_overrides(theme: &mut Theme, theme_file: &ThemeFile, raw: &serde_json::Value) {
1854 theme.name = theme_file.name.clone();
1856
1857 for section in ["editor", "ui", "search", "diagnostic", "syntax"] {
1858 let Some(obj) = raw.get(section).and_then(|v| v.as_object()) else {
1859 continue;
1860 };
1861 for (field, value) in obj {
1862 if value.is_null() {
1865 continue;
1866 }
1867 let key = format!("{}.{}", section, field);
1868 if let Ok(color_def) = serde_json::from_value::<ColorDef>(value.clone()) {
1869 if let Some(slot) = theme.resolve_theme_key_mut(&key) {
1870 *slot = color_def.into();
1871 }
1872 }
1873 }
1874 }
1875}
1876
1877impl Theme {
1878 pub fn is_light(&self) -> bool {
1884 color_to_rgb(self.editor_bg)
1885 .map(|(r, g, b)| relative_luminance(r, g, b) > 0.5)
1886 .unwrap_or(false)
1887 }
1888
1889 pub fn load_builtin(name: &str) -> Option<Self> {
1891 BUILTIN_THEMES
1892 .iter()
1893 .find(|t| t.name == name)
1894 .and_then(|t| serde_json::from_str::<ThemeFile>(t.json).ok())
1895 .map(|tf| tf.into())
1896 }
1897
1898 pub fn from_json(json: &str) -> Result<Self, String> {
1908 let raw: serde_json::Value =
1913 serde_json::from_str(json).map_err(|e| format!("Failed to parse theme JSON: {}", e))?;
1914 let theme_file: ThemeFile = serde_json::from_value(raw.clone())
1915 .map_err(|e| format!("Failed to parse theme: {}", e))?;
1916
1917 let mut theme = resolve_base_theme(&theme_file, &raw)?;
1918 apply_theme_overrides(&mut theme, &theme_file, &raw);
1919 Ok(theme)
1920 }
1921
1922 pub fn modifier_for_bg_key(&self, key: &str) -> Modifier {
1931 match key {
1932 "editor.selection_bg" => self.selection_modifier,
1933 "ui.semantic_highlight_bg" => self.semantic_highlight_modifier,
1934 _ => Modifier::empty(),
1935 }
1936 }
1937
1938 pub fn resolve_theme_key(&self, key: &str) -> Option<Color> {
1949 let parts: Vec<&str> = key.split('.').collect();
1951 if parts.len() != 2 {
1952 return None;
1953 }
1954
1955 let (section, field) = (parts[0], parts[1]);
1956
1957 match section {
1958 "editor" => match field {
1959 "after_eof_bg" => Some(self.after_eof_bg),
1960 "bg" => Some(self.editor_bg),
1961 "current_line_bg" => Some(self.current_line_bg),
1962 "cursor" => Some(self.cursor),
1963 "diff_add_bg" => Some(self.diff_add_bg),
1964 "diff_add_collision_fg" => self.diff_add_collision_fg,
1965 "diff_add_highlight_bg" => Some(self.diff_add_highlight_bg),
1966 "diff_modify_bg" => Some(self.diff_modify_bg),
1967 "diff_modify_collision_fg" => self.diff_modify_collision_fg,
1968 "diff_remove_bg" => Some(self.diff_remove_bg),
1969 "diff_remove_collision_fg" => self.diff_remove_collision_fg,
1970 "diff_remove_highlight_bg" => Some(self.diff_remove_highlight_bg),
1971 "fg" => Some(self.editor_fg),
1972 "inactive_cursor" => Some(self.inactive_cursor),
1973 "line_number_bg" => Some(self.line_number_bg),
1974 "line_number_fg" => Some(self.line_number_fg),
1975 "ruler_bg" => Some(self.ruler_bg),
1976 "selection_bg" => Some(self.selection_bg),
1977 "whitespace_indicator_fg" => Some(self.whitespace_indicator_fg),
1978 _ => None,
1979 },
1980 "ui" => match field {
1981 "compose_margin_bg" => Some(self.compose_margin_bg),
1982 "file_status_added_fg" => Some(self.file_status_added_fg),
1983 "file_status_conflicted_fg" => Some(self.file_status_conflicted_fg),
1984 "file_status_deleted_fg" => Some(self.file_status_deleted_fg),
1985 "file_status_modified_fg" => Some(self.file_status_modified_fg),
1986 "file_status_renamed_fg" => Some(self.file_status_renamed_fg),
1987 "file_status_untracked_fg" => Some(self.file_status_untracked_fg),
1988 "help_bg" => Some(self.help_bg),
1989 "help_fg" => Some(self.help_fg),
1990 "help_indicator_bg" => Some(self.help_indicator_bg),
1991 "help_indicator_fg" => Some(self.help_indicator_fg),
1992 "help_key_fg" => Some(self.help_key_fg),
1993 "help_separator_fg" => Some(self.help_separator_fg),
1994 "inline_code_bg" => Some(self.inline_code_bg),
1995 "menu_active_bg" => Some(self.menu_active_bg),
1996 "menu_active_fg" => Some(self.menu_active_fg),
1997 "menu_bg" => Some(self.menu_bg),
1998 "menu_border_fg" => Some(self.menu_border_fg),
1999 "menu_disabled_bg" => Some(self.menu_disabled_bg),
2000 "menu_disabled_fg" => Some(self.menu_disabled_fg),
2001 "menu_dropdown_bg" => Some(self.menu_dropdown_bg),
2002 "menu_dropdown_fg" => Some(self.menu_dropdown_fg),
2003 "menu_fg" => Some(self.menu_fg),
2004 "menu_highlight_bg" => Some(self.menu_highlight_bg),
2005 "menu_highlight_fg" => Some(self.menu_highlight_fg),
2006 "menu_hover_bg" => Some(self.menu_hover_bg),
2007 "menu_hover_fg" => Some(self.menu_hover_fg),
2008 "menu_separator_fg" => Some(self.menu_separator_fg),
2009 "popup_bg" => Some(self.popup_bg),
2010 "popup_border_fg" => Some(self.popup_border_fg),
2011 "popup_selection_bg" => Some(self.popup_selection_bg),
2012 "popup_selection_fg" => Some(self.popup_selection_fg),
2013 "popup_text_fg" => Some(self.popup_text_fg),
2014 "prompt_bg" => Some(self.prompt_bg),
2015 "prompt_fg" => Some(self.prompt_fg),
2016 "prompt_selection_bg" => Some(self.prompt_selection_bg),
2017 "prompt_selection_fg" => Some(self.prompt_selection_fg),
2018 "scrollbar_thumb_fg" => Some(self.scrollbar_thumb_fg),
2019 "scrollbar_thumb_hover_fg" => Some(self.scrollbar_thumb_hover_fg),
2020 "scrollbar_track_fg" => Some(self.scrollbar_track_fg),
2021 "scrollbar_track_hover_fg" => Some(self.scrollbar_track_hover_fg),
2022 "semantic_highlight_bg" => Some(self.semantic_highlight_bg),
2023 "settings_selected_bg" => Some(self.settings_selected_bg),
2024 "settings_selected_fg" => Some(self.settings_selected_fg),
2025 "split_separator_fg" => Some(self.split_separator_fg),
2026 "split_separator_hover_fg" => Some(self.split_separator_hover_fg),
2027 "status_bar_bg" => Some(self.status_bar_bg),
2028 "status_bar_fg" => Some(self.status_bar_fg),
2029 "status_error_indicator_bg" => Some(self.status_error_indicator_bg),
2030 "status_error_indicator_fg" => Some(self.status_error_indicator_fg),
2031 "status_error_indicator_hover_bg" => Some(self.status_error_indicator_hover_bg),
2032 "status_error_indicator_hover_fg" => Some(self.status_error_indicator_hover_fg),
2033 "status_lsp_actionable_bg" => Some(self.status_lsp_actionable_bg),
2034 "status_lsp_actionable_fg" => Some(self.status_lsp_actionable_fg),
2035 "status_lsp_on_bg" => Some(self.status_lsp_on_bg),
2036 "status_lsp_on_fg" => Some(self.status_lsp_on_fg),
2037 "status_palette_bg" => Some(self.status_palette_bg),
2038 "status_palette_fg" => Some(self.status_palette_fg),
2039 "status_warning_indicator_bg" => Some(self.status_warning_indicator_bg),
2040 "status_warning_indicator_fg" => Some(self.status_warning_indicator_fg),
2041 "status_warning_indicator_hover_bg" => Some(self.status_warning_indicator_hover_bg),
2042 "status_warning_indicator_hover_fg" => Some(self.status_warning_indicator_hover_fg),
2043 "suggestion_bg" => Some(self.suggestion_bg),
2044 "suggestion_fg" => Some(self.suggestion_fg),
2045 "suggestion_selected_bg" => Some(self.suggestion_selected_bg),
2046 "tab_active_bg" => Some(self.tab_active_bg),
2047 "tab_active_fg" => Some(self.tab_active_fg),
2048 "tab_close_hover_fg" => Some(self.tab_close_hover_fg),
2049 "tab_drop_zone_bg" => Some(self.tab_drop_zone_bg),
2050 "tab_drop_zone_border" => Some(self.tab_drop_zone_border),
2051 "tab_hover_bg" => Some(self.tab_hover_bg),
2052 "tab_inactive_bg" => Some(self.tab_inactive_bg),
2053 "tab_inactive_fg" => Some(self.tab_inactive_fg),
2054 "tab_separator_bg" => Some(self.tab_separator_bg),
2055 "terminal_bg" => Some(self.terminal_bg),
2056 "terminal_fg" => Some(self.terminal_fg),
2057 "text_input_selection_bg" => Some(self.text_input_selection_bg),
2058 _ => None,
2059 },
2060 "syntax" => match field {
2061 "comment" => Some(self.syntax_comment),
2062 "constant" => Some(self.syntax_constant),
2063 "function" => Some(self.syntax_function),
2064 "keyword" => Some(self.syntax_keyword),
2065 "operator" => Some(self.syntax_operator),
2066 "punctuation_bracket" => Some(self.syntax_punctuation_bracket),
2067 "punctuation_delimiter" => Some(self.syntax_punctuation_delimiter),
2068 "string" => Some(self.syntax_string),
2069 "type" => Some(self.syntax_type),
2070 "variable" => Some(self.syntax_variable),
2071 _ => None,
2072 },
2073 "diagnostic" => match field {
2074 "error_bg" => Some(self.diagnostic_error_bg),
2075 "error_fg" => Some(self.diagnostic_error_fg),
2076 "hint_bg" => Some(self.diagnostic_hint_bg),
2077 "hint_fg" => Some(self.diagnostic_hint_fg),
2078 "info_bg" => Some(self.diagnostic_info_bg),
2079 "info_fg" => Some(self.diagnostic_info_fg),
2080 "warning_bg" => Some(self.diagnostic_warning_bg),
2081 "warning_fg" => Some(self.diagnostic_warning_fg),
2082 _ => None,
2083 },
2084 "search" => match field {
2085 "label_bg" => Some(self.search_label_bg),
2086 "label_fg" => Some(self.search_label_fg),
2087 "match_bg" => Some(self.search_match_bg),
2088 "match_fg" => Some(self.search_match_fg),
2089 _ => None,
2090 },
2091 _ => None,
2092 }
2093 }
2094
2095 pub fn resolve_theme_key_mut(&mut self, key: &str) -> Option<&mut Color> {
2099 let parts: Vec<&str> = key.split('.').collect();
2100 if parts.len() != 2 {
2101 return None;
2102 }
2103 let (section, field) = (parts[0], parts[1]);
2104 match section {
2105 "editor" => match field {
2106 "bg" => Some(&mut self.editor_bg),
2107 "fg" => Some(&mut self.editor_fg),
2108 "cursor" => Some(&mut self.cursor),
2109 "inactive_cursor" => Some(&mut self.inactive_cursor),
2110 "selection_bg" => Some(&mut self.selection_bg),
2111 "current_line_bg" => Some(&mut self.current_line_bg),
2112 "line_number_fg" => Some(&mut self.line_number_fg),
2113 "line_number_bg" => Some(&mut self.line_number_bg),
2114 "diff_add_bg" => Some(&mut self.diff_add_bg),
2115 "diff_remove_bg" => Some(&mut self.diff_remove_bg),
2116 "diff_modify_bg" => Some(&mut self.diff_modify_bg),
2117 "diff_add_collision_fg" => self.diff_add_collision_fg.as_mut(),
2121 "diff_remove_collision_fg" => self.diff_remove_collision_fg.as_mut(),
2122 "diff_modify_collision_fg" => self.diff_modify_collision_fg.as_mut(),
2123 "ruler_bg" => Some(&mut self.ruler_bg),
2124 "whitespace_indicator_fg" => Some(&mut self.whitespace_indicator_fg),
2125 "diff_add_highlight_bg" => Some(&mut self.diff_add_highlight_bg),
2126 "diff_remove_highlight_bg" => Some(&mut self.diff_remove_highlight_bg),
2127 "after_eof_bg" => Some(&mut self.after_eof_bg),
2128 _ => None,
2129 },
2130 "ui" => match field {
2131 "tab_active_fg" => Some(&mut self.tab_active_fg),
2132 "tab_active_bg" => Some(&mut self.tab_active_bg),
2133 "tab_inactive_fg" => Some(&mut self.tab_inactive_fg),
2134 "tab_inactive_bg" => Some(&mut self.tab_inactive_bg),
2135 "status_bar_fg" => Some(&mut self.status_bar_fg),
2136 "status_bar_bg" => Some(&mut self.status_bar_bg),
2137 "status_palette_fg" => Some(&mut self.status_palette_fg),
2138 "status_palette_bg" => Some(&mut self.status_palette_bg),
2139 "status_lsp_on_fg" => Some(&mut self.status_lsp_on_fg),
2140 "status_lsp_on_bg" => Some(&mut self.status_lsp_on_bg),
2141 "status_lsp_actionable_fg" => Some(&mut self.status_lsp_actionable_fg),
2142 "status_lsp_actionable_bg" => Some(&mut self.status_lsp_actionable_bg),
2143 "prompt_fg" => Some(&mut self.prompt_fg),
2144 "prompt_bg" => Some(&mut self.prompt_bg),
2145 "prompt_selection_fg" => Some(&mut self.prompt_selection_fg),
2146 "prompt_selection_bg" => Some(&mut self.prompt_selection_bg),
2147 "popup_bg" => Some(&mut self.popup_bg),
2148 "popup_border_fg" => Some(&mut self.popup_border_fg),
2149 "popup_selection_bg" => Some(&mut self.popup_selection_bg),
2150 "popup_selection_fg" => Some(&mut self.popup_selection_fg),
2151 "popup_text_fg" => Some(&mut self.popup_text_fg),
2152 "text_input_selection_bg" => Some(&mut self.text_input_selection_bg),
2153 "menu_bg" => Some(&mut self.menu_bg),
2154 "menu_fg" => Some(&mut self.menu_fg),
2155 "menu_active_bg" => Some(&mut self.menu_active_bg),
2156 "menu_active_fg" => Some(&mut self.menu_active_fg),
2157 "menu_disabled_fg" => Some(&mut self.menu_disabled_fg),
2158 "menu_disabled_bg" => Some(&mut self.menu_disabled_bg),
2159 "help_bg" => Some(&mut self.help_bg),
2160 "help_fg" => Some(&mut self.help_fg),
2161 "help_key_fg" => Some(&mut self.help_key_fg),
2162 "split_separator_fg" => Some(&mut self.split_separator_fg),
2163 "scrollbar_track_fg" => Some(&mut self.scrollbar_track_fg),
2164 "scrollbar_thumb_fg" => Some(&mut self.scrollbar_thumb_fg),
2165 "scrollbar_track_hover_fg" => Some(&mut self.scrollbar_track_hover_fg),
2166 "scrollbar_thumb_hover_fg" => Some(&mut self.scrollbar_thumb_hover_fg),
2167 "semantic_highlight_bg" => Some(&mut self.semantic_highlight_bg),
2168 "file_status_added_fg" => Some(&mut self.file_status_added_fg),
2169 "file_status_modified_fg" => Some(&mut self.file_status_modified_fg),
2170 "file_status_deleted_fg" => Some(&mut self.file_status_deleted_fg),
2171 "file_status_renamed_fg" => Some(&mut self.file_status_renamed_fg),
2172 "file_status_untracked_fg" => Some(&mut self.file_status_untracked_fg),
2173 "file_status_conflicted_fg" => Some(&mut self.file_status_conflicted_fg),
2174 "menu_dropdown_bg" => Some(&mut self.menu_dropdown_bg),
2175 "menu_dropdown_fg" => Some(&mut self.menu_dropdown_fg),
2176 "menu_highlight_bg" => Some(&mut self.menu_highlight_bg),
2177 "menu_highlight_fg" => Some(&mut self.menu_highlight_fg),
2178 "menu_border_fg" => Some(&mut self.menu_border_fg),
2179 "menu_separator_fg" => Some(&mut self.menu_separator_fg),
2180 "menu_hover_bg" => Some(&mut self.menu_hover_bg),
2181 "menu_hover_fg" => Some(&mut self.menu_hover_fg),
2182 "tab_separator_bg" => Some(&mut self.tab_separator_bg),
2183 "tab_close_hover_fg" => Some(&mut self.tab_close_hover_fg),
2184 "tab_hover_bg" => Some(&mut self.tab_hover_bg),
2185 "inline_code_bg" => Some(&mut self.inline_code_bg),
2186 "split_separator_hover_fg" => Some(&mut self.split_separator_hover_fg),
2187 "compose_margin_bg" => Some(&mut self.compose_margin_bg),
2188 "terminal_bg" => Some(&mut self.terminal_bg),
2189 "terminal_fg" => Some(&mut self.terminal_fg),
2190 "status_warning_indicator_bg" => Some(&mut self.status_warning_indicator_bg),
2191 "status_warning_indicator_fg" => Some(&mut self.status_warning_indicator_fg),
2192 "status_error_indicator_bg" => Some(&mut self.status_error_indicator_bg),
2193 "status_error_indicator_fg" => Some(&mut self.status_error_indicator_fg),
2194 "status_warning_indicator_hover_bg" => {
2195 Some(&mut self.status_warning_indicator_hover_bg)
2196 }
2197 "status_warning_indicator_hover_fg" => {
2198 Some(&mut self.status_warning_indicator_hover_fg)
2199 }
2200 "status_error_indicator_hover_bg" => {
2201 Some(&mut self.status_error_indicator_hover_bg)
2202 }
2203 "status_error_indicator_hover_fg" => {
2204 Some(&mut self.status_error_indicator_hover_fg)
2205 }
2206 "tab_drop_zone_bg" => Some(&mut self.tab_drop_zone_bg),
2207 "tab_drop_zone_border" => Some(&mut self.tab_drop_zone_border),
2208 "settings_selected_bg" => Some(&mut self.settings_selected_bg),
2209 "settings_selected_fg" => Some(&mut self.settings_selected_fg),
2210 "suggestion_bg" => Some(&mut self.suggestion_bg),
2211 "suggestion_fg" => Some(&mut self.suggestion_fg),
2212 "suggestion_selected_bg" => Some(&mut self.suggestion_selected_bg),
2213 "help_separator_fg" => Some(&mut self.help_separator_fg),
2214 "help_indicator_fg" => Some(&mut self.help_indicator_fg),
2215 "help_indicator_bg" => Some(&mut self.help_indicator_bg),
2216 _ => None,
2217 },
2218 "syntax" => match field {
2219 "keyword" => Some(&mut self.syntax_keyword),
2220 "string" => Some(&mut self.syntax_string),
2221 "comment" => Some(&mut self.syntax_comment),
2222 "function" => Some(&mut self.syntax_function),
2223 "type" => Some(&mut self.syntax_type),
2224 "variable" => Some(&mut self.syntax_variable),
2225 "constant" => Some(&mut self.syntax_constant),
2226 "operator" => Some(&mut self.syntax_operator),
2227 "punctuation_bracket" => Some(&mut self.syntax_punctuation_bracket),
2228 "punctuation_delimiter" => Some(&mut self.syntax_punctuation_delimiter),
2229 _ => None,
2230 },
2231 "diagnostic" => match field {
2232 "error_fg" => Some(&mut self.diagnostic_error_fg),
2233 "error_bg" => Some(&mut self.diagnostic_error_bg),
2234 "warning_fg" => Some(&mut self.diagnostic_warning_fg),
2235 "warning_bg" => Some(&mut self.diagnostic_warning_bg),
2236 "info_fg" => Some(&mut self.diagnostic_info_fg),
2237 "info_bg" => Some(&mut self.diagnostic_info_bg),
2238 "hint_fg" => Some(&mut self.diagnostic_hint_fg),
2239 "hint_bg" => Some(&mut self.diagnostic_hint_bg),
2240 _ => None,
2241 },
2242 "search" => match field {
2243 "match_bg" => Some(&mut self.search_match_bg),
2244 "match_fg" => Some(&mut self.search_match_fg),
2245 "label_bg" => Some(&mut self.search_label_bg),
2246 "label_fg" => Some(&mut self.search_label_fg),
2247 _ => None,
2248 },
2249 _ => None,
2250 }
2251 }
2252
2253 pub fn override_colors<I, K>(&mut self, overrides: I) -> usize
2258 where
2259 I: IntoIterator<Item = (K, Color)>,
2260 K: AsRef<str>,
2261 {
2262 let mut applied = 0;
2263 for (key, color) in overrides {
2264 if let Some(slot) = self.resolve_theme_key_mut(key.as_ref()) {
2265 *slot = color;
2266 applied += 1;
2267 }
2268 }
2269 applied
2270 }
2271}
2272
2273pub fn get_theme_schema() -> serde_json::Value {
2281 use schemars::schema_for;
2282 let schema = schema_for!(ThemeFile);
2283 serde_json::to_value(&schema).unwrap_or_default()
2284}
2285
2286pub fn get_builtin_themes() -> serde_json::Value {
2288 let mut map = serde_json::Map::new();
2289 for theme in BUILTIN_THEMES {
2290 map.insert(
2291 theme.name.to_string(),
2292 serde_json::Value::String(theme.json.to_string()),
2293 );
2294 }
2295 serde_json::Value::Object(map)
2296}
2297
2298#[cfg(test)]
2299mod tests {
2300 use super::*;
2301
2302 #[test]
2303 fn test_load_builtin_theme() {
2304 let dark = Theme::load_builtin(THEME_DARK).expect("Dark theme must exist");
2305 assert_eq!(dark.name, THEME_DARK);
2306
2307 let light = Theme::load_builtin(THEME_LIGHT).expect("Light theme must exist");
2308 assert_eq!(light.name, THEME_LIGHT);
2309
2310 let high_contrast =
2311 Theme::load_builtin(THEME_HIGH_CONTRAST).expect("High contrast theme must exist");
2312 assert_eq!(high_contrast.name, THEME_HIGH_CONTRAST);
2313
2314 let terminal = Theme::load_builtin(THEME_TERMINAL).expect("Terminal theme must exist");
2315 assert_eq!(terminal.name, THEME_TERMINAL);
2316 assert_eq!(terminal.editor_bg, Color::Reset);
2320 assert_eq!(terminal.editor_fg, Color::Reset);
2321 assert_eq!(terminal.terminal_bg, Color::Reset);
2322 assert!(terminal.selection_modifier.contains(Modifier::REVERSED));
2325 assert!(terminal
2326 .semantic_highlight_modifier
2327 .contains(Modifier::BOLD));
2328 }
2329
2330 #[test]
2331 fn test_suggestion_fg_falls_back_and_contrasts() {
2332 let dracula = Theme::load_builtin(THEME_DRACULA).expect("Dracula theme must exist");
2339 assert_eq!(
2340 dracula.suggestion_fg, dracula.popup_text_fg,
2341 "suggestion_fg should fall back to popup_text_fg when unset"
2342 );
2343 assert_ne!(
2344 dracula.suggestion_fg, dracula.suggestion_bg,
2345 "suggestion_fg must contrast with suggestion_bg, not vanish into it"
2346 );
2347 }
2348
2349 #[test]
2350 fn test_modifier_def_round_trip() {
2351 let cases = [
2352 (vec!["reversed"], Modifier::REVERSED),
2353 (
2354 vec!["bold", "underlined"],
2355 Modifier::BOLD | Modifier::UNDERLINED,
2356 ),
2357 (vec!["italic", "dim"], Modifier::ITALIC | Modifier::DIM),
2358 (vec!["reverse"], Modifier::REVERSED), (vec!["underline"], Modifier::UNDERLINED), ];
2361 for (strs, expected) in cases {
2362 let def = ModifierDef(strs.iter().map(|s| s.to_string()).collect());
2363 let m: Modifier = (&def).into();
2364 assert_eq!(m, expected, "ModifierDef({:?}) -> Modifier", strs);
2365 }
2366 }
2367
2368 #[test]
2369 fn test_modifier_def_unknown_strings_are_dropped() {
2370 let def = ModifierDef(vec!["reversed".into(), "wibble".into(), "bold".into()]);
2373 let m: Modifier = (&def).into();
2374 assert_eq!(m, Modifier::REVERSED | Modifier::BOLD);
2375 }
2376
2377 #[test]
2378 fn test_themes_without_modifier_default_to_empty() {
2379 let dark = Theme::load_builtin(THEME_DARK).expect("Dark theme must exist");
2384 assert!(dark.selection_modifier.is_empty());
2385 assert!(dark.semantic_highlight_modifier.is_empty());
2386 }
2387
2388 #[test]
2389 fn test_modifier_for_bg_key_lookup() {
2390 let terminal = Theme::load_builtin(THEME_TERMINAL).expect("Terminal theme must exist");
2391 assert!(terminal
2394 .modifier_for_bg_key("editor.selection_bg")
2395 .contains(Modifier::REVERSED));
2396 assert!(terminal
2397 .modifier_for_bg_key("ui.semantic_highlight_bg")
2398 .contains(Modifier::BOLD));
2399 assert!(terminal
2402 .modifier_for_bg_key("ui.popup_selection_bg")
2403 .is_empty());
2404 assert!(terminal.modifier_for_bg_key("nonsense").is_empty());
2405 }
2406
2407 #[test]
2408 fn test_modifier_round_trip_via_theme_file() {
2409 let original = Theme::load_builtin(THEME_TERMINAL).expect("Terminal theme must exist");
2411 let file: ThemeFile = original.clone().into();
2412 let json = serde_json::to_string(&file).expect("serialize");
2413 let parsed: ThemeFile = serde_json::from_str(&json).expect("parse");
2414 let round_tripped: Theme = parsed.into();
2415 assert_eq!(
2416 round_tripped.selection_modifier,
2417 original.selection_modifier
2418 );
2419 assert_eq!(
2420 round_tripped.semantic_highlight_modifier,
2421 original.semantic_highlight_modifier
2422 );
2423 }
2424
2425 #[test]
2426 fn test_builtin_themes_match_schema() {
2427 for theme in BUILTIN_THEMES {
2428 let _: ThemeFile = serde_json::from_str(theme.json)
2429 .unwrap_or_else(|_| panic!("Theme '{}' does not match schema", theme.name));
2430 }
2431 }
2432
2433 #[test]
2434 fn test_from_json() {
2435 let json = r#"{"name":"test","editor":{},"ui":{},"search":{},"diagnostic":{},"syntax":{}}"#;
2436 let theme = Theme::from_json(json).expect("Should parse minimal theme");
2437 assert_eq!(theme.name, "test");
2438 }
2439
2440 #[test]
2452 fn test_minimal_user_theme_from_issue_1281_loads() {
2453 let json = r#"{
2455 "name": "gruvbox-light-orange",
2456 "editor": {
2457 "bg": [251, 241, 199],
2458 "fg": [60, 56, 54],
2459 "cursor": [254, 128, 25],
2460 "selection_bg": [213, 196, 161]
2461 },
2462 "syntax": {
2463 "keyword": [175, 58, 3],
2464 "string": [152, 151, 26],
2465 "comment": [146, 131, 116]
2466 }
2467}"#;
2468 let theme = Theme::from_json(json)
2469 .expect("Theme from issue #1281 should parse without `ui`/`search`/`diagnostic`");
2470 assert_eq!(theme.name, "gruvbox-light-orange");
2471
2472 assert_eq!(theme.editor_bg, Color::Rgb(251, 241, 199));
2474 assert_eq!(theme.editor_fg, Color::Rgb(60, 56, 54));
2475 assert_eq!(theme.cursor, Color::Rgb(254, 128, 25));
2476 assert_eq!(theme.selection_bg, Color::Rgb(213, 196, 161));
2477 assert_eq!(theme.syntax_keyword, Color::Rgb(175, 58, 3));
2478 assert_eq!(theme.syntax_string, Color::Rgb(152, 151, 26));
2479 assert_eq!(theme.syntax_comment, Color::Rgb(146, 131, 116));
2480
2481 let light = Theme::load_builtin(THEME_LIGHT).expect("light builtin");
2485 assert_eq!(
2486 theme.status_bar_fg, light.status_bar_fg,
2487 "ui.status_bar_fg should inherit from builtin://light when bg is bright"
2488 );
2489 assert_eq!(
2490 theme.diagnostic_error_fg, light.diagnostic_error_fg,
2491 "diagnostic.error_fg should inherit from builtin://light when bg is bright"
2492 );
2493 assert_eq!(
2494 theme.menu_bg, light.menu_bg,
2495 "ui.menu_bg should inherit from builtin://light when bg is bright"
2496 );
2497 }
2498
2499 #[test]
2502 fn test_extends_explicit_builtin_wins_over_auto_infer() {
2503 let json = r#"{
2506 "name": "explicit-light",
2507 "extends": "builtin://light",
2508 "editor": { "bg": [0, 0, 0] }
2509 }"#;
2510 let theme = Theme::from_json(json).expect("extends should resolve");
2511 let light = Theme::load_builtin(THEME_LIGHT).expect("light builtin");
2512
2513 assert_eq!(theme.editor_bg, Color::Rgb(0, 0, 0));
2515 assert_eq!(theme.menu_bg, light.menu_bg);
2517 assert_eq!(theme.tab_active_bg, light.tab_active_bg);
2518 assert_eq!(theme.diagnostic_warning_fg, light.diagnostic_warning_fg);
2519 }
2520
2521 #[test]
2526 fn test_extends_bare_builtin_name_works() {
2527 let json = r#"{ "name": "x", "extends": "high-contrast" }"#;
2528 let theme = Theme::from_json(json).expect("bare-name extends should resolve");
2529 let hc = Theme::load_builtin("high-contrast").expect("hc builtin");
2530 assert_eq!(theme.menu_bg, hc.menu_bg);
2531 }
2532
2533 #[test]
2538 fn test_extends_unknown_builtin_errors_with_helpful_message() {
2539 let json = r#"{ "name": "x", "extends": "builtin://no-such-theme" }"#;
2540 let err = Theme::from_json(json).expect_err("unknown extends must error");
2541 assert!(
2542 err.contains("no-such-theme"),
2543 "error should quote the bad value, got: {}",
2544 err
2545 );
2546 assert!(
2547 err.contains("dark") && err.contains("light"),
2548 "error should list available builtins, got: {}",
2549 err
2550 );
2551 }
2552
2553 #[test]
2557 fn test_auto_infer_dark_base_from_dark_bg() {
2558 let json = r#"{ "name": "x", "editor": { "bg": [20, 20, 30] } }"#;
2559 let theme = Theme::from_json(json).expect("should parse");
2560 let dark = Theme::load_builtin(THEME_DARK).expect("dark builtin");
2561 assert_eq!(theme.menu_bg, dark.menu_bg);
2562 assert_eq!(theme.diagnostic_error_fg, dark.diagnostic_error_fg);
2563 }
2564
2565 #[test]
2569 fn test_no_inheritance_signal_uses_hardcoded_defaults() {
2570 let json = r#"{ "name": "x" }"#;
2571 let theme = Theme::from_json(json).expect("should parse");
2572 assert_eq!(theme.editor_bg, Color::Rgb(30, 30, 30));
2575 }
2576
2577 #[test]
2581 fn test_theme_without_name_still_errors() {
2582 let json = r#"{ "editor": {} }"#;
2583 let err = Theme::from_json(json).expect_err("missing `name` must be an error");
2584 assert!(
2585 err.contains("name"),
2586 "error should mention the missing `name` field, got: {}",
2587 err
2588 );
2589 }
2590
2591 #[test]
2596 fn test_extends_overrides_compose_field_by_field() {
2597 let json = r#"{
2598 "name": "dark-with-pink-cursor",
2599 "extends": "builtin://dark",
2600 "editor": { "cursor": [255, 105, 180] }
2601 }"#;
2602 let theme = Theme::from_json(json).expect("should parse");
2603 let dark = Theme::load_builtin(THEME_DARK).expect("dark builtin");
2604
2605 assert_eq!(theme.cursor, Color::Rgb(255, 105, 180));
2607 assert_eq!(theme.editor_bg, dark.editor_bg);
2609 assert_eq!(theme.editor_fg, dark.editor_fg);
2610 assert_eq!(theme.selection_bg, dark.selection_bg);
2611 assert_eq!(theme.menu_bg, dark.menu_bg);
2613 assert_eq!(theme.syntax_keyword, dark.syntax_keyword);
2614 }
2615
2616 #[test]
2617 fn test_default_reset_color() {
2618 let color: Color = ColorDef::Named("Default".to_string()).into();
2620 assert_eq!(color, Color::Reset);
2621
2622 let color: Color = ColorDef::Named("Reset".to_string()).into();
2624 assert_eq!(color, Color::Reset);
2625 }
2626
2627 #[test]
2628 fn test_file_status_colors_fall_back_to_diagnostic_colors() {
2629 let json = r#"{
2631 "name": "test-fallback",
2632 "editor": {},
2633 "ui": {},
2634 "search": {},
2635 "diagnostic": {
2636 "error_fg": [220, 50, 47],
2637 "warning_fg": [181, 137, 0],
2638 "info_fg": [38, 139, 210],
2639 "hint_fg": [101, 123, 131]
2640 },
2641 "syntax": {}
2642 }"#;
2643 let theme = Theme::from_json(json).expect("Should parse theme without file_status keys");
2644
2645 assert_eq!(theme.file_status_added_fg, Color::Rgb(38, 139, 210));
2647 assert_eq!(theme.file_status_renamed_fg, Color::Rgb(38, 139, 210));
2648 assert_eq!(theme.file_status_modified_fg, Color::Rgb(181, 137, 0));
2650 assert_eq!(theme.file_status_deleted_fg, Color::Rgb(220, 50, 47));
2652 assert_eq!(theme.file_status_conflicted_fg, Color::Rgb(220, 50, 47));
2653 assert_eq!(theme.file_status_untracked_fg, Color::Rgb(101, 123, 131));
2655 }
2656
2657 #[test]
2658 fn test_file_status_colors_explicit_override() {
2659 let json = r#"{
2661 "name": "test-override",
2662 "editor": {},
2663 "ui": {
2664 "file_status_added_fg": [80, 250, 123],
2665 "file_status_modified_fg": [255, 184, 108]
2666 },
2667 "search": {},
2668 "diagnostic": {
2669 "info_fg": [38, 139, 210],
2670 "warning_fg": [181, 137, 0]
2671 },
2672 "syntax": {}
2673 }"#;
2674 let theme = Theme::from_json(json).expect("Should parse theme with file_status overrides");
2675
2676 assert_eq!(theme.file_status_added_fg, Color::Rgb(80, 250, 123));
2678 assert_eq!(theme.file_status_modified_fg, Color::Rgb(255, 184, 108));
2679 assert_eq!(theme.file_status_renamed_fg, Color::Rgb(38, 139, 210));
2681 }
2682
2683 #[test]
2684 fn test_file_status_colors_resolve_via_theme_key() {
2685 let json = r#"{
2686 "name": "test-resolve",
2687 "editor": {},
2688 "ui": {
2689 "file_status_added_fg": [80, 250, 123]
2690 },
2691 "search": {},
2692 "diagnostic": {
2693 "warning_fg": [181, 137, 0]
2694 },
2695 "syntax": {}
2696 }"#;
2697 let theme = Theme::from_json(json).expect("Should parse theme");
2698
2699 assert_eq!(
2701 theme.resolve_theme_key("ui.file_status_added_fg"),
2702 Some(Color::Rgb(80, 250, 123))
2703 );
2704 assert_eq!(
2705 theme.resolve_theme_key("ui.file_status_modified_fg"),
2706 Some(Color::Rgb(181, 137, 0))
2707 );
2708 }
2709
2710 #[test]
2711 fn override_colors_writes_known_keys_and_drops_unknowns() {
2712 let mut theme = Theme::load_builtin(THEME_DARK).expect("dark builtin");
2713 let applied = theme.override_colors([
2714 ("editor.bg".to_string(), Color::Rgb(10, 20, 30)),
2715 ("ui.status_bar_fg".to_string(), Color::Rgb(1, 2, 3)),
2716 ("does.not_exist".to_string(), Color::Rgb(9, 9, 9)),
2717 ("garbage_no_dot".to_string(), Color::Rgb(9, 9, 9)),
2718 ]);
2719 assert_eq!(applied, 2, "only the two valid keys should be applied");
2720 assert_eq!(
2721 theme.resolve_theme_key("editor.bg"),
2722 Some(Color::Rgb(10, 20, 30))
2723 );
2724 assert_eq!(
2725 theme.resolve_theme_key("ui.status_bar_fg"),
2726 Some(Color::Rgb(1, 2, 3))
2727 );
2728 }
2729
2730 #[test]
2731 fn resolve_theme_key_mut_matches_resolve_theme_key_domain() {
2732 let mut theme = Theme::load_builtin(THEME_DARK).expect("dark builtin");
2735 let probe = [
2736 "editor.bg",
2737 "editor.fg",
2738 "ui.status_bar_fg",
2739 "ui.tab_active_bg",
2740 "syntax.keyword",
2741 "diagnostic.error_fg",
2742 "search.match_bg",
2743 ];
2744 for key in probe {
2745 assert!(
2746 theme.resolve_theme_key(key).is_some(),
2747 "reader lost key {key}"
2748 );
2749 assert!(
2750 theme.resolve_theme_key_mut(key).is_some(),
2751 "mutator missing key {key}"
2752 );
2753 }
2754 }
2755
2756 fn schema_color_keys() -> Vec<(String, String)> {
2765 let theme = Theme::load_builtin(THEME_DARK).expect("dark builtin");
2766 let file: ThemeFile = theme.into();
2767 let value = serde_json::to_value(&file).expect("ThemeFile serializes");
2768 let obj = value.as_object().expect("ThemeFile is a JSON object");
2769
2770 let mut keys = Vec::new();
2771 for section in ["editor", "ui", "search", "diagnostic", "syntax"] {
2772 let fields = obj
2773 .get(section)
2774 .and_then(|v| v.as_object())
2775 .unwrap_or_else(|| panic!("section `{section}` missing from serialized ThemeFile"));
2776 for (field, val) in fields {
2777 if is_color_leaf(val) {
2778 keys.push((section.to_string(), field.clone()));
2779 }
2780 }
2781 }
2782 assert!(
2783 keys.len() >= 100,
2784 "expected the theme to expose at least ~100 color keys, found {} — \
2785 has the serialization shape changed?",
2786 keys.len()
2787 );
2788 keys
2789 }
2790
2791 fn is_color_leaf(v: &serde_json::Value) -> bool {
2795 v.is_string()
2796 || v.as_array()
2797 .is_some_and(|a| a.len() == 3 && a.iter().all(serde_json::Value::is_number))
2798 }
2799
2800 fn sentinel(i: usize) -> Color {
2804 Color::Rgb((i >> 8) as u8, (i & 0xff) as u8, 0x5a)
2805 }
2806
2807 #[test]
2808 fn every_exposed_color_key_resolves_in_both_directions() {
2809 let mut theme = Theme::load_builtin(THEME_DARK).expect("dark builtin");
2816 let mut missing_reader = Vec::new();
2817 let mut missing_mutator = Vec::new();
2818 for (section, field) in schema_color_keys() {
2819 let key = format!("{section}.{field}");
2820 if theme.resolve_theme_key(&key).is_none() {
2821 missing_reader.push(key.clone());
2822 }
2823 if theme.resolve_theme_key_mut(&key).is_none() {
2824 missing_mutator.push(key);
2825 }
2826 }
2827 assert!(
2828 missing_reader.is_empty() && missing_mutator.is_empty(),
2829 "theme color keys exposed by the JSON schema but dropped by a resolver:\n \
2830 resolve_theme_key: {missing_reader:?}\n \
2831 resolve_theme_key_mut: {missing_mutator:?}"
2832 );
2833 }
2834
2835 #[test]
2836 fn color_keys_round_trip_through_the_same_field_and_section() {
2837 let keys = schema_color_keys();
2848 let mut theme = Theme::load_builtin(THEME_DARK).expect("dark builtin");
2849
2850 let pairs: Vec<(String, Color)> = keys
2851 .iter()
2852 .enumerate()
2853 .map(|(i, (s, f))| (format!("{s}.{f}"), sentinel(i)))
2854 .collect();
2855 let applied = theme.override_colors(pairs.iter().map(|(k, c)| (k.as_str(), *c)));
2856 assert_eq!(
2857 applied,
2858 keys.len(),
2859 "override_colors should write every exposed key via resolve_theme_key_mut"
2860 );
2861
2862 for (i, (s, f)) in keys.iter().enumerate() {
2864 let key = format!("{s}.{f}");
2865 assert_eq!(
2866 theme.resolve_theme_key(&key),
2867 Some(sentinel(i)),
2868 "reader and mutator disagree on the field `{key}` addresses"
2869 );
2870 }
2871
2872 let file: ThemeFile = theme.into();
2875 let value = serde_json::to_value(&file).expect("ThemeFile serializes");
2876 let obj = value.as_object().expect("ThemeFile is a JSON object");
2877 for (i, (s, f)) in keys.iter().enumerate() {
2878 let leaf = obj
2879 .get(s)
2880 .and_then(|sec| sec.get(f))
2881 .unwrap_or_else(|| panic!("`{s}.{f}` vanished from serialized ThemeFile"));
2882 let color: Color = serde_json::from_value::<ColorDef>(leaf.clone())
2883 .expect("color leaf parses as ColorDef")
2884 .into();
2885 assert_eq!(
2886 color,
2887 sentinel(i),
2888 "`{s}.{f}` serialized back to the wrong field or section"
2889 );
2890 }
2891
2892 let reloaded = Theme::from_json(&value.to_string()).expect("from_json round-trips");
2895 for (i, (s, f)) in keys.iter().enumerate() {
2896 let key = format!("{s}.{f}");
2897 assert_eq!(
2898 reloaded.resolve_theme_key(&key),
2899 Some(sentinel(i)),
2900 "`{key}` did not survive ThemeFile -> JSON -> from_json"
2901 );
2902 }
2903 }
2904
2905 #[test]
2906 fn test_all_builtin_themes_set_prominent_palette_indicator() {
2907 for builtin in BUILTIN_THEMES {
2914 let theme = Theme::from_json(builtin.json)
2915 .unwrap_or_else(|e| panic!("Theme '{}' failed to parse: {}", builtin.name, e));
2916 assert!(
2917 theme.status_palette_fg != theme.status_bar_fg
2918 || theme.status_palette_bg != theme.status_bar_bg,
2919 "Theme '{}' must set status_palette_fg/bg to a prominent \
2920 accent distinct from status_bar_fg/bg",
2921 builtin.name
2922 );
2923 }
2924 }
2925
2926 #[test]
2927 fn test_all_builtin_themes_have_file_status_colors() {
2928 for builtin in BUILTIN_THEMES {
2930 let theme = Theme::from_json(builtin.json)
2931 .unwrap_or_else(|e| panic!("Theme '{}' failed to parse: {}", builtin.name, e));
2932
2933 for key in &[
2935 "ui.file_status_added_fg",
2936 "ui.file_status_modified_fg",
2937 "ui.file_status_deleted_fg",
2938 "ui.file_status_renamed_fg",
2939 "ui.file_status_untracked_fg",
2940 "ui.file_status_conflicted_fg",
2941 ] {
2942 assert!(
2943 theme.resolve_theme_key(key).is_some(),
2944 "Theme '{}' missing resolution for '{}'",
2945 builtin.name,
2946 key
2947 );
2948 }
2949 }
2950 }
2951}