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_popup_selection_fg")]
709 pub popup_selection_fg: ColorDef,
710 #[serde(default = "default_popup_text_fg")]
712 pub popup_text_fg: ColorDef,
713 #[serde(default = "default_suggestion_bg")]
715 pub suggestion_bg: ColorDef,
716 #[serde(default = "default_suggestion_selected_bg")]
718 pub suggestion_selected_bg: ColorDef,
719 #[serde(default = "default_help_bg")]
721 pub help_bg: ColorDef,
722 #[serde(default = "default_help_fg")]
724 pub help_fg: ColorDef,
725 #[serde(default = "default_help_key_fg")]
727 pub help_key_fg: ColorDef,
728 #[serde(default = "default_help_separator_fg")]
730 pub help_separator_fg: ColorDef,
731 #[serde(default = "default_help_indicator_fg")]
733 pub help_indicator_fg: ColorDef,
734 #[serde(default = "default_help_indicator_bg")]
736 pub help_indicator_bg: ColorDef,
737 #[serde(default = "default_inline_code_bg")]
739 pub inline_code_bg: ColorDef,
740 #[serde(default = "default_split_separator_fg")]
742 pub split_separator_fg: ColorDef,
743 #[serde(default = "default_split_separator_hover_fg")]
745 pub split_separator_hover_fg: ColorDef,
746 #[serde(default = "default_scrollbar_track_fg")]
748 pub scrollbar_track_fg: ColorDef,
749 #[serde(default = "default_scrollbar_thumb_fg")]
751 pub scrollbar_thumb_fg: ColorDef,
752 #[serde(default = "default_scrollbar_track_hover_fg")]
754 pub scrollbar_track_hover_fg: ColorDef,
755 #[serde(default = "default_scrollbar_thumb_hover_fg")]
757 pub scrollbar_thumb_hover_fg: ColorDef,
758 #[serde(default = "default_compose_margin_bg")]
760 pub compose_margin_bg: ColorDef,
761 #[serde(default = "default_semantic_highlight_bg")]
763 pub semantic_highlight_bg: ColorDef,
764 #[serde(default)]
771 pub semantic_highlight_modifier: Option<ModifierDef>,
772 #[serde(default = "default_terminal_bg")]
774 pub terminal_bg: ColorDef,
775 #[serde(default = "default_terminal_fg")]
777 pub terminal_fg: ColorDef,
778 #[serde(default = "default_status_warning_indicator_bg")]
780 pub status_warning_indicator_bg: ColorDef,
781 #[serde(default = "default_status_warning_indicator_fg")]
783 pub status_warning_indicator_fg: ColorDef,
784 #[serde(default = "default_status_error_indicator_bg")]
786 pub status_error_indicator_bg: ColorDef,
787 #[serde(default = "default_status_error_indicator_fg")]
789 pub status_error_indicator_fg: ColorDef,
790 #[serde(default = "default_status_warning_indicator_hover_bg")]
792 pub status_warning_indicator_hover_bg: ColorDef,
793 #[serde(default = "default_status_warning_indicator_hover_fg")]
795 pub status_warning_indicator_hover_fg: ColorDef,
796 #[serde(default = "default_status_error_indicator_hover_bg")]
798 pub status_error_indicator_hover_bg: ColorDef,
799 #[serde(default = "default_status_error_indicator_hover_fg")]
801 pub status_error_indicator_hover_fg: ColorDef,
802 #[serde(default = "default_tab_drop_zone_bg")]
804 pub tab_drop_zone_bg: ColorDef,
805 #[serde(default = "default_tab_drop_zone_border")]
807 pub tab_drop_zone_border: ColorDef,
808 #[serde(default = "default_settings_selected_bg")]
810 pub settings_selected_bg: ColorDef,
811 #[serde(default = "default_settings_selected_fg")]
813 pub settings_selected_fg: ColorDef,
814 #[serde(default)]
816 pub file_status_added_fg: Option<ColorDef>,
817 #[serde(default)]
819 pub file_status_modified_fg: Option<ColorDef>,
820 #[serde(default)]
822 pub file_status_deleted_fg: Option<ColorDef>,
823 #[serde(default)]
825 pub file_status_renamed_fg: Option<ColorDef>,
826 #[serde(default)]
828 pub file_status_untracked_fg: Option<ColorDef>,
829 #[serde(default)]
831 pub file_status_conflicted_fg: Option<ColorDef>,
832}
833
834fn default_tab_active_fg() -> ColorDef {
837 ColorDef::Named("Yellow".to_string())
838}
839fn default_tab_active_bg() -> ColorDef {
840 ColorDef::Named("Blue".to_string())
841}
842fn default_tab_inactive_fg() -> ColorDef {
843 ColorDef::Named("White".to_string())
844}
845fn default_tab_inactive_bg() -> ColorDef {
846 ColorDef::Named("DarkGray".to_string())
847}
848fn default_tab_separator_bg() -> ColorDef {
849 ColorDef::Named("Black".to_string())
850}
851fn default_tab_close_hover_fg() -> ColorDef {
852 ColorDef::Rgb(255, 100, 100) }
854fn default_tab_hover_bg() -> ColorDef {
855 ColorDef::Rgb(70, 70, 75) }
857
858fn default_menu_bg() -> ColorDef {
860 ColorDef::Rgb(60, 60, 65)
861}
862fn default_menu_fg() -> ColorDef {
863 ColorDef::Rgb(220, 220, 220)
864}
865fn default_menu_active_bg() -> ColorDef {
866 ColorDef::Rgb(60, 60, 60)
867}
868fn default_menu_active_fg() -> ColorDef {
869 ColorDef::Rgb(255, 255, 255)
870}
871fn default_menu_dropdown_bg() -> ColorDef {
872 ColorDef::Rgb(50, 50, 50)
873}
874fn default_menu_dropdown_fg() -> ColorDef {
875 ColorDef::Rgb(220, 220, 220)
876}
877fn default_menu_highlight_bg() -> ColorDef {
878 ColorDef::Rgb(70, 130, 180)
879}
880fn default_menu_highlight_fg() -> ColorDef {
881 ColorDef::Rgb(255, 255, 255)
882}
883fn default_menu_border_fg() -> ColorDef {
884 ColorDef::Rgb(100, 100, 100)
885}
886fn default_menu_separator_fg() -> ColorDef {
887 ColorDef::Rgb(80, 80, 80)
888}
889fn default_menu_hover_bg() -> ColorDef {
890 ColorDef::Rgb(55, 55, 55)
891}
892fn default_menu_hover_fg() -> ColorDef {
893 ColorDef::Rgb(255, 255, 255)
894}
895fn default_menu_disabled_fg() -> ColorDef {
896 ColorDef::Rgb(100, 100, 100) }
898fn default_menu_disabled_bg() -> ColorDef {
899 ColorDef::Rgb(50, 50, 50) }
901fn default_status_bar_fg() -> ColorDef {
903 ColorDef::Named("White".to_string())
904}
905fn default_status_bar_bg() -> ColorDef {
906 ColorDef::Named("DarkGray".to_string())
907}
908
909fn default_prompt_fg() -> ColorDef {
911 ColorDef::Named("White".to_string())
912}
913fn default_prompt_bg() -> ColorDef {
914 ColorDef::Named("Black".to_string())
915}
916fn default_prompt_selection_fg() -> ColorDef {
917 ColorDef::Named("White".to_string())
918}
919fn default_prompt_selection_bg() -> ColorDef {
920 ColorDef::Rgb(58, 79, 120)
921}
922
923fn default_popup_border_fg() -> ColorDef {
925 ColorDef::Named("Gray".to_string())
926}
927fn default_popup_bg() -> ColorDef {
928 ColorDef::Rgb(30, 30, 30)
929}
930fn default_popup_selection_bg() -> ColorDef {
931 ColorDef::Rgb(58, 79, 120)
932}
933fn default_popup_selection_fg() -> ColorDef {
934 ColorDef::Rgb(255, 255, 255) }
936fn default_popup_text_fg() -> ColorDef {
937 ColorDef::Named("White".to_string())
938}
939
940fn default_suggestion_bg() -> ColorDef {
942 ColorDef::Rgb(30, 30, 30)
943}
944fn default_suggestion_selected_bg() -> ColorDef {
945 ColorDef::Rgb(58, 79, 120)
946}
947
948fn default_help_bg() -> ColorDef {
950 ColorDef::Named("Black".to_string())
951}
952fn default_help_fg() -> ColorDef {
953 ColorDef::Named("White".to_string())
954}
955fn default_help_key_fg() -> ColorDef {
956 ColorDef::Named("Cyan".to_string())
957}
958fn default_help_separator_fg() -> ColorDef {
959 ColorDef::Named("DarkGray".to_string())
960}
961fn default_help_indicator_fg() -> ColorDef {
962 ColorDef::Named("Red".to_string())
963}
964fn default_help_indicator_bg() -> ColorDef {
965 ColorDef::Named("Black".to_string())
966}
967
968fn default_inline_code_bg() -> ColorDef {
969 ColorDef::Named("DarkGray".to_string())
970}
971
972fn default_split_separator_fg() -> ColorDef {
974 ColorDef::Rgb(100, 100, 100)
975}
976fn default_split_separator_hover_fg() -> ColorDef {
977 ColorDef::Rgb(100, 149, 237) }
979fn default_scrollbar_track_fg() -> ColorDef {
980 ColorDef::Named("DarkGray".to_string())
981}
982fn default_scrollbar_thumb_fg() -> ColorDef {
983 ColorDef::Named("Gray".to_string())
984}
985fn default_scrollbar_track_hover_fg() -> ColorDef {
986 ColorDef::Named("Gray".to_string())
987}
988fn default_scrollbar_thumb_hover_fg() -> ColorDef {
989 ColorDef::Named("White".to_string())
990}
991fn default_compose_margin_bg() -> ColorDef {
992 ColorDef::Rgb(18, 18, 18) }
994fn default_semantic_highlight_bg() -> ColorDef {
995 ColorDef::Rgb(60, 60, 80) }
997fn default_terminal_bg() -> ColorDef {
998 ColorDef::Named("Default".to_string()) }
1000fn default_terminal_fg() -> ColorDef {
1001 ColorDef::Named("Default".to_string()) }
1003fn default_status_warning_indicator_bg() -> ColorDef {
1004 ColorDef::Rgb(181, 137, 0) }
1006fn default_status_warning_indicator_fg() -> ColorDef {
1007 ColorDef::Rgb(0, 0, 0) }
1009fn default_status_error_indicator_bg() -> ColorDef {
1010 ColorDef::Rgb(220, 50, 47) }
1012fn default_status_error_indicator_fg() -> ColorDef {
1013 ColorDef::Rgb(255, 255, 255) }
1015fn default_status_warning_indicator_hover_bg() -> ColorDef {
1016 ColorDef::Rgb(211, 167, 30) }
1018fn default_status_warning_indicator_hover_fg() -> ColorDef {
1019 ColorDef::Rgb(0, 0, 0) }
1021fn default_status_error_indicator_hover_bg() -> ColorDef {
1022 ColorDef::Rgb(250, 80, 77) }
1024fn default_status_error_indicator_hover_fg() -> ColorDef {
1025 ColorDef::Rgb(255, 255, 255) }
1027fn default_tab_drop_zone_bg() -> ColorDef {
1028 ColorDef::Rgb(70, 130, 180) }
1030fn default_tab_drop_zone_border() -> ColorDef {
1031 ColorDef::Rgb(100, 149, 237) }
1033fn default_settings_selected_bg() -> ColorDef {
1034 ColorDef::Rgb(60, 60, 70) }
1036fn default_settings_selected_fg() -> ColorDef {
1037 ColorDef::Rgb(255, 255, 255) }
1039#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1041pub struct SearchColors {
1042 #[serde(default = "default_search_match_bg")]
1044 pub match_bg: ColorDef,
1045 #[serde(default = "default_search_match_fg")]
1047 pub match_fg: ColorDef,
1048 #[serde(default = "default_search_label_bg")]
1052 pub label_bg: ColorDef,
1053 #[serde(default = "default_search_label_fg")]
1057 pub label_fg: ColorDef,
1058}
1059
1060fn default_search_match_bg() -> ColorDef {
1062 ColorDef::Rgb(100, 100, 20)
1063}
1064fn default_search_match_fg() -> ColorDef {
1065 ColorDef::Rgb(255, 255, 255)
1066}
1067fn default_search_label_bg() -> ColorDef {
1072 ColorDef::Rgb(199, 78, 189)
1073}
1074fn default_search_label_fg() -> ColorDef {
1075 ColorDef::Rgb(255, 255, 255)
1076}
1077
1078#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1080pub struct DiagnosticColors {
1081 #[serde(default = "default_diagnostic_error_fg")]
1083 pub error_fg: ColorDef,
1084 #[serde(default = "default_diagnostic_error_bg")]
1086 pub error_bg: ColorDef,
1087 #[serde(default = "default_diagnostic_warning_fg")]
1089 pub warning_fg: ColorDef,
1090 #[serde(default = "default_diagnostic_warning_bg")]
1092 pub warning_bg: ColorDef,
1093 #[serde(default = "default_diagnostic_info_fg")]
1095 pub info_fg: ColorDef,
1096 #[serde(default = "default_diagnostic_info_bg")]
1098 pub info_bg: ColorDef,
1099 #[serde(default = "default_diagnostic_hint_fg")]
1101 pub hint_fg: ColorDef,
1102 #[serde(default = "default_diagnostic_hint_bg")]
1104 pub hint_bg: ColorDef,
1105}
1106
1107fn default_diagnostic_error_fg() -> ColorDef {
1109 ColorDef::Named("Red".to_string())
1110}
1111fn default_diagnostic_error_bg() -> ColorDef {
1112 ColorDef::Rgb(60, 20, 20)
1113}
1114fn default_diagnostic_warning_fg() -> ColorDef {
1115 ColorDef::Named("Yellow".to_string())
1116}
1117fn default_diagnostic_warning_bg() -> ColorDef {
1118 ColorDef::Rgb(60, 50, 0)
1119}
1120fn default_diagnostic_info_fg() -> ColorDef {
1121 ColorDef::Named("Blue".to_string())
1122}
1123fn default_diagnostic_info_bg() -> ColorDef {
1124 ColorDef::Rgb(0, 30, 60)
1125}
1126fn default_diagnostic_hint_fg() -> ColorDef {
1127 ColorDef::Named("Gray".to_string())
1128}
1129fn default_diagnostic_hint_bg() -> ColorDef {
1130 ColorDef::Rgb(30, 30, 30)
1131}
1132
1133#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1135pub struct SyntaxColors {
1136 #[serde(default = "default_syntax_keyword")]
1138 pub keyword: ColorDef,
1139 #[serde(default = "default_syntax_string")]
1141 pub string: ColorDef,
1142 #[serde(default = "default_syntax_comment")]
1144 pub comment: ColorDef,
1145 #[serde(default = "default_syntax_function")]
1147 pub function: ColorDef,
1148 #[serde(rename = "type", default = "default_syntax_type")]
1150 pub type_: ColorDef,
1151 #[serde(default = "default_syntax_variable")]
1153 pub variable: ColorDef,
1154 #[serde(default = "default_syntax_constant")]
1156 pub constant: ColorDef,
1157 #[serde(default = "default_syntax_operator")]
1159 pub operator: ColorDef,
1160 #[serde(default = "default_syntax_punctuation_bracket")]
1162 pub punctuation_bracket: ColorDef,
1163 #[serde(default = "default_syntax_punctuation_delimiter")]
1165 pub punctuation_delimiter: ColorDef,
1166}
1167
1168fn default_syntax_keyword() -> ColorDef {
1170 ColorDef::Rgb(86, 156, 214)
1171}
1172fn default_syntax_string() -> ColorDef {
1173 ColorDef::Rgb(206, 145, 120)
1174}
1175fn default_syntax_comment() -> ColorDef {
1176 ColorDef::Rgb(106, 153, 85)
1177}
1178fn default_syntax_function() -> ColorDef {
1179 ColorDef::Rgb(220, 220, 170)
1180}
1181fn default_syntax_type() -> ColorDef {
1182 ColorDef::Rgb(78, 201, 176)
1183}
1184fn default_syntax_variable() -> ColorDef {
1185 ColorDef::Rgb(156, 220, 254)
1186}
1187fn default_syntax_constant() -> ColorDef {
1188 ColorDef::Rgb(79, 193, 255)
1189}
1190fn default_syntax_operator() -> ColorDef {
1191 ColorDef::Rgb(212, 212, 212)
1192}
1193fn default_syntax_punctuation_bracket() -> ColorDef {
1194 ColorDef::Rgb(212, 212, 212) }
1196fn default_syntax_punctuation_delimiter() -> ColorDef {
1197 ColorDef::Rgb(212, 212, 212) }
1199
1200#[derive(Debug, Clone)]
1202pub struct Theme {
1203 pub name: String,
1205
1206 pub editor_bg: Color,
1208 pub editor_fg: Color,
1209 pub cursor: Color,
1210 pub inactive_cursor: Color,
1211 pub selection_bg: Color,
1212 pub selection_modifier: Modifier,
1217 pub current_line_bg: Color,
1218 pub line_number_fg: Color,
1219 pub line_number_bg: Color,
1220
1221 pub after_eof_bg: Color,
1223
1224 pub ruler_bg: Color,
1226
1227 pub whitespace_indicator_fg: Color,
1229
1230 pub diff_add_bg: Color,
1232 pub diff_remove_bg: Color,
1233 pub diff_modify_bg: Color,
1234 pub diff_add_highlight_bg: Color,
1236 pub diff_remove_highlight_bg: Color,
1238 pub diff_add_fg: Option<Color>,
1249 pub diff_remove_fg: Option<Color>,
1250 pub diff_modify_fg: Option<Color>,
1251
1252 pub tab_active_fg: Color,
1254 pub tab_active_bg: Color,
1255 pub tab_inactive_fg: Color,
1256 pub tab_inactive_bg: Color,
1257 pub tab_separator_bg: Color,
1258 pub tab_close_hover_fg: Color,
1259 pub tab_hover_bg: Color,
1260
1261 pub menu_bg: Color,
1263 pub menu_fg: Color,
1264 pub menu_active_bg: Color,
1265 pub menu_active_fg: Color,
1266 pub menu_dropdown_bg: Color,
1267 pub menu_dropdown_fg: Color,
1268 pub menu_highlight_bg: Color,
1269 pub menu_highlight_fg: Color,
1270 pub menu_border_fg: Color,
1271 pub menu_separator_fg: Color,
1272 pub menu_hover_bg: Color,
1273 pub menu_hover_fg: Color,
1274 pub menu_disabled_fg: Color,
1275 pub menu_disabled_bg: Color,
1276
1277 pub status_bar_fg: Color,
1278 pub status_bar_bg: Color,
1279 pub status_palette_fg: Color,
1281 pub status_palette_bg: Color,
1282 pub status_lsp_on_fg: Color,
1284 pub status_lsp_on_bg: Color,
1285 pub status_lsp_actionable_fg: Color,
1288 pub status_lsp_actionable_bg: Color,
1289 pub prompt_fg: Color,
1290 pub prompt_bg: Color,
1291 pub prompt_selection_fg: Color,
1292 pub prompt_selection_bg: Color,
1293
1294 pub popup_border_fg: Color,
1295 pub popup_bg: Color,
1296 pub popup_selection_bg: Color,
1297 pub popup_selection_fg: Color,
1298 pub popup_text_fg: Color,
1299
1300 pub suggestion_bg: Color,
1301 pub suggestion_selected_bg: Color,
1302
1303 pub help_bg: Color,
1304 pub help_fg: Color,
1305 pub help_key_fg: Color,
1306 pub help_separator_fg: Color,
1307
1308 pub help_indicator_fg: Color,
1309 pub help_indicator_bg: Color,
1310
1311 pub inline_code_bg: Color,
1313
1314 pub split_separator_fg: Color,
1315 pub split_separator_hover_fg: Color,
1316
1317 pub scrollbar_track_fg: Color,
1319 pub scrollbar_thumb_fg: Color,
1320 pub scrollbar_track_hover_fg: Color,
1321 pub scrollbar_thumb_hover_fg: Color,
1322
1323 pub compose_margin_bg: Color,
1325
1326 pub semantic_highlight_bg: Color,
1328 pub semantic_highlight_modifier: Modifier,
1333
1334 pub terminal_bg: Color,
1336 pub terminal_fg: Color,
1337
1338 pub status_warning_indicator_bg: Color,
1340 pub status_warning_indicator_fg: Color,
1341 pub status_error_indicator_bg: Color,
1342 pub status_error_indicator_fg: Color,
1343 pub status_warning_indicator_hover_bg: Color,
1344 pub status_warning_indicator_hover_fg: Color,
1345 pub status_error_indicator_hover_bg: Color,
1346 pub status_error_indicator_hover_fg: Color,
1347
1348 pub tab_drop_zone_bg: Color,
1350 pub tab_drop_zone_border: Color,
1351
1352 pub settings_selected_bg: Color,
1354 pub settings_selected_fg: Color,
1355
1356 pub file_status_added_fg: Color,
1358 pub file_status_modified_fg: Color,
1359 pub file_status_deleted_fg: Color,
1360 pub file_status_renamed_fg: Color,
1361 pub file_status_untracked_fg: Color,
1362 pub file_status_conflicted_fg: Color,
1363
1364 pub search_match_bg: Color,
1366 pub search_match_fg: Color,
1367 pub search_label_bg: Color,
1368 pub search_label_fg: Color,
1369
1370 pub diagnostic_error_fg: Color,
1372 pub diagnostic_error_bg: Color,
1373 pub diagnostic_warning_fg: Color,
1374 pub diagnostic_warning_bg: Color,
1375 pub diagnostic_info_fg: Color,
1376 pub diagnostic_info_bg: Color,
1377 pub diagnostic_hint_fg: Color,
1378 pub diagnostic_hint_bg: Color,
1379
1380 pub syntax_keyword: Color,
1382 pub syntax_string: Color,
1383 pub syntax_comment: Color,
1384 pub syntax_function: Color,
1385 pub syntax_type: Color,
1386 pub syntax_variable: Color,
1387 pub syntax_constant: Color,
1388 pub syntax_operator: Color,
1389 pub syntax_punctuation_bracket: Color,
1390 pub syntax_punctuation_delimiter: Color,
1391}
1392
1393impl From<ThemeFile> for Theme {
1394 fn from(file: ThemeFile) -> Self {
1395 Self {
1396 name: file.name,
1397 editor_bg: file.editor.bg.clone().into(),
1398 editor_fg: file.editor.fg.into(),
1399 cursor: file.editor.cursor.into(),
1400 inactive_cursor: file.editor.inactive_cursor.into(),
1401 selection_bg: file.editor.selection_bg.into(),
1402 selection_modifier: file
1403 .editor
1404 .selection_modifier
1405 .as_ref()
1406 .map(Modifier::from)
1407 .unwrap_or(Modifier::empty()),
1408 current_line_bg: file.editor.current_line_bg.into(),
1409 line_number_fg: file.editor.line_number_fg.into(),
1410 line_number_bg: file.editor.line_number_bg.into(),
1411 after_eof_bg: file
1414 .editor
1415 .after_eof_bg
1416 .clone()
1417 .map(|c| c.into())
1418 .unwrap_or_else(|| shade_toward_contrast(file.editor.bg.clone().into(), 10)),
1419 ruler_bg: file.editor.ruler_bg.into(),
1420 whitespace_indicator_fg: file.editor.whitespace_indicator_fg.into(),
1421 diff_add_bg: file.editor.diff_add_bg.clone().into(),
1422 diff_remove_bg: file.editor.diff_remove_bg.clone().into(),
1423 diff_modify_bg: file.editor.diff_modify_bg.into(),
1424 diff_add_highlight_bg: file
1426 .editor
1427 .diff_add_highlight_bg
1428 .map(|c| c.into())
1429 .unwrap_or_else(|| brighten_color(file.editor.diff_add_bg.into(), 40)),
1430 diff_remove_highlight_bg: file
1431 .editor
1432 .diff_remove_highlight_bg
1433 .map(|c| c.into())
1434 .unwrap_or_else(|| brighten_color(file.editor.diff_remove_bg.into(), 40)),
1435 diff_add_fg: file.editor.diff_add_fg.clone().map(|c| c.into()),
1436 diff_remove_fg: file.editor.diff_remove_fg.clone().map(|c| c.into()),
1437 diff_modify_fg: file.editor.diff_modify_fg.clone().map(|c| c.into()),
1438 tab_active_fg: file.ui.tab_active_fg.into(),
1439 tab_active_bg: file.ui.tab_active_bg.into(),
1440 tab_inactive_fg: file.ui.tab_inactive_fg.into(),
1441 tab_inactive_bg: file.ui.tab_inactive_bg.into(),
1442 tab_separator_bg: file.ui.tab_separator_bg.into(),
1443 tab_close_hover_fg: file.ui.tab_close_hover_fg.into(),
1444 tab_hover_bg: file.ui.tab_hover_bg.into(),
1445 menu_bg: file.ui.menu_bg.into(),
1446 menu_fg: file.ui.menu_fg.into(),
1447 menu_active_bg: file.ui.menu_active_bg.into(),
1448 menu_active_fg: file.ui.menu_active_fg.into(),
1449 menu_dropdown_bg: file.ui.menu_dropdown_bg.into(),
1450 menu_dropdown_fg: file.ui.menu_dropdown_fg.into(),
1451 menu_highlight_bg: file.ui.menu_highlight_bg.into(),
1452 menu_highlight_fg: file.ui.menu_highlight_fg.into(),
1453 menu_border_fg: file.ui.menu_border_fg.into(),
1454 menu_separator_fg: file.ui.menu_separator_fg.into(),
1455 menu_hover_bg: file.ui.menu_hover_bg.into(),
1456 menu_hover_fg: file.ui.menu_hover_fg.into(),
1457 menu_disabled_fg: file.ui.menu_disabled_fg.into(),
1458 menu_disabled_bg: file.ui.menu_disabled_bg.into(),
1459 status_bar_fg: file.ui.status_bar_fg.clone().into(),
1460 status_bar_bg: file.ui.status_bar_bg.clone().into(),
1461 status_palette_fg: file
1462 .ui
1463 .status_palette_fg
1464 .clone()
1465 .map(|c| c.into())
1466 .unwrap_or_else(|| file.ui.status_bar_fg.clone().into()),
1467 status_palette_bg: file
1468 .ui
1469 .status_palette_bg
1470 .clone()
1471 .map(|c| c.into())
1472 .unwrap_or_else(|| file.ui.status_bar_bg.clone().into()),
1473 status_lsp_on_fg: file
1474 .ui
1475 .status_lsp_on_fg
1476 .clone()
1477 .map(|c| c.into())
1478 .unwrap_or_else(|| file.ui.status_bar_fg.clone().into()),
1479 status_lsp_on_bg: file
1480 .ui
1481 .status_lsp_on_bg
1482 .clone()
1483 .map(|c| c.into())
1484 .unwrap_or_else(|| file.ui.status_bar_bg.clone().into()),
1485 status_lsp_actionable_fg: file
1486 .ui
1487 .status_lsp_actionable_fg
1488 .clone()
1489 .map(|c| c.into())
1490 .unwrap_or_else(|| file.ui.status_warning_indicator_fg.clone().into()),
1491 status_lsp_actionable_bg: file
1492 .ui
1493 .status_lsp_actionable_bg
1494 .clone()
1495 .map(|c| c.into())
1496 .unwrap_or_else(|| file.ui.status_warning_indicator_bg.clone().into()),
1497 prompt_fg: file.ui.prompt_fg.into(),
1498 prompt_bg: file.ui.prompt_bg.into(),
1499 prompt_selection_fg: file.ui.prompt_selection_fg.into(),
1500 prompt_selection_bg: file.ui.prompt_selection_bg.into(),
1501 popup_border_fg: file.ui.popup_border_fg.into(),
1502 popup_bg: file.ui.popup_bg.into(),
1503 popup_selection_bg: file.ui.popup_selection_bg.into(),
1504 popup_selection_fg: file.ui.popup_selection_fg.into(),
1505 popup_text_fg: file.ui.popup_text_fg.into(),
1506 suggestion_bg: file.ui.suggestion_bg.into(),
1507 suggestion_selected_bg: file.ui.suggestion_selected_bg.into(),
1508 help_bg: file.ui.help_bg.into(),
1509 help_fg: file.ui.help_fg.into(),
1510 help_key_fg: file.ui.help_key_fg.into(),
1511 help_separator_fg: file.ui.help_separator_fg.into(),
1512 help_indicator_fg: file.ui.help_indicator_fg.into(),
1513 help_indicator_bg: file.ui.help_indicator_bg.into(),
1514 inline_code_bg: file.ui.inline_code_bg.into(),
1515 split_separator_fg: file.ui.split_separator_fg.into(),
1516 split_separator_hover_fg: file.ui.split_separator_hover_fg.into(),
1517 scrollbar_track_fg: file.ui.scrollbar_track_fg.into(),
1518 scrollbar_thumb_fg: file.ui.scrollbar_thumb_fg.into(),
1519 scrollbar_track_hover_fg: file.ui.scrollbar_track_hover_fg.into(),
1520 scrollbar_thumb_hover_fg: file.ui.scrollbar_thumb_hover_fg.into(),
1521 compose_margin_bg: file.ui.compose_margin_bg.into(),
1522 semantic_highlight_bg: file.ui.semantic_highlight_bg.into(),
1523 semantic_highlight_modifier: file
1524 .ui
1525 .semantic_highlight_modifier
1526 .as_ref()
1527 .map(Modifier::from)
1528 .unwrap_or(Modifier::empty()),
1529 terminal_bg: file.ui.terminal_bg.into(),
1530 terminal_fg: file.ui.terminal_fg.into(),
1531 status_warning_indicator_bg: file.ui.status_warning_indicator_bg.into(),
1532 status_warning_indicator_fg: file.ui.status_warning_indicator_fg.into(),
1533 status_error_indicator_bg: file.ui.status_error_indicator_bg.into(),
1534 status_error_indicator_fg: file.ui.status_error_indicator_fg.into(),
1535 status_warning_indicator_hover_bg: file.ui.status_warning_indicator_hover_bg.into(),
1536 status_warning_indicator_hover_fg: file.ui.status_warning_indicator_hover_fg.into(),
1537 status_error_indicator_hover_bg: file.ui.status_error_indicator_hover_bg.into(),
1538 status_error_indicator_hover_fg: file.ui.status_error_indicator_hover_fg.into(),
1539 tab_drop_zone_bg: file.ui.tab_drop_zone_bg.into(),
1540 tab_drop_zone_border: file.ui.tab_drop_zone_border.into(),
1541 settings_selected_bg: file.ui.settings_selected_bg.into(),
1542 settings_selected_fg: file.ui.settings_selected_fg.into(),
1543 file_status_added_fg: file
1544 .ui
1545 .file_status_added_fg
1546 .map(|c| c.into())
1547 .unwrap_or_else(|| file.diagnostic.info_fg.clone().into()),
1548 file_status_modified_fg: file
1549 .ui
1550 .file_status_modified_fg
1551 .map(|c| c.into())
1552 .unwrap_or_else(|| file.diagnostic.warning_fg.clone().into()),
1553 file_status_deleted_fg: file
1554 .ui
1555 .file_status_deleted_fg
1556 .map(|c| c.into())
1557 .unwrap_or_else(|| file.diagnostic.error_fg.clone().into()),
1558 file_status_renamed_fg: file
1559 .ui
1560 .file_status_renamed_fg
1561 .map(|c| c.into())
1562 .unwrap_or_else(|| file.diagnostic.info_fg.clone().into()),
1563 file_status_untracked_fg: file
1564 .ui
1565 .file_status_untracked_fg
1566 .map(|c| c.into())
1567 .unwrap_or_else(|| file.diagnostic.hint_fg.clone().into()),
1568 file_status_conflicted_fg: file
1569 .ui
1570 .file_status_conflicted_fg
1571 .map(|c| c.into())
1572 .unwrap_or_else(|| file.diagnostic.error_fg.clone().into()),
1573 search_match_bg: file.search.match_bg.into(),
1574 search_match_fg: file.search.match_fg.into(),
1575 search_label_bg: file.search.label_bg.into(),
1576 search_label_fg: file.search.label_fg.into(),
1577 diagnostic_error_fg: file.diagnostic.error_fg.into(),
1578 diagnostic_error_bg: file.diagnostic.error_bg.into(),
1579 diagnostic_warning_fg: file.diagnostic.warning_fg.into(),
1580 diagnostic_warning_bg: file.diagnostic.warning_bg.into(),
1581 diagnostic_info_fg: file.diagnostic.info_fg.into(),
1582 diagnostic_info_bg: file.diagnostic.info_bg.into(),
1583 diagnostic_hint_fg: file.diagnostic.hint_fg.into(),
1584 diagnostic_hint_bg: file.diagnostic.hint_bg.into(),
1585 syntax_keyword: file.syntax.keyword.into(),
1586 syntax_string: file.syntax.string.into(),
1587 syntax_comment: file.syntax.comment.into(),
1588 syntax_function: file.syntax.function.into(),
1589 syntax_type: file.syntax.type_.into(),
1590 syntax_variable: file.syntax.variable.into(),
1591 syntax_constant: file.syntax.constant.into(),
1592 syntax_operator: file.syntax.operator.into(),
1593 syntax_punctuation_bracket: file.syntax.punctuation_bracket.into(),
1594 syntax_punctuation_delimiter: file.syntax.punctuation_delimiter.into(),
1595 }
1596 }
1597}
1598
1599impl From<Theme> for ThemeFile {
1600 fn from(theme: Theme) -> Self {
1601 Self {
1602 name: theme.name,
1603 extends: None,
1606 editor: EditorColors {
1607 bg: theme.editor_bg.into(),
1608 fg: theme.editor_fg.into(),
1609 cursor: theme.cursor.into(),
1610 inactive_cursor: theme.inactive_cursor.into(),
1611 selection_bg: theme.selection_bg.into(),
1612 selection_modifier: if theme.selection_modifier.is_empty() {
1613 None
1614 } else {
1615 Some(theme.selection_modifier.into())
1616 },
1617 current_line_bg: theme.current_line_bg.into(),
1618 line_number_fg: theme.line_number_fg.into(),
1619 line_number_bg: theme.line_number_bg.into(),
1620 diff_add_bg: theme.diff_add_bg.into(),
1621 diff_remove_bg: theme.diff_remove_bg.into(),
1622 diff_add_highlight_bg: Some(theme.diff_add_highlight_bg.into()),
1623 diff_remove_highlight_bg: Some(theme.diff_remove_highlight_bg.into()),
1624 diff_modify_bg: theme.diff_modify_bg.into(),
1625 diff_add_fg: theme.diff_add_fg.map(|c| c.into()),
1626 diff_remove_fg: theme.diff_remove_fg.map(|c| c.into()),
1627 diff_modify_fg: theme.diff_modify_fg.map(|c| c.into()),
1628 ruler_bg: theme.ruler_bg.into(),
1629 whitespace_indicator_fg: theme.whitespace_indicator_fg.into(),
1630 after_eof_bg: Some(theme.after_eof_bg.into()),
1631 },
1632 ui: UiColors {
1633 tab_active_fg: theme.tab_active_fg.into(),
1634 tab_active_bg: theme.tab_active_bg.into(),
1635 tab_inactive_fg: theme.tab_inactive_fg.into(),
1636 tab_inactive_bg: theme.tab_inactive_bg.into(),
1637 tab_separator_bg: theme.tab_separator_bg.into(),
1638 tab_close_hover_fg: theme.tab_close_hover_fg.into(),
1639 tab_hover_bg: theme.tab_hover_bg.into(),
1640 menu_bg: theme.menu_bg.into(),
1641 menu_fg: theme.menu_fg.into(),
1642 menu_active_bg: theme.menu_active_bg.into(),
1643 menu_active_fg: theme.menu_active_fg.into(),
1644 menu_dropdown_bg: theme.menu_dropdown_bg.into(),
1645 menu_dropdown_fg: theme.menu_dropdown_fg.into(),
1646 menu_highlight_bg: theme.menu_highlight_bg.into(),
1647 menu_highlight_fg: theme.menu_highlight_fg.into(),
1648 menu_border_fg: theme.menu_border_fg.into(),
1649 menu_separator_fg: theme.menu_separator_fg.into(),
1650 menu_hover_bg: theme.menu_hover_bg.into(),
1651 menu_hover_fg: theme.menu_hover_fg.into(),
1652 menu_disabled_fg: theme.menu_disabled_fg.into(),
1653 menu_disabled_bg: theme.menu_disabled_bg.into(),
1654 status_bar_fg: theme.status_bar_fg.into(),
1655 status_bar_bg: theme.status_bar_bg.into(),
1656 status_palette_fg: Some(theme.status_palette_fg.into()),
1657 status_palette_bg: Some(theme.status_palette_bg.into()),
1658 status_lsp_on_fg: Some(theme.status_lsp_on_fg.into()),
1659 status_lsp_on_bg: Some(theme.status_lsp_on_bg.into()),
1660 status_lsp_actionable_fg: Some(theme.status_lsp_actionable_fg.into()),
1661 status_lsp_actionable_bg: Some(theme.status_lsp_actionable_bg.into()),
1662 prompt_fg: theme.prompt_fg.into(),
1663 prompt_bg: theme.prompt_bg.into(),
1664 prompt_selection_fg: theme.prompt_selection_fg.into(),
1665 prompt_selection_bg: theme.prompt_selection_bg.into(),
1666 popup_border_fg: theme.popup_border_fg.into(),
1667 popup_bg: theme.popup_bg.into(),
1668 popup_selection_bg: theme.popup_selection_bg.into(),
1669 popup_selection_fg: theme.popup_selection_fg.into(),
1670 popup_text_fg: theme.popup_text_fg.into(),
1671 suggestion_bg: theme.suggestion_bg.into(),
1672 suggestion_selected_bg: theme.suggestion_selected_bg.into(),
1673 help_bg: theme.help_bg.into(),
1674 help_fg: theme.help_fg.into(),
1675 help_key_fg: theme.help_key_fg.into(),
1676 help_separator_fg: theme.help_separator_fg.into(),
1677 help_indicator_fg: theme.help_indicator_fg.into(),
1678 help_indicator_bg: theme.help_indicator_bg.into(),
1679 inline_code_bg: theme.inline_code_bg.into(),
1680 split_separator_fg: theme.split_separator_fg.into(),
1681 split_separator_hover_fg: theme.split_separator_hover_fg.into(),
1682 scrollbar_track_fg: theme.scrollbar_track_fg.into(),
1683 scrollbar_thumb_fg: theme.scrollbar_thumb_fg.into(),
1684 scrollbar_track_hover_fg: theme.scrollbar_track_hover_fg.into(),
1685 scrollbar_thumb_hover_fg: theme.scrollbar_thumb_hover_fg.into(),
1686 compose_margin_bg: theme.compose_margin_bg.into(),
1687 semantic_highlight_bg: theme.semantic_highlight_bg.into(),
1688 semantic_highlight_modifier: if theme.semantic_highlight_modifier.is_empty() {
1689 None
1690 } else {
1691 Some(theme.semantic_highlight_modifier.into())
1692 },
1693 terminal_bg: theme.terminal_bg.into(),
1694 terminal_fg: theme.terminal_fg.into(),
1695 status_warning_indicator_bg: theme.status_warning_indicator_bg.into(),
1696 status_warning_indicator_fg: theme.status_warning_indicator_fg.into(),
1697 status_error_indicator_bg: theme.status_error_indicator_bg.into(),
1698 status_error_indicator_fg: theme.status_error_indicator_fg.into(),
1699 status_warning_indicator_hover_bg: theme.status_warning_indicator_hover_bg.into(),
1700 status_warning_indicator_hover_fg: theme.status_warning_indicator_hover_fg.into(),
1701 status_error_indicator_hover_bg: theme.status_error_indicator_hover_bg.into(),
1702 status_error_indicator_hover_fg: theme.status_error_indicator_hover_fg.into(),
1703 tab_drop_zone_bg: theme.tab_drop_zone_bg.into(),
1704 tab_drop_zone_border: theme.tab_drop_zone_border.into(),
1705 settings_selected_bg: theme.settings_selected_bg.into(),
1706 settings_selected_fg: theme.settings_selected_fg.into(),
1707 file_status_added_fg: Some(theme.file_status_added_fg.into()),
1708 file_status_modified_fg: Some(theme.file_status_modified_fg.into()),
1709 file_status_deleted_fg: Some(theme.file_status_deleted_fg.into()),
1710 file_status_renamed_fg: Some(theme.file_status_renamed_fg.into()),
1711 file_status_untracked_fg: Some(theme.file_status_untracked_fg.into()),
1712 file_status_conflicted_fg: Some(theme.file_status_conflicted_fg.into()),
1713 },
1714 search: SearchColors {
1715 match_bg: theme.search_match_bg.into(),
1716 match_fg: theme.search_match_fg.into(),
1717 label_bg: theme.search_label_bg.into(),
1718 label_fg: theme.search_label_fg.into(),
1719 },
1720 diagnostic: DiagnosticColors {
1721 error_fg: theme.diagnostic_error_fg.into(),
1722 error_bg: theme.diagnostic_error_bg.into(),
1723 warning_fg: theme.diagnostic_warning_fg.into(),
1724 warning_bg: theme.diagnostic_warning_bg.into(),
1725 info_fg: theme.diagnostic_info_fg.into(),
1726 info_bg: theme.diagnostic_info_bg.into(),
1727 hint_fg: theme.diagnostic_hint_fg.into(),
1728 hint_bg: theme.diagnostic_hint_bg.into(),
1729 },
1730 syntax: SyntaxColors {
1731 keyword: theme.syntax_keyword.into(),
1732 string: theme.syntax_string.into(),
1733 comment: theme.syntax_comment.into(),
1734 function: theme.syntax_function.into(),
1735 type_: theme.syntax_type.into(),
1736 variable: theme.syntax_variable.into(),
1737 constant: theme.syntax_constant.into(),
1738 operator: theme.syntax_operator.into(),
1739 punctuation_bracket: theme.syntax_punctuation_bracket.into(),
1740 punctuation_delimiter: theme.syntax_punctuation_delimiter.into(),
1741 },
1742 }
1743 }
1744}
1745
1746fn resolve_base_theme(theme_file: &ThemeFile, raw: &serde_json::Value) -> Result<Theme, String> {
1753 if let Some(extends) = theme_file.extends.as_deref() {
1755 let name = extends.strip_prefix("builtin://").unwrap_or(extends);
1756 return Theme::load_builtin(name).ok_or_else(|| {
1757 let available: Vec<&str> = BUILTIN_THEMES.iter().map(|t| t.name).collect();
1758 format!(
1759 "theme `extends: {:?}` does not match any built-in theme. \
1760 Available: {}. \
1761 Inheriting from other user themes is not yet supported.",
1762 extends,
1763 available.join(", ")
1764 )
1765 });
1766 }
1767
1768 if let Some(bg) = raw
1774 .get("editor")
1775 .and_then(|e| e.get("bg"))
1776 .cloned()
1777 .and_then(|v| serde_json::from_value::<ColorDef>(v).ok())
1778 {
1779 let color: Color = bg.into();
1780 if let Some((r, g, b)) = color_to_rgb(color) {
1781 let lum = relative_luminance(r, g, b);
1782 let base_name = if lum > 0.5 { THEME_LIGHT } else { THEME_DARK };
1783 if let Some(base) = Theme::load_builtin(base_name) {
1784 return Ok(base);
1785 }
1786 }
1787 }
1788
1789 Ok(theme_file.clone().into())
1791}
1792
1793fn relative_luminance(r: u8, g: u8, b: u8) -> f64 {
1796 0.2126 * (r as f64 / 255.0) + 0.7152 * (g as f64 / 255.0) + 0.0722 * (b as f64 / 255.0)
1797}
1798
1799fn apply_theme_overrides(theme: &mut Theme, theme_file: &ThemeFile, raw: &serde_json::Value) {
1804 theme.name = theme_file.name.clone();
1806
1807 for section in ["editor", "ui", "search", "diagnostic", "syntax"] {
1808 let Some(obj) = raw.get(section).and_then(|v| v.as_object()) else {
1809 continue;
1810 };
1811 for (field, value) in obj {
1812 if value.is_null() {
1815 continue;
1816 }
1817 let key = format!("{}.{}", section, field);
1818 if let Ok(color_def) = serde_json::from_value::<ColorDef>(value.clone()) {
1819 if let Some(slot) = theme.resolve_theme_key_mut(&key) {
1820 *slot = color_def.into();
1821 }
1822 }
1823 }
1824 }
1825}
1826
1827impl Theme {
1828 pub fn is_light(&self) -> bool {
1834 color_to_rgb(self.editor_bg)
1835 .map(|(r, g, b)| relative_luminance(r, g, b) > 0.5)
1836 .unwrap_or(false)
1837 }
1838
1839 pub fn load_builtin(name: &str) -> Option<Self> {
1841 BUILTIN_THEMES
1842 .iter()
1843 .find(|t| t.name == name)
1844 .and_then(|t| serde_json::from_str::<ThemeFile>(t.json).ok())
1845 .map(|tf| tf.into())
1846 }
1847
1848 pub fn from_json(json: &str) -> Result<Self, String> {
1858 let raw: serde_json::Value =
1863 serde_json::from_str(json).map_err(|e| format!("Failed to parse theme JSON: {}", e))?;
1864 let theme_file: ThemeFile = serde_json::from_value(raw.clone())
1865 .map_err(|e| format!("Failed to parse theme: {}", e))?;
1866
1867 let mut theme = resolve_base_theme(&theme_file, &raw)?;
1868 apply_theme_overrides(&mut theme, &theme_file, &raw);
1869 Ok(theme)
1870 }
1871
1872 pub fn modifier_for_bg_key(&self, key: &str) -> Modifier {
1881 match key {
1882 "editor.selection_bg" => self.selection_modifier,
1883 "ui.semantic_highlight_bg" => self.semantic_highlight_modifier,
1884 _ => Modifier::empty(),
1885 }
1886 }
1887
1888 pub fn resolve_theme_key(&self, key: &str) -> Option<Color> {
1899 let parts: Vec<&str> = key.split('.').collect();
1901 if parts.len() != 2 {
1902 return None;
1903 }
1904
1905 let (section, field) = (parts[0], parts[1]);
1906
1907 match section {
1908 "editor" => match field {
1909 "bg" => Some(self.editor_bg),
1910 "fg" => Some(self.editor_fg),
1911 "cursor" => Some(self.cursor),
1912 "inactive_cursor" => Some(self.inactive_cursor),
1913 "selection_bg" => Some(self.selection_bg),
1914 "current_line_bg" => Some(self.current_line_bg),
1915 "line_number_fg" => Some(self.line_number_fg),
1916 "line_number_bg" => Some(self.line_number_bg),
1917 "diff_add_bg" => Some(self.diff_add_bg),
1918 "diff_remove_bg" => Some(self.diff_remove_bg),
1919 "diff_modify_bg" => Some(self.diff_modify_bg),
1920 "diff_add_fg" => self.diff_add_fg,
1926 "diff_remove_fg" => self.diff_remove_fg,
1927 "diff_modify_fg" => self.diff_modify_fg,
1928 "ruler_bg" => Some(self.ruler_bg),
1929 "whitespace_indicator_fg" => Some(self.whitespace_indicator_fg),
1930 _ => None,
1931 },
1932 "ui" => match field {
1933 "tab_active_fg" => Some(self.tab_active_fg),
1934 "tab_active_bg" => Some(self.tab_active_bg),
1935 "tab_inactive_fg" => Some(self.tab_inactive_fg),
1936 "tab_inactive_bg" => Some(self.tab_inactive_bg),
1937 "status_bar_fg" => Some(self.status_bar_fg),
1938 "status_bar_bg" => Some(self.status_bar_bg),
1939 "status_palette_fg" => Some(self.status_palette_fg),
1940 "status_palette_bg" => Some(self.status_palette_bg),
1941 "status_lsp_on_fg" => Some(self.status_lsp_on_fg),
1942 "status_lsp_on_bg" => Some(self.status_lsp_on_bg),
1943 "status_lsp_actionable_fg" => Some(self.status_lsp_actionable_fg),
1944 "status_lsp_actionable_bg" => Some(self.status_lsp_actionable_bg),
1945 "prompt_fg" => Some(self.prompt_fg),
1946 "prompt_bg" => Some(self.prompt_bg),
1947 "prompt_selection_fg" => Some(self.prompt_selection_fg),
1948 "prompt_selection_bg" => Some(self.prompt_selection_bg),
1949 "popup_bg" => Some(self.popup_bg),
1950 "popup_border_fg" => Some(self.popup_border_fg),
1951 "popup_selection_bg" => Some(self.popup_selection_bg),
1952 "popup_selection_fg" => Some(self.popup_selection_fg),
1953 "popup_text_fg" => Some(self.popup_text_fg),
1954 "menu_bg" => Some(self.menu_bg),
1955 "menu_fg" => Some(self.menu_fg),
1956 "menu_active_bg" => Some(self.menu_active_bg),
1957 "menu_active_fg" => Some(self.menu_active_fg),
1958 "help_bg" => Some(self.help_bg),
1959 "help_fg" => Some(self.help_fg),
1960 "help_key_fg" => Some(self.help_key_fg),
1961 "split_separator_fg" => Some(self.split_separator_fg),
1962 "scrollbar_track_fg" => Some(self.scrollbar_track_fg),
1963 "scrollbar_thumb_fg" => Some(self.scrollbar_thumb_fg),
1964 "scrollbar_track_hover_fg" => Some(self.scrollbar_track_hover_fg),
1965 "scrollbar_thumb_hover_fg" => Some(self.scrollbar_thumb_hover_fg),
1966 "semantic_highlight_bg" => Some(self.semantic_highlight_bg),
1967 "file_status_added_fg" => Some(self.file_status_added_fg),
1968 "file_status_modified_fg" => Some(self.file_status_modified_fg),
1969 "file_status_deleted_fg" => Some(self.file_status_deleted_fg),
1970 "file_status_renamed_fg" => Some(self.file_status_renamed_fg),
1971 "file_status_untracked_fg" => Some(self.file_status_untracked_fg),
1972 "file_status_conflicted_fg" => Some(self.file_status_conflicted_fg),
1973 _ => None,
1974 },
1975 "syntax" => match field {
1976 "keyword" => Some(self.syntax_keyword),
1977 "string" => Some(self.syntax_string),
1978 "comment" => Some(self.syntax_comment),
1979 "function" => Some(self.syntax_function),
1980 "type" => Some(self.syntax_type),
1981 "variable" => Some(self.syntax_variable),
1982 "constant" => Some(self.syntax_constant),
1983 "operator" => Some(self.syntax_operator),
1984 "punctuation_bracket" => Some(self.syntax_punctuation_bracket),
1985 "punctuation_delimiter" => Some(self.syntax_punctuation_delimiter),
1986 _ => None,
1987 },
1988 "diagnostic" => match field {
1989 "error_fg" => Some(self.diagnostic_error_fg),
1990 "error_bg" => Some(self.diagnostic_error_bg),
1991 "warning_fg" => Some(self.diagnostic_warning_fg),
1992 "warning_bg" => Some(self.diagnostic_warning_bg),
1993 "info_fg" => Some(self.diagnostic_info_fg),
1994 "info_bg" => Some(self.diagnostic_info_bg),
1995 "hint_fg" => Some(self.diagnostic_hint_fg),
1996 "hint_bg" => Some(self.diagnostic_hint_bg),
1997 _ => None,
1998 },
1999 "search" => match field {
2000 "match_bg" => Some(self.search_match_bg),
2001 "match_fg" => Some(self.search_match_fg),
2002 "label_bg" => Some(self.search_label_bg),
2003 "label_fg" => Some(self.search_label_fg),
2004 _ => None,
2005 },
2006 _ => None,
2007 }
2008 }
2009
2010 pub fn resolve_theme_key_mut(&mut self, key: &str) -> Option<&mut Color> {
2014 let parts: Vec<&str> = key.split('.').collect();
2015 if parts.len() != 2 {
2016 return None;
2017 }
2018 let (section, field) = (parts[0], parts[1]);
2019 match section {
2020 "editor" => match field {
2021 "bg" => Some(&mut self.editor_bg),
2022 "fg" => Some(&mut self.editor_fg),
2023 "cursor" => Some(&mut self.cursor),
2024 "inactive_cursor" => Some(&mut self.inactive_cursor),
2025 "selection_bg" => Some(&mut self.selection_bg),
2026 "current_line_bg" => Some(&mut self.current_line_bg),
2027 "line_number_fg" => Some(&mut self.line_number_fg),
2028 "line_number_bg" => Some(&mut self.line_number_bg),
2029 "diff_add_bg" => Some(&mut self.diff_add_bg),
2030 "diff_remove_bg" => Some(&mut self.diff_remove_bg),
2031 "diff_modify_bg" => Some(&mut self.diff_modify_bg),
2032 "diff_add_fg" => self.diff_add_fg.as_mut(),
2040 "diff_remove_fg" => self.diff_remove_fg.as_mut(),
2041 "diff_modify_fg" => self.diff_modify_fg.as_mut(),
2042 "ruler_bg" => Some(&mut self.ruler_bg),
2043 "whitespace_indicator_fg" => Some(&mut self.whitespace_indicator_fg),
2044 _ => None,
2045 },
2046 "ui" => match field {
2047 "tab_active_fg" => Some(&mut self.tab_active_fg),
2048 "tab_active_bg" => Some(&mut self.tab_active_bg),
2049 "tab_inactive_fg" => Some(&mut self.tab_inactive_fg),
2050 "tab_inactive_bg" => Some(&mut self.tab_inactive_bg),
2051 "status_bar_fg" => Some(&mut self.status_bar_fg),
2052 "status_bar_bg" => Some(&mut self.status_bar_bg),
2053 "status_palette_fg" => Some(&mut self.status_palette_fg),
2054 "status_palette_bg" => Some(&mut self.status_palette_bg),
2055 "status_lsp_on_fg" => Some(&mut self.status_lsp_on_fg),
2056 "status_lsp_on_bg" => Some(&mut self.status_lsp_on_bg),
2057 "status_lsp_actionable_fg" => Some(&mut self.status_lsp_actionable_fg),
2058 "status_lsp_actionable_bg" => Some(&mut self.status_lsp_actionable_bg),
2059 "prompt_fg" => Some(&mut self.prompt_fg),
2060 "prompt_bg" => Some(&mut self.prompt_bg),
2061 "prompt_selection_fg" => Some(&mut self.prompt_selection_fg),
2062 "prompt_selection_bg" => Some(&mut self.prompt_selection_bg),
2063 "popup_bg" => Some(&mut self.popup_bg),
2064 "popup_border_fg" => Some(&mut self.popup_border_fg),
2065 "popup_selection_bg" => Some(&mut self.popup_selection_bg),
2066 "popup_selection_fg" => Some(&mut self.popup_selection_fg),
2067 "popup_text_fg" => Some(&mut self.popup_text_fg),
2068 "menu_bg" => Some(&mut self.menu_bg),
2069 "menu_fg" => Some(&mut self.menu_fg),
2070 "menu_active_bg" => Some(&mut self.menu_active_bg),
2071 "menu_active_fg" => Some(&mut self.menu_active_fg),
2072 "help_bg" => Some(&mut self.help_bg),
2073 "help_fg" => Some(&mut self.help_fg),
2074 "help_key_fg" => Some(&mut self.help_key_fg),
2075 "split_separator_fg" => Some(&mut self.split_separator_fg),
2076 "scrollbar_track_fg" => Some(&mut self.scrollbar_track_fg),
2077 "scrollbar_thumb_fg" => Some(&mut self.scrollbar_thumb_fg),
2078 "scrollbar_track_hover_fg" => Some(&mut self.scrollbar_track_hover_fg),
2079 "scrollbar_thumb_hover_fg" => Some(&mut self.scrollbar_thumb_hover_fg),
2080 "semantic_highlight_bg" => Some(&mut self.semantic_highlight_bg),
2081 "file_status_added_fg" => Some(&mut self.file_status_added_fg),
2082 "file_status_modified_fg" => Some(&mut self.file_status_modified_fg),
2083 "file_status_deleted_fg" => Some(&mut self.file_status_deleted_fg),
2084 "file_status_renamed_fg" => Some(&mut self.file_status_renamed_fg),
2085 "file_status_untracked_fg" => Some(&mut self.file_status_untracked_fg),
2086 "file_status_conflicted_fg" => Some(&mut self.file_status_conflicted_fg),
2087 _ => None,
2088 },
2089 "syntax" => match field {
2090 "keyword" => Some(&mut self.syntax_keyword),
2091 "string" => Some(&mut self.syntax_string),
2092 "comment" => Some(&mut self.syntax_comment),
2093 "function" => Some(&mut self.syntax_function),
2094 "type" => Some(&mut self.syntax_type),
2095 "variable" => Some(&mut self.syntax_variable),
2096 "constant" => Some(&mut self.syntax_constant),
2097 "operator" => Some(&mut self.syntax_operator),
2098 "punctuation_bracket" => Some(&mut self.syntax_punctuation_bracket),
2099 "punctuation_delimiter" => Some(&mut self.syntax_punctuation_delimiter),
2100 _ => None,
2101 },
2102 "diagnostic" => match field {
2103 "error_fg" => Some(&mut self.diagnostic_error_fg),
2104 "error_bg" => Some(&mut self.diagnostic_error_bg),
2105 "warning_fg" => Some(&mut self.diagnostic_warning_fg),
2106 "warning_bg" => Some(&mut self.diagnostic_warning_bg),
2107 "info_fg" => Some(&mut self.diagnostic_info_fg),
2108 "info_bg" => Some(&mut self.diagnostic_info_bg),
2109 "hint_fg" => Some(&mut self.diagnostic_hint_fg),
2110 "hint_bg" => Some(&mut self.diagnostic_hint_bg),
2111 _ => None,
2112 },
2113 "search" => match field {
2114 "match_bg" => Some(&mut self.search_match_bg),
2115 "match_fg" => Some(&mut self.search_match_fg),
2116 "label_bg" => Some(&mut self.search_label_bg),
2117 "label_fg" => Some(&mut self.search_label_fg),
2118 _ => None,
2119 },
2120 _ => None,
2121 }
2122 }
2123
2124 pub fn override_colors<I, K>(&mut self, overrides: I) -> usize
2129 where
2130 I: IntoIterator<Item = (K, Color)>,
2131 K: AsRef<str>,
2132 {
2133 let mut applied = 0;
2134 for (key, color) in overrides {
2135 if let Some(slot) = self.resolve_theme_key_mut(key.as_ref()) {
2136 *slot = color;
2137 applied += 1;
2138 }
2139 }
2140 applied
2141 }
2142}
2143
2144pub fn get_theme_schema() -> serde_json::Value {
2152 use schemars::schema_for;
2153 let schema = schema_for!(ThemeFile);
2154 serde_json::to_value(&schema).unwrap_or_default()
2155}
2156
2157pub fn get_builtin_themes() -> serde_json::Value {
2159 let mut map = serde_json::Map::new();
2160 for theme in BUILTIN_THEMES {
2161 map.insert(
2162 theme.name.to_string(),
2163 serde_json::Value::String(theme.json.to_string()),
2164 );
2165 }
2166 serde_json::Value::Object(map)
2167}
2168
2169#[cfg(test)]
2170mod tests {
2171 use super::*;
2172
2173 #[test]
2174 fn test_load_builtin_theme() {
2175 let dark = Theme::load_builtin(THEME_DARK).expect("Dark theme must exist");
2176 assert_eq!(dark.name, THEME_DARK);
2177
2178 let light = Theme::load_builtin(THEME_LIGHT).expect("Light theme must exist");
2179 assert_eq!(light.name, THEME_LIGHT);
2180
2181 let high_contrast =
2182 Theme::load_builtin(THEME_HIGH_CONTRAST).expect("High contrast theme must exist");
2183 assert_eq!(high_contrast.name, THEME_HIGH_CONTRAST);
2184
2185 let terminal = Theme::load_builtin(THEME_TERMINAL).expect("Terminal theme must exist");
2186 assert_eq!(terminal.name, THEME_TERMINAL);
2187 assert_eq!(terminal.editor_bg, Color::Reset);
2191 assert_eq!(terminal.editor_fg, Color::Reset);
2192 assert_eq!(terminal.terminal_bg, Color::Reset);
2193 assert!(terminal.selection_modifier.contains(Modifier::REVERSED));
2196 assert!(terminal
2197 .semantic_highlight_modifier
2198 .contains(Modifier::BOLD));
2199 }
2200
2201 #[test]
2202 fn test_modifier_def_round_trip() {
2203 let cases = [
2204 (vec!["reversed"], Modifier::REVERSED),
2205 (
2206 vec!["bold", "underlined"],
2207 Modifier::BOLD | Modifier::UNDERLINED,
2208 ),
2209 (vec!["italic", "dim"], Modifier::ITALIC | Modifier::DIM),
2210 (vec!["reverse"], Modifier::REVERSED), (vec!["underline"], Modifier::UNDERLINED), ];
2213 for (strs, expected) in cases {
2214 let def = ModifierDef(strs.iter().map(|s| s.to_string()).collect());
2215 let m: Modifier = (&def).into();
2216 assert_eq!(m, expected, "ModifierDef({:?}) -> Modifier", strs);
2217 }
2218 }
2219
2220 #[test]
2221 fn test_modifier_def_unknown_strings_are_dropped() {
2222 let def = ModifierDef(vec!["reversed".into(), "wibble".into(), "bold".into()]);
2225 let m: Modifier = (&def).into();
2226 assert_eq!(m, Modifier::REVERSED | Modifier::BOLD);
2227 }
2228
2229 #[test]
2230 fn test_themes_without_modifier_default_to_empty() {
2231 let dark = Theme::load_builtin(THEME_DARK).expect("Dark theme must exist");
2236 assert!(dark.selection_modifier.is_empty());
2237 assert!(dark.semantic_highlight_modifier.is_empty());
2238 }
2239
2240 #[test]
2241 fn test_modifier_for_bg_key_lookup() {
2242 let terminal = Theme::load_builtin(THEME_TERMINAL).expect("Terminal theme must exist");
2243 assert!(terminal
2246 .modifier_for_bg_key("editor.selection_bg")
2247 .contains(Modifier::REVERSED));
2248 assert!(terminal
2249 .modifier_for_bg_key("ui.semantic_highlight_bg")
2250 .contains(Modifier::BOLD));
2251 assert!(terminal
2254 .modifier_for_bg_key("ui.popup_selection_bg")
2255 .is_empty());
2256 assert!(terminal.modifier_for_bg_key("nonsense").is_empty());
2257 }
2258
2259 #[test]
2260 fn test_modifier_round_trip_via_theme_file() {
2261 let original = Theme::load_builtin(THEME_TERMINAL).expect("Terminal theme must exist");
2263 let file: ThemeFile = original.clone().into();
2264 let json = serde_json::to_string(&file).expect("serialize");
2265 let parsed: ThemeFile = serde_json::from_str(&json).expect("parse");
2266 let round_tripped: Theme = parsed.into();
2267 assert_eq!(
2268 round_tripped.selection_modifier,
2269 original.selection_modifier
2270 );
2271 assert_eq!(
2272 round_tripped.semantic_highlight_modifier,
2273 original.semantic_highlight_modifier
2274 );
2275 }
2276
2277 #[test]
2278 fn test_builtin_themes_match_schema() {
2279 for theme in BUILTIN_THEMES {
2280 let _: ThemeFile = serde_json::from_str(theme.json)
2281 .unwrap_or_else(|_| panic!("Theme '{}' does not match schema", theme.name));
2282 }
2283 }
2284
2285 #[test]
2286 fn test_from_json() {
2287 let json = r#"{"name":"test","editor":{},"ui":{},"search":{},"diagnostic":{},"syntax":{}}"#;
2288 let theme = Theme::from_json(json).expect("Should parse minimal theme");
2289 assert_eq!(theme.name, "test");
2290 }
2291
2292 #[test]
2304 fn test_minimal_user_theme_from_issue_1281_loads() {
2305 let json = r#"{
2307 "name": "gruvbox-light-orange",
2308 "editor": {
2309 "bg": [251, 241, 199],
2310 "fg": [60, 56, 54],
2311 "cursor": [254, 128, 25],
2312 "selection_bg": [213, 196, 161]
2313 },
2314 "syntax": {
2315 "keyword": [175, 58, 3],
2316 "string": [152, 151, 26],
2317 "comment": [146, 131, 116]
2318 }
2319}"#;
2320 let theme = Theme::from_json(json)
2321 .expect("Theme from issue #1281 should parse without `ui`/`search`/`diagnostic`");
2322 assert_eq!(theme.name, "gruvbox-light-orange");
2323
2324 assert_eq!(theme.editor_bg, Color::Rgb(251, 241, 199));
2326 assert_eq!(theme.editor_fg, Color::Rgb(60, 56, 54));
2327 assert_eq!(theme.cursor, Color::Rgb(254, 128, 25));
2328 assert_eq!(theme.selection_bg, Color::Rgb(213, 196, 161));
2329 assert_eq!(theme.syntax_keyword, Color::Rgb(175, 58, 3));
2330 assert_eq!(theme.syntax_string, Color::Rgb(152, 151, 26));
2331 assert_eq!(theme.syntax_comment, Color::Rgb(146, 131, 116));
2332
2333 let light = Theme::load_builtin(THEME_LIGHT).expect("light builtin");
2337 assert_eq!(
2338 theme.status_bar_fg, light.status_bar_fg,
2339 "ui.status_bar_fg should inherit from builtin://light when bg is bright"
2340 );
2341 assert_eq!(
2342 theme.diagnostic_error_fg, light.diagnostic_error_fg,
2343 "diagnostic.error_fg should inherit from builtin://light when bg is bright"
2344 );
2345 assert_eq!(
2346 theme.menu_bg, light.menu_bg,
2347 "ui.menu_bg should inherit from builtin://light when bg is bright"
2348 );
2349 }
2350
2351 #[test]
2354 fn test_extends_explicit_builtin_wins_over_auto_infer() {
2355 let json = r#"{
2358 "name": "explicit-light",
2359 "extends": "builtin://light",
2360 "editor": { "bg": [0, 0, 0] }
2361 }"#;
2362 let theme = Theme::from_json(json).expect("extends should resolve");
2363 let light = Theme::load_builtin(THEME_LIGHT).expect("light builtin");
2364
2365 assert_eq!(theme.editor_bg, Color::Rgb(0, 0, 0));
2367 assert_eq!(theme.menu_bg, light.menu_bg);
2369 assert_eq!(theme.tab_active_bg, light.tab_active_bg);
2370 assert_eq!(theme.diagnostic_warning_fg, light.diagnostic_warning_fg);
2371 }
2372
2373 #[test]
2378 fn test_extends_bare_builtin_name_works() {
2379 let json = r#"{ "name": "x", "extends": "high-contrast" }"#;
2380 let theme = Theme::from_json(json).expect("bare-name extends should resolve");
2381 let hc = Theme::load_builtin("high-contrast").expect("hc builtin");
2382 assert_eq!(theme.menu_bg, hc.menu_bg);
2383 }
2384
2385 #[test]
2390 fn test_extends_unknown_builtin_errors_with_helpful_message() {
2391 let json = r#"{ "name": "x", "extends": "builtin://no-such-theme" }"#;
2392 let err = Theme::from_json(json).expect_err("unknown extends must error");
2393 assert!(
2394 err.contains("no-such-theme"),
2395 "error should quote the bad value, got: {}",
2396 err
2397 );
2398 assert!(
2399 err.contains("dark") && err.contains("light"),
2400 "error should list available builtins, got: {}",
2401 err
2402 );
2403 }
2404
2405 #[test]
2409 fn test_auto_infer_dark_base_from_dark_bg() {
2410 let json = r#"{ "name": "x", "editor": { "bg": [20, 20, 30] } }"#;
2411 let theme = Theme::from_json(json).expect("should parse");
2412 let dark = Theme::load_builtin(THEME_DARK).expect("dark builtin");
2413 assert_eq!(theme.menu_bg, dark.menu_bg);
2414 assert_eq!(theme.diagnostic_error_fg, dark.diagnostic_error_fg);
2415 }
2416
2417 #[test]
2421 fn test_no_inheritance_signal_uses_hardcoded_defaults() {
2422 let json = r#"{ "name": "x" }"#;
2423 let theme = Theme::from_json(json).expect("should parse");
2424 assert_eq!(theme.editor_bg, Color::Rgb(30, 30, 30));
2427 }
2428
2429 #[test]
2433 fn test_theme_without_name_still_errors() {
2434 let json = r#"{ "editor": {} }"#;
2435 let err = Theme::from_json(json).expect_err("missing `name` must be an error");
2436 assert!(
2437 err.contains("name"),
2438 "error should mention the missing `name` field, got: {}",
2439 err
2440 );
2441 }
2442
2443 #[test]
2448 fn test_extends_overrides_compose_field_by_field() {
2449 let json = r#"{
2450 "name": "dark-with-pink-cursor",
2451 "extends": "builtin://dark",
2452 "editor": { "cursor": [255, 105, 180] }
2453 }"#;
2454 let theme = Theme::from_json(json).expect("should parse");
2455 let dark = Theme::load_builtin(THEME_DARK).expect("dark builtin");
2456
2457 assert_eq!(theme.cursor, Color::Rgb(255, 105, 180));
2459 assert_eq!(theme.editor_bg, dark.editor_bg);
2461 assert_eq!(theme.editor_fg, dark.editor_fg);
2462 assert_eq!(theme.selection_bg, dark.selection_bg);
2463 assert_eq!(theme.menu_bg, dark.menu_bg);
2465 assert_eq!(theme.syntax_keyword, dark.syntax_keyword);
2466 }
2467
2468 #[test]
2469 fn test_default_reset_color() {
2470 let color: Color = ColorDef::Named("Default".to_string()).into();
2472 assert_eq!(color, Color::Reset);
2473
2474 let color: Color = ColorDef::Named("Reset".to_string()).into();
2476 assert_eq!(color, Color::Reset);
2477 }
2478
2479 #[test]
2480 fn test_file_status_colors_fall_back_to_diagnostic_colors() {
2481 let json = r#"{
2483 "name": "test-fallback",
2484 "editor": {},
2485 "ui": {},
2486 "search": {},
2487 "diagnostic": {
2488 "error_fg": [220, 50, 47],
2489 "warning_fg": [181, 137, 0],
2490 "info_fg": [38, 139, 210],
2491 "hint_fg": [101, 123, 131]
2492 },
2493 "syntax": {}
2494 }"#;
2495 let theme = Theme::from_json(json).expect("Should parse theme without file_status keys");
2496
2497 assert_eq!(theme.file_status_added_fg, Color::Rgb(38, 139, 210));
2499 assert_eq!(theme.file_status_renamed_fg, Color::Rgb(38, 139, 210));
2500 assert_eq!(theme.file_status_modified_fg, Color::Rgb(181, 137, 0));
2502 assert_eq!(theme.file_status_deleted_fg, Color::Rgb(220, 50, 47));
2504 assert_eq!(theme.file_status_conflicted_fg, Color::Rgb(220, 50, 47));
2505 assert_eq!(theme.file_status_untracked_fg, Color::Rgb(101, 123, 131));
2507 }
2508
2509 #[test]
2510 fn test_file_status_colors_explicit_override() {
2511 let json = r#"{
2513 "name": "test-override",
2514 "editor": {},
2515 "ui": {
2516 "file_status_added_fg": [80, 250, 123],
2517 "file_status_modified_fg": [255, 184, 108]
2518 },
2519 "search": {},
2520 "diagnostic": {
2521 "info_fg": [38, 139, 210],
2522 "warning_fg": [181, 137, 0]
2523 },
2524 "syntax": {}
2525 }"#;
2526 let theme = Theme::from_json(json).expect("Should parse theme with file_status overrides");
2527
2528 assert_eq!(theme.file_status_added_fg, Color::Rgb(80, 250, 123));
2530 assert_eq!(theme.file_status_modified_fg, Color::Rgb(255, 184, 108));
2531 assert_eq!(theme.file_status_renamed_fg, Color::Rgb(38, 139, 210));
2533 }
2534
2535 #[test]
2536 fn test_file_status_colors_resolve_via_theme_key() {
2537 let json = r#"{
2538 "name": "test-resolve",
2539 "editor": {},
2540 "ui": {
2541 "file_status_added_fg": [80, 250, 123]
2542 },
2543 "search": {},
2544 "diagnostic": {
2545 "warning_fg": [181, 137, 0]
2546 },
2547 "syntax": {}
2548 }"#;
2549 let theme = Theme::from_json(json).expect("Should parse theme");
2550
2551 assert_eq!(
2553 theme.resolve_theme_key("ui.file_status_added_fg"),
2554 Some(Color::Rgb(80, 250, 123))
2555 );
2556 assert_eq!(
2557 theme.resolve_theme_key("ui.file_status_modified_fg"),
2558 Some(Color::Rgb(181, 137, 0))
2559 );
2560 }
2561
2562 #[test]
2563 fn override_colors_writes_known_keys_and_drops_unknowns() {
2564 let mut theme = Theme::load_builtin(THEME_DARK).expect("dark builtin");
2565 let applied = theme.override_colors([
2566 ("editor.bg".to_string(), Color::Rgb(10, 20, 30)),
2567 ("ui.status_bar_fg".to_string(), Color::Rgb(1, 2, 3)),
2568 ("does.not_exist".to_string(), Color::Rgb(9, 9, 9)),
2569 ("garbage_no_dot".to_string(), Color::Rgb(9, 9, 9)),
2570 ]);
2571 assert_eq!(applied, 2, "only the two valid keys should be applied");
2572 assert_eq!(
2573 theme.resolve_theme_key("editor.bg"),
2574 Some(Color::Rgb(10, 20, 30))
2575 );
2576 assert_eq!(
2577 theme.resolve_theme_key("ui.status_bar_fg"),
2578 Some(Color::Rgb(1, 2, 3))
2579 );
2580 }
2581
2582 #[test]
2583 fn resolve_theme_key_mut_matches_resolve_theme_key_domain() {
2584 let mut theme = Theme::load_builtin(THEME_DARK).expect("dark builtin");
2587 let probe = [
2588 "editor.bg",
2589 "editor.fg",
2590 "ui.status_bar_fg",
2591 "ui.tab_active_bg",
2592 "syntax.keyword",
2593 "diagnostic.error_fg",
2594 "search.match_bg",
2595 ];
2596 for key in probe {
2597 assert!(
2598 theme.resolve_theme_key(key).is_some(),
2599 "reader lost key {key}"
2600 );
2601 assert!(
2602 theme.resolve_theme_key_mut(key).is_some(),
2603 "mutator missing key {key}"
2604 );
2605 }
2606 }
2607
2608 #[test]
2609 fn test_all_builtin_themes_set_prominent_palette_indicator() {
2610 for builtin in BUILTIN_THEMES {
2617 let theme = Theme::from_json(builtin.json)
2618 .unwrap_or_else(|e| panic!("Theme '{}' failed to parse: {}", builtin.name, e));
2619 assert!(
2620 theme.status_palette_fg != theme.status_bar_fg
2621 || theme.status_palette_bg != theme.status_bar_bg,
2622 "Theme '{}' must set status_palette_fg/bg to a prominent \
2623 accent distinct from status_bar_fg/bg",
2624 builtin.name
2625 );
2626 }
2627 }
2628
2629 #[test]
2630 fn test_all_builtin_themes_have_file_status_colors() {
2631 for builtin in BUILTIN_THEMES {
2633 let theme = Theme::from_json(builtin.json)
2634 .unwrap_or_else(|e| panic!("Theme '{}' failed to parse: {}", builtin.name, e));
2635
2636 for key in &[
2638 "ui.file_status_added_fg",
2639 "ui.file_status_modified_fg",
2640 "ui.file_status_deleted_fg",
2641 "ui.file_status_renamed_fg",
2642 "ui.file_status_untracked_fg",
2643 "ui.file_status_conflicted_fg",
2644 ] {
2645 assert!(
2646 theme.resolve_theme_key(key).is_some(),
2647 "Theme '{}' missing resolution for '{}'",
2648 builtin.name,
2649 key
2650 );
2651 }
2652 }
2653 }
2654}