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)]
591pub struct UiColors {
592 #[serde(default = "default_tab_active_fg")]
594 pub tab_active_fg: ColorDef,
595 #[serde(default = "default_tab_active_bg")]
597 pub tab_active_bg: ColorDef,
598 #[serde(default = "default_tab_inactive_fg")]
600 pub tab_inactive_fg: ColorDef,
601 #[serde(default = "default_tab_inactive_bg")]
603 pub tab_inactive_bg: ColorDef,
604 #[serde(default = "default_tab_separator_bg")]
606 pub tab_separator_bg: ColorDef,
607 #[serde(default = "default_tab_close_hover_fg")]
609 pub tab_close_hover_fg: ColorDef,
610 #[serde(default = "default_tab_hover_bg")]
612 pub tab_hover_bg: ColorDef,
613 #[serde(default = "default_menu_bg")]
615 pub menu_bg: ColorDef,
616 #[serde(default = "default_menu_fg")]
618 pub menu_fg: ColorDef,
619 #[serde(default = "default_menu_active_bg")]
621 pub menu_active_bg: ColorDef,
622 #[serde(default = "default_menu_active_fg")]
624 pub menu_active_fg: ColorDef,
625 #[serde(default = "default_menu_dropdown_bg")]
627 pub menu_dropdown_bg: ColorDef,
628 #[serde(default = "default_menu_dropdown_fg")]
630 pub menu_dropdown_fg: ColorDef,
631 #[serde(default = "default_menu_highlight_bg")]
633 pub menu_highlight_bg: ColorDef,
634 #[serde(default = "default_menu_highlight_fg")]
636 pub menu_highlight_fg: ColorDef,
637 #[serde(default = "default_menu_border_fg")]
639 pub menu_border_fg: ColorDef,
640 #[serde(default = "default_menu_separator_fg")]
642 pub menu_separator_fg: ColorDef,
643 #[serde(default = "default_menu_hover_bg")]
645 pub menu_hover_bg: ColorDef,
646 #[serde(default = "default_menu_hover_fg")]
648 pub menu_hover_fg: ColorDef,
649 #[serde(default = "default_menu_disabled_fg")]
651 pub menu_disabled_fg: ColorDef,
652 #[serde(default = "default_menu_disabled_bg")]
654 pub menu_disabled_bg: ColorDef,
655 #[serde(default = "default_status_bar_fg")]
657 pub status_bar_fg: ColorDef,
658 #[serde(default = "default_status_bar_bg")]
660 pub status_bar_bg: ColorDef,
661 #[serde(default)]
663 pub status_palette_fg: Option<ColorDef>,
664 #[serde(default)]
666 pub status_palette_bg: Option<ColorDef>,
667 #[serde(default)]
669 pub status_lsp_on_fg: Option<ColorDef>,
670 #[serde(default)]
672 pub status_lsp_on_bg: Option<ColorDef>,
673 #[serde(default)]
677 pub status_lsp_actionable_fg: Option<ColorDef>,
678 #[serde(default)]
681 pub status_lsp_actionable_bg: Option<ColorDef>,
682 #[serde(default = "default_prompt_fg")]
684 pub prompt_fg: ColorDef,
685 #[serde(default = "default_prompt_bg")]
687 pub prompt_bg: ColorDef,
688 #[serde(default = "default_prompt_selection_fg")]
690 pub prompt_selection_fg: ColorDef,
691 #[serde(default = "default_prompt_selection_bg")]
693 pub prompt_selection_bg: ColorDef,
694 #[serde(default = "default_popup_border_fg")]
696 pub popup_border_fg: ColorDef,
697 #[serde(default = "default_popup_bg")]
699 pub popup_bg: ColorDef,
700 #[serde(default = "default_popup_selection_bg")]
702 pub popup_selection_bg: ColorDef,
703 #[serde(default = "default_text_input_selection_bg")]
711 pub text_input_selection_bg: ColorDef,
712 #[serde(default = "default_popup_selection_fg")]
714 pub popup_selection_fg: ColorDef,
715 #[serde(default = "default_popup_text_fg")]
717 pub popup_text_fg: ColorDef,
718 #[serde(default = "default_suggestion_bg")]
720 pub suggestion_bg: ColorDef,
721 #[serde(default = "default_suggestion_selected_bg")]
723 pub suggestion_selected_bg: ColorDef,
724 #[serde(default = "default_help_bg")]
726 pub help_bg: ColorDef,
727 #[serde(default = "default_help_fg")]
729 pub help_fg: ColorDef,
730 #[serde(default = "default_help_key_fg")]
732 pub help_key_fg: ColorDef,
733 #[serde(default = "default_help_separator_fg")]
735 pub help_separator_fg: ColorDef,
736 #[serde(default = "default_help_indicator_fg")]
738 pub help_indicator_fg: ColorDef,
739 #[serde(default = "default_help_indicator_bg")]
741 pub help_indicator_bg: ColorDef,
742 #[serde(default = "default_inline_code_bg")]
744 pub inline_code_bg: ColorDef,
745 #[serde(default = "default_split_separator_fg")]
747 pub split_separator_fg: ColorDef,
748 #[serde(default = "default_split_separator_hover_fg")]
750 pub split_separator_hover_fg: ColorDef,
751 #[serde(default = "default_scrollbar_track_fg")]
753 pub scrollbar_track_fg: ColorDef,
754 #[serde(default = "default_scrollbar_thumb_fg")]
756 pub scrollbar_thumb_fg: ColorDef,
757 #[serde(default = "default_scrollbar_track_hover_fg")]
759 pub scrollbar_track_hover_fg: ColorDef,
760 #[serde(default = "default_scrollbar_thumb_hover_fg")]
762 pub scrollbar_thumb_hover_fg: ColorDef,
763 #[serde(default = "default_compose_margin_bg")]
765 pub compose_margin_bg: ColorDef,
766 #[serde(default = "default_semantic_highlight_bg")]
768 pub semantic_highlight_bg: ColorDef,
769 #[serde(default)]
776 pub semantic_highlight_modifier: Option<ModifierDef>,
777 #[serde(default = "default_terminal_bg")]
779 pub terminal_bg: ColorDef,
780 #[serde(default = "default_terminal_fg")]
782 pub terminal_fg: ColorDef,
783 #[serde(default = "default_status_warning_indicator_bg")]
785 pub status_warning_indicator_bg: ColorDef,
786 #[serde(default = "default_status_warning_indicator_fg")]
788 pub status_warning_indicator_fg: ColorDef,
789 #[serde(default = "default_status_error_indicator_bg")]
791 pub status_error_indicator_bg: ColorDef,
792 #[serde(default = "default_status_error_indicator_fg")]
794 pub status_error_indicator_fg: ColorDef,
795 #[serde(default = "default_status_warning_indicator_hover_bg")]
797 pub status_warning_indicator_hover_bg: ColorDef,
798 #[serde(default = "default_status_warning_indicator_hover_fg")]
800 pub status_warning_indicator_hover_fg: ColorDef,
801 #[serde(default = "default_status_error_indicator_hover_bg")]
803 pub status_error_indicator_hover_bg: ColorDef,
804 #[serde(default = "default_status_error_indicator_hover_fg")]
806 pub status_error_indicator_hover_fg: ColorDef,
807 #[serde(default = "default_tab_drop_zone_bg")]
809 pub tab_drop_zone_bg: ColorDef,
810 #[serde(default = "default_tab_drop_zone_border")]
812 pub tab_drop_zone_border: ColorDef,
813 #[serde(default = "default_settings_selected_bg")]
815 pub settings_selected_bg: ColorDef,
816 #[serde(default = "default_settings_selected_fg")]
818 pub settings_selected_fg: ColorDef,
819 #[serde(default)]
821 pub file_status_added_fg: Option<ColorDef>,
822 #[serde(default)]
824 pub file_status_modified_fg: Option<ColorDef>,
825 #[serde(default)]
827 pub file_status_deleted_fg: Option<ColorDef>,
828 #[serde(default)]
830 pub file_status_renamed_fg: Option<ColorDef>,
831 #[serde(default)]
833 pub file_status_untracked_fg: Option<ColorDef>,
834 #[serde(default)]
836 pub file_status_conflicted_fg: Option<ColorDef>,
837}
838
839fn default_tab_active_fg() -> ColorDef {
842 ColorDef::Named("Yellow".to_string())
843}
844fn default_tab_active_bg() -> ColorDef {
845 ColorDef::Named("Blue".to_string())
846}
847fn default_tab_inactive_fg() -> ColorDef {
848 ColorDef::Named("White".to_string())
849}
850fn default_tab_inactive_bg() -> ColorDef {
851 ColorDef::Named("DarkGray".to_string())
852}
853fn default_tab_separator_bg() -> ColorDef {
854 ColorDef::Named("Black".to_string())
855}
856fn default_tab_close_hover_fg() -> ColorDef {
857 ColorDef::Rgb(255, 100, 100) }
859fn default_tab_hover_bg() -> ColorDef {
860 ColorDef::Rgb(70, 70, 75) }
862
863fn default_menu_bg() -> ColorDef {
865 ColorDef::Rgb(60, 60, 65)
866}
867fn default_menu_fg() -> ColorDef {
868 ColorDef::Rgb(220, 220, 220)
869}
870fn default_menu_active_bg() -> ColorDef {
871 ColorDef::Rgb(60, 60, 60)
872}
873fn default_menu_active_fg() -> ColorDef {
874 ColorDef::Rgb(255, 255, 255)
875}
876fn default_menu_dropdown_bg() -> ColorDef {
877 ColorDef::Rgb(50, 50, 50)
878}
879fn default_menu_dropdown_fg() -> ColorDef {
880 ColorDef::Rgb(220, 220, 220)
881}
882fn default_menu_highlight_bg() -> ColorDef {
883 ColorDef::Rgb(70, 130, 180)
884}
885fn default_menu_highlight_fg() -> ColorDef {
886 ColorDef::Rgb(255, 255, 255)
887}
888fn default_menu_border_fg() -> ColorDef {
889 ColorDef::Rgb(100, 100, 100)
890}
891fn default_menu_separator_fg() -> ColorDef {
892 ColorDef::Rgb(80, 80, 80)
893}
894fn default_menu_hover_bg() -> ColorDef {
895 ColorDef::Rgb(55, 55, 55)
896}
897fn default_menu_hover_fg() -> ColorDef {
898 ColorDef::Rgb(255, 255, 255)
899}
900fn default_menu_disabled_fg() -> ColorDef {
901 ColorDef::Rgb(100, 100, 100) }
903fn default_menu_disabled_bg() -> ColorDef {
904 ColorDef::Rgb(50, 50, 50) }
906fn default_status_bar_fg() -> ColorDef {
908 ColorDef::Named("White".to_string())
909}
910fn default_status_bar_bg() -> ColorDef {
911 ColorDef::Named("DarkGray".to_string())
912}
913
914fn default_prompt_fg() -> ColorDef {
916 ColorDef::Named("White".to_string())
917}
918fn default_prompt_bg() -> ColorDef {
919 ColorDef::Named("Black".to_string())
920}
921fn default_prompt_selection_fg() -> ColorDef {
922 ColorDef::Named("White".to_string())
923}
924fn default_prompt_selection_bg() -> ColorDef {
925 ColorDef::Rgb(58, 79, 120)
926}
927
928fn default_popup_border_fg() -> ColorDef {
930 ColorDef::Named("Gray".to_string())
931}
932fn default_popup_bg() -> ColorDef {
933 ColorDef::Rgb(30, 30, 30)
934}
935fn default_popup_selection_bg() -> ColorDef {
936 ColorDef::Rgb(58, 79, 120)
937}
938fn default_text_input_selection_bg() -> ColorDef {
939 ColorDef::Rgb(58, 79, 120)
943}
944fn default_popup_selection_fg() -> ColorDef {
945 ColorDef::Rgb(255, 255, 255) }
947fn default_popup_text_fg() -> ColorDef {
948 ColorDef::Named("White".to_string())
949}
950
951fn default_suggestion_bg() -> ColorDef {
953 ColorDef::Rgb(30, 30, 30)
954}
955fn default_suggestion_selected_bg() -> ColorDef {
956 ColorDef::Rgb(58, 79, 120)
957}
958
959fn default_help_bg() -> ColorDef {
961 ColorDef::Named("Black".to_string())
962}
963fn default_help_fg() -> ColorDef {
964 ColorDef::Named("White".to_string())
965}
966fn default_help_key_fg() -> ColorDef {
967 ColorDef::Named("Cyan".to_string())
968}
969fn default_help_separator_fg() -> ColorDef {
970 ColorDef::Named("DarkGray".to_string())
971}
972fn default_help_indicator_fg() -> ColorDef {
973 ColorDef::Named("Red".to_string())
974}
975fn default_help_indicator_bg() -> ColorDef {
976 ColorDef::Named("Black".to_string())
977}
978
979fn default_inline_code_bg() -> ColorDef {
980 ColorDef::Named("DarkGray".to_string())
981}
982
983fn default_split_separator_fg() -> ColorDef {
985 ColorDef::Rgb(100, 100, 100)
986}
987fn default_split_separator_hover_fg() -> ColorDef {
988 ColorDef::Rgb(100, 149, 237) }
990fn default_scrollbar_track_fg() -> ColorDef {
991 ColorDef::Named("DarkGray".to_string())
992}
993fn default_scrollbar_thumb_fg() -> ColorDef {
994 ColorDef::Named("Gray".to_string())
995}
996fn default_scrollbar_track_hover_fg() -> ColorDef {
997 ColorDef::Named("Gray".to_string())
998}
999fn default_scrollbar_thumb_hover_fg() -> ColorDef {
1000 ColorDef::Named("White".to_string())
1001}
1002fn default_compose_margin_bg() -> ColorDef {
1003 ColorDef::Rgb(18, 18, 18) }
1005fn default_semantic_highlight_bg() -> ColorDef {
1006 ColorDef::Rgb(60, 60, 80) }
1008fn default_terminal_bg() -> ColorDef {
1009 ColorDef::Named("Default".to_string()) }
1011fn default_terminal_fg() -> ColorDef {
1012 ColorDef::Named("Default".to_string()) }
1014fn default_status_warning_indicator_bg() -> ColorDef {
1015 ColorDef::Rgb(181, 137, 0) }
1017fn default_status_warning_indicator_fg() -> ColorDef {
1018 ColorDef::Rgb(0, 0, 0) }
1020fn default_status_error_indicator_bg() -> ColorDef {
1021 ColorDef::Rgb(220, 50, 47) }
1023fn default_status_error_indicator_fg() -> ColorDef {
1024 ColorDef::Rgb(255, 255, 255) }
1026fn default_status_warning_indicator_hover_bg() -> ColorDef {
1027 ColorDef::Rgb(211, 167, 30) }
1029fn default_status_warning_indicator_hover_fg() -> ColorDef {
1030 ColorDef::Rgb(0, 0, 0) }
1032fn default_status_error_indicator_hover_bg() -> ColorDef {
1033 ColorDef::Rgb(250, 80, 77) }
1035fn default_status_error_indicator_hover_fg() -> ColorDef {
1036 ColorDef::Rgb(255, 255, 255) }
1038fn default_tab_drop_zone_bg() -> ColorDef {
1039 ColorDef::Rgb(70, 130, 180) }
1041fn default_tab_drop_zone_border() -> ColorDef {
1042 ColorDef::Rgb(100, 149, 237) }
1044fn default_settings_selected_bg() -> ColorDef {
1045 ColorDef::Rgb(60, 60, 70) }
1047fn default_settings_selected_fg() -> ColorDef {
1048 ColorDef::Rgb(255, 255, 255) }
1050#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1052pub struct SearchColors {
1053 #[serde(default = "default_search_match_bg")]
1055 pub match_bg: ColorDef,
1056 #[serde(default = "default_search_match_fg")]
1058 pub match_fg: ColorDef,
1059 #[serde(default = "default_search_label_bg")]
1063 pub label_bg: ColorDef,
1064 #[serde(default = "default_search_label_fg")]
1068 pub label_fg: ColorDef,
1069}
1070
1071fn default_search_match_bg() -> ColorDef {
1073 ColorDef::Rgb(100, 100, 20)
1074}
1075fn default_search_match_fg() -> ColorDef {
1076 ColorDef::Rgb(255, 255, 255)
1077}
1078fn default_search_label_bg() -> ColorDef {
1083 ColorDef::Rgb(199, 78, 189)
1084}
1085fn default_search_label_fg() -> ColorDef {
1086 ColorDef::Rgb(255, 255, 255)
1087}
1088
1089#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1091pub struct DiagnosticColors {
1092 #[serde(default = "default_diagnostic_error_fg")]
1094 pub error_fg: ColorDef,
1095 #[serde(default = "default_diagnostic_error_bg")]
1097 pub error_bg: ColorDef,
1098 #[serde(default = "default_diagnostic_warning_fg")]
1100 pub warning_fg: ColorDef,
1101 #[serde(default = "default_diagnostic_warning_bg")]
1103 pub warning_bg: ColorDef,
1104 #[serde(default = "default_diagnostic_info_fg")]
1106 pub info_fg: ColorDef,
1107 #[serde(default = "default_diagnostic_info_bg")]
1109 pub info_bg: ColorDef,
1110 #[serde(default = "default_diagnostic_hint_fg")]
1112 pub hint_fg: ColorDef,
1113 #[serde(default = "default_diagnostic_hint_bg")]
1115 pub hint_bg: ColorDef,
1116}
1117
1118fn default_diagnostic_error_fg() -> ColorDef {
1120 ColorDef::Named("Red".to_string())
1121}
1122fn default_diagnostic_error_bg() -> ColorDef {
1123 ColorDef::Rgb(60, 20, 20)
1124}
1125fn default_diagnostic_warning_fg() -> ColorDef {
1126 ColorDef::Named("Yellow".to_string())
1127}
1128fn default_diagnostic_warning_bg() -> ColorDef {
1129 ColorDef::Rgb(60, 50, 0)
1130}
1131fn default_diagnostic_info_fg() -> ColorDef {
1132 ColorDef::Named("Blue".to_string())
1133}
1134fn default_diagnostic_info_bg() -> ColorDef {
1135 ColorDef::Rgb(0, 30, 60)
1136}
1137fn default_diagnostic_hint_fg() -> ColorDef {
1138 ColorDef::Named("Gray".to_string())
1139}
1140fn default_diagnostic_hint_bg() -> ColorDef {
1141 ColorDef::Rgb(30, 30, 30)
1142}
1143
1144#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1146pub struct SyntaxColors {
1147 #[serde(default = "default_syntax_keyword")]
1149 pub keyword: ColorDef,
1150 #[serde(default = "default_syntax_string")]
1152 pub string: ColorDef,
1153 #[serde(default = "default_syntax_comment")]
1155 pub comment: ColorDef,
1156 #[serde(default = "default_syntax_function")]
1158 pub function: ColorDef,
1159 #[serde(rename = "type", default = "default_syntax_type")]
1161 pub type_: ColorDef,
1162 #[serde(default = "default_syntax_variable")]
1164 pub variable: ColorDef,
1165 #[serde(default = "default_syntax_constant")]
1167 pub constant: ColorDef,
1168 #[serde(default = "default_syntax_operator")]
1170 pub operator: ColorDef,
1171 #[serde(default = "default_syntax_punctuation_bracket")]
1173 pub punctuation_bracket: ColorDef,
1174 #[serde(default = "default_syntax_punctuation_delimiter")]
1176 pub punctuation_delimiter: ColorDef,
1177}
1178
1179fn default_syntax_keyword() -> ColorDef {
1181 ColorDef::Rgb(86, 156, 214)
1182}
1183fn default_syntax_string() -> ColorDef {
1184 ColorDef::Rgb(206, 145, 120)
1185}
1186fn default_syntax_comment() -> ColorDef {
1187 ColorDef::Rgb(106, 153, 85)
1188}
1189fn default_syntax_function() -> ColorDef {
1190 ColorDef::Rgb(220, 220, 170)
1191}
1192fn default_syntax_type() -> ColorDef {
1193 ColorDef::Rgb(78, 201, 176)
1194}
1195fn default_syntax_variable() -> ColorDef {
1196 ColorDef::Rgb(156, 220, 254)
1197}
1198fn default_syntax_constant() -> ColorDef {
1199 ColorDef::Rgb(79, 193, 255)
1200}
1201fn default_syntax_operator() -> ColorDef {
1202 ColorDef::Rgb(212, 212, 212)
1203}
1204fn default_syntax_punctuation_bracket() -> ColorDef {
1205 ColorDef::Rgb(212, 212, 212) }
1207fn default_syntax_punctuation_delimiter() -> ColorDef {
1208 ColorDef::Rgb(212, 212, 212) }
1210
1211#[derive(Debug, Clone)]
1213pub struct Theme {
1214 pub name: String,
1216
1217 pub editor_bg: Color,
1219 pub editor_fg: Color,
1220 pub cursor: Color,
1221 pub inactive_cursor: Color,
1222 pub selection_bg: Color,
1223 pub selection_modifier: Modifier,
1228 pub current_line_bg: Color,
1229 pub line_number_fg: Color,
1230 pub line_number_bg: Color,
1231
1232 pub after_eof_bg: Color,
1234
1235 pub ruler_bg: Color,
1237
1238 pub whitespace_indicator_fg: Color,
1240
1241 pub diff_add_bg: Color,
1243 pub diff_remove_bg: Color,
1244 pub diff_modify_bg: Color,
1245 pub diff_add_highlight_bg: Color,
1247 pub diff_remove_highlight_bg: Color,
1249 pub diff_add_collision_fg: Option<Color>,
1253 pub diff_remove_collision_fg: Option<Color>,
1254 pub diff_modify_collision_fg: Option<Color>,
1255
1256 pub tab_active_fg: Color,
1258 pub tab_active_bg: Color,
1259 pub tab_inactive_fg: Color,
1260 pub tab_inactive_bg: Color,
1261 pub tab_separator_bg: Color,
1262 pub tab_close_hover_fg: Color,
1263 pub tab_hover_bg: Color,
1264
1265 pub menu_bg: Color,
1267 pub menu_fg: Color,
1268 pub menu_active_bg: Color,
1269 pub menu_active_fg: Color,
1270 pub menu_dropdown_bg: Color,
1271 pub menu_dropdown_fg: Color,
1272 pub menu_highlight_bg: Color,
1273 pub menu_highlight_fg: Color,
1274 pub menu_border_fg: Color,
1275 pub menu_separator_fg: Color,
1276 pub menu_hover_bg: Color,
1277 pub menu_hover_fg: Color,
1278 pub menu_disabled_fg: Color,
1279 pub menu_disabled_bg: Color,
1280
1281 pub status_bar_fg: Color,
1282 pub status_bar_bg: Color,
1283 pub status_palette_fg: Color,
1285 pub status_palette_bg: Color,
1286 pub status_lsp_on_fg: Color,
1288 pub status_lsp_on_bg: Color,
1289 pub status_lsp_actionable_fg: Color,
1292 pub status_lsp_actionable_bg: Color,
1293 pub prompt_fg: Color,
1294 pub prompt_bg: Color,
1295 pub prompt_selection_fg: Color,
1296 pub prompt_selection_bg: Color,
1297
1298 pub popup_border_fg: Color,
1299 pub popup_bg: Color,
1300 pub popup_selection_bg: Color,
1301 pub popup_selection_fg: Color,
1302 pub popup_text_fg: Color,
1303 pub text_input_selection_bg: Color,
1307
1308 pub suggestion_bg: Color,
1309 pub suggestion_selected_bg: Color,
1310
1311 pub help_bg: Color,
1312 pub help_fg: Color,
1313 pub help_key_fg: Color,
1314 pub help_separator_fg: Color,
1315
1316 pub help_indicator_fg: Color,
1317 pub help_indicator_bg: Color,
1318
1319 pub inline_code_bg: Color,
1321
1322 pub split_separator_fg: Color,
1323 pub split_separator_hover_fg: Color,
1324
1325 pub scrollbar_track_fg: Color,
1327 pub scrollbar_thumb_fg: Color,
1328 pub scrollbar_track_hover_fg: Color,
1329 pub scrollbar_thumb_hover_fg: Color,
1330
1331 pub compose_margin_bg: Color,
1333
1334 pub semantic_highlight_bg: Color,
1336 pub semantic_highlight_modifier: Modifier,
1341
1342 pub terminal_bg: Color,
1344 pub terminal_fg: Color,
1345
1346 pub status_warning_indicator_bg: Color,
1348 pub status_warning_indicator_fg: Color,
1349 pub status_error_indicator_bg: Color,
1350 pub status_error_indicator_fg: Color,
1351 pub status_warning_indicator_hover_bg: Color,
1352 pub status_warning_indicator_hover_fg: Color,
1353 pub status_error_indicator_hover_bg: Color,
1354 pub status_error_indicator_hover_fg: Color,
1355
1356 pub tab_drop_zone_bg: Color,
1358 pub tab_drop_zone_border: Color,
1359
1360 pub settings_selected_bg: Color,
1362 pub settings_selected_fg: Color,
1363
1364 pub file_status_added_fg: Color,
1366 pub file_status_modified_fg: Color,
1367 pub file_status_deleted_fg: Color,
1368 pub file_status_renamed_fg: Color,
1369 pub file_status_untracked_fg: Color,
1370 pub file_status_conflicted_fg: Color,
1371
1372 pub search_match_bg: Color,
1374 pub search_match_fg: Color,
1375 pub search_label_bg: Color,
1376 pub search_label_fg: Color,
1377
1378 pub diagnostic_error_fg: Color,
1380 pub diagnostic_error_bg: Color,
1381 pub diagnostic_warning_fg: Color,
1382 pub diagnostic_warning_bg: Color,
1383 pub diagnostic_info_fg: Color,
1384 pub diagnostic_info_bg: Color,
1385 pub diagnostic_hint_fg: Color,
1386 pub diagnostic_hint_bg: Color,
1387
1388 pub syntax_keyword: Color,
1390 pub syntax_string: Color,
1391 pub syntax_comment: Color,
1392 pub syntax_function: Color,
1393 pub syntax_type: Color,
1394 pub syntax_variable: Color,
1395 pub syntax_constant: Color,
1396 pub syntax_operator: Color,
1397 pub syntax_punctuation_bracket: Color,
1398 pub syntax_punctuation_delimiter: Color,
1399}
1400
1401impl From<ThemeFile> for Theme {
1402 fn from(file: ThemeFile) -> Self {
1403 Self {
1404 name: file.name,
1405 editor_bg: file.editor.bg.clone().into(),
1406 editor_fg: file.editor.fg.into(),
1407 cursor: file.editor.cursor.into(),
1408 inactive_cursor: file.editor.inactive_cursor.into(),
1409 selection_bg: file.editor.selection_bg.into(),
1410 selection_modifier: file
1411 .editor
1412 .selection_modifier
1413 .as_ref()
1414 .map(Modifier::from)
1415 .unwrap_or(Modifier::empty()),
1416 current_line_bg: file.editor.current_line_bg.into(),
1417 line_number_fg: file.editor.line_number_fg.into(),
1418 line_number_bg: file.editor.line_number_bg.into(),
1419 after_eof_bg: file
1422 .editor
1423 .after_eof_bg
1424 .clone()
1425 .map(|c| c.into())
1426 .unwrap_or_else(|| shade_toward_contrast(file.editor.bg.clone().into(), 10)),
1427 ruler_bg: file.editor.ruler_bg.into(),
1428 whitespace_indicator_fg: file.editor.whitespace_indicator_fg.into(),
1429 diff_add_bg: file.editor.diff_add_bg.clone().into(),
1430 diff_remove_bg: file.editor.diff_remove_bg.clone().into(),
1431 diff_modify_bg: file.editor.diff_modify_bg.into(),
1432 diff_add_highlight_bg: file
1434 .editor
1435 .diff_add_highlight_bg
1436 .map(|c| c.into())
1437 .unwrap_or_else(|| brighten_color(file.editor.diff_add_bg.into(), 40)),
1438 diff_remove_highlight_bg: file
1439 .editor
1440 .diff_remove_highlight_bg
1441 .map(|c| c.into())
1442 .unwrap_or_else(|| brighten_color(file.editor.diff_remove_bg.into(), 40)),
1443 diff_add_collision_fg: file.editor.diff_add_collision_fg.clone().map(|c| c.into()),
1444 diff_remove_collision_fg: file
1445 .editor
1446 .diff_remove_collision_fg
1447 .clone()
1448 .map(|c| c.into()),
1449 diff_modify_collision_fg: file
1450 .editor
1451 .diff_modify_collision_fg
1452 .clone()
1453 .map(|c| c.into()),
1454 tab_active_fg: file.ui.tab_active_fg.into(),
1455 tab_active_bg: file.ui.tab_active_bg.into(),
1456 tab_inactive_fg: file.ui.tab_inactive_fg.into(),
1457 tab_inactive_bg: file.ui.tab_inactive_bg.into(),
1458 tab_separator_bg: file.ui.tab_separator_bg.into(),
1459 tab_close_hover_fg: file.ui.tab_close_hover_fg.into(),
1460 tab_hover_bg: file.ui.tab_hover_bg.into(),
1461 menu_bg: file.ui.menu_bg.into(),
1462 menu_fg: file.ui.menu_fg.into(),
1463 menu_active_bg: file.ui.menu_active_bg.into(),
1464 menu_active_fg: file.ui.menu_active_fg.into(),
1465 menu_dropdown_bg: file.ui.menu_dropdown_bg.into(),
1466 menu_dropdown_fg: file.ui.menu_dropdown_fg.into(),
1467 menu_highlight_bg: file.ui.menu_highlight_bg.into(),
1468 menu_highlight_fg: file.ui.menu_highlight_fg.into(),
1469 menu_border_fg: file.ui.menu_border_fg.into(),
1470 menu_separator_fg: file.ui.menu_separator_fg.into(),
1471 menu_hover_bg: file.ui.menu_hover_bg.into(),
1472 menu_hover_fg: file.ui.menu_hover_fg.into(),
1473 menu_disabled_fg: file.ui.menu_disabled_fg.into(),
1474 menu_disabled_bg: file.ui.menu_disabled_bg.into(),
1475 status_bar_fg: file.ui.status_bar_fg.clone().into(),
1476 status_bar_bg: file.ui.status_bar_bg.clone().into(),
1477 status_palette_fg: file
1478 .ui
1479 .status_palette_fg
1480 .clone()
1481 .map(|c| c.into())
1482 .unwrap_or_else(|| file.ui.status_bar_fg.clone().into()),
1483 status_palette_bg: file
1484 .ui
1485 .status_palette_bg
1486 .clone()
1487 .map(|c| c.into())
1488 .unwrap_or_else(|| file.ui.status_bar_bg.clone().into()),
1489 status_lsp_on_fg: file
1490 .ui
1491 .status_lsp_on_fg
1492 .clone()
1493 .map(|c| c.into())
1494 .unwrap_or_else(|| file.ui.status_bar_fg.clone().into()),
1495 status_lsp_on_bg: file
1496 .ui
1497 .status_lsp_on_bg
1498 .clone()
1499 .map(|c| c.into())
1500 .unwrap_or_else(|| file.ui.status_bar_bg.clone().into()),
1501 status_lsp_actionable_fg: file
1502 .ui
1503 .status_lsp_actionable_fg
1504 .clone()
1505 .map(|c| c.into())
1506 .unwrap_or_else(|| file.ui.status_warning_indicator_fg.clone().into()),
1507 status_lsp_actionable_bg: file
1508 .ui
1509 .status_lsp_actionable_bg
1510 .clone()
1511 .map(|c| c.into())
1512 .unwrap_or_else(|| file.ui.status_warning_indicator_bg.clone().into()),
1513 prompt_fg: file.ui.prompt_fg.into(),
1514 prompt_bg: file.ui.prompt_bg.into(),
1515 prompt_selection_fg: file.ui.prompt_selection_fg.into(),
1516 prompt_selection_bg: file.ui.prompt_selection_bg.into(),
1517 popup_border_fg: file.ui.popup_border_fg.into(),
1518 popup_bg: file.ui.popup_bg.into(),
1519 popup_selection_bg: file.ui.popup_selection_bg.into(),
1520 popup_selection_fg: file.ui.popup_selection_fg.into(),
1521 popup_text_fg: file.ui.popup_text_fg.into(),
1522 text_input_selection_bg: file.ui.text_input_selection_bg.into(),
1523 suggestion_bg: file.ui.suggestion_bg.into(),
1524 suggestion_selected_bg: file.ui.suggestion_selected_bg.into(),
1525 help_bg: file.ui.help_bg.into(),
1526 help_fg: file.ui.help_fg.into(),
1527 help_key_fg: file.ui.help_key_fg.into(),
1528 help_separator_fg: file.ui.help_separator_fg.into(),
1529 help_indicator_fg: file.ui.help_indicator_fg.into(),
1530 help_indicator_bg: file.ui.help_indicator_bg.into(),
1531 inline_code_bg: file.ui.inline_code_bg.into(),
1532 split_separator_fg: file.ui.split_separator_fg.into(),
1533 split_separator_hover_fg: file.ui.split_separator_hover_fg.into(),
1534 scrollbar_track_fg: file.ui.scrollbar_track_fg.into(),
1535 scrollbar_thumb_fg: file.ui.scrollbar_thumb_fg.into(),
1536 scrollbar_track_hover_fg: file.ui.scrollbar_track_hover_fg.into(),
1537 scrollbar_thumb_hover_fg: file.ui.scrollbar_thumb_hover_fg.into(),
1538 compose_margin_bg: file.ui.compose_margin_bg.into(),
1539 semantic_highlight_bg: file.ui.semantic_highlight_bg.into(),
1540 semantic_highlight_modifier: file
1541 .ui
1542 .semantic_highlight_modifier
1543 .as_ref()
1544 .map(Modifier::from)
1545 .unwrap_or(Modifier::empty()),
1546 terminal_bg: file.ui.terminal_bg.into(),
1547 terminal_fg: file.ui.terminal_fg.into(),
1548 status_warning_indicator_bg: file.ui.status_warning_indicator_bg.into(),
1549 status_warning_indicator_fg: file.ui.status_warning_indicator_fg.into(),
1550 status_error_indicator_bg: file.ui.status_error_indicator_bg.into(),
1551 status_error_indicator_fg: file.ui.status_error_indicator_fg.into(),
1552 status_warning_indicator_hover_bg: file.ui.status_warning_indicator_hover_bg.into(),
1553 status_warning_indicator_hover_fg: file.ui.status_warning_indicator_hover_fg.into(),
1554 status_error_indicator_hover_bg: file.ui.status_error_indicator_hover_bg.into(),
1555 status_error_indicator_hover_fg: file.ui.status_error_indicator_hover_fg.into(),
1556 tab_drop_zone_bg: file.ui.tab_drop_zone_bg.into(),
1557 tab_drop_zone_border: file.ui.tab_drop_zone_border.into(),
1558 settings_selected_bg: file.ui.settings_selected_bg.into(),
1559 settings_selected_fg: file.ui.settings_selected_fg.into(),
1560 file_status_added_fg: file
1561 .ui
1562 .file_status_added_fg
1563 .map(|c| c.into())
1564 .unwrap_or_else(|| file.diagnostic.info_fg.clone().into()),
1565 file_status_modified_fg: file
1566 .ui
1567 .file_status_modified_fg
1568 .map(|c| c.into())
1569 .unwrap_or_else(|| file.diagnostic.warning_fg.clone().into()),
1570 file_status_deleted_fg: file
1571 .ui
1572 .file_status_deleted_fg
1573 .map(|c| c.into())
1574 .unwrap_or_else(|| file.diagnostic.error_fg.clone().into()),
1575 file_status_renamed_fg: file
1576 .ui
1577 .file_status_renamed_fg
1578 .map(|c| c.into())
1579 .unwrap_or_else(|| file.diagnostic.info_fg.clone().into()),
1580 file_status_untracked_fg: file
1581 .ui
1582 .file_status_untracked_fg
1583 .map(|c| c.into())
1584 .unwrap_or_else(|| file.diagnostic.hint_fg.clone().into()),
1585 file_status_conflicted_fg: file
1586 .ui
1587 .file_status_conflicted_fg
1588 .map(|c| c.into())
1589 .unwrap_or_else(|| file.diagnostic.error_fg.clone().into()),
1590 search_match_bg: file.search.match_bg.into(),
1591 search_match_fg: file.search.match_fg.into(),
1592 search_label_bg: file.search.label_bg.into(),
1593 search_label_fg: file.search.label_fg.into(),
1594 diagnostic_error_fg: file.diagnostic.error_fg.into(),
1595 diagnostic_error_bg: file.diagnostic.error_bg.into(),
1596 diagnostic_warning_fg: file.diagnostic.warning_fg.into(),
1597 diagnostic_warning_bg: file.diagnostic.warning_bg.into(),
1598 diagnostic_info_fg: file.diagnostic.info_fg.into(),
1599 diagnostic_info_bg: file.diagnostic.info_bg.into(),
1600 diagnostic_hint_fg: file.diagnostic.hint_fg.into(),
1601 diagnostic_hint_bg: file.diagnostic.hint_bg.into(),
1602 syntax_keyword: file.syntax.keyword.into(),
1603 syntax_string: file.syntax.string.into(),
1604 syntax_comment: file.syntax.comment.into(),
1605 syntax_function: file.syntax.function.into(),
1606 syntax_type: file.syntax.type_.into(),
1607 syntax_variable: file.syntax.variable.into(),
1608 syntax_constant: file.syntax.constant.into(),
1609 syntax_operator: file.syntax.operator.into(),
1610 syntax_punctuation_bracket: file.syntax.punctuation_bracket.into(),
1611 syntax_punctuation_delimiter: file.syntax.punctuation_delimiter.into(),
1612 }
1613 }
1614}
1615
1616impl From<Theme> for ThemeFile {
1617 fn from(theme: Theme) -> Self {
1618 Self {
1619 name: theme.name,
1620 extends: None,
1623 editor: EditorColors {
1624 bg: theme.editor_bg.into(),
1625 fg: theme.editor_fg.into(),
1626 cursor: theme.cursor.into(),
1627 inactive_cursor: theme.inactive_cursor.into(),
1628 selection_bg: theme.selection_bg.into(),
1629 selection_modifier: if theme.selection_modifier.is_empty() {
1630 None
1631 } else {
1632 Some(theme.selection_modifier.into())
1633 },
1634 current_line_bg: theme.current_line_bg.into(),
1635 line_number_fg: theme.line_number_fg.into(),
1636 line_number_bg: theme.line_number_bg.into(),
1637 diff_add_bg: theme.diff_add_bg.into(),
1638 diff_remove_bg: theme.diff_remove_bg.into(),
1639 diff_add_highlight_bg: Some(theme.diff_add_highlight_bg.into()),
1640 diff_remove_highlight_bg: Some(theme.diff_remove_highlight_bg.into()),
1641 diff_modify_bg: theme.diff_modify_bg.into(),
1642 diff_add_collision_fg: theme.diff_add_collision_fg.map(|c| c.into()),
1643 diff_remove_collision_fg: theme.diff_remove_collision_fg.map(|c| c.into()),
1644 diff_modify_collision_fg: theme.diff_modify_collision_fg.map(|c| c.into()),
1645 ruler_bg: theme.ruler_bg.into(),
1646 whitespace_indicator_fg: theme.whitespace_indicator_fg.into(),
1647 after_eof_bg: Some(theme.after_eof_bg.into()),
1648 },
1649 ui: UiColors {
1650 tab_active_fg: theme.tab_active_fg.into(),
1651 tab_active_bg: theme.tab_active_bg.into(),
1652 tab_inactive_fg: theme.tab_inactive_fg.into(),
1653 tab_inactive_bg: theme.tab_inactive_bg.into(),
1654 tab_separator_bg: theme.tab_separator_bg.into(),
1655 tab_close_hover_fg: theme.tab_close_hover_fg.into(),
1656 tab_hover_bg: theme.tab_hover_bg.into(),
1657 menu_bg: theme.menu_bg.into(),
1658 menu_fg: theme.menu_fg.into(),
1659 menu_active_bg: theme.menu_active_bg.into(),
1660 menu_active_fg: theme.menu_active_fg.into(),
1661 menu_dropdown_bg: theme.menu_dropdown_bg.into(),
1662 menu_dropdown_fg: theme.menu_dropdown_fg.into(),
1663 menu_highlight_bg: theme.menu_highlight_bg.into(),
1664 menu_highlight_fg: theme.menu_highlight_fg.into(),
1665 menu_border_fg: theme.menu_border_fg.into(),
1666 menu_separator_fg: theme.menu_separator_fg.into(),
1667 menu_hover_bg: theme.menu_hover_bg.into(),
1668 menu_hover_fg: theme.menu_hover_fg.into(),
1669 menu_disabled_fg: theme.menu_disabled_fg.into(),
1670 menu_disabled_bg: theme.menu_disabled_bg.into(),
1671 status_bar_fg: theme.status_bar_fg.into(),
1672 status_bar_bg: theme.status_bar_bg.into(),
1673 status_palette_fg: Some(theme.status_palette_fg.into()),
1674 status_palette_bg: Some(theme.status_palette_bg.into()),
1675 status_lsp_on_fg: Some(theme.status_lsp_on_fg.into()),
1676 status_lsp_on_bg: Some(theme.status_lsp_on_bg.into()),
1677 status_lsp_actionable_fg: Some(theme.status_lsp_actionable_fg.into()),
1678 status_lsp_actionable_bg: Some(theme.status_lsp_actionable_bg.into()),
1679 prompt_fg: theme.prompt_fg.into(),
1680 prompt_bg: theme.prompt_bg.into(),
1681 prompt_selection_fg: theme.prompt_selection_fg.into(),
1682 prompt_selection_bg: theme.prompt_selection_bg.into(),
1683 popup_border_fg: theme.popup_border_fg.into(),
1684 popup_bg: theme.popup_bg.into(),
1685 popup_selection_bg: theme.popup_selection_bg.into(),
1686 popup_selection_fg: theme.popup_selection_fg.into(),
1687 popup_text_fg: theme.popup_text_fg.into(),
1688 text_input_selection_bg: theme.text_input_selection_bg.into(),
1689 suggestion_bg: theme.suggestion_bg.into(),
1690 suggestion_selected_bg: theme.suggestion_selected_bg.into(),
1691 help_bg: theme.help_bg.into(),
1692 help_fg: theme.help_fg.into(),
1693 help_key_fg: theme.help_key_fg.into(),
1694 help_separator_fg: theme.help_separator_fg.into(),
1695 help_indicator_fg: theme.help_indicator_fg.into(),
1696 help_indicator_bg: theme.help_indicator_bg.into(),
1697 inline_code_bg: theme.inline_code_bg.into(),
1698 split_separator_fg: theme.split_separator_fg.into(),
1699 split_separator_hover_fg: theme.split_separator_hover_fg.into(),
1700 scrollbar_track_fg: theme.scrollbar_track_fg.into(),
1701 scrollbar_thumb_fg: theme.scrollbar_thumb_fg.into(),
1702 scrollbar_track_hover_fg: theme.scrollbar_track_hover_fg.into(),
1703 scrollbar_thumb_hover_fg: theme.scrollbar_thumb_hover_fg.into(),
1704 compose_margin_bg: theme.compose_margin_bg.into(),
1705 semantic_highlight_bg: theme.semantic_highlight_bg.into(),
1706 semantic_highlight_modifier: if theme.semantic_highlight_modifier.is_empty() {
1707 None
1708 } else {
1709 Some(theme.semantic_highlight_modifier.into())
1710 },
1711 terminal_bg: theme.terminal_bg.into(),
1712 terminal_fg: theme.terminal_fg.into(),
1713 status_warning_indicator_bg: theme.status_warning_indicator_bg.into(),
1714 status_warning_indicator_fg: theme.status_warning_indicator_fg.into(),
1715 status_error_indicator_bg: theme.status_error_indicator_bg.into(),
1716 status_error_indicator_fg: theme.status_error_indicator_fg.into(),
1717 status_warning_indicator_hover_bg: theme.status_warning_indicator_hover_bg.into(),
1718 status_warning_indicator_hover_fg: theme.status_warning_indicator_hover_fg.into(),
1719 status_error_indicator_hover_bg: theme.status_error_indicator_hover_bg.into(),
1720 status_error_indicator_hover_fg: theme.status_error_indicator_hover_fg.into(),
1721 tab_drop_zone_bg: theme.tab_drop_zone_bg.into(),
1722 tab_drop_zone_border: theme.tab_drop_zone_border.into(),
1723 settings_selected_bg: theme.settings_selected_bg.into(),
1724 settings_selected_fg: theme.settings_selected_fg.into(),
1725 file_status_added_fg: Some(theme.file_status_added_fg.into()),
1726 file_status_modified_fg: Some(theme.file_status_modified_fg.into()),
1727 file_status_deleted_fg: Some(theme.file_status_deleted_fg.into()),
1728 file_status_renamed_fg: Some(theme.file_status_renamed_fg.into()),
1729 file_status_untracked_fg: Some(theme.file_status_untracked_fg.into()),
1730 file_status_conflicted_fg: Some(theme.file_status_conflicted_fg.into()),
1731 },
1732 search: SearchColors {
1733 match_bg: theme.search_match_bg.into(),
1734 match_fg: theme.search_match_fg.into(),
1735 label_bg: theme.search_label_bg.into(),
1736 label_fg: theme.search_label_fg.into(),
1737 },
1738 diagnostic: DiagnosticColors {
1739 error_fg: theme.diagnostic_error_fg.into(),
1740 error_bg: theme.diagnostic_error_bg.into(),
1741 warning_fg: theme.diagnostic_warning_fg.into(),
1742 warning_bg: theme.diagnostic_warning_bg.into(),
1743 info_fg: theme.diagnostic_info_fg.into(),
1744 info_bg: theme.diagnostic_info_bg.into(),
1745 hint_fg: theme.diagnostic_hint_fg.into(),
1746 hint_bg: theme.diagnostic_hint_bg.into(),
1747 },
1748 syntax: SyntaxColors {
1749 keyword: theme.syntax_keyword.into(),
1750 string: theme.syntax_string.into(),
1751 comment: theme.syntax_comment.into(),
1752 function: theme.syntax_function.into(),
1753 type_: theme.syntax_type.into(),
1754 variable: theme.syntax_variable.into(),
1755 constant: theme.syntax_constant.into(),
1756 operator: theme.syntax_operator.into(),
1757 punctuation_bracket: theme.syntax_punctuation_bracket.into(),
1758 punctuation_delimiter: theme.syntax_punctuation_delimiter.into(),
1759 },
1760 }
1761 }
1762}
1763
1764fn resolve_base_theme(theme_file: &ThemeFile, raw: &serde_json::Value) -> Result<Theme, String> {
1771 if let Some(extends) = theme_file.extends.as_deref() {
1773 let name = extends.strip_prefix("builtin://").unwrap_or(extends);
1774 return Theme::load_builtin(name).ok_or_else(|| {
1775 let available: Vec<&str> = BUILTIN_THEMES.iter().map(|t| t.name).collect();
1776 format!(
1777 "theme `extends: {:?}` does not match any built-in theme. \
1778 Available: {}. \
1779 Inheriting from other user themes is not yet supported.",
1780 extends,
1781 available.join(", ")
1782 )
1783 });
1784 }
1785
1786 if let Some(bg) = raw
1792 .get("editor")
1793 .and_then(|e| e.get("bg"))
1794 .cloned()
1795 .and_then(|v| serde_json::from_value::<ColorDef>(v).ok())
1796 {
1797 let color: Color = bg.into();
1798 if let Some((r, g, b)) = color_to_rgb(color) {
1799 let lum = relative_luminance(r, g, b);
1800 let base_name = if lum > 0.5 { THEME_LIGHT } else { THEME_DARK };
1801 if let Some(base) = Theme::load_builtin(base_name) {
1802 return Ok(base);
1803 }
1804 }
1805 }
1806
1807 Ok(theme_file.clone().into())
1809}
1810
1811fn relative_luminance(r: u8, g: u8, b: u8) -> f64 {
1814 0.2126 * (r as f64 / 255.0) + 0.7152 * (g as f64 / 255.0) + 0.0722 * (b as f64 / 255.0)
1815}
1816
1817fn apply_theme_overrides(theme: &mut Theme, theme_file: &ThemeFile, raw: &serde_json::Value) {
1822 theme.name = theme_file.name.clone();
1824
1825 for section in ["editor", "ui", "search", "diagnostic", "syntax"] {
1826 let Some(obj) = raw.get(section).and_then(|v| v.as_object()) else {
1827 continue;
1828 };
1829 for (field, value) in obj {
1830 if value.is_null() {
1833 continue;
1834 }
1835 let key = format!("{}.{}", section, field);
1836 if let Ok(color_def) = serde_json::from_value::<ColorDef>(value.clone()) {
1837 if let Some(slot) = theme.resolve_theme_key_mut(&key) {
1838 *slot = color_def.into();
1839 }
1840 }
1841 }
1842 }
1843}
1844
1845impl Theme {
1846 pub fn is_light(&self) -> bool {
1852 color_to_rgb(self.editor_bg)
1853 .map(|(r, g, b)| relative_luminance(r, g, b) > 0.5)
1854 .unwrap_or(false)
1855 }
1856
1857 pub fn load_builtin(name: &str) -> Option<Self> {
1859 BUILTIN_THEMES
1860 .iter()
1861 .find(|t| t.name == name)
1862 .and_then(|t| serde_json::from_str::<ThemeFile>(t.json).ok())
1863 .map(|tf| tf.into())
1864 }
1865
1866 pub fn from_json(json: &str) -> Result<Self, String> {
1876 let raw: serde_json::Value =
1881 serde_json::from_str(json).map_err(|e| format!("Failed to parse theme JSON: {}", e))?;
1882 let theme_file: ThemeFile = serde_json::from_value(raw.clone())
1883 .map_err(|e| format!("Failed to parse theme: {}", e))?;
1884
1885 let mut theme = resolve_base_theme(&theme_file, &raw)?;
1886 apply_theme_overrides(&mut theme, &theme_file, &raw);
1887 Ok(theme)
1888 }
1889
1890 pub fn modifier_for_bg_key(&self, key: &str) -> Modifier {
1899 match key {
1900 "editor.selection_bg" => self.selection_modifier,
1901 "ui.semantic_highlight_bg" => self.semantic_highlight_modifier,
1902 _ => Modifier::empty(),
1903 }
1904 }
1905
1906 pub fn resolve_theme_key(&self, key: &str) -> Option<Color> {
1917 let parts: Vec<&str> = key.split('.').collect();
1919 if parts.len() != 2 {
1920 return None;
1921 }
1922
1923 let (section, field) = (parts[0], parts[1]);
1924
1925 match section {
1926 "editor" => match field {
1927 "after_eof_bg" => Some(self.after_eof_bg),
1928 "bg" => Some(self.editor_bg),
1929 "current_line_bg" => Some(self.current_line_bg),
1930 "cursor" => Some(self.cursor),
1931 "diff_add_bg" => Some(self.diff_add_bg),
1932 "diff_add_collision_fg" => self.diff_add_collision_fg,
1933 "diff_add_highlight_bg" => Some(self.diff_add_highlight_bg),
1934 "diff_modify_bg" => Some(self.diff_modify_bg),
1935 "diff_modify_collision_fg" => self.diff_modify_collision_fg,
1936 "diff_remove_bg" => Some(self.diff_remove_bg),
1937 "diff_remove_collision_fg" => self.diff_remove_collision_fg,
1938 "diff_remove_highlight_bg" => Some(self.diff_remove_highlight_bg),
1939 "fg" => Some(self.editor_fg),
1940 "inactive_cursor" => Some(self.inactive_cursor),
1941 "line_number_bg" => Some(self.line_number_bg),
1942 "line_number_fg" => Some(self.line_number_fg),
1943 "ruler_bg" => Some(self.ruler_bg),
1944 "selection_bg" => Some(self.selection_bg),
1945 "whitespace_indicator_fg" => Some(self.whitespace_indicator_fg),
1946 _ => None,
1947 },
1948 "ui" => match field {
1949 "compose_margin_bg" => Some(self.compose_margin_bg),
1950 "file_status_added_fg" => Some(self.file_status_added_fg),
1951 "file_status_conflicted_fg" => Some(self.file_status_conflicted_fg),
1952 "file_status_deleted_fg" => Some(self.file_status_deleted_fg),
1953 "file_status_modified_fg" => Some(self.file_status_modified_fg),
1954 "file_status_renamed_fg" => Some(self.file_status_renamed_fg),
1955 "file_status_untracked_fg" => Some(self.file_status_untracked_fg),
1956 "help_bg" => Some(self.help_bg),
1957 "help_fg" => Some(self.help_fg),
1958 "help_indicator_bg" => Some(self.help_indicator_bg),
1959 "help_indicator_fg" => Some(self.help_indicator_fg),
1960 "help_key_fg" => Some(self.help_key_fg),
1961 "help_separator_fg" => Some(self.help_separator_fg),
1962 "inline_code_bg" => Some(self.inline_code_bg),
1963 "menu_active_bg" => Some(self.menu_active_bg),
1964 "menu_active_fg" => Some(self.menu_active_fg),
1965 "menu_bg" => Some(self.menu_bg),
1966 "menu_border_fg" => Some(self.menu_border_fg),
1967 "menu_disabled_bg" => Some(self.menu_disabled_bg),
1968 "menu_disabled_fg" => Some(self.menu_disabled_fg),
1969 "menu_dropdown_bg" => Some(self.menu_dropdown_bg),
1970 "menu_dropdown_fg" => Some(self.menu_dropdown_fg),
1971 "menu_fg" => Some(self.menu_fg),
1972 "menu_highlight_bg" => Some(self.menu_highlight_bg),
1973 "menu_highlight_fg" => Some(self.menu_highlight_fg),
1974 "menu_hover_bg" => Some(self.menu_hover_bg),
1975 "menu_hover_fg" => Some(self.menu_hover_fg),
1976 "menu_separator_fg" => Some(self.menu_separator_fg),
1977 "popup_bg" => Some(self.popup_bg),
1978 "popup_border_fg" => Some(self.popup_border_fg),
1979 "popup_selection_bg" => Some(self.popup_selection_bg),
1980 "popup_selection_fg" => Some(self.popup_selection_fg),
1981 "popup_text_fg" => Some(self.popup_text_fg),
1982 "prompt_bg" => Some(self.prompt_bg),
1983 "prompt_fg" => Some(self.prompt_fg),
1984 "prompt_selection_bg" => Some(self.prompt_selection_bg),
1985 "prompt_selection_fg" => Some(self.prompt_selection_fg),
1986 "scrollbar_thumb_fg" => Some(self.scrollbar_thumb_fg),
1987 "scrollbar_thumb_hover_fg" => Some(self.scrollbar_thumb_hover_fg),
1988 "scrollbar_track_fg" => Some(self.scrollbar_track_fg),
1989 "scrollbar_track_hover_fg" => Some(self.scrollbar_track_hover_fg),
1990 "semantic_highlight_bg" => Some(self.semantic_highlight_bg),
1991 "settings_selected_bg" => Some(self.settings_selected_bg),
1992 "settings_selected_fg" => Some(self.settings_selected_fg),
1993 "split_separator_fg" => Some(self.split_separator_fg),
1994 "split_separator_hover_fg" => Some(self.split_separator_hover_fg),
1995 "status_bar_bg" => Some(self.status_bar_bg),
1996 "status_bar_fg" => Some(self.status_bar_fg),
1997 "status_error_indicator_bg" => Some(self.status_error_indicator_bg),
1998 "status_error_indicator_fg" => Some(self.status_error_indicator_fg),
1999 "status_error_indicator_hover_bg" => Some(self.status_error_indicator_hover_bg),
2000 "status_error_indicator_hover_fg" => Some(self.status_error_indicator_hover_fg),
2001 "status_lsp_actionable_bg" => Some(self.status_lsp_actionable_bg),
2002 "status_lsp_actionable_fg" => Some(self.status_lsp_actionable_fg),
2003 "status_lsp_on_bg" => Some(self.status_lsp_on_bg),
2004 "status_lsp_on_fg" => Some(self.status_lsp_on_fg),
2005 "status_palette_bg" => Some(self.status_palette_bg),
2006 "status_palette_fg" => Some(self.status_palette_fg),
2007 "status_warning_indicator_bg" => Some(self.status_warning_indicator_bg),
2008 "status_warning_indicator_fg" => Some(self.status_warning_indicator_fg),
2009 "status_warning_indicator_hover_bg" => Some(self.status_warning_indicator_hover_bg),
2010 "status_warning_indicator_hover_fg" => Some(self.status_warning_indicator_hover_fg),
2011 "suggestion_bg" => Some(self.suggestion_bg),
2012 "suggestion_selected_bg" => Some(self.suggestion_selected_bg),
2013 "tab_active_bg" => Some(self.tab_active_bg),
2014 "tab_active_fg" => Some(self.tab_active_fg),
2015 "tab_close_hover_fg" => Some(self.tab_close_hover_fg),
2016 "tab_drop_zone_bg" => Some(self.tab_drop_zone_bg),
2017 "tab_drop_zone_border" => Some(self.tab_drop_zone_border),
2018 "tab_hover_bg" => Some(self.tab_hover_bg),
2019 "tab_inactive_bg" => Some(self.tab_inactive_bg),
2020 "tab_inactive_fg" => Some(self.tab_inactive_fg),
2021 "tab_separator_bg" => Some(self.tab_separator_bg),
2022 "terminal_bg" => Some(self.terminal_bg),
2023 "terminal_fg" => Some(self.terminal_fg),
2024 "text_input_selection_bg" => Some(self.text_input_selection_bg),
2025 _ => None,
2026 },
2027 "syntax" => match field {
2028 "comment" => Some(self.syntax_comment),
2029 "constant" => Some(self.syntax_constant),
2030 "function" => Some(self.syntax_function),
2031 "keyword" => Some(self.syntax_keyword),
2032 "operator" => Some(self.syntax_operator),
2033 "punctuation_bracket" => Some(self.syntax_punctuation_bracket),
2034 "punctuation_delimiter" => Some(self.syntax_punctuation_delimiter),
2035 "string" => Some(self.syntax_string),
2036 "type" => Some(self.syntax_type),
2037 "variable" => Some(self.syntax_variable),
2038 _ => None,
2039 },
2040 "diagnostic" => match field {
2041 "error_bg" => Some(self.diagnostic_error_bg),
2042 "error_fg" => Some(self.diagnostic_error_fg),
2043 "hint_bg" => Some(self.diagnostic_hint_bg),
2044 "hint_fg" => Some(self.diagnostic_hint_fg),
2045 "info_bg" => Some(self.diagnostic_info_bg),
2046 "info_fg" => Some(self.diagnostic_info_fg),
2047 "warning_bg" => Some(self.diagnostic_warning_bg),
2048 "warning_fg" => Some(self.diagnostic_warning_fg),
2049 _ => None,
2050 },
2051 "search" => match field {
2052 "label_bg" => Some(self.search_label_bg),
2053 "label_fg" => Some(self.search_label_fg),
2054 "match_bg" => Some(self.search_match_bg),
2055 "match_fg" => Some(self.search_match_fg),
2056 _ => None,
2057 },
2058 _ => None,
2059 }
2060 }
2061
2062 pub fn resolve_theme_key_mut(&mut self, key: &str) -> Option<&mut Color> {
2066 let parts: Vec<&str> = key.split('.').collect();
2067 if parts.len() != 2 {
2068 return None;
2069 }
2070 let (section, field) = (parts[0], parts[1]);
2071 match section {
2072 "editor" => match field {
2073 "bg" => Some(&mut self.editor_bg),
2074 "fg" => Some(&mut self.editor_fg),
2075 "cursor" => Some(&mut self.cursor),
2076 "inactive_cursor" => Some(&mut self.inactive_cursor),
2077 "selection_bg" => Some(&mut self.selection_bg),
2078 "current_line_bg" => Some(&mut self.current_line_bg),
2079 "line_number_fg" => Some(&mut self.line_number_fg),
2080 "line_number_bg" => Some(&mut self.line_number_bg),
2081 "diff_add_bg" => Some(&mut self.diff_add_bg),
2082 "diff_remove_bg" => Some(&mut self.diff_remove_bg),
2083 "diff_modify_bg" => Some(&mut self.diff_modify_bg),
2084 "diff_add_collision_fg" => self.diff_add_collision_fg.as_mut(),
2088 "diff_remove_collision_fg" => self.diff_remove_collision_fg.as_mut(),
2089 "diff_modify_collision_fg" => self.diff_modify_collision_fg.as_mut(),
2090 "ruler_bg" => Some(&mut self.ruler_bg),
2091 "whitespace_indicator_fg" => Some(&mut self.whitespace_indicator_fg),
2092 "diff_add_highlight_bg" => Some(&mut self.diff_add_highlight_bg),
2093 "diff_remove_highlight_bg" => Some(&mut self.diff_remove_highlight_bg),
2094 "after_eof_bg" => Some(&mut self.after_eof_bg),
2095 _ => None,
2096 },
2097 "ui" => match field {
2098 "tab_active_fg" => Some(&mut self.tab_active_fg),
2099 "tab_active_bg" => Some(&mut self.tab_active_bg),
2100 "tab_inactive_fg" => Some(&mut self.tab_inactive_fg),
2101 "tab_inactive_bg" => Some(&mut self.tab_inactive_bg),
2102 "status_bar_fg" => Some(&mut self.status_bar_fg),
2103 "status_bar_bg" => Some(&mut self.status_bar_bg),
2104 "status_palette_fg" => Some(&mut self.status_palette_fg),
2105 "status_palette_bg" => Some(&mut self.status_palette_bg),
2106 "status_lsp_on_fg" => Some(&mut self.status_lsp_on_fg),
2107 "status_lsp_on_bg" => Some(&mut self.status_lsp_on_bg),
2108 "status_lsp_actionable_fg" => Some(&mut self.status_lsp_actionable_fg),
2109 "status_lsp_actionable_bg" => Some(&mut self.status_lsp_actionable_bg),
2110 "prompt_fg" => Some(&mut self.prompt_fg),
2111 "prompt_bg" => Some(&mut self.prompt_bg),
2112 "prompt_selection_fg" => Some(&mut self.prompt_selection_fg),
2113 "prompt_selection_bg" => Some(&mut self.prompt_selection_bg),
2114 "popup_bg" => Some(&mut self.popup_bg),
2115 "popup_border_fg" => Some(&mut self.popup_border_fg),
2116 "popup_selection_bg" => Some(&mut self.popup_selection_bg),
2117 "popup_selection_fg" => Some(&mut self.popup_selection_fg),
2118 "popup_text_fg" => Some(&mut self.popup_text_fg),
2119 "text_input_selection_bg" => Some(&mut self.text_input_selection_bg),
2120 "menu_bg" => Some(&mut self.menu_bg),
2121 "menu_fg" => Some(&mut self.menu_fg),
2122 "menu_active_bg" => Some(&mut self.menu_active_bg),
2123 "menu_active_fg" => Some(&mut self.menu_active_fg),
2124 "menu_disabled_fg" => Some(&mut self.menu_disabled_fg),
2125 "menu_disabled_bg" => Some(&mut self.menu_disabled_bg),
2126 "help_bg" => Some(&mut self.help_bg),
2127 "help_fg" => Some(&mut self.help_fg),
2128 "help_key_fg" => Some(&mut self.help_key_fg),
2129 "split_separator_fg" => Some(&mut self.split_separator_fg),
2130 "scrollbar_track_fg" => Some(&mut self.scrollbar_track_fg),
2131 "scrollbar_thumb_fg" => Some(&mut self.scrollbar_thumb_fg),
2132 "scrollbar_track_hover_fg" => Some(&mut self.scrollbar_track_hover_fg),
2133 "scrollbar_thumb_hover_fg" => Some(&mut self.scrollbar_thumb_hover_fg),
2134 "semantic_highlight_bg" => Some(&mut self.semantic_highlight_bg),
2135 "file_status_added_fg" => Some(&mut self.file_status_added_fg),
2136 "file_status_modified_fg" => Some(&mut self.file_status_modified_fg),
2137 "file_status_deleted_fg" => Some(&mut self.file_status_deleted_fg),
2138 "file_status_renamed_fg" => Some(&mut self.file_status_renamed_fg),
2139 "file_status_untracked_fg" => Some(&mut self.file_status_untracked_fg),
2140 "file_status_conflicted_fg" => Some(&mut self.file_status_conflicted_fg),
2141 "menu_dropdown_bg" => Some(&mut self.menu_dropdown_bg),
2142 "menu_dropdown_fg" => Some(&mut self.menu_dropdown_fg),
2143 "menu_highlight_bg" => Some(&mut self.menu_highlight_bg),
2144 "menu_highlight_fg" => Some(&mut self.menu_highlight_fg),
2145 "menu_border_fg" => Some(&mut self.menu_border_fg),
2146 "menu_separator_fg" => Some(&mut self.menu_separator_fg),
2147 "menu_hover_bg" => Some(&mut self.menu_hover_bg),
2148 "menu_hover_fg" => Some(&mut self.menu_hover_fg),
2149 "tab_separator_bg" => Some(&mut self.tab_separator_bg),
2150 "tab_close_hover_fg" => Some(&mut self.tab_close_hover_fg),
2151 "tab_hover_bg" => Some(&mut self.tab_hover_bg),
2152 "inline_code_bg" => Some(&mut self.inline_code_bg),
2153 "split_separator_hover_fg" => Some(&mut self.split_separator_hover_fg),
2154 "compose_margin_bg" => Some(&mut self.compose_margin_bg),
2155 "terminal_bg" => Some(&mut self.terminal_bg),
2156 "terminal_fg" => Some(&mut self.terminal_fg),
2157 "status_warning_indicator_bg" => Some(&mut self.status_warning_indicator_bg),
2158 "status_warning_indicator_fg" => Some(&mut self.status_warning_indicator_fg),
2159 "status_error_indicator_bg" => Some(&mut self.status_error_indicator_bg),
2160 "status_error_indicator_fg" => Some(&mut self.status_error_indicator_fg),
2161 "status_warning_indicator_hover_bg" => {
2162 Some(&mut self.status_warning_indicator_hover_bg)
2163 }
2164 "status_warning_indicator_hover_fg" => {
2165 Some(&mut self.status_warning_indicator_hover_fg)
2166 }
2167 "status_error_indicator_hover_bg" => {
2168 Some(&mut self.status_error_indicator_hover_bg)
2169 }
2170 "status_error_indicator_hover_fg" => {
2171 Some(&mut self.status_error_indicator_hover_fg)
2172 }
2173 "tab_drop_zone_bg" => Some(&mut self.tab_drop_zone_bg),
2174 "tab_drop_zone_border" => Some(&mut self.tab_drop_zone_border),
2175 "settings_selected_bg" => Some(&mut self.settings_selected_bg),
2176 "settings_selected_fg" => Some(&mut self.settings_selected_fg),
2177 "suggestion_bg" => Some(&mut self.suggestion_bg),
2178 "suggestion_selected_bg" => Some(&mut self.suggestion_selected_bg),
2179 "help_separator_fg" => Some(&mut self.help_separator_fg),
2180 "help_indicator_fg" => Some(&mut self.help_indicator_fg),
2181 "help_indicator_bg" => Some(&mut self.help_indicator_bg),
2182 _ => None,
2183 },
2184 "syntax" => match field {
2185 "keyword" => Some(&mut self.syntax_keyword),
2186 "string" => Some(&mut self.syntax_string),
2187 "comment" => Some(&mut self.syntax_comment),
2188 "function" => Some(&mut self.syntax_function),
2189 "type" => Some(&mut self.syntax_type),
2190 "variable" => Some(&mut self.syntax_variable),
2191 "constant" => Some(&mut self.syntax_constant),
2192 "operator" => Some(&mut self.syntax_operator),
2193 "punctuation_bracket" => Some(&mut self.syntax_punctuation_bracket),
2194 "punctuation_delimiter" => Some(&mut self.syntax_punctuation_delimiter),
2195 _ => None,
2196 },
2197 "diagnostic" => match field {
2198 "error_fg" => Some(&mut self.diagnostic_error_fg),
2199 "error_bg" => Some(&mut self.diagnostic_error_bg),
2200 "warning_fg" => Some(&mut self.diagnostic_warning_fg),
2201 "warning_bg" => Some(&mut self.diagnostic_warning_bg),
2202 "info_fg" => Some(&mut self.diagnostic_info_fg),
2203 "info_bg" => Some(&mut self.diagnostic_info_bg),
2204 "hint_fg" => Some(&mut self.diagnostic_hint_fg),
2205 "hint_bg" => Some(&mut self.diagnostic_hint_bg),
2206 _ => None,
2207 },
2208 "search" => match field {
2209 "match_bg" => Some(&mut self.search_match_bg),
2210 "match_fg" => Some(&mut self.search_match_fg),
2211 "label_bg" => Some(&mut self.search_label_bg),
2212 "label_fg" => Some(&mut self.search_label_fg),
2213 _ => None,
2214 },
2215 _ => None,
2216 }
2217 }
2218
2219 pub fn override_colors<I, K>(&mut self, overrides: I) -> usize
2224 where
2225 I: IntoIterator<Item = (K, Color)>,
2226 K: AsRef<str>,
2227 {
2228 let mut applied = 0;
2229 for (key, color) in overrides {
2230 if let Some(slot) = self.resolve_theme_key_mut(key.as_ref()) {
2231 *slot = color;
2232 applied += 1;
2233 }
2234 }
2235 applied
2236 }
2237}
2238
2239pub fn get_theme_schema() -> serde_json::Value {
2247 use schemars::schema_for;
2248 let schema = schema_for!(ThemeFile);
2249 serde_json::to_value(&schema).unwrap_or_default()
2250}
2251
2252pub fn get_builtin_themes() -> serde_json::Value {
2254 let mut map = serde_json::Map::new();
2255 for theme in BUILTIN_THEMES {
2256 map.insert(
2257 theme.name.to_string(),
2258 serde_json::Value::String(theme.json.to_string()),
2259 );
2260 }
2261 serde_json::Value::Object(map)
2262}
2263
2264#[cfg(test)]
2265mod tests {
2266 use super::*;
2267
2268 #[test]
2269 fn test_load_builtin_theme() {
2270 let dark = Theme::load_builtin(THEME_DARK).expect("Dark theme must exist");
2271 assert_eq!(dark.name, THEME_DARK);
2272
2273 let light = Theme::load_builtin(THEME_LIGHT).expect("Light theme must exist");
2274 assert_eq!(light.name, THEME_LIGHT);
2275
2276 let high_contrast =
2277 Theme::load_builtin(THEME_HIGH_CONTRAST).expect("High contrast theme must exist");
2278 assert_eq!(high_contrast.name, THEME_HIGH_CONTRAST);
2279
2280 let terminal = Theme::load_builtin(THEME_TERMINAL).expect("Terminal theme must exist");
2281 assert_eq!(terminal.name, THEME_TERMINAL);
2282 assert_eq!(terminal.editor_bg, Color::Reset);
2286 assert_eq!(terminal.editor_fg, Color::Reset);
2287 assert_eq!(terminal.terminal_bg, Color::Reset);
2288 assert!(terminal.selection_modifier.contains(Modifier::REVERSED));
2291 assert!(terminal
2292 .semantic_highlight_modifier
2293 .contains(Modifier::BOLD));
2294 }
2295
2296 #[test]
2297 fn test_modifier_def_round_trip() {
2298 let cases = [
2299 (vec!["reversed"], Modifier::REVERSED),
2300 (
2301 vec!["bold", "underlined"],
2302 Modifier::BOLD | Modifier::UNDERLINED,
2303 ),
2304 (vec!["italic", "dim"], Modifier::ITALIC | Modifier::DIM),
2305 (vec!["reverse"], Modifier::REVERSED), (vec!["underline"], Modifier::UNDERLINED), ];
2308 for (strs, expected) in cases {
2309 let def = ModifierDef(strs.iter().map(|s| s.to_string()).collect());
2310 let m: Modifier = (&def).into();
2311 assert_eq!(m, expected, "ModifierDef({:?}) -> Modifier", strs);
2312 }
2313 }
2314
2315 #[test]
2316 fn test_modifier_def_unknown_strings_are_dropped() {
2317 let def = ModifierDef(vec!["reversed".into(), "wibble".into(), "bold".into()]);
2320 let m: Modifier = (&def).into();
2321 assert_eq!(m, Modifier::REVERSED | Modifier::BOLD);
2322 }
2323
2324 #[test]
2325 fn test_themes_without_modifier_default_to_empty() {
2326 let dark = Theme::load_builtin(THEME_DARK).expect("Dark theme must exist");
2331 assert!(dark.selection_modifier.is_empty());
2332 assert!(dark.semantic_highlight_modifier.is_empty());
2333 }
2334
2335 #[test]
2336 fn test_modifier_for_bg_key_lookup() {
2337 let terminal = Theme::load_builtin(THEME_TERMINAL).expect("Terminal theme must exist");
2338 assert!(terminal
2341 .modifier_for_bg_key("editor.selection_bg")
2342 .contains(Modifier::REVERSED));
2343 assert!(terminal
2344 .modifier_for_bg_key("ui.semantic_highlight_bg")
2345 .contains(Modifier::BOLD));
2346 assert!(terminal
2349 .modifier_for_bg_key("ui.popup_selection_bg")
2350 .is_empty());
2351 assert!(terminal.modifier_for_bg_key("nonsense").is_empty());
2352 }
2353
2354 #[test]
2355 fn test_modifier_round_trip_via_theme_file() {
2356 let original = Theme::load_builtin(THEME_TERMINAL).expect("Terminal theme must exist");
2358 let file: ThemeFile = original.clone().into();
2359 let json = serde_json::to_string(&file).expect("serialize");
2360 let parsed: ThemeFile = serde_json::from_str(&json).expect("parse");
2361 let round_tripped: Theme = parsed.into();
2362 assert_eq!(
2363 round_tripped.selection_modifier,
2364 original.selection_modifier
2365 );
2366 assert_eq!(
2367 round_tripped.semantic_highlight_modifier,
2368 original.semantic_highlight_modifier
2369 );
2370 }
2371
2372 #[test]
2373 fn test_builtin_themes_match_schema() {
2374 for theme in BUILTIN_THEMES {
2375 let _: ThemeFile = serde_json::from_str(theme.json)
2376 .unwrap_or_else(|_| panic!("Theme '{}' does not match schema", theme.name));
2377 }
2378 }
2379
2380 #[test]
2381 fn test_from_json() {
2382 let json = r#"{"name":"test","editor":{},"ui":{},"search":{},"diagnostic":{},"syntax":{}}"#;
2383 let theme = Theme::from_json(json).expect("Should parse minimal theme");
2384 assert_eq!(theme.name, "test");
2385 }
2386
2387 #[test]
2399 fn test_minimal_user_theme_from_issue_1281_loads() {
2400 let json = r#"{
2402 "name": "gruvbox-light-orange",
2403 "editor": {
2404 "bg": [251, 241, 199],
2405 "fg": [60, 56, 54],
2406 "cursor": [254, 128, 25],
2407 "selection_bg": [213, 196, 161]
2408 },
2409 "syntax": {
2410 "keyword": [175, 58, 3],
2411 "string": [152, 151, 26],
2412 "comment": [146, 131, 116]
2413 }
2414}"#;
2415 let theme = Theme::from_json(json)
2416 .expect("Theme from issue #1281 should parse without `ui`/`search`/`diagnostic`");
2417 assert_eq!(theme.name, "gruvbox-light-orange");
2418
2419 assert_eq!(theme.editor_bg, Color::Rgb(251, 241, 199));
2421 assert_eq!(theme.editor_fg, Color::Rgb(60, 56, 54));
2422 assert_eq!(theme.cursor, Color::Rgb(254, 128, 25));
2423 assert_eq!(theme.selection_bg, Color::Rgb(213, 196, 161));
2424 assert_eq!(theme.syntax_keyword, Color::Rgb(175, 58, 3));
2425 assert_eq!(theme.syntax_string, Color::Rgb(152, 151, 26));
2426 assert_eq!(theme.syntax_comment, Color::Rgb(146, 131, 116));
2427
2428 let light = Theme::load_builtin(THEME_LIGHT).expect("light builtin");
2432 assert_eq!(
2433 theme.status_bar_fg, light.status_bar_fg,
2434 "ui.status_bar_fg should inherit from builtin://light when bg is bright"
2435 );
2436 assert_eq!(
2437 theme.diagnostic_error_fg, light.diagnostic_error_fg,
2438 "diagnostic.error_fg should inherit from builtin://light when bg is bright"
2439 );
2440 assert_eq!(
2441 theme.menu_bg, light.menu_bg,
2442 "ui.menu_bg should inherit from builtin://light when bg is bright"
2443 );
2444 }
2445
2446 #[test]
2449 fn test_extends_explicit_builtin_wins_over_auto_infer() {
2450 let json = r#"{
2453 "name": "explicit-light",
2454 "extends": "builtin://light",
2455 "editor": { "bg": [0, 0, 0] }
2456 }"#;
2457 let theme = Theme::from_json(json).expect("extends should resolve");
2458 let light = Theme::load_builtin(THEME_LIGHT).expect("light builtin");
2459
2460 assert_eq!(theme.editor_bg, Color::Rgb(0, 0, 0));
2462 assert_eq!(theme.menu_bg, light.menu_bg);
2464 assert_eq!(theme.tab_active_bg, light.tab_active_bg);
2465 assert_eq!(theme.diagnostic_warning_fg, light.diagnostic_warning_fg);
2466 }
2467
2468 #[test]
2473 fn test_extends_bare_builtin_name_works() {
2474 let json = r#"{ "name": "x", "extends": "high-contrast" }"#;
2475 let theme = Theme::from_json(json).expect("bare-name extends should resolve");
2476 let hc = Theme::load_builtin("high-contrast").expect("hc builtin");
2477 assert_eq!(theme.menu_bg, hc.menu_bg);
2478 }
2479
2480 #[test]
2485 fn test_extends_unknown_builtin_errors_with_helpful_message() {
2486 let json = r#"{ "name": "x", "extends": "builtin://no-such-theme" }"#;
2487 let err = Theme::from_json(json).expect_err("unknown extends must error");
2488 assert!(
2489 err.contains("no-such-theme"),
2490 "error should quote the bad value, got: {}",
2491 err
2492 );
2493 assert!(
2494 err.contains("dark") && err.contains("light"),
2495 "error should list available builtins, got: {}",
2496 err
2497 );
2498 }
2499
2500 #[test]
2504 fn test_auto_infer_dark_base_from_dark_bg() {
2505 let json = r#"{ "name": "x", "editor": { "bg": [20, 20, 30] } }"#;
2506 let theme = Theme::from_json(json).expect("should parse");
2507 let dark = Theme::load_builtin(THEME_DARK).expect("dark builtin");
2508 assert_eq!(theme.menu_bg, dark.menu_bg);
2509 assert_eq!(theme.diagnostic_error_fg, dark.diagnostic_error_fg);
2510 }
2511
2512 #[test]
2516 fn test_no_inheritance_signal_uses_hardcoded_defaults() {
2517 let json = r#"{ "name": "x" }"#;
2518 let theme = Theme::from_json(json).expect("should parse");
2519 assert_eq!(theme.editor_bg, Color::Rgb(30, 30, 30));
2522 }
2523
2524 #[test]
2528 fn test_theme_without_name_still_errors() {
2529 let json = r#"{ "editor": {} }"#;
2530 let err = Theme::from_json(json).expect_err("missing `name` must be an error");
2531 assert!(
2532 err.contains("name"),
2533 "error should mention the missing `name` field, got: {}",
2534 err
2535 );
2536 }
2537
2538 #[test]
2543 fn test_extends_overrides_compose_field_by_field() {
2544 let json = r#"{
2545 "name": "dark-with-pink-cursor",
2546 "extends": "builtin://dark",
2547 "editor": { "cursor": [255, 105, 180] }
2548 }"#;
2549 let theme = Theme::from_json(json).expect("should parse");
2550 let dark = Theme::load_builtin(THEME_DARK).expect("dark builtin");
2551
2552 assert_eq!(theme.cursor, Color::Rgb(255, 105, 180));
2554 assert_eq!(theme.editor_bg, dark.editor_bg);
2556 assert_eq!(theme.editor_fg, dark.editor_fg);
2557 assert_eq!(theme.selection_bg, dark.selection_bg);
2558 assert_eq!(theme.menu_bg, dark.menu_bg);
2560 assert_eq!(theme.syntax_keyword, dark.syntax_keyword);
2561 }
2562
2563 #[test]
2564 fn test_default_reset_color() {
2565 let color: Color = ColorDef::Named("Default".to_string()).into();
2567 assert_eq!(color, Color::Reset);
2568
2569 let color: Color = ColorDef::Named("Reset".to_string()).into();
2571 assert_eq!(color, Color::Reset);
2572 }
2573
2574 #[test]
2575 fn test_file_status_colors_fall_back_to_diagnostic_colors() {
2576 let json = r#"{
2578 "name": "test-fallback",
2579 "editor": {},
2580 "ui": {},
2581 "search": {},
2582 "diagnostic": {
2583 "error_fg": [220, 50, 47],
2584 "warning_fg": [181, 137, 0],
2585 "info_fg": [38, 139, 210],
2586 "hint_fg": [101, 123, 131]
2587 },
2588 "syntax": {}
2589 }"#;
2590 let theme = Theme::from_json(json).expect("Should parse theme without file_status keys");
2591
2592 assert_eq!(theme.file_status_added_fg, Color::Rgb(38, 139, 210));
2594 assert_eq!(theme.file_status_renamed_fg, Color::Rgb(38, 139, 210));
2595 assert_eq!(theme.file_status_modified_fg, Color::Rgb(181, 137, 0));
2597 assert_eq!(theme.file_status_deleted_fg, Color::Rgb(220, 50, 47));
2599 assert_eq!(theme.file_status_conflicted_fg, Color::Rgb(220, 50, 47));
2600 assert_eq!(theme.file_status_untracked_fg, Color::Rgb(101, 123, 131));
2602 }
2603
2604 #[test]
2605 fn test_file_status_colors_explicit_override() {
2606 let json = r#"{
2608 "name": "test-override",
2609 "editor": {},
2610 "ui": {
2611 "file_status_added_fg": [80, 250, 123],
2612 "file_status_modified_fg": [255, 184, 108]
2613 },
2614 "search": {},
2615 "diagnostic": {
2616 "info_fg": [38, 139, 210],
2617 "warning_fg": [181, 137, 0]
2618 },
2619 "syntax": {}
2620 }"#;
2621 let theme = Theme::from_json(json).expect("Should parse theme with file_status overrides");
2622
2623 assert_eq!(theme.file_status_added_fg, Color::Rgb(80, 250, 123));
2625 assert_eq!(theme.file_status_modified_fg, Color::Rgb(255, 184, 108));
2626 assert_eq!(theme.file_status_renamed_fg, Color::Rgb(38, 139, 210));
2628 }
2629
2630 #[test]
2631 fn test_file_status_colors_resolve_via_theme_key() {
2632 let json = r#"{
2633 "name": "test-resolve",
2634 "editor": {},
2635 "ui": {
2636 "file_status_added_fg": [80, 250, 123]
2637 },
2638 "search": {},
2639 "diagnostic": {
2640 "warning_fg": [181, 137, 0]
2641 },
2642 "syntax": {}
2643 }"#;
2644 let theme = Theme::from_json(json).expect("Should parse theme");
2645
2646 assert_eq!(
2648 theme.resolve_theme_key("ui.file_status_added_fg"),
2649 Some(Color::Rgb(80, 250, 123))
2650 );
2651 assert_eq!(
2652 theme.resolve_theme_key("ui.file_status_modified_fg"),
2653 Some(Color::Rgb(181, 137, 0))
2654 );
2655 }
2656
2657 #[test]
2658 fn override_colors_writes_known_keys_and_drops_unknowns() {
2659 let mut theme = Theme::load_builtin(THEME_DARK).expect("dark builtin");
2660 let applied = theme.override_colors([
2661 ("editor.bg".to_string(), Color::Rgb(10, 20, 30)),
2662 ("ui.status_bar_fg".to_string(), Color::Rgb(1, 2, 3)),
2663 ("does.not_exist".to_string(), Color::Rgb(9, 9, 9)),
2664 ("garbage_no_dot".to_string(), Color::Rgb(9, 9, 9)),
2665 ]);
2666 assert_eq!(applied, 2, "only the two valid keys should be applied");
2667 assert_eq!(
2668 theme.resolve_theme_key("editor.bg"),
2669 Some(Color::Rgb(10, 20, 30))
2670 );
2671 assert_eq!(
2672 theme.resolve_theme_key("ui.status_bar_fg"),
2673 Some(Color::Rgb(1, 2, 3))
2674 );
2675 }
2676
2677 #[test]
2678 fn resolve_theme_key_mut_matches_resolve_theme_key_domain() {
2679 let mut theme = Theme::load_builtin(THEME_DARK).expect("dark builtin");
2682 let probe = [
2683 "editor.bg",
2684 "editor.fg",
2685 "ui.status_bar_fg",
2686 "ui.tab_active_bg",
2687 "syntax.keyword",
2688 "diagnostic.error_fg",
2689 "search.match_bg",
2690 ];
2691 for key in probe {
2692 assert!(
2693 theme.resolve_theme_key(key).is_some(),
2694 "reader lost key {key}"
2695 );
2696 assert!(
2697 theme.resolve_theme_key_mut(key).is_some(),
2698 "mutator missing key {key}"
2699 );
2700 }
2701 }
2702
2703 fn schema_color_keys() -> Vec<(String, String)> {
2712 let theme = Theme::load_builtin(THEME_DARK).expect("dark builtin");
2713 let file: ThemeFile = theme.into();
2714 let value = serde_json::to_value(&file).expect("ThemeFile serializes");
2715 let obj = value.as_object().expect("ThemeFile is a JSON object");
2716
2717 let mut keys = Vec::new();
2718 for section in ["editor", "ui", "search", "diagnostic", "syntax"] {
2719 let fields = obj
2720 .get(section)
2721 .and_then(|v| v.as_object())
2722 .unwrap_or_else(|| panic!("section `{section}` missing from serialized ThemeFile"));
2723 for (field, val) in fields {
2724 if is_color_leaf(val) {
2725 keys.push((section.to_string(), field.clone()));
2726 }
2727 }
2728 }
2729 assert!(
2730 keys.len() >= 100,
2731 "expected the theme to expose at least ~100 color keys, found {} — \
2732 has the serialization shape changed?",
2733 keys.len()
2734 );
2735 keys
2736 }
2737
2738 fn is_color_leaf(v: &serde_json::Value) -> bool {
2742 v.is_string()
2743 || v.as_array()
2744 .is_some_and(|a| a.len() == 3 && a.iter().all(serde_json::Value::is_number))
2745 }
2746
2747 fn sentinel(i: usize) -> Color {
2751 Color::Rgb((i >> 8) as u8, (i & 0xff) as u8, 0x5a)
2752 }
2753
2754 #[test]
2755 fn every_exposed_color_key_resolves_in_both_directions() {
2756 let mut theme = Theme::load_builtin(THEME_DARK).expect("dark builtin");
2763 let mut missing_reader = Vec::new();
2764 let mut missing_mutator = Vec::new();
2765 for (section, field) in schema_color_keys() {
2766 let key = format!("{section}.{field}");
2767 if theme.resolve_theme_key(&key).is_none() {
2768 missing_reader.push(key.clone());
2769 }
2770 if theme.resolve_theme_key_mut(&key).is_none() {
2771 missing_mutator.push(key);
2772 }
2773 }
2774 assert!(
2775 missing_reader.is_empty() && missing_mutator.is_empty(),
2776 "theme color keys exposed by the JSON schema but dropped by a resolver:\n \
2777 resolve_theme_key: {missing_reader:?}\n \
2778 resolve_theme_key_mut: {missing_mutator:?}"
2779 );
2780 }
2781
2782 #[test]
2783 fn color_keys_round_trip_through_the_same_field_and_section() {
2784 let keys = schema_color_keys();
2795 let mut theme = Theme::load_builtin(THEME_DARK).expect("dark builtin");
2796
2797 let pairs: Vec<(String, Color)> = keys
2798 .iter()
2799 .enumerate()
2800 .map(|(i, (s, f))| (format!("{s}.{f}"), sentinel(i)))
2801 .collect();
2802 let applied = theme.override_colors(pairs.iter().map(|(k, c)| (k.as_str(), *c)));
2803 assert_eq!(
2804 applied,
2805 keys.len(),
2806 "override_colors should write every exposed key via resolve_theme_key_mut"
2807 );
2808
2809 for (i, (s, f)) in keys.iter().enumerate() {
2811 let key = format!("{s}.{f}");
2812 assert_eq!(
2813 theme.resolve_theme_key(&key),
2814 Some(sentinel(i)),
2815 "reader and mutator disagree on the field `{key}` addresses"
2816 );
2817 }
2818
2819 let file: ThemeFile = theme.into();
2822 let value = serde_json::to_value(&file).expect("ThemeFile serializes");
2823 let obj = value.as_object().expect("ThemeFile is a JSON object");
2824 for (i, (s, f)) in keys.iter().enumerate() {
2825 let leaf = obj
2826 .get(s)
2827 .and_then(|sec| sec.get(f))
2828 .unwrap_or_else(|| panic!("`{s}.{f}` vanished from serialized ThemeFile"));
2829 let color: Color = serde_json::from_value::<ColorDef>(leaf.clone())
2830 .expect("color leaf parses as ColorDef")
2831 .into();
2832 assert_eq!(
2833 color,
2834 sentinel(i),
2835 "`{s}.{f}` serialized back to the wrong field or section"
2836 );
2837 }
2838
2839 let reloaded = Theme::from_json(&value.to_string()).expect("from_json round-trips");
2842 for (i, (s, f)) in keys.iter().enumerate() {
2843 let key = format!("{s}.{f}");
2844 assert_eq!(
2845 reloaded.resolve_theme_key(&key),
2846 Some(sentinel(i)),
2847 "`{key}` did not survive ThemeFile -> JSON -> from_json"
2848 );
2849 }
2850 }
2851
2852 #[test]
2853 fn test_all_builtin_themes_set_prominent_palette_indicator() {
2854 for builtin in BUILTIN_THEMES {
2861 let theme = Theme::from_json(builtin.json)
2862 .unwrap_or_else(|e| panic!("Theme '{}' failed to parse: {}", builtin.name, e));
2863 assert!(
2864 theme.status_palette_fg != theme.status_bar_fg
2865 || theme.status_palette_bg != theme.status_bar_bg,
2866 "Theme '{}' must set status_palette_fg/bg to a prominent \
2867 accent distinct from status_bar_fg/bg",
2868 builtin.name
2869 );
2870 }
2871 }
2872
2873 #[test]
2874 fn test_all_builtin_themes_have_file_status_colors() {
2875 for builtin in BUILTIN_THEMES {
2877 let theme = Theme::from_json(builtin.json)
2878 .unwrap_or_else(|e| panic!("Theme '{}' failed to parse: {}", builtin.name, e));
2879
2880 for key in &[
2882 "ui.file_status_added_fg",
2883 "ui.file_status_modified_fg",
2884 "ui.file_status_deleted_fg",
2885 "ui.file_status_renamed_fg",
2886 "ui.file_status_untracked_fg",
2887 "ui.file_status_conflicted_fg",
2888 ] {
2889 assert!(
2890 theme.resolve_theme_key(key).is_some(),
2891 "Theme '{}' missing resolution for '{}'",
2892 builtin.name,
2893 key
2894 );
2895 }
2896 }
2897 }
2898}