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)]
529 pub diff_add_fg: Option<ColorDef>,
530 #[serde(default)]
533 pub diff_remove_fg: Option<ColorDef>,
534 #[serde(default)]
537 pub diff_modify_fg: Option<ColorDef>,
538 #[serde(default = "default_ruler_bg")]
540 pub ruler_bg: ColorDef,
541 #[serde(default = "default_whitespace_indicator_fg")]
543 pub whitespace_indicator_fg: ColorDef,
544 #[serde(default)]
549 pub after_eof_bg: Option<ColorDef>,
550}
551
552fn default_editor_bg() -> ColorDef {
554 ColorDef::Rgb(30, 30, 30)
555}
556fn default_editor_fg() -> ColorDef {
557 ColorDef::Rgb(212, 212, 212)
558}
559fn default_cursor() -> ColorDef {
560 ColorDef::Rgb(255, 255, 255)
561}
562fn default_inactive_cursor() -> ColorDef {
563 ColorDef::Named("DarkGray".to_string())
564}
565fn default_selection_bg() -> ColorDef {
566 ColorDef::Rgb(38, 79, 120)
567}
568fn default_current_line_bg() -> ColorDef {
569 ColorDef::Rgb(40, 40, 40)
570}
571fn default_line_number_fg() -> ColorDef {
572 ColorDef::Rgb(100, 100, 100)
573}
574fn default_line_number_bg() -> ColorDef {
575 ColorDef::Rgb(30, 30, 30)
576}
577fn default_diff_add_bg() -> ColorDef {
578 ColorDef::Rgb(35, 60, 35) }
580fn default_diff_remove_bg() -> ColorDef {
581 ColorDef::Rgb(70, 35, 35) }
583fn default_diff_modify_bg() -> ColorDef {
584 ColorDef::Rgb(40, 38, 30) }
586fn default_ruler_bg() -> ColorDef {
587 ColorDef::Rgb(50, 50, 50) }
589fn default_whitespace_indicator_fg() -> ColorDef {
590 ColorDef::Rgb(70, 70, 70) }
592
593#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
595pub struct UiColors {
596 #[serde(default = "default_tab_active_fg")]
598 pub tab_active_fg: ColorDef,
599 #[serde(default = "default_tab_active_bg")]
601 pub tab_active_bg: ColorDef,
602 #[serde(default = "default_tab_inactive_fg")]
604 pub tab_inactive_fg: ColorDef,
605 #[serde(default = "default_tab_inactive_bg")]
607 pub tab_inactive_bg: ColorDef,
608 #[serde(default = "default_tab_separator_bg")]
610 pub tab_separator_bg: ColorDef,
611 #[serde(default = "default_tab_close_hover_fg")]
613 pub tab_close_hover_fg: ColorDef,
614 #[serde(default = "default_tab_hover_bg")]
616 pub tab_hover_bg: ColorDef,
617 #[serde(default = "default_menu_bg")]
619 pub menu_bg: ColorDef,
620 #[serde(default = "default_menu_fg")]
622 pub menu_fg: ColorDef,
623 #[serde(default = "default_menu_active_bg")]
625 pub menu_active_bg: ColorDef,
626 #[serde(default = "default_menu_active_fg")]
628 pub menu_active_fg: ColorDef,
629 #[serde(default = "default_menu_dropdown_bg")]
631 pub menu_dropdown_bg: ColorDef,
632 #[serde(default = "default_menu_dropdown_fg")]
634 pub menu_dropdown_fg: ColorDef,
635 #[serde(default = "default_menu_highlight_bg")]
637 pub menu_highlight_bg: ColorDef,
638 #[serde(default = "default_menu_highlight_fg")]
640 pub menu_highlight_fg: ColorDef,
641 #[serde(default = "default_menu_border_fg")]
643 pub menu_border_fg: ColorDef,
644 #[serde(default = "default_menu_separator_fg")]
646 pub menu_separator_fg: ColorDef,
647 #[serde(default = "default_menu_hover_bg")]
649 pub menu_hover_bg: ColorDef,
650 #[serde(default = "default_menu_hover_fg")]
652 pub menu_hover_fg: ColorDef,
653 #[serde(default = "default_menu_disabled_fg")]
655 pub menu_disabled_fg: ColorDef,
656 #[serde(default = "default_menu_disabled_bg")]
658 pub menu_disabled_bg: ColorDef,
659 #[serde(default = "default_status_bar_fg")]
661 pub status_bar_fg: ColorDef,
662 #[serde(default = "default_status_bar_bg")]
664 pub status_bar_bg: ColorDef,
665 #[serde(default)]
667 pub status_palette_fg: Option<ColorDef>,
668 #[serde(default)]
670 pub status_palette_bg: Option<ColorDef>,
671 #[serde(default)]
673 pub status_lsp_on_fg: Option<ColorDef>,
674 #[serde(default)]
676 pub status_lsp_on_bg: Option<ColorDef>,
677 #[serde(default)]
681 pub status_lsp_actionable_fg: Option<ColorDef>,
682 #[serde(default)]
685 pub status_lsp_actionable_bg: Option<ColorDef>,
686 #[serde(default = "default_prompt_fg")]
688 pub prompt_fg: ColorDef,
689 #[serde(default = "default_prompt_bg")]
691 pub prompt_bg: ColorDef,
692 #[serde(default = "default_prompt_selection_fg")]
694 pub prompt_selection_fg: ColorDef,
695 #[serde(default = "default_prompt_selection_bg")]
697 pub prompt_selection_bg: ColorDef,
698 #[serde(default = "default_popup_border_fg")]
700 pub popup_border_fg: ColorDef,
701 #[serde(default = "default_popup_bg")]
703 pub popup_bg: ColorDef,
704 #[serde(default = "default_popup_selection_bg")]
706 pub popup_selection_bg: ColorDef,
707 #[serde(default = "default_text_input_selection_bg")]
715 pub text_input_selection_bg: ColorDef,
716 #[serde(default = "default_popup_selection_fg")]
718 pub popup_selection_fg: ColorDef,
719 #[serde(default = "default_popup_text_fg")]
721 pub popup_text_fg: ColorDef,
722 #[serde(default = "default_suggestion_bg")]
724 pub suggestion_bg: ColorDef,
725 #[serde(default = "default_suggestion_selected_bg")]
727 pub suggestion_selected_bg: ColorDef,
728 #[serde(default = "default_help_bg")]
730 pub help_bg: ColorDef,
731 #[serde(default = "default_help_fg")]
733 pub help_fg: ColorDef,
734 #[serde(default = "default_help_key_fg")]
736 pub help_key_fg: ColorDef,
737 #[serde(default = "default_help_separator_fg")]
739 pub help_separator_fg: ColorDef,
740 #[serde(default = "default_help_indicator_fg")]
742 pub help_indicator_fg: ColorDef,
743 #[serde(default = "default_help_indicator_bg")]
745 pub help_indicator_bg: ColorDef,
746 #[serde(default = "default_inline_code_bg")]
748 pub inline_code_bg: ColorDef,
749 #[serde(default = "default_split_separator_fg")]
751 pub split_separator_fg: ColorDef,
752 #[serde(default = "default_split_separator_hover_fg")]
754 pub split_separator_hover_fg: ColorDef,
755 #[serde(default = "default_scrollbar_track_fg")]
757 pub scrollbar_track_fg: ColorDef,
758 #[serde(default = "default_scrollbar_thumb_fg")]
760 pub scrollbar_thumb_fg: ColorDef,
761 #[serde(default = "default_scrollbar_track_hover_fg")]
763 pub scrollbar_track_hover_fg: ColorDef,
764 #[serde(default = "default_scrollbar_thumb_hover_fg")]
766 pub scrollbar_thumb_hover_fg: ColorDef,
767 #[serde(default = "default_compose_margin_bg")]
769 pub compose_margin_bg: ColorDef,
770 #[serde(default = "default_semantic_highlight_bg")]
772 pub semantic_highlight_bg: ColorDef,
773 #[serde(default)]
780 pub semantic_highlight_modifier: Option<ModifierDef>,
781 #[serde(default = "default_terminal_bg")]
783 pub terminal_bg: ColorDef,
784 #[serde(default = "default_terminal_fg")]
786 pub terminal_fg: ColorDef,
787 #[serde(default = "default_status_warning_indicator_bg")]
789 pub status_warning_indicator_bg: ColorDef,
790 #[serde(default = "default_status_warning_indicator_fg")]
792 pub status_warning_indicator_fg: ColorDef,
793 #[serde(default = "default_status_error_indicator_bg")]
795 pub status_error_indicator_bg: ColorDef,
796 #[serde(default = "default_status_error_indicator_fg")]
798 pub status_error_indicator_fg: ColorDef,
799 #[serde(default = "default_status_warning_indicator_hover_bg")]
801 pub status_warning_indicator_hover_bg: ColorDef,
802 #[serde(default = "default_status_warning_indicator_hover_fg")]
804 pub status_warning_indicator_hover_fg: ColorDef,
805 #[serde(default = "default_status_error_indicator_hover_bg")]
807 pub status_error_indicator_hover_bg: ColorDef,
808 #[serde(default = "default_status_error_indicator_hover_fg")]
810 pub status_error_indicator_hover_fg: ColorDef,
811 #[serde(default = "default_tab_drop_zone_bg")]
813 pub tab_drop_zone_bg: ColorDef,
814 #[serde(default = "default_tab_drop_zone_border")]
816 pub tab_drop_zone_border: ColorDef,
817 #[serde(default = "default_settings_selected_bg")]
819 pub settings_selected_bg: ColorDef,
820 #[serde(default = "default_settings_selected_fg")]
822 pub settings_selected_fg: ColorDef,
823 #[serde(default)]
825 pub file_status_added_fg: Option<ColorDef>,
826 #[serde(default)]
828 pub file_status_modified_fg: Option<ColorDef>,
829 #[serde(default)]
831 pub file_status_deleted_fg: Option<ColorDef>,
832 #[serde(default)]
834 pub file_status_renamed_fg: Option<ColorDef>,
835 #[serde(default)]
837 pub file_status_untracked_fg: Option<ColorDef>,
838 #[serde(default)]
840 pub file_status_conflicted_fg: Option<ColorDef>,
841}
842
843fn default_tab_active_fg() -> ColorDef {
846 ColorDef::Named("Yellow".to_string())
847}
848fn default_tab_active_bg() -> ColorDef {
849 ColorDef::Named("Blue".to_string())
850}
851fn default_tab_inactive_fg() -> ColorDef {
852 ColorDef::Named("White".to_string())
853}
854fn default_tab_inactive_bg() -> ColorDef {
855 ColorDef::Named("DarkGray".to_string())
856}
857fn default_tab_separator_bg() -> ColorDef {
858 ColorDef::Named("Black".to_string())
859}
860fn default_tab_close_hover_fg() -> ColorDef {
861 ColorDef::Rgb(255, 100, 100) }
863fn default_tab_hover_bg() -> ColorDef {
864 ColorDef::Rgb(70, 70, 75) }
866
867fn default_menu_bg() -> ColorDef {
869 ColorDef::Rgb(60, 60, 65)
870}
871fn default_menu_fg() -> ColorDef {
872 ColorDef::Rgb(220, 220, 220)
873}
874fn default_menu_active_bg() -> ColorDef {
875 ColorDef::Rgb(60, 60, 60)
876}
877fn default_menu_active_fg() -> ColorDef {
878 ColorDef::Rgb(255, 255, 255)
879}
880fn default_menu_dropdown_bg() -> ColorDef {
881 ColorDef::Rgb(50, 50, 50)
882}
883fn default_menu_dropdown_fg() -> ColorDef {
884 ColorDef::Rgb(220, 220, 220)
885}
886fn default_menu_highlight_bg() -> ColorDef {
887 ColorDef::Rgb(70, 130, 180)
888}
889fn default_menu_highlight_fg() -> ColorDef {
890 ColorDef::Rgb(255, 255, 255)
891}
892fn default_menu_border_fg() -> ColorDef {
893 ColorDef::Rgb(100, 100, 100)
894}
895fn default_menu_separator_fg() -> ColorDef {
896 ColorDef::Rgb(80, 80, 80)
897}
898fn default_menu_hover_bg() -> ColorDef {
899 ColorDef::Rgb(55, 55, 55)
900}
901fn default_menu_hover_fg() -> ColorDef {
902 ColorDef::Rgb(255, 255, 255)
903}
904fn default_menu_disabled_fg() -> ColorDef {
905 ColorDef::Rgb(100, 100, 100) }
907fn default_menu_disabled_bg() -> ColorDef {
908 ColorDef::Rgb(50, 50, 50) }
910fn default_status_bar_fg() -> ColorDef {
912 ColorDef::Named("White".to_string())
913}
914fn default_status_bar_bg() -> ColorDef {
915 ColorDef::Named("DarkGray".to_string())
916}
917
918fn default_prompt_fg() -> ColorDef {
920 ColorDef::Named("White".to_string())
921}
922fn default_prompt_bg() -> ColorDef {
923 ColorDef::Named("Black".to_string())
924}
925fn default_prompt_selection_fg() -> ColorDef {
926 ColorDef::Named("White".to_string())
927}
928fn default_prompt_selection_bg() -> ColorDef {
929 ColorDef::Rgb(58, 79, 120)
930}
931
932fn default_popup_border_fg() -> ColorDef {
934 ColorDef::Named("Gray".to_string())
935}
936fn default_popup_bg() -> ColorDef {
937 ColorDef::Rgb(30, 30, 30)
938}
939fn default_popup_selection_bg() -> ColorDef {
940 ColorDef::Rgb(58, 79, 120)
941}
942fn default_text_input_selection_bg() -> ColorDef {
943 ColorDef::Rgb(58, 79, 120)
947}
948fn default_popup_selection_fg() -> ColorDef {
949 ColorDef::Rgb(255, 255, 255) }
951fn default_popup_text_fg() -> ColorDef {
952 ColorDef::Named("White".to_string())
953}
954
955fn default_suggestion_bg() -> ColorDef {
957 ColorDef::Rgb(30, 30, 30)
958}
959fn default_suggestion_selected_bg() -> ColorDef {
960 ColorDef::Rgb(58, 79, 120)
961}
962
963fn default_help_bg() -> ColorDef {
965 ColorDef::Named("Black".to_string())
966}
967fn default_help_fg() -> ColorDef {
968 ColorDef::Named("White".to_string())
969}
970fn default_help_key_fg() -> ColorDef {
971 ColorDef::Named("Cyan".to_string())
972}
973fn default_help_separator_fg() -> ColorDef {
974 ColorDef::Named("DarkGray".to_string())
975}
976fn default_help_indicator_fg() -> ColorDef {
977 ColorDef::Named("Red".to_string())
978}
979fn default_help_indicator_bg() -> ColorDef {
980 ColorDef::Named("Black".to_string())
981}
982
983fn default_inline_code_bg() -> ColorDef {
984 ColorDef::Named("DarkGray".to_string())
985}
986
987fn default_split_separator_fg() -> ColorDef {
989 ColorDef::Rgb(100, 100, 100)
990}
991fn default_split_separator_hover_fg() -> ColorDef {
992 ColorDef::Rgb(100, 149, 237) }
994fn default_scrollbar_track_fg() -> ColorDef {
995 ColorDef::Named("DarkGray".to_string())
996}
997fn default_scrollbar_thumb_fg() -> ColorDef {
998 ColorDef::Named("Gray".to_string())
999}
1000fn default_scrollbar_track_hover_fg() -> ColorDef {
1001 ColorDef::Named("Gray".to_string())
1002}
1003fn default_scrollbar_thumb_hover_fg() -> ColorDef {
1004 ColorDef::Named("White".to_string())
1005}
1006fn default_compose_margin_bg() -> ColorDef {
1007 ColorDef::Rgb(18, 18, 18) }
1009fn default_semantic_highlight_bg() -> ColorDef {
1010 ColorDef::Rgb(60, 60, 80) }
1012fn default_terminal_bg() -> ColorDef {
1013 ColorDef::Named("Default".to_string()) }
1015fn default_terminal_fg() -> ColorDef {
1016 ColorDef::Named("Default".to_string()) }
1018fn default_status_warning_indicator_bg() -> ColorDef {
1019 ColorDef::Rgb(181, 137, 0) }
1021fn default_status_warning_indicator_fg() -> ColorDef {
1022 ColorDef::Rgb(0, 0, 0) }
1024fn default_status_error_indicator_bg() -> ColorDef {
1025 ColorDef::Rgb(220, 50, 47) }
1027fn default_status_error_indicator_fg() -> ColorDef {
1028 ColorDef::Rgb(255, 255, 255) }
1030fn default_status_warning_indicator_hover_bg() -> ColorDef {
1031 ColorDef::Rgb(211, 167, 30) }
1033fn default_status_warning_indicator_hover_fg() -> ColorDef {
1034 ColorDef::Rgb(0, 0, 0) }
1036fn default_status_error_indicator_hover_bg() -> ColorDef {
1037 ColorDef::Rgb(250, 80, 77) }
1039fn default_status_error_indicator_hover_fg() -> ColorDef {
1040 ColorDef::Rgb(255, 255, 255) }
1042fn default_tab_drop_zone_bg() -> ColorDef {
1043 ColorDef::Rgb(70, 130, 180) }
1045fn default_tab_drop_zone_border() -> ColorDef {
1046 ColorDef::Rgb(100, 149, 237) }
1048fn default_settings_selected_bg() -> ColorDef {
1049 ColorDef::Rgb(60, 60, 70) }
1051fn default_settings_selected_fg() -> ColorDef {
1052 ColorDef::Rgb(255, 255, 255) }
1054#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1056pub struct SearchColors {
1057 #[serde(default = "default_search_match_bg")]
1059 pub match_bg: ColorDef,
1060 #[serde(default = "default_search_match_fg")]
1062 pub match_fg: ColorDef,
1063 #[serde(default = "default_search_label_bg")]
1067 pub label_bg: ColorDef,
1068 #[serde(default = "default_search_label_fg")]
1072 pub label_fg: ColorDef,
1073}
1074
1075fn default_search_match_bg() -> ColorDef {
1077 ColorDef::Rgb(100, 100, 20)
1078}
1079fn default_search_match_fg() -> ColorDef {
1080 ColorDef::Rgb(255, 255, 255)
1081}
1082fn default_search_label_bg() -> ColorDef {
1087 ColorDef::Rgb(199, 78, 189)
1088}
1089fn default_search_label_fg() -> ColorDef {
1090 ColorDef::Rgb(255, 255, 255)
1091}
1092
1093#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1095pub struct DiagnosticColors {
1096 #[serde(default = "default_diagnostic_error_fg")]
1098 pub error_fg: ColorDef,
1099 #[serde(default = "default_diagnostic_error_bg")]
1101 pub error_bg: ColorDef,
1102 #[serde(default = "default_diagnostic_warning_fg")]
1104 pub warning_fg: ColorDef,
1105 #[serde(default = "default_diagnostic_warning_bg")]
1107 pub warning_bg: ColorDef,
1108 #[serde(default = "default_diagnostic_info_fg")]
1110 pub info_fg: ColorDef,
1111 #[serde(default = "default_diagnostic_info_bg")]
1113 pub info_bg: ColorDef,
1114 #[serde(default = "default_diagnostic_hint_fg")]
1116 pub hint_fg: ColorDef,
1117 #[serde(default = "default_diagnostic_hint_bg")]
1119 pub hint_bg: ColorDef,
1120}
1121
1122fn default_diagnostic_error_fg() -> ColorDef {
1124 ColorDef::Named("Red".to_string())
1125}
1126fn default_diagnostic_error_bg() -> ColorDef {
1127 ColorDef::Rgb(60, 20, 20)
1128}
1129fn default_diagnostic_warning_fg() -> ColorDef {
1130 ColorDef::Named("Yellow".to_string())
1131}
1132fn default_diagnostic_warning_bg() -> ColorDef {
1133 ColorDef::Rgb(60, 50, 0)
1134}
1135fn default_diagnostic_info_fg() -> ColorDef {
1136 ColorDef::Named("Blue".to_string())
1137}
1138fn default_diagnostic_info_bg() -> ColorDef {
1139 ColorDef::Rgb(0, 30, 60)
1140}
1141fn default_diagnostic_hint_fg() -> ColorDef {
1142 ColorDef::Named("Gray".to_string())
1143}
1144fn default_diagnostic_hint_bg() -> ColorDef {
1145 ColorDef::Rgb(30, 30, 30)
1146}
1147
1148#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1150pub struct SyntaxColors {
1151 #[serde(default = "default_syntax_keyword")]
1153 pub keyword: ColorDef,
1154 #[serde(default = "default_syntax_string")]
1156 pub string: ColorDef,
1157 #[serde(default = "default_syntax_comment")]
1159 pub comment: ColorDef,
1160 #[serde(default = "default_syntax_function")]
1162 pub function: ColorDef,
1163 #[serde(rename = "type", default = "default_syntax_type")]
1165 pub type_: ColorDef,
1166 #[serde(default = "default_syntax_variable")]
1168 pub variable: ColorDef,
1169 #[serde(default = "default_syntax_constant")]
1171 pub constant: ColorDef,
1172 #[serde(default = "default_syntax_operator")]
1174 pub operator: ColorDef,
1175 #[serde(default = "default_syntax_punctuation_bracket")]
1177 pub punctuation_bracket: ColorDef,
1178 #[serde(default = "default_syntax_punctuation_delimiter")]
1180 pub punctuation_delimiter: ColorDef,
1181}
1182
1183fn default_syntax_keyword() -> ColorDef {
1185 ColorDef::Rgb(86, 156, 214)
1186}
1187fn default_syntax_string() -> ColorDef {
1188 ColorDef::Rgb(206, 145, 120)
1189}
1190fn default_syntax_comment() -> ColorDef {
1191 ColorDef::Rgb(106, 153, 85)
1192}
1193fn default_syntax_function() -> ColorDef {
1194 ColorDef::Rgb(220, 220, 170)
1195}
1196fn default_syntax_type() -> ColorDef {
1197 ColorDef::Rgb(78, 201, 176)
1198}
1199fn default_syntax_variable() -> ColorDef {
1200 ColorDef::Rgb(156, 220, 254)
1201}
1202fn default_syntax_constant() -> ColorDef {
1203 ColorDef::Rgb(79, 193, 255)
1204}
1205fn default_syntax_operator() -> ColorDef {
1206 ColorDef::Rgb(212, 212, 212)
1207}
1208fn default_syntax_punctuation_bracket() -> ColorDef {
1209 ColorDef::Rgb(212, 212, 212) }
1211fn default_syntax_punctuation_delimiter() -> ColorDef {
1212 ColorDef::Rgb(212, 212, 212) }
1214
1215#[derive(Debug, Clone)]
1217pub struct Theme {
1218 pub name: String,
1220
1221 pub editor_bg: Color,
1223 pub editor_fg: Color,
1224 pub cursor: Color,
1225 pub inactive_cursor: Color,
1226 pub selection_bg: Color,
1227 pub selection_modifier: Modifier,
1232 pub current_line_bg: Color,
1233 pub line_number_fg: Color,
1234 pub line_number_bg: Color,
1235
1236 pub after_eof_bg: Color,
1238
1239 pub ruler_bg: Color,
1241
1242 pub whitespace_indicator_fg: Color,
1244
1245 pub diff_add_bg: Color,
1247 pub diff_remove_bg: Color,
1248 pub diff_modify_bg: Color,
1249 pub diff_add_highlight_bg: Color,
1251 pub diff_remove_highlight_bg: Color,
1253 pub diff_add_fg: Option<Color>,
1264 pub diff_remove_fg: Option<Color>,
1265 pub diff_modify_fg: Option<Color>,
1266
1267 pub tab_active_fg: Color,
1269 pub tab_active_bg: Color,
1270 pub tab_inactive_fg: Color,
1271 pub tab_inactive_bg: Color,
1272 pub tab_separator_bg: Color,
1273 pub tab_close_hover_fg: Color,
1274 pub tab_hover_bg: Color,
1275
1276 pub menu_bg: Color,
1278 pub menu_fg: Color,
1279 pub menu_active_bg: Color,
1280 pub menu_active_fg: Color,
1281 pub menu_dropdown_bg: Color,
1282 pub menu_dropdown_fg: Color,
1283 pub menu_highlight_bg: Color,
1284 pub menu_highlight_fg: Color,
1285 pub menu_border_fg: Color,
1286 pub menu_separator_fg: Color,
1287 pub menu_hover_bg: Color,
1288 pub menu_hover_fg: Color,
1289 pub menu_disabled_fg: Color,
1290 pub menu_disabled_bg: Color,
1291
1292 pub status_bar_fg: Color,
1293 pub status_bar_bg: Color,
1294 pub status_palette_fg: Color,
1296 pub status_palette_bg: Color,
1297 pub status_lsp_on_fg: Color,
1299 pub status_lsp_on_bg: Color,
1300 pub status_lsp_actionable_fg: Color,
1303 pub status_lsp_actionable_bg: Color,
1304 pub prompt_fg: Color,
1305 pub prompt_bg: Color,
1306 pub prompt_selection_fg: Color,
1307 pub prompt_selection_bg: Color,
1308
1309 pub popup_border_fg: Color,
1310 pub popup_bg: Color,
1311 pub popup_selection_bg: Color,
1312 pub popup_selection_fg: Color,
1313 pub popup_text_fg: Color,
1314 pub text_input_selection_bg: Color,
1318
1319 pub suggestion_bg: Color,
1320 pub suggestion_selected_bg: Color,
1321
1322 pub help_bg: Color,
1323 pub help_fg: Color,
1324 pub help_key_fg: Color,
1325 pub help_separator_fg: Color,
1326
1327 pub help_indicator_fg: Color,
1328 pub help_indicator_bg: Color,
1329
1330 pub inline_code_bg: Color,
1332
1333 pub split_separator_fg: Color,
1334 pub split_separator_hover_fg: Color,
1335
1336 pub scrollbar_track_fg: Color,
1338 pub scrollbar_thumb_fg: Color,
1339 pub scrollbar_track_hover_fg: Color,
1340 pub scrollbar_thumb_hover_fg: Color,
1341
1342 pub compose_margin_bg: Color,
1344
1345 pub semantic_highlight_bg: Color,
1347 pub semantic_highlight_modifier: Modifier,
1352
1353 pub terminal_bg: Color,
1355 pub terminal_fg: Color,
1356
1357 pub status_warning_indicator_bg: Color,
1359 pub status_warning_indicator_fg: Color,
1360 pub status_error_indicator_bg: Color,
1361 pub status_error_indicator_fg: Color,
1362 pub status_warning_indicator_hover_bg: Color,
1363 pub status_warning_indicator_hover_fg: Color,
1364 pub status_error_indicator_hover_bg: Color,
1365 pub status_error_indicator_hover_fg: Color,
1366
1367 pub tab_drop_zone_bg: Color,
1369 pub tab_drop_zone_border: Color,
1370
1371 pub settings_selected_bg: Color,
1373 pub settings_selected_fg: Color,
1374
1375 pub file_status_added_fg: Color,
1377 pub file_status_modified_fg: Color,
1378 pub file_status_deleted_fg: Color,
1379 pub file_status_renamed_fg: Color,
1380 pub file_status_untracked_fg: Color,
1381 pub file_status_conflicted_fg: Color,
1382
1383 pub search_match_bg: Color,
1385 pub search_match_fg: Color,
1386 pub search_label_bg: Color,
1387 pub search_label_fg: Color,
1388
1389 pub diagnostic_error_fg: Color,
1391 pub diagnostic_error_bg: Color,
1392 pub diagnostic_warning_fg: Color,
1393 pub diagnostic_warning_bg: Color,
1394 pub diagnostic_info_fg: Color,
1395 pub diagnostic_info_bg: Color,
1396 pub diagnostic_hint_fg: Color,
1397 pub diagnostic_hint_bg: Color,
1398
1399 pub syntax_keyword: Color,
1401 pub syntax_string: Color,
1402 pub syntax_comment: Color,
1403 pub syntax_function: Color,
1404 pub syntax_type: Color,
1405 pub syntax_variable: Color,
1406 pub syntax_constant: Color,
1407 pub syntax_operator: Color,
1408 pub syntax_punctuation_bracket: Color,
1409 pub syntax_punctuation_delimiter: Color,
1410}
1411
1412impl From<ThemeFile> for Theme {
1413 fn from(file: ThemeFile) -> Self {
1414 Self {
1415 name: file.name,
1416 editor_bg: file.editor.bg.clone().into(),
1417 editor_fg: file.editor.fg.into(),
1418 cursor: file.editor.cursor.into(),
1419 inactive_cursor: file.editor.inactive_cursor.into(),
1420 selection_bg: file.editor.selection_bg.into(),
1421 selection_modifier: file
1422 .editor
1423 .selection_modifier
1424 .as_ref()
1425 .map(Modifier::from)
1426 .unwrap_or(Modifier::empty()),
1427 current_line_bg: file.editor.current_line_bg.into(),
1428 line_number_fg: file.editor.line_number_fg.into(),
1429 line_number_bg: file.editor.line_number_bg.into(),
1430 after_eof_bg: file
1433 .editor
1434 .after_eof_bg
1435 .clone()
1436 .map(|c| c.into())
1437 .unwrap_or_else(|| shade_toward_contrast(file.editor.bg.clone().into(), 10)),
1438 ruler_bg: file.editor.ruler_bg.into(),
1439 whitespace_indicator_fg: file.editor.whitespace_indicator_fg.into(),
1440 diff_add_bg: file.editor.diff_add_bg.clone().into(),
1441 diff_remove_bg: file.editor.diff_remove_bg.clone().into(),
1442 diff_modify_bg: file.editor.diff_modify_bg.into(),
1443 diff_add_highlight_bg: file
1445 .editor
1446 .diff_add_highlight_bg
1447 .map(|c| c.into())
1448 .unwrap_or_else(|| brighten_color(file.editor.diff_add_bg.into(), 40)),
1449 diff_remove_highlight_bg: file
1450 .editor
1451 .diff_remove_highlight_bg
1452 .map(|c| c.into())
1453 .unwrap_or_else(|| brighten_color(file.editor.diff_remove_bg.into(), 40)),
1454 diff_add_fg: file.editor.diff_add_fg.clone().map(|c| c.into()),
1455 diff_remove_fg: file.editor.diff_remove_fg.clone().map(|c| c.into()),
1456 diff_modify_fg: file.editor.diff_modify_fg.clone().map(|c| c.into()),
1457 tab_active_fg: file.ui.tab_active_fg.into(),
1458 tab_active_bg: file.ui.tab_active_bg.into(),
1459 tab_inactive_fg: file.ui.tab_inactive_fg.into(),
1460 tab_inactive_bg: file.ui.tab_inactive_bg.into(),
1461 tab_separator_bg: file.ui.tab_separator_bg.into(),
1462 tab_close_hover_fg: file.ui.tab_close_hover_fg.into(),
1463 tab_hover_bg: file.ui.tab_hover_bg.into(),
1464 menu_bg: file.ui.menu_bg.into(),
1465 menu_fg: file.ui.menu_fg.into(),
1466 menu_active_bg: file.ui.menu_active_bg.into(),
1467 menu_active_fg: file.ui.menu_active_fg.into(),
1468 menu_dropdown_bg: file.ui.menu_dropdown_bg.into(),
1469 menu_dropdown_fg: file.ui.menu_dropdown_fg.into(),
1470 menu_highlight_bg: file.ui.menu_highlight_bg.into(),
1471 menu_highlight_fg: file.ui.menu_highlight_fg.into(),
1472 menu_border_fg: file.ui.menu_border_fg.into(),
1473 menu_separator_fg: file.ui.menu_separator_fg.into(),
1474 menu_hover_bg: file.ui.menu_hover_bg.into(),
1475 menu_hover_fg: file.ui.menu_hover_fg.into(),
1476 menu_disabled_fg: file.ui.menu_disabled_fg.into(),
1477 menu_disabled_bg: file.ui.menu_disabled_bg.into(),
1478 status_bar_fg: file.ui.status_bar_fg.clone().into(),
1479 status_bar_bg: file.ui.status_bar_bg.clone().into(),
1480 status_palette_fg: file
1481 .ui
1482 .status_palette_fg
1483 .clone()
1484 .map(|c| c.into())
1485 .unwrap_or_else(|| file.ui.status_bar_fg.clone().into()),
1486 status_palette_bg: file
1487 .ui
1488 .status_palette_bg
1489 .clone()
1490 .map(|c| c.into())
1491 .unwrap_or_else(|| file.ui.status_bar_bg.clone().into()),
1492 status_lsp_on_fg: file
1493 .ui
1494 .status_lsp_on_fg
1495 .clone()
1496 .map(|c| c.into())
1497 .unwrap_or_else(|| file.ui.status_bar_fg.clone().into()),
1498 status_lsp_on_bg: file
1499 .ui
1500 .status_lsp_on_bg
1501 .clone()
1502 .map(|c| c.into())
1503 .unwrap_or_else(|| file.ui.status_bar_bg.clone().into()),
1504 status_lsp_actionable_fg: file
1505 .ui
1506 .status_lsp_actionable_fg
1507 .clone()
1508 .map(|c| c.into())
1509 .unwrap_or_else(|| file.ui.status_warning_indicator_fg.clone().into()),
1510 status_lsp_actionable_bg: file
1511 .ui
1512 .status_lsp_actionable_bg
1513 .clone()
1514 .map(|c| c.into())
1515 .unwrap_or_else(|| file.ui.status_warning_indicator_bg.clone().into()),
1516 prompt_fg: file.ui.prompt_fg.into(),
1517 prompt_bg: file.ui.prompt_bg.into(),
1518 prompt_selection_fg: file.ui.prompt_selection_fg.into(),
1519 prompt_selection_bg: file.ui.prompt_selection_bg.into(),
1520 popup_border_fg: file.ui.popup_border_fg.into(),
1521 popup_bg: file.ui.popup_bg.into(),
1522 popup_selection_bg: file.ui.popup_selection_bg.into(),
1523 popup_selection_fg: file.ui.popup_selection_fg.into(),
1524 popup_text_fg: file.ui.popup_text_fg.into(),
1525 text_input_selection_bg: file.ui.text_input_selection_bg.into(),
1526 suggestion_bg: file.ui.suggestion_bg.into(),
1527 suggestion_selected_bg: file.ui.suggestion_selected_bg.into(),
1528 help_bg: file.ui.help_bg.into(),
1529 help_fg: file.ui.help_fg.into(),
1530 help_key_fg: file.ui.help_key_fg.into(),
1531 help_separator_fg: file.ui.help_separator_fg.into(),
1532 help_indicator_fg: file.ui.help_indicator_fg.into(),
1533 help_indicator_bg: file.ui.help_indicator_bg.into(),
1534 inline_code_bg: file.ui.inline_code_bg.into(),
1535 split_separator_fg: file.ui.split_separator_fg.into(),
1536 split_separator_hover_fg: file.ui.split_separator_hover_fg.into(),
1537 scrollbar_track_fg: file.ui.scrollbar_track_fg.into(),
1538 scrollbar_thumb_fg: file.ui.scrollbar_thumb_fg.into(),
1539 scrollbar_track_hover_fg: file.ui.scrollbar_track_hover_fg.into(),
1540 scrollbar_thumb_hover_fg: file.ui.scrollbar_thumb_hover_fg.into(),
1541 compose_margin_bg: file.ui.compose_margin_bg.into(),
1542 semantic_highlight_bg: file.ui.semantic_highlight_bg.into(),
1543 semantic_highlight_modifier: file
1544 .ui
1545 .semantic_highlight_modifier
1546 .as_ref()
1547 .map(Modifier::from)
1548 .unwrap_or(Modifier::empty()),
1549 terminal_bg: file.ui.terminal_bg.into(),
1550 terminal_fg: file.ui.terminal_fg.into(),
1551 status_warning_indicator_bg: file.ui.status_warning_indicator_bg.into(),
1552 status_warning_indicator_fg: file.ui.status_warning_indicator_fg.into(),
1553 status_error_indicator_bg: file.ui.status_error_indicator_bg.into(),
1554 status_error_indicator_fg: file.ui.status_error_indicator_fg.into(),
1555 status_warning_indicator_hover_bg: file.ui.status_warning_indicator_hover_bg.into(),
1556 status_warning_indicator_hover_fg: file.ui.status_warning_indicator_hover_fg.into(),
1557 status_error_indicator_hover_bg: file.ui.status_error_indicator_hover_bg.into(),
1558 status_error_indicator_hover_fg: file.ui.status_error_indicator_hover_fg.into(),
1559 tab_drop_zone_bg: file.ui.tab_drop_zone_bg.into(),
1560 tab_drop_zone_border: file.ui.tab_drop_zone_border.into(),
1561 settings_selected_bg: file.ui.settings_selected_bg.into(),
1562 settings_selected_fg: file.ui.settings_selected_fg.into(),
1563 file_status_added_fg: file
1564 .ui
1565 .file_status_added_fg
1566 .map(|c| c.into())
1567 .unwrap_or_else(|| file.diagnostic.info_fg.clone().into()),
1568 file_status_modified_fg: file
1569 .ui
1570 .file_status_modified_fg
1571 .map(|c| c.into())
1572 .unwrap_or_else(|| file.diagnostic.warning_fg.clone().into()),
1573 file_status_deleted_fg: file
1574 .ui
1575 .file_status_deleted_fg
1576 .map(|c| c.into())
1577 .unwrap_or_else(|| file.diagnostic.error_fg.clone().into()),
1578 file_status_renamed_fg: file
1579 .ui
1580 .file_status_renamed_fg
1581 .map(|c| c.into())
1582 .unwrap_or_else(|| file.diagnostic.info_fg.clone().into()),
1583 file_status_untracked_fg: file
1584 .ui
1585 .file_status_untracked_fg
1586 .map(|c| c.into())
1587 .unwrap_or_else(|| file.diagnostic.hint_fg.clone().into()),
1588 file_status_conflicted_fg: file
1589 .ui
1590 .file_status_conflicted_fg
1591 .map(|c| c.into())
1592 .unwrap_or_else(|| file.diagnostic.error_fg.clone().into()),
1593 search_match_bg: file.search.match_bg.into(),
1594 search_match_fg: file.search.match_fg.into(),
1595 search_label_bg: file.search.label_bg.into(),
1596 search_label_fg: file.search.label_fg.into(),
1597 diagnostic_error_fg: file.diagnostic.error_fg.into(),
1598 diagnostic_error_bg: file.diagnostic.error_bg.into(),
1599 diagnostic_warning_fg: file.diagnostic.warning_fg.into(),
1600 diagnostic_warning_bg: file.diagnostic.warning_bg.into(),
1601 diagnostic_info_fg: file.diagnostic.info_fg.into(),
1602 diagnostic_info_bg: file.diagnostic.info_bg.into(),
1603 diagnostic_hint_fg: file.diagnostic.hint_fg.into(),
1604 diagnostic_hint_bg: file.diagnostic.hint_bg.into(),
1605 syntax_keyword: file.syntax.keyword.into(),
1606 syntax_string: file.syntax.string.into(),
1607 syntax_comment: file.syntax.comment.into(),
1608 syntax_function: file.syntax.function.into(),
1609 syntax_type: file.syntax.type_.into(),
1610 syntax_variable: file.syntax.variable.into(),
1611 syntax_constant: file.syntax.constant.into(),
1612 syntax_operator: file.syntax.operator.into(),
1613 syntax_punctuation_bracket: file.syntax.punctuation_bracket.into(),
1614 syntax_punctuation_delimiter: file.syntax.punctuation_delimiter.into(),
1615 }
1616 }
1617}
1618
1619impl From<Theme> for ThemeFile {
1620 fn from(theme: Theme) -> Self {
1621 Self {
1622 name: theme.name,
1623 extends: None,
1626 editor: EditorColors {
1627 bg: theme.editor_bg.into(),
1628 fg: theme.editor_fg.into(),
1629 cursor: theme.cursor.into(),
1630 inactive_cursor: theme.inactive_cursor.into(),
1631 selection_bg: theme.selection_bg.into(),
1632 selection_modifier: if theme.selection_modifier.is_empty() {
1633 None
1634 } else {
1635 Some(theme.selection_modifier.into())
1636 },
1637 current_line_bg: theme.current_line_bg.into(),
1638 line_number_fg: theme.line_number_fg.into(),
1639 line_number_bg: theme.line_number_bg.into(),
1640 diff_add_bg: theme.diff_add_bg.into(),
1641 diff_remove_bg: theme.diff_remove_bg.into(),
1642 diff_add_highlight_bg: Some(theme.diff_add_highlight_bg.into()),
1643 diff_remove_highlight_bg: Some(theme.diff_remove_highlight_bg.into()),
1644 diff_modify_bg: theme.diff_modify_bg.into(),
1645 diff_add_fg: theme.diff_add_fg.map(|c| c.into()),
1646 diff_remove_fg: theme.diff_remove_fg.map(|c| c.into()),
1647 diff_modify_fg: theme.diff_modify_fg.map(|c| c.into()),
1648 ruler_bg: theme.ruler_bg.into(),
1649 whitespace_indicator_fg: theme.whitespace_indicator_fg.into(),
1650 after_eof_bg: Some(theme.after_eof_bg.into()),
1651 },
1652 ui: UiColors {
1653 tab_active_fg: theme.tab_active_fg.into(),
1654 tab_active_bg: theme.tab_active_bg.into(),
1655 tab_inactive_fg: theme.tab_inactive_fg.into(),
1656 tab_inactive_bg: theme.tab_inactive_bg.into(),
1657 tab_separator_bg: theme.tab_separator_bg.into(),
1658 tab_close_hover_fg: theme.tab_close_hover_fg.into(),
1659 tab_hover_bg: theme.tab_hover_bg.into(),
1660 menu_bg: theme.menu_bg.into(),
1661 menu_fg: theme.menu_fg.into(),
1662 menu_active_bg: theme.menu_active_bg.into(),
1663 menu_active_fg: theme.menu_active_fg.into(),
1664 menu_dropdown_bg: theme.menu_dropdown_bg.into(),
1665 menu_dropdown_fg: theme.menu_dropdown_fg.into(),
1666 menu_highlight_bg: theme.menu_highlight_bg.into(),
1667 menu_highlight_fg: theme.menu_highlight_fg.into(),
1668 menu_border_fg: theme.menu_border_fg.into(),
1669 menu_separator_fg: theme.menu_separator_fg.into(),
1670 menu_hover_bg: theme.menu_hover_bg.into(),
1671 menu_hover_fg: theme.menu_hover_fg.into(),
1672 menu_disabled_fg: theme.menu_disabled_fg.into(),
1673 menu_disabled_bg: theme.menu_disabled_bg.into(),
1674 status_bar_fg: theme.status_bar_fg.into(),
1675 status_bar_bg: theme.status_bar_bg.into(),
1676 status_palette_fg: Some(theme.status_palette_fg.into()),
1677 status_palette_bg: Some(theme.status_palette_bg.into()),
1678 status_lsp_on_fg: Some(theme.status_lsp_on_fg.into()),
1679 status_lsp_on_bg: Some(theme.status_lsp_on_bg.into()),
1680 status_lsp_actionable_fg: Some(theme.status_lsp_actionable_fg.into()),
1681 status_lsp_actionable_bg: Some(theme.status_lsp_actionable_bg.into()),
1682 prompt_fg: theme.prompt_fg.into(),
1683 prompt_bg: theme.prompt_bg.into(),
1684 prompt_selection_fg: theme.prompt_selection_fg.into(),
1685 prompt_selection_bg: theme.prompt_selection_bg.into(),
1686 popup_border_fg: theme.popup_border_fg.into(),
1687 popup_bg: theme.popup_bg.into(),
1688 popup_selection_bg: theme.popup_selection_bg.into(),
1689 popup_selection_fg: theme.popup_selection_fg.into(),
1690 popup_text_fg: theme.popup_text_fg.into(),
1691 text_input_selection_bg: theme.text_input_selection_bg.into(),
1692 suggestion_bg: theme.suggestion_bg.into(),
1693 suggestion_selected_bg: theme.suggestion_selected_bg.into(),
1694 help_bg: theme.help_bg.into(),
1695 help_fg: theme.help_fg.into(),
1696 help_key_fg: theme.help_key_fg.into(),
1697 help_separator_fg: theme.help_separator_fg.into(),
1698 help_indicator_fg: theme.help_indicator_fg.into(),
1699 help_indicator_bg: theme.help_indicator_bg.into(),
1700 inline_code_bg: theme.inline_code_bg.into(),
1701 split_separator_fg: theme.split_separator_fg.into(),
1702 split_separator_hover_fg: theme.split_separator_hover_fg.into(),
1703 scrollbar_track_fg: theme.scrollbar_track_fg.into(),
1704 scrollbar_thumb_fg: theme.scrollbar_thumb_fg.into(),
1705 scrollbar_track_hover_fg: theme.scrollbar_track_hover_fg.into(),
1706 scrollbar_thumb_hover_fg: theme.scrollbar_thumb_hover_fg.into(),
1707 compose_margin_bg: theme.compose_margin_bg.into(),
1708 semantic_highlight_bg: theme.semantic_highlight_bg.into(),
1709 semantic_highlight_modifier: if theme.semantic_highlight_modifier.is_empty() {
1710 None
1711 } else {
1712 Some(theme.semantic_highlight_modifier.into())
1713 },
1714 terminal_bg: theme.terminal_bg.into(),
1715 terminal_fg: theme.terminal_fg.into(),
1716 status_warning_indicator_bg: theme.status_warning_indicator_bg.into(),
1717 status_warning_indicator_fg: theme.status_warning_indicator_fg.into(),
1718 status_error_indicator_bg: theme.status_error_indicator_bg.into(),
1719 status_error_indicator_fg: theme.status_error_indicator_fg.into(),
1720 status_warning_indicator_hover_bg: theme.status_warning_indicator_hover_bg.into(),
1721 status_warning_indicator_hover_fg: theme.status_warning_indicator_hover_fg.into(),
1722 status_error_indicator_hover_bg: theme.status_error_indicator_hover_bg.into(),
1723 status_error_indicator_hover_fg: theme.status_error_indicator_hover_fg.into(),
1724 tab_drop_zone_bg: theme.tab_drop_zone_bg.into(),
1725 tab_drop_zone_border: theme.tab_drop_zone_border.into(),
1726 settings_selected_bg: theme.settings_selected_bg.into(),
1727 settings_selected_fg: theme.settings_selected_fg.into(),
1728 file_status_added_fg: Some(theme.file_status_added_fg.into()),
1729 file_status_modified_fg: Some(theme.file_status_modified_fg.into()),
1730 file_status_deleted_fg: Some(theme.file_status_deleted_fg.into()),
1731 file_status_renamed_fg: Some(theme.file_status_renamed_fg.into()),
1732 file_status_untracked_fg: Some(theme.file_status_untracked_fg.into()),
1733 file_status_conflicted_fg: Some(theme.file_status_conflicted_fg.into()),
1734 },
1735 search: SearchColors {
1736 match_bg: theme.search_match_bg.into(),
1737 match_fg: theme.search_match_fg.into(),
1738 label_bg: theme.search_label_bg.into(),
1739 label_fg: theme.search_label_fg.into(),
1740 },
1741 diagnostic: DiagnosticColors {
1742 error_fg: theme.diagnostic_error_fg.into(),
1743 error_bg: theme.diagnostic_error_bg.into(),
1744 warning_fg: theme.diagnostic_warning_fg.into(),
1745 warning_bg: theme.diagnostic_warning_bg.into(),
1746 info_fg: theme.diagnostic_info_fg.into(),
1747 info_bg: theme.diagnostic_info_bg.into(),
1748 hint_fg: theme.diagnostic_hint_fg.into(),
1749 hint_bg: theme.diagnostic_hint_bg.into(),
1750 },
1751 syntax: SyntaxColors {
1752 keyword: theme.syntax_keyword.into(),
1753 string: theme.syntax_string.into(),
1754 comment: theme.syntax_comment.into(),
1755 function: theme.syntax_function.into(),
1756 type_: theme.syntax_type.into(),
1757 variable: theme.syntax_variable.into(),
1758 constant: theme.syntax_constant.into(),
1759 operator: theme.syntax_operator.into(),
1760 punctuation_bracket: theme.syntax_punctuation_bracket.into(),
1761 punctuation_delimiter: theme.syntax_punctuation_delimiter.into(),
1762 },
1763 }
1764 }
1765}
1766
1767fn resolve_base_theme(theme_file: &ThemeFile, raw: &serde_json::Value) -> Result<Theme, String> {
1774 if let Some(extends) = theme_file.extends.as_deref() {
1776 let name = extends.strip_prefix("builtin://").unwrap_or(extends);
1777 return Theme::load_builtin(name).ok_or_else(|| {
1778 let available: Vec<&str> = BUILTIN_THEMES.iter().map(|t| t.name).collect();
1779 format!(
1780 "theme `extends: {:?}` does not match any built-in theme. \
1781 Available: {}. \
1782 Inheriting from other user themes is not yet supported.",
1783 extends,
1784 available.join(", ")
1785 )
1786 });
1787 }
1788
1789 if let Some(bg) = raw
1795 .get("editor")
1796 .and_then(|e| e.get("bg"))
1797 .cloned()
1798 .and_then(|v| serde_json::from_value::<ColorDef>(v).ok())
1799 {
1800 let color: Color = bg.into();
1801 if let Some((r, g, b)) = color_to_rgb(color) {
1802 let lum = relative_luminance(r, g, b);
1803 let base_name = if lum > 0.5 { THEME_LIGHT } else { THEME_DARK };
1804 if let Some(base) = Theme::load_builtin(base_name) {
1805 return Ok(base);
1806 }
1807 }
1808 }
1809
1810 Ok(theme_file.clone().into())
1812}
1813
1814fn relative_luminance(r: u8, g: u8, b: u8) -> f64 {
1817 0.2126 * (r as f64 / 255.0) + 0.7152 * (g as f64 / 255.0) + 0.0722 * (b as f64 / 255.0)
1818}
1819
1820fn apply_theme_overrides(theme: &mut Theme, theme_file: &ThemeFile, raw: &serde_json::Value) {
1825 theme.name = theme_file.name.clone();
1827
1828 for section in ["editor", "ui", "search", "diagnostic", "syntax"] {
1829 let Some(obj) = raw.get(section).and_then(|v| v.as_object()) else {
1830 continue;
1831 };
1832 for (field, value) in obj {
1833 if value.is_null() {
1836 continue;
1837 }
1838 let key = format!("{}.{}", section, field);
1839 if let Ok(color_def) = serde_json::from_value::<ColorDef>(value.clone()) {
1840 if let Some(slot) = theme.resolve_theme_key_mut(&key) {
1841 *slot = color_def.into();
1842 }
1843 }
1844 }
1845 }
1846}
1847
1848impl Theme {
1849 pub fn is_light(&self) -> bool {
1855 color_to_rgb(self.editor_bg)
1856 .map(|(r, g, b)| relative_luminance(r, g, b) > 0.5)
1857 .unwrap_or(false)
1858 }
1859
1860 pub fn load_builtin(name: &str) -> Option<Self> {
1862 BUILTIN_THEMES
1863 .iter()
1864 .find(|t| t.name == name)
1865 .and_then(|t| serde_json::from_str::<ThemeFile>(t.json).ok())
1866 .map(|tf| tf.into())
1867 }
1868
1869 pub fn from_json(json: &str) -> Result<Self, String> {
1879 let raw: serde_json::Value =
1884 serde_json::from_str(json).map_err(|e| format!("Failed to parse theme JSON: {}", e))?;
1885 let theme_file: ThemeFile = serde_json::from_value(raw.clone())
1886 .map_err(|e| format!("Failed to parse theme: {}", e))?;
1887
1888 let mut theme = resolve_base_theme(&theme_file, &raw)?;
1889 apply_theme_overrides(&mut theme, &theme_file, &raw);
1890 Ok(theme)
1891 }
1892
1893 pub fn modifier_for_bg_key(&self, key: &str) -> Modifier {
1902 match key {
1903 "editor.selection_bg" => self.selection_modifier,
1904 "ui.semantic_highlight_bg" => self.semantic_highlight_modifier,
1905 _ => Modifier::empty(),
1906 }
1907 }
1908
1909 pub fn resolve_theme_key(&self, key: &str) -> Option<Color> {
1920 let parts: Vec<&str> = key.split('.').collect();
1922 if parts.len() != 2 {
1923 return None;
1924 }
1925
1926 let (section, field) = (parts[0], parts[1]);
1927
1928 match section {
1929 "editor" => match field {
1930 "bg" => Some(self.editor_bg),
1931 "fg" => Some(self.editor_fg),
1932 "cursor" => Some(self.cursor),
1933 "inactive_cursor" => Some(self.inactive_cursor),
1934 "selection_bg" => Some(self.selection_bg),
1935 "current_line_bg" => Some(self.current_line_bg),
1936 "line_number_fg" => Some(self.line_number_fg),
1937 "line_number_bg" => Some(self.line_number_bg),
1938 "diff_add_bg" => Some(self.diff_add_bg),
1939 "diff_remove_bg" => Some(self.diff_remove_bg),
1940 "diff_modify_bg" => Some(self.diff_modify_bg),
1941 "diff_add_fg" => self.diff_add_fg,
1947 "diff_remove_fg" => self.diff_remove_fg,
1948 "diff_modify_fg" => self.diff_modify_fg,
1949 "ruler_bg" => Some(self.ruler_bg),
1950 "whitespace_indicator_fg" => Some(self.whitespace_indicator_fg),
1951 _ => None,
1952 },
1953 "ui" => match field {
1954 "tab_active_fg" => Some(self.tab_active_fg),
1955 "tab_active_bg" => Some(self.tab_active_bg),
1956 "tab_inactive_fg" => Some(self.tab_inactive_fg),
1957 "tab_inactive_bg" => Some(self.tab_inactive_bg),
1958 "status_bar_fg" => Some(self.status_bar_fg),
1959 "status_bar_bg" => Some(self.status_bar_bg),
1960 "status_palette_fg" => Some(self.status_palette_fg),
1961 "status_palette_bg" => Some(self.status_palette_bg),
1962 "status_lsp_on_fg" => Some(self.status_lsp_on_fg),
1963 "status_lsp_on_bg" => Some(self.status_lsp_on_bg),
1964 "status_lsp_actionable_fg" => Some(self.status_lsp_actionable_fg),
1965 "status_lsp_actionable_bg" => Some(self.status_lsp_actionable_bg),
1966 "prompt_fg" => Some(self.prompt_fg),
1967 "prompt_bg" => Some(self.prompt_bg),
1968 "prompt_selection_fg" => Some(self.prompt_selection_fg),
1969 "prompt_selection_bg" => Some(self.prompt_selection_bg),
1970 "popup_bg" => Some(self.popup_bg),
1971 "popup_border_fg" => Some(self.popup_border_fg),
1972 "popup_selection_bg" => Some(self.popup_selection_bg),
1973 "popup_selection_fg" => Some(self.popup_selection_fg),
1974 "popup_text_fg" => Some(self.popup_text_fg),
1975 "text_input_selection_bg" => Some(self.text_input_selection_bg),
1976 "menu_bg" => Some(self.menu_bg),
1977 "menu_fg" => Some(self.menu_fg),
1978 "menu_active_bg" => Some(self.menu_active_bg),
1979 "menu_active_fg" => Some(self.menu_active_fg),
1980 "menu_disabled_fg" => Some(self.menu_disabled_fg),
1981 "menu_disabled_bg" => Some(self.menu_disabled_bg),
1982 "help_bg" => Some(self.help_bg),
1983 "help_fg" => Some(self.help_fg),
1984 "help_key_fg" => Some(self.help_key_fg),
1985 "split_separator_fg" => Some(self.split_separator_fg),
1986 "scrollbar_track_fg" => Some(self.scrollbar_track_fg),
1987 "scrollbar_thumb_fg" => Some(self.scrollbar_thumb_fg),
1988 "scrollbar_track_hover_fg" => Some(self.scrollbar_track_hover_fg),
1989 "scrollbar_thumb_hover_fg" => Some(self.scrollbar_thumb_hover_fg),
1990 "semantic_highlight_bg" => Some(self.semantic_highlight_bg),
1991 "file_status_added_fg" => Some(self.file_status_added_fg),
1992 "file_status_modified_fg" => Some(self.file_status_modified_fg),
1993 "file_status_deleted_fg" => Some(self.file_status_deleted_fg),
1994 "file_status_renamed_fg" => Some(self.file_status_renamed_fg),
1995 "file_status_untracked_fg" => Some(self.file_status_untracked_fg),
1996 "file_status_conflicted_fg" => Some(self.file_status_conflicted_fg),
1997 _ => None,
1998 },
1999 "syntax" => match field {
2000 "keyword" => Some(self.syntax_keyword),
2001 "string" => Some(self.syntax_string),
2002 "comment" => Some(self.syntax_comment),
2003 "function" => Some(self.syntax_function),
2004 "type" => Some(self.syntax_type),
2005 "variable" => Some(self.syntax_variable),
2006 "constant" => Some(self.syntax_constant),
2007 "operator" => Some(self.syntax_operator),
2008 "punctuation_bracket" => Some(self.syntax_punctuation_bracket),
2009 "punctuation_delimiter" => Some(self.syntax_punctuation_delimiter),
2010 _ => None,
2011 },
2012 "diagnostic" => match field {
2013 "error_fg" => Some(self.diagnostic_error_fg),
2014 "error_bg" => Some(self.diagnostic_error_bg),
2015 "warning_fg" => Some(self.diagnostic_warning_fg),
2016 "warning_bg" => Some(self.diagnostic_warning_bg),
2017 "info_fg" => Some(self.diagnostic_info_fg),
2018 "info_bg" => Some(self.diagnostic_info_bg),
2019 "hint_fg" => Some(self.diagnostic_hint_fg),
2020 "hint_bg" => Some(self.diagnostic_hint_bg),
2021 _ => None,
2022 },
2023 "search" => match field {
2024 "match_bg" => Some(self.search_match_bg),
2025 "match_fg" => Some(self.search_match_fg),
2026 "label_bg" => Some(self.search_label_bg),
2027 "label_fg" => Some(self.search_label_fg),
2028 _ => None,
2029 },
2030 _ => None,
2031 }
2032 }
2033
2034 pub fn resolve_theme_key_mut(&mut self, key: &str) -> Option<&mut Color> {
2038 let parts: Vec<&str> = key.split('.').collect();
2039 if parts.len() != 2 {
2040 return None;
2041 }
2042 let (section, field) = (parts[0], parts[1]);
2043 match section {
2044 "editor" => match field {
2045 "bg" => Some(&mut self.editor_bg),
2046 "fg" => Some(&mut self.editor_fg),
2047 "cursor" => Some(&mut self.cursor),
2048 "inactive_cursor" => Some(&mut self.inactive_cursor),
2049 "selection_bg" => Some(&mut self.selection_bg),
2050 "current_line_bg" => Some(&mut self.current_line_bg),
2051 "line_number_fg" => Some(&mut self.line_number_fg),
2052 "line_number_bg" => Some(&mut self.line_number_bg),
2053 "diff_add_bg" => Some(&mut self.diff_add_bg),
2054 "diff_remove_bg" => Some(&mut self.diff_remove_bg),
2055 "diff_modify_bg" => Some(&mut self.diff_modify_bg),
2056 "diff_add_fg" => self.diff_add_fg.as_mut(),
2064 "diff_remove_fg" => self.diff_remove_fg.as_mut(),
2065 "diff_modify_fg" => self.diff_modify_fg.as_mut(),
2066 "ruler_bg" => Some(&mut self.ruler_bg),
2067 "whitespace_indicator_fg" => Some(&mut self.whitespace_indicator_fg),
2068 _ => None,
2069 },
2070 "ui" => match field {
2071 "tab_active_fg" => Some(&mut self.tab_active_fg),
2072 "tab_active_bg" => Some(&mut self.tab_active_bg),
2073 "tab_inactive_fg" => Some(&mut self.tab_inactive_fg),
2074 "tab_inactive_bg" => Some(&mut self.tab_inactive_bg),
2075 "status_bar_fg" => Some(&mut self.status_bar_fg),
2076 "status_bar_bg" => Some(&mut self.status_bar_bg),
2077 "status_palette_fg" => Some(&mut self.status_palette_fg),
2078 "status_palette_bg" => Some(&mut self.status_palette_bg),
2079 "status_lsp_on_fg" => Some(&mut self.status_lsp_on_fg),
2080 "status_lsp_on_bg" => Some(&mut self.status_lsp_on_bg),
2081 "status_lsp_actionable_fg" => Some(&mut self.status_lsp_actionable_fg),
2082 "status_lsp_actionable_bg" => Some(&mut self.status_lsp_actionable_bg),
2083 "prompt_fg" => Some(&mut self.prompt_fg),
2084 "prompt_bg" => Some(&mut self.prompt_bg),
2085 "prompt_selection_fg" => Some(&mut self.prompt_selection_fg),
2086 "prompt_selection_bg" => Some(&mut self.prompt_selection_bg),
2087 "popup_bg" => Some(&mut self.popup_bg),
2088 "popup_border_fg" => Some(&mut self.popup_border_fg),
2089 "popup_selection_bg" => Some(&mut self.popup_selection_bg),
2090 "popup_selection_fg" => Some(&mut self.popup_selection_fg),
2091 "popup_text_fg" => Some(&mut self.popup_text_fg),
2092 "text_input_selection_bg" => Some(&mut self.text_input_selection_bg),
2093 "menu_bg" => Some(&mut self.menu_bg),
2094 "menu_fg" => Some(&mut self.menu_fg),
2095 "menu_active_bg" => Some(&mut self.menu_active_bg),
2096 "menu_active_fg" => Some(&mut self.menu_active_fg),
2097 "menu_disabled_fg" => Some(&mut self.menu_disabled_fg),
2098 "menu_disabled_bg" => Some(&mut self.menu_disabled_bg),
2099 "help_bg" => Some(&mut self.help_bg),
2100 "help_fg" => Some(&mut self.help_fg),
2101 "help_key_fg" => Some(&mut self.help_key_fg),
2102 "split_separator_fg" => Some(&mut self.split_separator_fg),
2103 "scrollbar_track_fg" => Some(&mut self.scrollbar_track_fg),
2104 "scrollbar_thumb_fg" => Some(&mut self.scrollbar_thumb_fg),
2105 "scrollbar_track_hover_fg" => Some(&mut self.scrollbar_track_hover_fg),
2106 "scrollbar_thumb_hover_fg" => Some(&mut self.scrollbar_thumb_hover_fg),
2107 "semantic_highlight_bg" => Some(&mut self.semantic_highlight_bg),
2108 "file_status_added_fg" => Some(&mut self.file_status_added_fg),
2109 "file_status_modified_fg" => Some(&mut self.file_status_modified_fg),
2110 "file_status_deleted_fg" => Some(&mut self.file_status_deleted_fg),
2111 "file_status_renamed_fg" => Some(&mut self.file_status_renamed_fg),
2112 "file_status_untracked_fg" => Some(&mut self.file_status_untracked_fg),
2113 "file_status_conflicted_fg" => Some(&mut self.file_status_conflicted_fg),
2114 _ => None,
2115 },
2116 "syntax" => match field {
2117 "keyword" => Some(&mut self.syntax_keyword),
2118 "string" => Some(&mut self.syntax_string),
2119 "comment" => Some(&mut self.syntax_comment),
2120 "function" => Some(&mut self.syntax_function),
2121 "type" => Some(&mut self.syntax_type),
2122 "variable" => Some(&mut self.syntax_variable),
2123 "constant" => Some(&mut self.syntax_constant),
2124 "operator" => Some(&mut self.syntax_operator),
2125 "punctuation_bracket" => Some(&mut self.syntax_punctuation_bracket),
2126 "punctuation_delimiter" => Some(&mut self.syntax_punctuation_delimiter),
2127 _ => None,
2128 },
2129 "diagnostic" => match field {
2130 "error_fg" => Some(&mut self.diagnostic_error_fg),
2131 "error_bg" => Some(&mut self.diagnostic_error_bg),
2132 "warning_fg" => Some(&mut self.diagnostic_warning_fg),
2133 "warning_bg" => Some(&mut self.diagnostic_warning_bg),
2134 "info_fg" => Some(&mut self.diagnostic_info_fg),
2135 "info_bg" => Some(&mut self.diagnostic_info_bg),
2136 "hint_fg" => Some(&mut self.diagnostic_hint_fg),
2137 "hint_bg" => Some(&mut self.diagnostic_hint_bg),
2138 _ => None,
2139 },
2140 "search" => match field {
2141 "match_bg" => Some(&mut self.search_match_bg),
2142 "match_fg" => Some(&mut self.search_match_fg),
2143 "label_bg" => Some(&mut self.search_label_bg),
2144 "label_fg" => Some(&mut self.search_label_fg),
2145 _ => None,
2146 },
2147 _ => None,
2148 }
2149 }
2150
2151 pub fn override_colors<I, K>(&mut self, overrides: I) -> usize
2156 where
2157 I: IntoIterator<Item = (K, Color)>,
2158 K: AsRef<str>,
2159 {
2160 let mut applied = 0;
2161 for (key, color) in overrides {
2162 if let Some(slot) = self.resolve_theme_key_mut(key.as_ref()) {
2163 *slot = color;
2164 applied += 1;
2165 }
2166 }
2167 applied
2168 }
2169}
2170
2171pub fn get_theme_schema() -> serde_json::Value {
2179 use schemars::schema_for;
2180 let schema = schema_for!(ThemeFile);
2181 serde_json::to_value(&schema).unwrap_or_default()
2182}
2183
2184pub fn get_builtin_themes() -> serde_json::Value {
2186 let mut map = serde_json::Map::new();
2187 for theme in BUILTIN_THEMES {
2188 map.insert(
2189 theme.name.to_string(),
2190 serde_json::Value::String(theme.json.to_string()),
2191 );
2192 }
2193 serde_json::Value::Object(map)
2194}
2195
2196#[cfg(test)]
2197mod tests {
2198 use super::*;
2199
2200 #[test]
2201 fn test_load_builtin_theme() {
2202 let dark = Theme::load_builtin(THEME_DARK).expect("Dark theme must exist");
2203 assert_eq!(dark.name, THEME_DARK);
2204
2205 let light = Theme::load_builtin(THEME_LIGHT).expect("Light theme must exist");
2206 assert_eq!(light.name, THEME_LIGHT);
2207
2208 let high_contrast =
2209 Theme::load_builtin(THEME_HIGH_CONTRAST).expect("High contrast theme must exist");
2210 assert_eq!(high_contrast.name, THEME_HIGH_CONTRAST);
2211
2212 let terminal = Theme::load_builtin(THEME_TERMINAL).expect("Terminal theme must exist");
2213 assert_eq!(terminal.name, THEME_TERMINAL);
2214 assert_eq!(terminal.editor_bg, Color::Reset);
2218 assert_eq!(terminal.editor_fg, Color::Reset);
2219 assert_eq!(terminal.terminal_bg, Color::Reset);
2220 assert!(terminal.selection_modifier.contains(Modifier::REVERSED));
2223 assert!(terminal
2224 .semantic_highlight_modifier
2225 .contains(Modifier::BOLD));
2226 }
2227
2228 #[test]
2229 fn test_modifier_def_round_trip() {
2230 let cases = [
2231 (vec!["reversed"], Modifier::REVERSED),
2232 (
2233 vec!["bold", "underlined"],
2234 Modifier::BOLD | Modifier::UNDERLINED,
2235 ),
2236 (vec!["italic", "dim"], Modifier::ITALIC | Modifier::DIM),
2237 (vec!["reverse"], Modifier::REVERSED), (vec!["underline"], Modifier::UNDERLINED), ];
2240 for (strs, expected) in cases {
2241 let def = ModifierDef(strs.iter().map(|s| s.to_string()).collect());
2242 let m: Modifier = (&def).into();
2243 assert_eq!(m, expected, "ModifierDef({:?}) -> Modifier", strs);
2244 }
2245 }
2246
2247 #[test]
2248 fn test_modifier_def_unknown_strings_are_dropped() {
2249 let def = ModifierDef(vec!["reversed".into(), "wibble".into(), "bold".into()]);
2252 let m: Modifier = (&def).into();
2253 assert_eq!(m, Modifier::REVERSED | Modifier::BOLD);
2254 }
2255
2256 #[test]
2257 fn test_themes_without_modifier_default_to_empty() {
2258 let dark = Theme::load_builtin(THEME_DARK).expect("Dark theme must exist");
2263 assert!(dark.selection_modifier.is_empty());
2264 assert!(dark.semantic_highlight_modifier.is_empty());
2265 }
2266
2267 #[test]
2268 fn test_modifier_for_bg_key_lookup() {
2269 let terminal = Theme::load_builtin(THEME_TERMINAL).expect("Terminal theme must exist");
2270 assert!(terminal
2273 .modifier_for_bg_key("editor.selection_bg")
2274 .contains(Modifier::REVERSED));
2275 assert!(terminal
2276 .modifier_for_bg_key("ui.semantic_highlight_bg")
2277 .contains(Modifier::BOLD));
2278 assert!(terminal
2281 .modifier_for_bg_key("ui.popup_selection_bg")
2282 .is_empty());
2283 assert!(terminal.modifier_for_bg_key("nonsense").is_empty());
2284 }
2285
2286 #[test]
2287 fn test_modifier_round_trip_via_theme_file() {
2288 let original = Theme::load_builtin(THEME_TERMINAL).expect("Terminal theme must exist");
2290 let file: ThemeFile = original.clone().into();
2291 let json = serde_json::to_string(&file).expect("serialize");
2292 let parsed: ThemeFile = serde_json::from_str(&json).expect("parse");
2293 let round_tripped: Theme = parsed.into();
2294 assert_eq!(
2295 round_tripped.selection_modifier,
2296 original.selection_modifier
2297 );
2298 assert_eq!(
2299 round_tripped.semantic_highlight_modifier,
2300 original.semantic_highlight_modifier
2301 );
2302 }
2303
2304 #[test]
2305 fn test_builtin_themes_match_schema() {
2306 for theme in BUILTIN_THEMES {
2307 let _: ThemeFile = serde_json::from_str(theme.json)
2308 .unwrap_or_else(|_| panic!("Theme '{}' does not match schema", theme.name));
2309 }
2310 }
2311
2312 #[test]
2313 fn test_from_json() {
2314 let json = r#"{"name":"test","editor":{},"ui":{},"search":{},"diagnostic":{},"syntax":{}}"#;
2315 let theme = Theme::from_json(json).expect("Should parse minimal theme");
2316 assert_eq!(theme.name, "test");
2317 }
2318
2319 #[test]
2331 fn test_minimal_user_theme_from_issue_1281_loads() {
2332 let json = r#"{
2334 "name": "gruvbox-light-orange",
2335 "editor": {
2336 "bg": [251, 241, 199],
2337 "fg": [60, 56, 54],
2338 "cursor": [254, 128, 25],
2339 "selection_bg": [213, 196, 161]
2340 },
2341 "syntax": {
2342 "keyword": [175, 58, 3],
2343 "string": [152, 151, 26],
2344 "comment": [146, 131, 116]
2345 }
2346}"#;
2347 let theme = Theme::from_json(json)
2348 .expect("Theme from issue #1281 should parse without `ui`/`search`/`diagnostic`");
2349 assert_eq!(theme.name, "gruvbox-light-orange");
2350
2351 assert_eq!(theme.editor_bg, Color::Rgb(251, 241, 199));
2353 assert_eq!(theme.editor_fg, Color::Rgb(60, 56, 54));
2354 assert_eq!(theme.cursor, Color::Rgb(254, 128, 25));
2355 assert_eq!(theme.selection_bg, Color::Rgb(213, 196, 161));
2356 assert_eq!(theme.syntax_keyword, Color::Rgb(175, 58, 3));
2357 assert_eq!(theme.syntax_string, Color::Rgb(152, 151, 26));
2358 assert_eq!(theme.syntax_comment, Color::Rgb(146, 131, 116));
2359
2360 let light = Theme::load_builtin(THEME_LIGHT).expect("light builtin");
2364 assert_eq!(
2365 theme.status_bar_fg, light.status_bar_fg,
2366 "ui.status_bar_fg should inherit from builtin://light when bg is bright"
2367 );
2368 assert_eq!(
2369 theme.diagnostic_error_fg, light.diagnostic_error_fg,
2370 "diagnostic.error_fg should inherit from builtin://light when bg is bright"
2371 );
2372 assert_eq!(
2373 theme.menu_bg, light.menu_bg,
2374 "ui.menu_bg should inherit from builtin://light when bg is bright"
2375 );
2376 }
2377
2378 #[test]
2381 fn test_extends_explicit_builtin_wins_over_auto_infer() {
2382 let json = r#"{
2385 "name": "explicit-light",
2386 "extends": "builtin://light",
2387 "editor": { "bg": [0, 0, 0] }
2388 }"#;
2389 let theme = Theme::from_json(json).expect("extends should resolve");
2390 let light = Theme::load_builtin(THEME_LIGHT).expect("light builtin");
2391
2392 assert_eq!(theme.editor_bg, Color::Rgb(0, 0, 0));
2394 assert_eq!(theme.menu_bg, light.menu_bg);
2396 assert_eq!(theme.tab_active_bg, light.tab_active_bg);
2397 assert_eq!(theme.diagnostic_warning_fg, light.diagnostic_warning_fg);
2398 }
2399
2400 #[test]
2405 fn test_extends_bare_builtin_name_works() {
2406 let json = r#"{ "name": "x", "extends": "high-contrast" }"#;
2407 let theme = Theme::from_json(json).expect("bare-name extends should resolve");
2408 let hc = Theme::load_builtin("high-contrast").expect("hc builtin");
2409 assert_eq!(theme.menu_bg, hc.menu_bg);
2410 }
2411
2412 #[test]
2417 fn test_extends_unknown_builtin_errors_with_helpful_message() {
2418 let json = r#"{ "name": "x", "extends": "builtin://no-such-theme" }"#;
2419 let err = Theme::from_json(json).expect_err("unknown extends must error");
2420 assert!(
2421 err.contains("no-such-theme"),
2422 "error should quote the bad value, got: {}",
2423 err
2424 );
2425 assert!(
2426 err.contains("dark") && err.contains("light"),
2427 "error should list available builtins, got: {}",
2428 err
2429 );
2430 }
2431
2432 #[test]
2436 fn test_auto_infer_dark_base_from_dark_bg() {
2437 let json = r#"{ "name": "x", "editor": { "bg": [20, 20, 30] } }"#;
2438 let theme = Theme::from_json(json).expect("should parse");
2439 let dark = Theme::load_builtin(THEME_DARK).expect("dark builtin");
2440 assert_eq!(theme.menu_bg, dark.menu_bg);
2441 assert_eq!(theme.diagnostic_error_fg, dark.diagnostic_error_fg);
2442 }
2443
2444 #[test]
2448 fn test_no_inheritance_signal_uses_hardcoded_defaults() {
2449 let json = r#"{ "name": "x" }"#;
2450 let theme = Theme::from_json(json).expect("should parse");
2451 assert_eq!(theme.editor_bg, Color::Rgb(30, 30, 30));
2454 }
2455
2456 #[test]
2460 fn test_theme_without_name_still_errors() {
2461 let json = r#"{ "editor": {} }"#;
2462 let err = Theme::from_json(json).expect_err("missing `name` must be an error");
2463 assert!(
2464 err.contains("name"),
2465 "error should mention the missing `name` field, got: {}",
2466 err
2467 );
2468 }
2469
2470 #[test]
2475 fn test_extends_overrides_compose_field_by_field() {
2476 let json = r#"{
2477 "name": "dark-with-pink-cursor",
2478 "extends": "builtin://dark",
2479 "editor": { "cursor": [255, 105, 180] }
2480 }"#;
2481 let theme = Theme::from_json(json).expect("should parse");
2482 let dark = Theme::load_builtin(THEME_DARK).expect("dark builtin");
2483
2484 assert_eq!(theme.cursor, Color::Rgb(255, 105, 180));
2486 assert_eq!(theme.editor_bg, dark.editor_bg);
2488 assert_eq!(theme.editor_fg, dark.editor_fg);
2489 assert_eq!(theme.selection_bg, dark.selection_bg);
2490 assert_eq!(theme.menu_bg, dark.menu_bg);
2492 assert_eq!(theme.syntax_keyword, dark.syntax_keyword);
2493 }
2494
2495 #[test]
2496 fn test_default_reset_color() {
2497 let color: Color = ColorDef::Named("Default".to_string()).into();
2499 assert_eq!(color, Color::Reset);
2500
2501 let color: Color = ColorDef::Named("Reset".to_string()).into();
2503 assert_eq!(color, Color::Reset);
2504 }
2505
2506 #[test]
2507 fn test_file_status_colors_fall_back_to_diagnostic_colors() {
2508 let json = r#"{
2510 "name": "test-fallback",
2511 "editor": {},
2512 "ui": {},
2513 "search": {},
2514 "diagnostic": {
2515 "error_fg": [220, 50, 47],
2516 "warning_fg": [181, 137, 0],
2517 "info_fg": [38, 139, 210],
2518 "hint_fg": [101, 123, 131]
2519 },
2520 "syntax": {}
2521 }"#;
2522 let theme = Theme::from_json(json).expect("Should parse theme without file_status keys");
2523
2524 assert_eq!(theme.file_status_added_fg, Color::Rgb(38, 139, 210));
2526 assert_eq!(theme.file_status_renamed_fg, Color::Rgb(38, 139, 210));
2527 assert_eq!(theme.file_status_modified_fg, Color::Rgb(181, 137, 0));
2529 assert_eq!(theme.file_status_deleted_fg, Color::Rgb(220, 50, 47));
2531 assert_eq!(theme.file_status_conflicted_fg, Color::Rgb(220, 50, 47));
2532 assert_eq!(theme.file_status_untracked_fg, Color::Rgb(101, 123, 131));
2534 }
2535
2536 #[test]
2537 fn test_file_status_colors_explicit_override() {
2538 let json = r#"{
2540 "name": "test-override",
2541 "editor": {},
2542 "ui": {
2543 "file_status_added_fg": [80, 250, 123],
2544 "file_status_modified_fg": [255, 184, 108]
2545 },
2546 "search": {},
2547 "diagnostic": {
2548 "info_fg": [38, 139, 210],
2549 "warning_fg": [181, 137, 0]
2550 },
2551 "syntax": {}
2552 }"#;
2553 let theme = Theme::from_json(json).expect("Should parse theme with file_status overrides");
2554
2555 assert_eq!(theme.file_status_added_fg, Color::Rgb(80, 250, 123));
2557 assert_eq!(theme.file_status_modified_fg, Color::Rgb(255, 184, 108));
2558 assert_eq!(theme.file_status_renamed_fg, Color::Rgb(38, 139, 210));
2560 }
2561
2562 #[test]
2563 fn test_file_status_colors_resolve_via_theme_key() {
2564 let json = r#"{
2565 "name": "test-resolve",
2566 "editor": {},
2567 "ui": {
2568 "file_status_added_fg": [80, 250, 123]
2569 },
2570 "search": {},
2571 "diagnostic": {
2572 "warning_fg": [181, 137, 0]
2573 },
2574 "syntax": {}
2575 }"#;
2576 let theme = Theme::from_json(json).expect("Should parse theme");
2577
2578 assert_eq!(
2580 theme.resolve_theme_key("ui.file_status_added_fg"),
2581 Some(Color::Rgb(80, 250, 123))
2582 );
2583 assert_eq!(
2584 theme.resolve_theme_key("ui.file_status_modified_fg"),
2585 Some(Color::Rgb(181, 137, 0))
2586 );
2587 }
2588
2589 #[test]
2590 fn override_colors_writes_known_keys_and_drops_unknowns() {
2591 let mut theme = Theme::load_builtin(THEME_DARK).expect("dark builtin");
2592 let applied = theme.override_colors([
2593 ("editor.bg".to_string(), Color::Rgb(10, 20, 30)),
2594 ("ui.status_bar_fg".to_string(), Color::Rgb(1, 2, 3)),
2595 ("does.not_exist".to_string(), Color::Rgb(9, 9, 9)),
2596 ("garbage_no_dot".to_string(), Color::Rgb(9, 9, 9)),
2597 ]);
2598 assert_eq!(applied, 2, "only the two valid keys should be applied");
2599 assert_eq!(
2600 theme.resolve_theme_key("editor.bg"),
2601 Some(Color::Rgb(10, 20, 30))
2602 );
2603 assert_eq!(
2604 theme.resolve_theme_key("ui.status_bar_fg"),
2605 Some(Color::Rgb(1, 2, 3))
2606 );
2607 }
2608
2609 #[test]
2610 fn resolve_theme_key_mut_matches_resolve_theme_key_domain() {
2611 let mut theme = Theme::load_builtin(THEME_DARK).expect("dark builtin");
2614 let probe = [
2615 "editor.bg",
2616 "editor.fg",
2617 "ui.status_bar_fg",
2618 "ui.tab_active_bg",
2619 "syntax.keyword",
2620 "diagnostic.error_fg",
2621 "search.match_bg",
2622 ];
2623 for key in probe {
2624 assert!(
2625 theme.resolve_theme_key(key).is_some(),
2626 "reader lost key {key}"
2627 );
2628 assert!(
2629 theme.resolve_theme_key_mut(key).is_some(),
2630 "mutator missing key {key}"
2631 );
2632 }
2633 }
2634
2635 #[test]
2636 fn test_all_builtin_themes_set_prominent_palette_indicator() {
2637 for builtin in BUILTIN_THEMES {
2644 let theme = Theme::from_json(builtin.json)
2645 .unwrap_or_else(|e| panic!("Theme '{}' failed to parse: {}", builtin.name, e));
2646 assert!(
2647 theme.status_palette_fg != theme.status_bar_fg
2648 || theme.status_palette_bg != theme.status_bar_bg,
2649 "Theme '{}' must set status_palette_fg/bg to a prominent \
2650 accent distinct from status_bar_fg/bg",
2651 builtin.name
2652 );
2653 }
2654 }
2655
2656 #[test]
2657 fn test_all_builtin_themes_have_file_status_colors() {
2658 for builtin in BUILTIN_THEMES {
2660 let theme = Theme::from_json(builtin.json)
2661 .unwrap_or_else(|e| panic!("Theme '{}' failed to parse: {}", builtin.name, e));
2662
2663 for key in &[
2665 "ui.file_status_added_fg",
2666 "ui.file_status_modified_fg",
2667 "ui.file_status_deleted_fg",
2668 "ui.file_status_renamed_fg",
2669 "ui.file_status_untracked_fg",
2670 "ui.file_status_conflicted_fg",
2671 ] {
2672 assert!(
2673 theme.resolve_theme_key(key).is_some(),
2674 "Theme '{}' missing resolution for '{}'",
2675 builtin.name,
2676 key
2677 );
2678 }
2679 }
2680 }
2681}