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
282impl From<Color> for ColorDef {
283 fn from(color: Color) -> Self {
284 match color {
285 Color::Rgb(r, g, b) => ColorDef::Rgb(r, g, b),
286 Color::White => ColorDef::Named("White".to_string()),
287 Color::Black => ColorDef::Named("Black".to_string()),
288 Color::Red => ColorDef::Named("Red".to_string()),
289 Color::Green => ColorDef::Named("Green".to_string()),
290 Color::Blue => ColorDef::Named("Blue".to_string()),
291 Color::Yellow => ColorDef::Named("Yellow".to_string()),
292 Color::Magenta => ColorDef::Named("Magenta".to_string()),
293 Color::Cyan => ColorDef::Named("Cyan".to_string()),
294 Color::Gray => ColorDef::Named("Gray".to_string()),
295 Color::DarkGray => ColorDef::Named("DarkGray".to_string()),
296 Color::LightRed => ColorDef::Named("LightRed".to_string()),
297 Color::LightGreen => ColorDef::Named("LightGreen".to_string()),
298 Color::LightBlue => ColorDef::Named("LightBlue".to_string()),
299 Color::LightYellow => ColorDef::Named("LightYellow".to_string()),
300 Color::LightMagenta => ColorDef::Named("LightMagenta".to_string()),
301 Color::LightCyan => ColorDef::Named("LightCyan".to_string()),
302 Color::Reset => ColorDef::Named("Default".to_string()),
303 Color::Indexed(_) => {
304 if let Some((r, g, b)) = color_to_rgb(color) {
306 ColorDef::Rgb(r, g, b)
307 } else {
308 ColorDef::Named("Default".to_string())
309 }
310 }
311 }
312 }
313}
314
315#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
338pub struct ThemeFile {
339 pub name: String,
341 #[serde(default, skip_serializing_if = "Option::is_none")]
347 pub extends: Option<String>,
348 #[serde(default = "default_editor_colors")]
350 pub editor: EditorColors,
351 #[serde(default = "default_ui_colors")]
353 pub ui: UiColors,
354 #[serde(default = "default_search_colors")]
356 pub search: SearchColors,
357 #[serde(default = "default_diagnostic_colors")]
359 pub diagnostic: DiagnosticColors,
360 #[serde(default = "default_syntax_colors")]
362 pub syntax: SyntaxColors,
363}
364
365fn default_section<T: serde::de::DeserializeOwned>(section: &'static str) -> T {
370 serde_json::from_str("{}").unwrap_or_else(|e| {
371 panic!(
372 "theme section `{}` must be default-constructible from `{{}}` \
373 (every field needs `#[serde(default = ...)]`): {}",
374 section, e
375 )
376 })
377}
378
379fn default_editor_colors() -> EditorColors {
380 default_section("editor")
381}
382
383fn default_ui_colors() -> UiColors {
384 default_section("ui")
385}
386
387fn default_search_colors() -> SearchColors {
388 default_section("search")
389}
390
391fn default_diagnostic_colors() -> DiagnosticColors {
392 default_section("diagnostic")
393}
394
395fn default_syntax_colors() -> SyntaxColors {
396 default_section("syntax")
397}
398
399#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
401pub struct EditorColors {
402 #[serde(default = "default_editor_bg")]
404 pub bg: ColorDef,
405 #[serde(default = "default_editor_fg")]
407 pub fg: ColorDef,
408 #[serde(default = "default_cursor")]
410 pub cursor: ColorDef,
411 #[serde(default = "default_inactive_cursor")]
413 pub inactive_cursor: ColorDef,
414 #[serde(default = "default_selection_bg")]
416 pub selection_bg: ColorDef,
417 #[serde(default)]
425 pub selection_modifier: Option<ModifierDef>,
426 #[serde(default = "default_current_line_bg")]
428 pub current_line_bg: ColorDef,
429 #[serde(default = "default_line_number_fg")]
431 pub line_number_fg: ColorDef,
432 #[serde(default = "default_line_number_bg")]
434 pub line_number_bg: ColorDef,
435 #[serde(default = "default_diff_add_bg")]
437 pub diff_add_bg: ColorDef,
438 #[serde(default = "default_diff_remove_bg")]
440 pub diff_remove_bg: ColorDef,
441 #[serde(default)]
444 pub diff_add_highlight_bg: Option<ColorDef>,
445 #[serde(default)]
448 pub diff_remove_highlight_bg: Option<ColorDef>,
449 #[serde(default = "default_diff_modify_bg")]
451 pub diff_modify_bg: ColorDef,
452 #[serde(default = "default_ruler_bg")]
454 pub ruler_bg: ColorDef,
455 #[serde(default = "default_whitespace_indicator_fg")]
457 pub whitespace_indicator_fg: ColorDef,
458 #[serde(default)]
463 pub after_eof_bg: Option<ColorDef>,
464}
465
466fn default_editor_bg() -> ColorDef {
468 ColorDef::Rgb(30, 30, 30)
469}
470fn default_editor_fg() -> ColorDef {
471 ColorDef::Rgb(212, 212, 212)
472}
473fn default_cursor() -> ColorDef {
474 ColorDef::Rgb(255, 255, 255)
475}
476fn default_inactive_cursor() -> ColorDef {
477 ColorDef::Named("DarkGray".to_string())
478}
479fn default_selection_bg() -> ColorDef {
480 ColorDef::Rgb(38, 79, 120)
481}
482fn default_current_line_bg() -> ColorDef {
483 ColorDef::Rgb(40, 40, 40)
484}
485fn default_line_number_fg() -> ColorDef {
486 ColorDef::Rgb(100, 100, 100)
487}
488fn default_line_number_bg() -> ColorDef {
489 ColorDef::Rgb(30, 30, 30)
490}
491fn default_diff_add_bg() -> ColorDef {
492 ColorDef::Rgb(35, 60, 35) }
494fn default_diff_remove_bg() -> ColorDef {
495 ColorDef::Rgb(70, 35, 35) }
497fn default_diff_modify_bg() -> ColorDef {
498 ColorDef::Rgb(40, 38, 30) }
500fn default_ruler_bg() -> ColorDef {
501 ColorDef::Rgb(50, 50, 50) }
503fn default_whitespace_indicator_fg() -> ColorDef {
504 ColorDef::Rgb(70, 70, 70) }
506
507#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
509pub struct UiColors {
510 #[serde(default = "default_tab_active_fg")]
512 pub tab_active_fg: ColorDef,
513 #[serde(default = "default_tab_active_bg")]
515 pub tab_active_bg: ColorDef,
516 #[serde(default = "default_tab_inactive_fg")]
518 pub tab_inactive_fg: ColorDef,
519 #[serde(default = "default_tab_inactive_bg")]
521 pub tab_inactive_bg: ColorDef,
522 #[serde(default = "default_tab_separator_bg")]
524 pub tab_separator_bg: ColorDef,
525 #[serde(default = "default_tab_close_hover_fg")]
527 pub tab_close_hover_fg: ColorDef,
528 #[serde(default = "default_tab_hover_bg")]
530 pub tab_hover_bg: ColorDef,
531 #[serde(default = "default_menu_bg")]
533 pub menu_bg: ColorDef,
534 #[serde(default = "default_menu_fg")]
536 pub menu_fg: ColorDef,
537 #[serde(default = "default_menu_active_bg")]
539 pub menu_active_bg: ColorDef,
540 #[serde(default = "default_menu_active_fg")]
542 pub menu_active_fg: ColorDef,
543 #[serde(default = "default_menu_dropdown_bg")]
545 pub menu_dropdown_bg: ColorDef,
546 #[serde(default = "default_menu_dropdown_fg")]
548 pub menu_dropdown_fg: ColorDef,
549 #[serde(default = "default_menu_highlight_bg")]
551 pub menu_highlight_bg: ColorDef,
552 #[serde(default = "default_menu_highlight_fg")]
554 pub menu_highlight_fg: ColorDef,
555 #[serde(default = "default_menu_border_fg")]
557 pub menu_border_fg: ColorDef,
558 #[serde(default = "default_menu_separator_fg")]
560 pub menu_separator_fg: ColorDef,
561 #[serde(default = "default_menu_hover_bg")]
563 pub menu_hover_bg: ColorDef,
564 #[serde(default = "default_menu_hover_fg")]
566 pub menu_hover_fg: ColorDef,
567 #[serde(default = "default_menu_disabled_fg")]
569 pub menu_disabled_fg: ColorDef,
570 #[serde(default = "default_menu_disabled_bg")]
572 pub menu_disabled_bg: ColorDef,
573 #[serde(default = "default_status_bar_fg")]
575 pub status_bar_fg: ColorDef,
576 #[serde(default = "default_status_bar_bg")]
578 pub status_bar_bg: ColorDef,
579 #[serde(default)]
581 pub status_palette_fg: Option<ColorDef>,
582 #[serde(default)]
584 pub status_palette_bg: Option<ColorDef>,
585 #[serde(default)]
587 pub status_lsp_on_fg: Option<ColorDef>,
588 #[serde(default)]
590 pub status_lsp_on_bg: Option<ColorDef>,
591 #[serde(default)]
595 pub status_lsp_actionable_fg: Option<ColorDef>,
596 #[serde(default)]
599 pub status_lsp_actionable_bg: Option<ColorDef>,
600 #[serde(default = "default_prompt_fg")]
602 pub prompt_fg: ColorDef,
603 #[serde(default = "default_prompt_bg")]
605 pub prompt_bg: ColorDef,
606 #[serde(default = "default_prompt_selection_fg")]
608 pub prompt_selection_fg: ColorDef,
609 #[serde(default = "default_prompt_selection_bg")]
611 pub prompt_selection_bg: ColorDef,
612 #[serde(default = "default_popup_border_fg")]
614 pub popup_border_fg: ColorDef,
615 #[serde(default = "default_popup_bg")]
617 pub popup_bg: ColorDef,
618 #[serde(default = "default_popup_selection_bg")]
620 pub popup_selection_bg: ColorDef,
621 #[serde(default = "default_popup_selection_fg")]
623 pub popup_selection_fg: ColorDef,
624 #[serde(default = "default_popup_text_fg")]
626 pub popup_text_fg: ColorDef,
627 #[serde(default = "default_suggestion_bg")]
629 pub suggestion_bg: ColorDef,
630 #[serde(default = "default_suggestion_selected_bg")]
632 pub suggestion_selected_bg: ColorDef,
633 #[serde(default = "default_help_bg")]
635 pub help_bg: ColorDef,
636 #[serde(default = "default_help_fg")]
638 pub help_fg: ColorDef,
639 #[serde(default = "default_help_key_fg")]
641 pub help_key_fg: ColorDef,
642 #[serde(default = "default_help_separator_fg")]
644 pub help_separator_fg: ColorDef,
645 #[serde(default = "default_help_indicator_fg")]
647 pub help_indicator_fg: ColorDef,
648 #[serde(default = "default_help_indicator_bg")]
650 pub help_indicator_bg: ColorDef,
651 #[serde(default = "default_inline_code_bg")]
653 pub inline_code_bg: ColorDef,
654 #[serde(default = "default_split_separator_fg")]
656 pub split_separator_fg: ColorDef,
657 #[serde(default = "default_split_separator_hover_fg")]
659 pub split_separator_hover_fg: ColorDef,
660 #[serde(default = "default_scrollbar_track_fg")]
662 pub scrollbar_track_fg: ColorDef,
663 #[serde(default = "default_scrollbar_thumb_fg")]
665 pub scrollbar_thumb_fg: ColorDef,
666 #[serde(default = "default_scrollbar_track_hover_fg")]
668 pub scrollbar_track_hover_fg: ColorDef,
669 #[serde(default = "default_scrollbar_thumb_hover_fg")]
671 pub scrollbar_thumb_hover_fg: ColorDef,
672 #[serde(default = "default_compose_margin_bg")]
674 pub compose_margin_bg: ColorDef,
675 #[serde(default = "default_semantic_highlight_bg")]
677 pub semantic_highlight_bg: ColorDef,
678 #[serde(default)]
685 pub semantic_highlight_modifier: Option<ModifierDef>,
686 #[serde(default = "default_terminal_bg")]
688 pub terminal_bg: ColorDef,
689 #[serde(default = "default_terminal_fg")]
691 pub terminal_fg: ColorDef,
692 #[serde(default = "default_status_warning_indicator_bg")]
694 pub status_warning_indicator_bg: ColorDef,
695 #[serde(default = "default_status_warning_indicator_fg")]
697 pub status_warning_indicator_fg: ColorDef,
698 #[serde(default = "default_status_error_indicator_bg")]
700 pub status_error_indicator_bg: ColorDef,
701 #[serde(default = "default_status_error_indicator_fg")]
703 pub status_error_indicator_fg: ColorDef,
704 #[serde(default = "default_status_warning_indicator_hover_bg")]
706 pub status_warning_indicator_hover_bg: ColorDef,
707 #[serde(default = "default_status_warning_indicator_hover_fg")]
709 pub status_warning_indicator_hover_fg: ColorDef,
710 #[serde(default = "default_status_error_indicator_hover_bg")]
712 pub status_error_indicator_hover_bg: ColorDef,
713 #[serde(default = "default_status_error_indicator_hover_fg")]
715 pub status_error_indicator_hover_fg: ColorDef,
716 #[serde(default = "default_tab_drop_zone_bg")]
718 pub tab_drop_zone_bg: ColorDef,
719 #[serde(default = "default_tab_drop_zone_border")]
721 pub tab_drop_zone_border: ColorDef,
722 #[serde(default = "default_settings_selected_bg")]
724 pub settings_selected_bg: ColorDef,
725 #[serde(default = "default_settings_selected_fg")]
727 pub settings_selected_fg: ColorDef,
728 #[serde(default)]
730 pub file_status_added_fg: Option<ColorDef>,
731 #[serde(default)]
733 pub file_status_modified_fg: Option<ColorDef>,
734 #[serde(default)]
736 pub file_status_deleted_fg: Option<ColorDef>,
737 #[serde(default)]
739 pub file_status_renamed_fg: Option<ColorDef>,
740 #[serde(default)]
742 pub file_status_untracked_fg: Option<ColorDef>,
743 #[serde(default)]
745 pub file_status_conflicted_fg: Option<ColorDef>,
746}
747
748fn default_tab_active_fg() -> ColorDef {
751 ColorDef::Named("Yellow".to_string())
752}
753fn default_tab_active_bg() -> ColorDef {
754 ColorDef::Named("Blue".to_string())
755}
756fn default_tab_inactive_fg() -> ColorDef {
757 ColorDef::Named("White".to_string())
758}
759fn default_tab_inactive_bg() -> ColorDef {
760 ColorDef::Named("DarkGray".to_string())
761}
762fn default_tab_separator_bg() -> ColorDef {
763 ColorDef::Named("Black".to_string())
764}
765fn default_tab_close_hover_fg() -> ColorDef {
766 ColorDef::Rgb(255, 100, 100) }
768fn default_tab_hover_bg() -> ColorDef {
769 ColorDef::Rgb(70, 70, 75) }
771
772fn default_menu_bg() -> ColorDef {
774 ColorDef::Rgb(60, 60, 65)
775}
776fn default_menu_fg() -> ColorDef {
777 ColorDef::Rgb(220, 220, 220)
778}
779fn default_menu_active_bg() -> ColorDef {
780 ColorDef::Rgb(60, 60, 60)
781}
782fn default_menu_active_fg() -> ColorDef {
783 ColorDef::Rgb(255, 255, 255)
784}
785fn default_menu_dropdown_bg() -> ColorDef {
786 ColorDef::Rgb(50, 50, 50)
787}
788fn default_menu_dropdown_fg() -> ColorDef {
789 ColorDef::Rgb(220, 220, 220)
790}
791fn default_menu_highlight_bg() -> ColorDef {
792 ColorDef::Rgb(70, 130, 180)
793}
794fn default_menu_highlight_fg() -> ColorDef {
795 ColorDef::Rgb(255, 255, 255)
796}
797fn default_menu_border_fg() -> ColorDef {
798 ColorDef::Rgb(100, 100, 100)
799}
800fn default_menu_separator_fg() -> ColorDef {
801 ColorDef::Rgb(80, 80, 80)
802}
803fn default_menu_hover_bg() -> ColorDef {
804 ColorDef::Rgb(55, 55, 55)
805}
806fn default_menu_hover_fg() -> ColorDef {
807 ColorDef::Rgb(255, 255, 255)
808}
809fn default_menu_disabled_fg() -> ColorDef {
810 ColorDef::Rgb(100, 100, 100) }
812fn default_menu_disabled_bg() -> ColorDef {
813 ColorDef::Rgb(50, 50, 50) }
815fn default_status_bar_fg() -> ColorDef {
817 ColorDef::Named("White".to_string())
818}
819fn default_status_bar_bg() -> ColorDef {
820 ColorDef::Named("DarkGray".to_string())
821}
822
823fn default_prompt_fg() -> ColorDef {
825 ColorDef::Named("White".to_string())
826}
827fn default_prompt_bg() -> ColorDef {
828 ColorDef::Named("Black".to_string())
829}
830fn default_prompt_selection_fg() -> ColorDef {
831 ColorDef::Named("White".to_string())
832}
833fn default_prompt_selection_bg() -> ColorDef {
834 ColorDef::Rgb(58, 79, 120)
835}
836
837fn default_popup_border_fg() -> ColorDef {
839 ColorDef::Named("Gray".to_string())
840}
841fn default_popup_bg() -> ColorDef {
842 ColorDef::Rgb(30, 30, 30)
843}
844fn default_popup_selection_bg() -> ColorDef {
845 ColorDef::Rgb(58, 79, 120)
846}
847fn default_popup_selection_fg() -> ColorDef {
848 ColorDef::Rgb(255, 255, 255) }
850fn default_popup_text_fg() -> ColorDef {
851 ColorDef::Named("White".to_string())
852}
853
854fn default_suggestion_bg() -> ColorDef {
856 ColorDef::Rgb(30, 30, 30)
857}
858fn default_suggestion_selected_bg() -> ColorDef {
859 ColorDef::Rgb(58, 79, 120)
860}
861
862fn default_help_bg() -> ColorDef {
864 ColorDef::Named("Black".to_string())
865}
866fn default_help_fg() -> ColorDef {
867 ColorDef::Named("White".to_string())
868}
869fn default_help_key_fg() -> ColorDef {
870 ColorDef::Named("Cyan".to_string())
871}
872fn default_help_separator_fg() -> ColorDef {
873 ColorDef::Named("DarkGray".to_string())
874}
875fn default_help_indicator_fg() -> ColorDef {
876 ColorDef::Named("Red".to_string())
877}
878fn default_help_indicator_bg() -> ColorDef {
879 ColorDef::Named("Black".to_string())
880}
881
882fn default_inline_code_bg() -> ColorDef {
883 ColorDef::Named("DarkGray".to_string())
884}
885
886fn default_split_separator_fg() -> ColorDef {
888 ColorDef::Rgb(100, 100, 100)
889}
890fn default_split_separator_hover_fg() -> ColorDef {
891 ColorDef::Rgb(100, 149, 237) }
893fn default_scrollbar_track_fg() -> ColorDef {
894 ColorDef::Named("DarkGray".to_string())
895}
896fn default_scrollbar_thumb_fg() -> ColorDef {
897 ColorDef::Named("Gray".to_string())
898}
899fn default_scrollbar_track_hover_fg() -> ColorDef {
900 ColorDef::Named("Gray".to_string())
901}
902fn default_scrollbar_thumb_hover_fg() -> ColorDef {
903 ColorDef::Named("White".to_string())
904}
905fn default_compose_margin_bg() -> ColorDef {
906 ColorDef::Rgb(18, 18, 18) }
908fn default_semantic_highlight_bg() -> ColorDef {
909 ColorDef::Rgb(60, 60, 80) }
911fn default_terminal_bg() -> ColorDef {
912 ColorDef::Named("Default".to_string()) }
914fn default_terminal_fg() -> ColorDef {
915 ColorDef::Named("Default".to_string()) }
917fn default_status_warning_indicator_bg() -> ColorDef {
918 ColorDef::Rgb(181, 137, 0) }
920fn default_status_warning_indicator_fg() -> ColorDef {
921 ColorDef::Rgb(0, 0, 0) }
923fn default_status_error_indicator_bg() -> ColorDef {
924 ColorDef::Rgb(220, 50, 47) }
926fn default_status_error_indicator_fg() -> ColorDef {
927 ColorDef::Rgb(255, 255, 255) }
929fn default_status_warning_indicator_hover_bg() -> ColorDef {
930 ColorDef::Rgb(211, 167, 30) }
932fn default_status_warning_indicator_hover_fg() -> ColorDef {
933 ColorDef::Rgb(0, 0, 0) }
935fn default_status_error_indicator_hover_bg() -> ColorDef {
936 ColorDef::Rgb(250, 80, 77) }
938fn default_status_error_indicator_hover_fg() -> ColorDef {
939 ColorDef::Rgb(255, 255, 255) }
941fn default_tab_drop_zone_bg() -> ColorDef {
942 ColorDef::Rgb(70, 130, 180) }
944fn default_tab_drop_zone_border() -> ColorDef {
945 ColorDef::Rgb(100, 149, 237) }
947fn default_settings_selected_bg() -> ColorDef {
948 ColorDef::Rgb(60, 60, 70) }
950fn default_settings_selected_fg() -> ColorDef {
951 ColorDef::Rgb(255, 255, 255) }
953#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
955pub struct SearchColors {
956 #[serde(default = "default_search_match_bg")]
958 pub match_bg: ColorDef,
959 #[serde(default = "default_search_match_fg")]
961 pub match_fg: ColorDef,
962 #[serde(default = "default_search_label_bg")]
966 pub label_bg: ColorDef,
967 #[serde(default = "default_search_label_fg")]
971 pub label_fg: ColorDef,
972}
973
974fn default_search_match_bg() -> ColorDef {
976 ColorDef::Rgb(100, 100, 20)
977}
978fn default_search_match_fg() -> ColorDef {
979 ColorDef::Rgb(255, 255, 255)
980}
981fn default_search_label_bg() -> ColorDef {
986 ColorDef::Rgb(199, 78, 189)
987}
988fn default_search_label_fg() -> ColorDef {
989 ColorDef::Rgb(255, 255, 255)
990}
991
992#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
994pub struct DiagnosticColors {
995 #[serde(default = "default_diagnostic_error_fg")]
997 pub error_fg: ColorDef,
998 #[serde(default = "default_diagnostic_error_bg")]
1000 pub error_bg: ColorDef,
1001 #[serde(default = "default_diagnostic_warning_fg")]
1003 pub warning_fg: ColorDef,
1004 #[serde(default = "default_diagnostic_warning_bg")]
1006 pub warning_bg: ColorDef,
1007 #[serde(default = "default_diagnostic_info_fg")]
1009 pub info_fg: ColorDef,
1010 #[serde(default = "default_diagnostic_info_bg")]
1012 pub info_bg: ColorDef,
1013 #[serde(default = "default_diagnostic_hint_fg")]
1015 pub hint_fg: ColorDef,
1016 #[serde(default = "default_diagnostic_hint_bg")]
1018 pub hint_bg: ColorDef,
1019}
1020
1021fn default_diagnostic_error_fg() -> ColorDef {
1023 ColorDef::Named("Red".to_string())
1024}
1025fn default_diagnostic_error_bg() -> ColorDef {
1026 ColorDef::Rgb(60, 20, 20)
1027}
1028fn default_diagnostic_warning_fg() -> ColorDef {
1029 ColorDef::Named("Yellow".to_string())
1030}
1031fn default_diagnostic_warning_bg() -> ColorDef {
1032 ColorDef::Rgb(60, 50, 0)
1033}
1034fn default_diagnostic_info_fg() -> ColorDef {
1035 ColorDef::Named("Blue".to_string())
1036}
1037fn default_diagnostic_info_bg() -> ColorDef {
1038 ColorDef::Rgb(0, 30, 60)
1039}
1040fn default_diagnostic_hint_fg() -> ColorDef {
1041 ColorDef::Named("Gray".to_string())
1042}
1043fn default_diagnostic_hint_bg() -> ColorDef {
1044 ColorDef::Rgb(30, 30, 30)
1045}
1046
1047#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1049pub struct SyntaxColors {
1050 #[serde(default = "default_syntax_keyword")]
1052 pub keyword: ColorDef,
1053 #[serde(default = "default_syntax_string")]
1055 pub string: ColorDef,
1056 #[serde(default = "default_syntax_comment")]
1058 pub comment: ColorDef,
1059 #[serde(default = "default_syntax_function")]
1061 pub function: ColorDef,
1062 #[serde(rename = "type", default = "default_syntax_type")]
1064 pub type_: ColorDef,
1065 #[serde(default = "default_syntax_variable")]
1067 pub variable: ColorDef,
1068 #[serde(default = "default_syntax_constant")]
1070 pub constant: ColorDef,
1071 #[serde(default = "default_syntax_operator")]
1073 pub operator: ColorDef,
1074 #[serde(default = "default_syntax_punctuation_bracket")]
1076 pub punctuation_bracket: ColorDef,
1077 #[serde(default = "default_syntax_punctuation_delimiter")]
1079 pub punctuation_delimiter: ColorDef,
1080}
1081
1082fn default_syntax_keyword() -> ColorDef {
1084 ColorDef::Rgb(86, 156, 214)
1085}
1086fn default_syntax_string() -> ColorDef {
1087 ColorDef::Rgb(206, 145, 120)
1088}
1089fn default_syntax_comment() -> ColorDef {
1090 ColorDef::Rgb(106, 153, 85)
1091}
1092fn default_syntax_function() -> ColorDef {
1093 ColorDef::Rgb(220, 220, 170)
1094}
1095fn default_syntax_type() -> ColorDef {
1096 ColorDef::Rgb(78, 201, 176)
1097}
1098fn default_syntax_variable() -> ColorDef {
1099 ColorDef::Rgb(156, 220, 254)
1100}
1101fn default_syntax_constant() -> ColorDef {
1102 ColorDef::Rgb(79, 193, 255)
1103}
1104fn default_syntax_operator() -> ColorDef {
1105 ColorDef::Rgb(212, 212, 212)
1106}
1107fn default_syntax_punctuation_bracket() -> ColorDef {
1108 ColorDef::Rgb(212, 212, 212) }
1110fn default_syntax_punctuation_delimiter() -> ColorDef {
1111 ColorDef::Rgb(212, 212, 212) }
1113
1114#[derive(Debug, Clone)]
1116pub struct Theme {
1117 pub name: String,
1119
1120 pub editor_bg: Color,
1122 pub editor_fg: Color,
1123 pub cursor: Color,
1124 pub inactive_cursor: Color,
1125 pub selection_bg: Color,
1126 pub selection_modifier: Modifier,
1131 pub current_line_bg: Color,
1132 pub line_number_fg: Color,
1133 pub line_number_bg: Color,
1134
1135 pub after_eof_bg: Color,
1137
1138 pub ruler_bg: Color,
1140
1141 pub whitespace_indicator_fg: Color,
1143
1144 pub diff_add_bg: Color,
1146 pub diff_remove_bg: Color,
1147 pub diff_modify_bg: Color,
1148 pub diff_add_highlight_bg: Color,
1150 pub diff_remove_highlight_bg: Color,
1152
1153 pub tab_active_fg: Color,
1155 pub tab_active_bg: Color,
1156 pub tab_inactive_fg: Color,
1157 pub tab_inactive_bg: Color,
1158 pub tab_separator_bg: Color,
1159 pub tab_close_hover_fg: Color,
1160 pub tab_hover_bg: Color,
1161
1162 pub menu_bg: Color,
1164 pub menu_fg: Color,
1165 pub menu_active_bg: Color,
1166 pub menu_active_fg: Color,
1167 pub menu_dropdown_bg: Color,
1168 pub menu_dropdown_fg: Color,
1169 pub menu_highlight_bg: Color,
1170 pub menu_highlight_fg: Color,
1171 pub menu_border_fg: Color,
1172 pub menu_separator_fg: Color,
1173 pub menu_hover_bg: Color,
1174 pub menu_hover_fg: Color,
1175 pub menu_disabled_fg: Color,
1176 pub menu_disabled_bg: Color,
1177
1178 pub status_bar_fg: Color,
1179 pub status_bar_bg: Color,
1180 pub status_palette_fg: Color,
1182 pub status_palette_bg: Color,
1183 pub status_lsp_on_fg: Color,
1185 pub status_lsp_on_bg: Color,
1186 pub status_lsp_actionable_fg: Color,
1189 pub status_lsp_actionable_bg: Color,
1190 pub prompt_fg: Color,
1191 pub prompt_bg: Color,
1192 pub prompt_selection_fg: Color,
1193 pub prompt_selection_bg: Color,
1194
1195 pub popup_border_fg: Color,
1196 pub popup_bg: Color,
1197 pub popup_selection_bg: Color,
1198 pub popup_selection_fg: Color,
1199 pub popup_text_fg: Color,
1200
1201 pub suggestion_bg: Color,
1202 pub suggestion_selected_bg: Color,
1203
1204 pub help_bg: Color,
1205 pub help_fg: Color,
1206 pub help_key_fg: Color,
1207 pub help_separator_fg: Color,
1208
1209 pub help_indicator_fg: Color,
1210 pub help_indicator_bg: Color,
1211
1212 pub inline_code_bg: Color,
1214
1215 pub split_separator_fg: Color,
1216 pub split_separator_hover_fg: Color,
1217
1218 pub scrollbar_track_fg: Color,
1220 pub scrollbar_thumb_fg: Color,
1221 pub scrollbar_track_hover_fg: Color,
1222 pub scrollbar_thumb_hover_fg: Color,
1223
1224 pub compose_margin_bg: Color,
1226
1227 pub semantic_highlight_bg: Color,
1229 pub semantic_highlight_modifier: Modifier,
1234
1235 pub terminal_bg: Color,
1237 pub terminal_fg: Color,
1238
1239 pub status_warning_indicator_bg: Color,
1241 pub status_warning_indicator_fg: Color,
1242 pub status_error_indicator_bg: Color,
1243 pub status_error_indicator_fg: Color,
1244 pub status_warning_indicator_hover_bg: Color,
1245 pub status_warning_indicator_hover_fg: Color,
1246 pub status_error_indicator_hover_bg: Color,
1247 pub status_error_indicator_hover_fg: Color,
1248
1249 pub tab_drop_zone_bg: Color,
1251 pub tab_drop_zone_border: Color,
1252
1253 pub settings_selected_bg: Color,
1255 pub settings_selected_fg: Color,
1256
1257 pub file_status_added_fg: Color,
1259 pub file_status_modified_fg: Color,
1260 pub file_status_deleted_fg: Color,
1261 pub file_status_renamed_fg: Color,
1262 pub file_status_untracked_fg: Color,
1263 pub file_status_conflicted_fg: Color,
1264
1265 pub search_match_bg: Color,
1267 pub search_match_fg: Color,
1268 pub search_label_bg: Color,
1269 pub search_label_fg: Color,
1270
1271 pub diagnostic_error_fg: Color,
1273 pub diagnostic_error_bg: Color,
1274 pub diagnostic_warning_fg: Color,
1275 pub diagnostic_warning_bg: Color,
1276 pub diagnostic_info_fg: Color,
1277 pub diagnostic_info_bg: Color,
1278 pub diagnostic_hint_fg: Color,
1279 pub diagnostic_hint_bg: Color,
1280
1281 pub syntax_keyword: Color,
1283 pub syntax_string: Color,
1284 pub syntax_comment: Color,
1285 pub syntax_function: Color,
1286 pub syntax_type: Color,
1287 pub syntax_variable: Color,
1288 pub syntax_constant: Color,
1289 pub syntax_operator: Color,
1290 pub syntax_punctuation_bracket: Color,
1291 pub syntax_punctuation_delimiter: Color,
1292}
1293
1294impl From<ThemeFile> for Theme {
1295 fn from(file: ThemeFile) -> Self {
1296 Self {
1297 name: file.name,
1298 editor_bg: file.editor.bg.clone().into(),
1299 editor_fg: file.editor.fg.into(),
1300 cursor: file.editor.cursor.into(),
1301 inactive_cursor: file.editor.inactive_cursor.into(),
1302 selection_bg: file.editor.selection_bg.into(),
1303 selection_modifier: file
1304 .editor
1305 .selection_modifier
1306 .as_ref()
1307 .map(Modifier::from)
1308 .unwrap_or(Modifier::empty()),
1309 current_line_bg: file.editor.current_line_bg.into(),
1310 line_number_fg: file.editor.line_number_fg.into(),
1311 line_number_bg: file.editor.line_number_bg.into(),
1312 after_eof_bg: file
1315 .editor
1316 .after_eof_bg
1317 .clone()
1318 .map(|c| c.into())
1319 .unwrap_or_else(|| shade_toward_contrast(file.editor.bg.clone().into(), 10)),
1320 ruler_bg: file.editor.ruler_bg.into(),
1321 whitespace_indicator_fg: file.editor.whitespace_indicator_fg.into(),
1322 diff_add_bg: file.editor.diff_add_bg.clone().into(),
1323 diff_remove_bg: file.editor.diff_remove_bg.clone().into(),
1324 diff_modify_bg: file.editor.diff_modify_bg.into(),
1325 diff_add_highlight_bg: file
1327 .editor
1328 .diff_add_highlight_bg
1329 .map(|c| c.into())
1330 .unwrap_or_else(|| brighten_color(file.editor.diff_add_bg.into(), 40)),
1331 diff_remove_highlight_bg: file
1332 .editor
1333 .diff_remove_highlight_bg
1334 .map(|c| c.into())
1335 .unwrap_or_else(|| brighten_color(file.editor.diff_remove_bg.into(), 40)),
1336 tab_active_fg: file.ui.tab_active_fg.into(),
1337 tab_active_bg: file.ui.tab_active_bg.into(),
1338 tab_inactive_fg: file.ui.tab_inactive_fg.into(),
1339 tab_inactive_bg: file.ui.tab_inactive_bg.into(),
1340 tab_separator_bg: file.ui.tab_separator_bg.into(),
1341 tab_close_hover_fg: file.ui.tab_close_hover_fg.into(),
1342 tab_hover_bg: file.ui.tab_hover_bg.into(),
1343 menu_bg: file.ui.menu_bg.into(),
1344 menu_fg: file.ui.menu_fg.into(),
1345 menu_active_bg: file.ui.menu_active_bg.into(),
1346 menu_active_fg: file.ui.menu_active_fg.into(),
1347 menu_dropdown_bg: file.ui.menu_dropdown_bg.into(),
1348 menu_dropdown_fg: file.ui.menu_dropdown_fg.into(),
1349 menu_highlight_bg: file.ui.menu_highlight_bg.into(),
1350 menu_highlight_fg: file.ui.menu_highlight_fg.into(),
1351 menu_border_fg: file.ui.menu_border_fg.into(),
1352 menu_separator_fg: file.ui.menu_separator_fg.into(),
1353 menu_hover_bg: file.ui.menu_hover_bg.into(),
1354 menu_hover_fg: file.ui.menu_hover_fg.into(),
1355 menu_disabled_fg: file.ui.menu_disabled_fg.into(),
1356 menu_disabled_bg: file.ui.menu_disabled_bg.into(),
1357 status_bar_fg: file.ui.status_bar_fg.clone().into(),
1358 status_bar_bg: file.ui.status_bar_bg.clone().into(),
1359 status_palette_fg: file
1360 .ui
1361 .status_palette_fg
1362 .clone()
1363 .map(|c| c.into())
1364 .unwrap_or_else(|| file.ui.status_bar_fg.clone().into()),
1365 status_palette_bg: file
1366 .ui
1367 .status_palette_bg
1368 .clone()
1369 .map(|c| c.into())
1370 .unwrap_or_else(|| file.ui.status_bar_bg.clone().into()),
1371 status_lsp_on_fg: file
1372 .ui
1373 .status_lsp_on_fg
1374 .clone()
1375 .map(|c| c.into())
1376 .unwrap_or_else(|| file.ui.status_bar_fg.clone().into()),
1377 status_lsp_on_bg: file
1378 .ui
1379 .status_lsp_on_bg
1380 .clone()
1381 .map(|c| c.into())
1382 .unwrap_or_else(|| file.ui.status_bar_bg.clone().into()),
1383 status_lsp_actionable_fg: file
1384 .ui
1385 .status_lsp_actionable_fg
1386 .clone()
1387 .map(|c| c.into())
1388 .unwrap_or_else(|| file.ui.status_warning_indicator_fg.clone().into()),
1389 status_lsp_actionable_bg: file
1390 .ui
1391 .status_lsp_actionable_bg
1392 .clone()
1393 .map(|c| c.into())
1394 .unwrap_or_else(|| file.ui.status_warning_indicator_bg.clone().into()),
1395 prompt_fg: file.ui.prompt_fg.into(),
1396 prompt_bg: file.ui.prompt_bg.into(),
1397 prompt_selection_fg: file.ui.prompt_selection_fg.into(),
1398 prompt_selection_bg: file.ui.prompt_selection_bg.into(),
1399 popup_border_fg: file.ui.popup_border_fg.into(),
1400 popup_bg: file.ui.popup_bg.into(),
1401 popup_selection_bg: file.ui.popup_selection_bg.into(),
1402 popup_selection_fg: file.ui.popup_selection_fg.into(),
1403 popup_text_fg: file.ui.popup_text_fg.into(),
1404 suggestion_bg: file.ui.suggestion_bg.into(),
1405 suggestion_selected_bg: file.ui.suggestion_selected_bg.into(),
1406 help_bg: file.ui.help_bg.into(),
1407 help_fg: file.ui.help_fg.into(),
1408 help_key_fg: file.ui.help_key_fg.into(),
1409 help_separator_fg: file.ui.help_separator_fg.into(),
1410 help_indicator_fg: file.ui.help_indicator_fg.into(),
1411 help_indicator_bg: file.ui.help_indicator_bg.into(),
1412 inline_code_bg: file.ui.inline_code_bg.into(),
1413 split_separator_fg: file.ui.split_separator_fg.into(),
1414 split_separator_hover_fg: file.ui.split_separator_hover_fg.into(),
1415 scrollbar_track_fg: file.ui.scrollbar_track_fg.into(),
1416 scrollbar_thumb_fg: file.ui.scrollbar_thumb_fg.into(),
1417 scrollbar_track_hover_fg: file.ui.scrollbar_track_hover_fg.into(),
1418 scrollbar_thumb_hover_fg: file.ui.scrollbar_thumb_hover_fg.into(),
1419 compose_margin_bg: file.ui.compose_margin_bg.into(),
1420 semantic_highlight_bg: file.ui.semantic_highlight_bg.into(),
1421 semantic_highlight_modifier: file
1422 .ui
1423 .semantic_highlight_modifier
1424 .as_ref()
1425 .map(Modifier::from)
1426 .unwrap_or(Modifier::empty()),
1427 terminal_bg: file.ui.terminal_bg.into(),
1428 terminal_fg: file.ui.terminal_fg.into(),
1429 status_warning_indicator_bg: file.ui.status_warning_indicator_bg.into(),
1430 status_warning_indicator_fg: file.ui.status_warning_indicator_fg.into(),
1431 status_error_indicator_bg: file.ui.status_error_indicator_bg.into(),
1432 status_error_indicator_fg: file.ui.status_error_indicator_fg.into(),
1433 status_warning_indicator_hover_bg: file.ui.status_warning_indicator_hover_bg.into(),
1434 status_warning_indicator_hover_fg: file.ui.status_warning_indicator_hover_fg.into(),
1435 status_error_indicator_hover_bg: file.ui.status_error_indicator_hover_bg.into(),
1436 status_error_indicator_hover_fg: file.ui.status_error_indicator_hover_fg.into(),
1437 tab_drop_zone_bg: file.ui.tab_drop_zone_bg.into(),
1438 tab_drop_zone_border: file.ui.tab_drop_zone_border.into(),
1439 settings_selected_bg: file.ui.settings_selected_bg.into(),
1440 settings_selected_fg: file.ui.settings_selected_fg.into(),
1441 file_status_added_fg: file
1442 .ui
1443 .file_status_added_fg
1444 .map(|c| c.into())
1445 .unwrap_or_else(|| file.diagnostic.info_fg.clone().into()),
1446 file_status_modified_fg: file
1447 .ui
1448 .file_status_modified_fg
1449 .map(|c| c.into())
1450 .unwrap_or_else(|| file.diagnostic.warning_fg.clone().into()),
1451 file_status_deleted_fg: file
1452 .ui
1453 .file_status_deleted_fg
1454 .map(|c| c.into())
1455 .unwrap_or_else(|| file.diagnostic.error_fg.clone().into()),
1456 file_status_renamed_fg: file
1457 .ui
1458 .file_status_renamed_fg
1459 .map(|c| c.into())
1460 .unwrap_or_else(|| file.diagnostic.info_fg.clone().into()),
1461 file_status_untracked_fg: file
1462 .ui
1463 .file_status_untracked_fg
1464 .map(|c| c.into())
1465 .unwrap_or_else(|| file.diagnostic.hint_fg.clone().into()),
1466 file_status_conflicted_fg: file
1467 .ui
1468 .file_status_conflicted_fg
1469 .map(|c| c.into())
1470 .unwrap_or_else(|| file.diagnostic.error_fg.clone().into()),
1471 search_match_bg: file.search.match_bg.into(),
1472 search_match_fg: file.search.match_fg.into(),
1473 search_label_bg: file.search.label_bg.into(),
1474 search_label_fg: file.search.label_fg.into(),
1475 diagnostic_error_fg: file.diagnostic.error_fg.into(),
1476 diagnostic_error_bg: file.diagnostic.error_bg.into(),
1477 diagnostic_warning_fg: file.diagnostic.warning_fg.into(),
1478 diagnostic_warning_bg: file.diagnostic.warning_bg.into(),
1479 diagnostic_info_fg: file.diagnostic.info_fg.into(),
1480 diagnostic_info_bg: file.diagnostic.info_bg.into(),
1481 diagnostic_hint_fg: file.diagnostic.hint_fg.into(),
1482 diagnostic_hint_bg: file.diagnostic.hint_bg.into(),
1483 syntax_keyword: file.syntax.keyword.into(),
1484 syntax_string: file.syntax.string.into(),
1485 syntax_comment: file.syntax.comment.into(),
1486 syntax_function: file.syntax.function.into(),
1487 syntax_type: file.syntax.type_.into(),
1488 syntax_variable: file.syntax.variable.into(),
1489 syntax_constant: file.syntax.constant.into(),
1490 syntax_operator: file.syntax.operator.into(),
1491 syntax_punctuation_bracket: file.syntax.punctuation_bracket.into(),
1492 syntax_punctuation_delimiter: file.syntax.punctuation_delimiter.into(),
1493 }
1494 }
1495}
1496
1497impl From<Theme> for ThemeFile {
1498 fn from(theme: Theme) -> Self {
1499 Self {
1500 name: theme.name,
1501 extends: None,
1504 editor: EditorColors {
1505 bg: theme.editor_bg.into(),
1506 fg: theme.editor_fg.into(),
1507 cursor: theme.cursor.into(),
1508 inactive_cursor: theme.inactive_cursor.into(),
1509 selection_bg: theme.selection_bg.into(),
1510 selection_modifier: if theme.selection_modifier.is_empty() {
1511 None
1512 } else {
1513 Some(theme.selection_modifier.into())
1514 },
1515 current_line_bg: theme.current_line_bg.into(),
1516 line_number_fg: theme.line_number_fg.into(),
1517 line_number_bg: theme.line_number_bg.into(),
1518 diff_add_bg: theme.diff_add_bg.into(),
1519 diff_remove_bg: theme.diff_remove_bg.into(),
1520 diff_add_highlight_bg: Some(theme.diff_add_highlight_bg.into()),
1521 diff_remove_highlight_bg: Some(theme.diff_remove_highlight_bg.into()),
1522 diff_modify_bg: theme.diff_modify_bg.into(),
1523 ruler_bg: theme.ruler_bg.into(),
1524 whitespace_indicator_fg: theme.whitespace_indicator_fg.into(),
1525 after_eof_bg: Some(theme.after_eof_bg.into()),
1526 },
1527 ui: UiColors {
1528 tab_active_fg: theme.tab_active_fg.into(),
1529 tab_active_bg: theme.tab_active_bg.into(),
1530 tab_inactive_fg: theme.tab_inactive_fg.into(),
1531 tab_inactive_bg: theme.tab_inactive_bg.into(),
1532 tab_separator_bg: theme.tab_separator_bg.into(),
1533 tab_close_hover_fg: theme.tab_close_hover_fg.into(),
1534 tab_hover_bg: theme.tab_hover_bg.into(),
1535 menu_bg: theme.menu_bg.into(),
1536 menu_fg: theme.menu_fg.into(),
1537 menu_active_bg: theme.menu_active_bg.into(),
1538 menu_active_fg: theme.menu_active_fg.into(),
1539 menu_dropdown_bg: theme.menu_dropdown_bg.into(),
1540 menu_dropdown_fg: theme.menu_dropdown_fg.into(),
1541 menu_highlight_bg: theme.menu_highlight_bg.into(),
1542 menu_highlight_fg: theme.menu_highlight_fg.into(),
1543 menu_border_fg: theme.menu_border_fg.into(),
1544 menu_separator_fg: theme.menu_separator_fg.into(),
1545 menu_hover_bg: theme.menu_hover_bg.into(),
1546 menu_hover_fg: theme.menu_hover_fg.into(),
1547 menu_disabled_fg: theme.menu_disabled_fg.into(),
1548 menu_disabled_bg: theme.menu_disabled_bg.into(),
1549 status_bar_fg: theme.status_bar_fg.into(),
1550 status_bar_bg: theme.status_bar_bg.into(),
1551 status_palette_fg: Some(theme.status_palette_fg.into()),
1552 status_palette_bg: Some(theme.status_palette_bg.into()),
1553 status_lsp_on_fg: Some(theme.status_lsp_on_fg.into()),
1554 status_lsp_on_bg: Some(theme.status_lsp_on_bg.into()),
1555 status_lsp_actionable_fg: Some(theme.status_lsp_actionable_fg.into()),
1556 status_lsp_actionable_bg: Some(theme.status_lsp_actionable_bg.into()),
1557 prompt_fg: theme.prompt_fg.into(),
1558 prompt_bg: theme.prompt_bg.into(),
1559 prompt_selection_fg: theme.prompt_selection_fg.into(),
1560 prompt_selection_bg: theme.prompt_selection_bg.into(),
1561 popup_border_fg: theme.popup_border_fg.into(),
1562 popup_bg: theme.popup_bg.into(),
1563 popup_selection_bg: theme.popup_selection_bg.into(),
1564 popup_selection_fg: theme.popup_selection_fg.into(),
1565 popup_text_fg: theme.popup_text_fg.into(),
1566 suggestion_bg: theme.suggestion_bg.into(),
1567 suggestion_selected_bg: theme.suggestion_selected_bg.into(),
1568 help_bg: theme.help_bg.into(),
1569 help_fg: theme.help_fg.into(),
1570 help_key_fg: theme.help_key_fg.into(),
1571 help_separator_fg: theme.help_separator_fg.into(),
1572 help_indicator_fg: theme.help_indicator_fg.into(),
1573 help_indicator_bg: theme.help_indicator_bg.into(),
1574 inline_code_bg: theme.inline_code_bg.into(),
1575 split_separator_fg: theme.split_separator_fg.into(),
1576 split_separator_hover_fg: theme.split_separator_hover_fg.into(),
1577 scrollbar_track_fg: theme.scrollbar_track_fg.into(),
1578 scrollbar_thumb_fg: theme.scrollbar_thumb_fg.into(),
1579 scrollbar_track_hover_fg: theme.scrollbar_track_hover_fg.into(),
1580 scrollbar_thumb_hover_fg: theme.scrollbar_thumb_hover_fg.into(),
1581 compose_margin_bg: theme.compose_margin_bg.into(),
1582 semantic_highlight_bg: theme.semantic_highlight_bg.into(),
1583 semantic_highlight_modifier: if theme.semantic_highlight_modifier.is_empty() {
1584 None
1585 } else {
1586 Some(theme.semantic_highlight_modifier.into())
1587 },
1588 terminal_bg: theme.terminal_bg.into(),
1589 terminal_fg: theme.terminal_fg.into(),
1590 status_warning_indicator_bg: theme.status_warning_indicator_bg.into(),
1591 status_warning_indicator_fg: theme.status_warning_indicator_fg.into(),
1592 status_error_indicator_bg: theme.status_error_indicator_bg.into(),
1593 status_error_indicator_fg: theme.status_error_indicator_fg.into(),
1594 status_warning_indicator_hover_bg: theme.status_warning_indicator_hover_bg.into(),
1595 status_warning_indicator_hover_fg: theme.status_warning_indicator_hover_fg.into(),
1596 status_error_indicator_hover_bg: theme.status_error_indicator_hover_bg.into(),
1597 status_error_indicator_hover_fg: theme.status_error_indicator_hover_fg.into(),
1598 tab_drop_zone_bg: theme.tab_drop_zone_bg.into(),
1599 tab_drop_zone_border: theme.tab_drop_zone_border.into(),
1600 settings_selected_bg: theme.settings_selected_bg.into(),
1601 settings_selected_fg: theme.settings_selected_fg.into(),
1602 file_status_added_fg: Some(theme.file_status_added_fg.into()),
1603 file_status_modified_fg: Some(theme.file_status_modified_fg.into()),
1604 file_status_deleted_fg: Some(theme.file_status_deleted_fg.into()),
1605 file_status_renamed_fg: Some(theme.file_status_renamed_fg.into()),
1606 file_status_untracked_fg: Some(theme.file_status_untracked_fg.into()),
1607 file_status_conflicted_fg: Some(theme.file_status_conflicted_fg.into()),
1608 },
1609 search: SearchColors {
1610 match_bg: theme.search_match_bg.into(),
1611 match_fg: theme.search_match_fg.into(),
1612 label_bg: theme.search_label_bg.into(),
1613 label_fg: theme.search_label_fg.into(),
1614 },
1615 diagnostic: DiagnosticColors {
1616 error_fg: theme.diagnostic_error_fg.into(),
1617 error_bg: theme.diagnostic_error_bg.into(),
1618 warning_fg: theme.diagnostic_warning_fg.into(),
1619 warning_bg: theme.diagnostic_warning_bg.into(),
1620 info_fg: theme.diagnostic_info_fg.into(),
1621 info_bg: theme.diagnostic_info_bg.into(),
1622 hint_fg: theme.diagnostic_hint_fg.into(),
1623 hint_bg: theme.diagnostic_hint_bg.into(),
1624 },
1625 syntax: SyntaxColors {
1626 keyword: theme.syntax_keyword.into(),
1627 string: theme.syntax_string.into(),
1628 comment: theme.syntax_comment.into(),
1629 function: theme.syntax_function.into(),
1630 type_: theme.syntax_type.into(),
1631 variable: theme.syntax_variable.into(),
1632 constant: theme.syntax_constant.into(),
1633 operator: theme.syntax_operator.into(),
1634 punctuation_bracket: theme.syntax_punctuation_bracket.into(),
1635 punctuation_delimiter: theme.syntax_punctuation_delimiter.into(),
1636 },
1637 }
1638 }
1639}
1640
1641fn resolve_base_theme(theme_file: &ThemeFile, raw: &serde_json::Value) -> Result<Theme, String> {
1648 if let Some(extends) = theme_file.extends.as_deref() {
1650 let name = extends.strip_prefix("builtin://").unwrap_or(extends);
1651 return Theme::load_builtin(name).ok_or_else(|| {
1652 let available: Vec<&str> = BUILTIN_THEMES.iter().map(|t| t.name).collect();
1653 format!(
1654 "theme `extends: {:?}` does not match any built-in theme. \
1655 Available: {}. \
1656 Inheriting from other user themes is not yet supported.",
1657 extends,
1658 available.join(", ")
1659 )
1660 });
1661 }
1662
1663 if let Some(bg) = raw
1669 .get("editor")
1670 .and_then(|e| e.get("bg"))
1671 .cloned()
1672 .and_then(|v| serde_json::from_value::<ColorDef>(v).ok())
1673 {
1674 let color: Color = bg.into();
1675 if let Some((r, g, b)) = color_to_rgb(color) {
1676 let lum = relative_luminance(r, g, b);
1677 let base_name = if lum > 0.5 { THEME_LIGHT } else { THEME_DARK };
1678 if let Some(base) = Theme::load_builtin(base_name) {
1679 return Ok(base);
1680 }
1681 }
1682 }
1683
1684 Ok(theme_file.clone().into())
1686}
1687
1688fn relative_luminance(r: u8, g: u8, b: u8) -> f64 {
1691 0.2126 * (r as f64 / 255.0) + 0.7152 * (g as f64 / 255.0) + 0.0722 * (b as f64 / 255.0)
1692}
1693
1694fn apply_theme_overrides(theme: &mut Theme, theme_file: &ThemeFile, raw: &serde_json::Value) {
1699 theme.name = theme_file.name.clone();
1701
1702 for section in ["editor", "ui", "search", "diagnostic", "syntax"] {
1703 let Some(obj) = raw.get(section).and_then(|v| v.as_object()) else {
1704 continue;
1705 };
1706 for (field, value) in obj {
1707 if value.is_null() {
1710 continue;
1711 }
1712 let key = format!("{}.{}", section, field);
1713 if let Ok(color_def) = serde_json::from_value::<ColorDef>(value.clone()) {
1714 if let Some(slot) = theme.resolve_theme_key_mut(&key) {
1715 *slot = color_def.into();
1716 }
1717 }
1718 }
1719 }
1720}
1721
1722impl Theme {
1723 pub fn is_light(&self) -> bool {
1729 color_to_rgb(self.editor_bg)
1730 .map(|(r, g, b)| relative_luminance(r, g, b) > 0.5)
1731 .unwrap_or(false)
1732 }
1733
1734 pub fn load_builtin(name: &str) -> Option<Self> {
1736 BUILTIN_THEMES
1737 .iter()
1738 .find(|t| t.name == name)
1739 .and_then(|t| serde_json::from_str::<ThemeFile>(t.json).ok())
1740 .map(|tf| tf.into())
1741 }
1742
1743 pub fn from_json(json: &str) -> Result<Self, String> {
1753 let raw: serde_json::Value =
1758 serde_json::from_str(json).map_err(|e| format!("Failed to parse theme JSON: {}", e))?;
1759 let theme_file: ThemeFile = serde_json::from_value(raw.clone())
1760 .map_err(|e| format!("Failed to parse theme: {}", e))?;
1761
1762 let mut theme = resolve_base_theme(&theme_file, &raw)?;
1763 apply_theme_overrides(&mut theme, &theme_file, &raw);
1764 Ok(theme)
1765 }
1766
1767 pub fn modifier_for_bg_key(&self, key: &str) -> Modifier {
1776 match key {
1777 "editor.selection_bg" => self.selection_modifier,
1778 "ui.semantic_highlight_bg" => self.semantic_highlight_modifier,
1779 _ => Modifier::empty(),
1780 }
1781 }
1782
1783 pub fn resolve_theme_key(&self, key: &str) -> Option<Color> {
1794 let parts: Vec<&str> = key.split('.').collect();
1796 if parts.len() != 2 {
1797 return None;
1798 }
1799
1800 let (section, field) = (parts[0], parts[1]);
1801
1802 match section {
1803 "editor" => match field {
1804 "bg" => Some(self.editor_bg),
1805 "fg" => Some(self.editor_fg),
1806 "cursor" => Some(self.cursor),
1807 "inactive_cursor" => Some(self.inactive_cursor),
1808 "selection_bg" => Some(self.selection_bg),
1809 "current_line_bg" => Some(self.current_line_bg),
1810 "line_number_fg" => Some(self.line_number_fg),
1811 "line_number_bg" => Some(self.line_number_bg),
1812 "diff_add_bg" => Some(self.diff_add_bg),
1813 "diff_remove_bg" => Some(self.diff_remove_bg),
1814 "diff_modify_bg" => Some(self.diff_modify_bg),
1815 "ruler_bg" => Some(self.ruler_bg),
1816 "whitespace_indicator_fg" => Some(self.whitespace_indicator_fg),
1817 _ => None,
1818 },
1819 "ui" => match field {
1820 "tab_active_fg" => Some(self.tab_active_fg),
1821 "tab_active_bg" => Some(self.tab_active_bg),
1822 "tab_inactive_fg" => Some(self.tab_inactive_fg),
1823 "tab_inactive_bg" => Some(self.tab_inactive_bg),
1824 "status_bar_fg" => Some(self.status_bar_fg),
1825 "status_bar_bg" => Some(self.status_bar_bg),
1826 "status_palette_fg" => Some(self.status_palette_fg),
1827 "status_palette_bg" => Some(self.status_palette_bg),
1828 "status_lsp_on_fg" => Some(self.status_lsp_on_fg),
1829 "status_lsp_on_bg" => Some(self.status_lsp_on_bg),
1830 "status_lsp_actionable_fg" => Some(self.status_lsp_actionable_fg),
1831 "status_lsp_actionable_bg" => Some(self.status_lsp_actionable_bg),
1832 "prompt_fg" => Some(self.prompt_fg),
1833 "prompt_bg" => Some(self.prompt_bg),
1834 "prompt_selection_fg" => Some(self.prompt_selection_fg),
1835 "prompt_selection_bg" => Some(self.prompt_selection_bg),
1836 "popup_bg" => Some(self.popup_bg),
1837 "popup_border_fg" => Some(self.popup_border_fg),
1838 "popup_selection_bg" => Some(self.popup_selection_bg),
1839 "popup_selection_fg" => Some(self.popup_selection_fg),
1840 "popup_text_fg" => Some(self.popup_text_fg),
1841 "menu_bg" => Some(self.menu_bg),
1842 "menu_fg" => Some(self.menu_fg),
1843 "menu_active_bg" => Some(self.menu_active_bg),
1844 "menu_active_fg" => Some(self.menu_active_fg),
1845 "help_bg" => Some(self.help_bg),
1846 "help_fg" => Some(self.help_fg),
1847 "help_key_fg" => Some(self.help_key_fg),
1848 "split_separator_fg" => Some(self.split_separator_fg),
1849 "scrollbar_thumb_fg" => Some(self.scrollbar_thumb_fg),
1850 "semantic_highlight_bg" => Some(self.semantic_highlight_bg),
1851 "file_status_added_fg" => Some(self.file_status_added_fg),
1852 "file_status_modified_fg" => Some(self.file_status_modified_fg),
1853 "file_status_deleted_fg" => Some(self.file_status_deleted_fg),
1854 "file_status_renamed_fg" => Some(self.file_status_renamed_fg),
1855 "file_status_untracked_fg" => Some(self.file_status_untracked_fg),
1856 "file_status_conflicted_fg" => Some(self.file_status_conflicted_fg),
1857 _ => None,
1858 },
1859 "syntax" => match field {
1860 "keyword" => Some(self.syntax_keyword),
1861 "string" => Some(self.syntax_string),
1862 "comment" => Some(self.syntax_comment),
1863 "function" => Some(self.syntax_function),
1864 "type" => Some(self.syntax_type),
1865 "variable" => Some(self.syntax_variable),
1866 "constant" => Some(self.syntax_constant),
1867 "operator" => Some(self.syntax_operator),
1868 "punctuation_bracket" => Some(self.syntax_punctuation_bracket),
1869 "punctuation_delimiter" => Some(self.syntax_punctuation_delimiter),
1870 _ => None,
1871 },
1872 "diagnostic" => match field {
1873 "error_fg" => Some(self.diagnostic_error_fg),
1874 "error_bg" => Some(self.diagnostic_error_bg),
1875 "warning_fg" => Some(self.diagnostic_warning_fg),
1876 "warning_bg" => Some(self.diagnostic_warning_bg),
1877 "info_fg" => Some(self.diagnostic_info_fg),
1878 "info_bg" => Some(self.diagnostic_info_bg),
1879 "hint_fg" => Some(self.diagnostic_hint_fg),
1880 "hint_bg" => Some(self.diagnostic_hint_bg),
1881 _ => None,
1882 },
1883 "search" => match field {
1884 "match_bg" => Some(self.search_match_bg),
1885 "match_fg" => Some(self.search_match_fg),
1886 "label_bg" => Some(self.search_label_bg),
1887 "label_fg" => Some(self.search_label_fg),
1888 _ => None,
1889 },
1890 _ => None,
1891 }
1892 }
1893
1894 pub fn resolve_theme_key_mut(&mut self, key: &str) -> Option<&mut Color> {
1898 let parts: Vec<&str> = key.split('.').collect();
1899 if parts.len() != 2 {
1900 return None;
1901 }
1902 let (section, field) = (parts[0], parts[1]);
1903 match section {
1904 "editor" => match field {
1905 "bg" => Some(&mut self.editor_bg),
1906 "fg" => Some(&mut self.editor_fg),
1907 "cursor" => Some(&mut self.cursor),
1908 "inactive_cursor" => Some(&mut self.inactive_cursor),
1909 "selection_bg" => Some(&mut self.selection_bg),
1910 "current_line_bg" => Some(&mut self.current_line_bg),
1911 "line_number_fg" => Some(&mut self.line_number_fg),
1912 "line_number_bg" => Some(&mut self.line_number_bg),
1913 "diff_add_bg" => Some(&mut self.diff_add_bg),
1914 "diff_remove_bg" => Some(&mut self.diff_remove_bg),
1915 "diff_modify_bg" => Some(&mut self.diff_modify_bg),
1916 "ruler_bg" => Some(&mut self.ruler_bg),
1917 "whitespace_indicator_fg" => Some(&mut self.whitespace_indicator_fg),
1918 _ => None,
1919 },
1920 "ui" => match field {
1921 "tab_active_fg" => Some(&mut self.tab_active_fg),
1922 "tab_active_bg" => Some(&mut self.tab_active_bg),
1923 "tab_inactive_fg" => Some(&mut self.tab_inactive_fg),
1924 "tab_inactive_bg" => Some(&mut self.tab_inactive_bg),
1925 "status_bar_fg" => Some(&mut self.status_bar_fg),
1926 "status_bar_bg" => Some(&mut self.status_bar_bg),
1927 "status_palette_fg" => Some(&mut self.status_palette_fg),
1928 "status_palette_bg" => Some(&mut self.status_palette_bg),
1929 "status_lsp_on_fg" => Some(&mut self.status_lsp_on_fg),
1930 "status_lsp_on_bg" => Some(&mut self.status_lsp_on_bg),
1931 "status_lsp_actionable_fg" => Some(&mut self.status_lsp_actionable_fg),
1932 "status_lsp_actionable_bg" => Some(&mut self.status_lsp_actionable_bg),
1933 "prompt_fg" => Some(&mut self.prompt_fg),
1934 "prompt_bg" => Some(&mut self.prompt_bg),
1935 "prompt_selection_fg" => Some(&mut self.prompt_selection_fg),
1936 "prompt_selection_bg" => Some(&mut self.prompt_selection_bg),
1937 "popup_bg" => Some(&mut self.popup_bg),
1938 "popup_border_fg" => Some(&mut self.popup_border_fg),
1939 "popup_selection_bg" => Some(&mut self.popup_selection_bg),
1940 "popup_selection_fg" => Some(&mut self.popup_selection_fg),
1941 "popup_text_fg" => Some(&mut self.popup_text_fg),
1942 "menu_bg" => Some(&mut self.menu_bg),
1943 "menu_fg" => Some(&mut self.menu_fg),
1944 "menu_active_bg" => Some(&mut self.menu_active_bg),
1945 "menu_active_fg" => Some(&mut self.menu_active_fg),
1946 "help_bg" => Some(&mut self.help_bg),
1947 "help_fg" => Some(&mut self.help_fg),
1948 "help_key_fg" => Some(&mut self.help_key_fg),
1949 "split_separator_fg" => Some(&mut self.split_separator_fg),
1950 "scrollbar_thumb_fg" => Some(&mut self.scrollbar_thumb_fg),
1951 "semantic_highlight_bg" => Some(&mut self.semantic_highlight_bg),
1952 "file_status_added_fg" => Some(&mut self.file_status_added_fg),
1953 "file_status_modified_fg" => Some(&mut self.file_status_modified_fg),
1954 "file_status_deleted_fg" => Some(&mut self.file_status_deleted_fg),
1955 "file_status_renamed_fg" => Some(&mut self.file_status_renamed_fg),
1956 "file_status_untracked_fg" => Some(&mut self.file_status_untracked_fg),
1957 "file_status_conflicted_fg" => Some(&mut self.file_status_conflicted_fg),
1958 _ => None,
1959 },
1960 "syntax" => match field {
1961 "keyword" => Some(&mut self.syntax_keyword),
1962 "string" => Some(&mut self.syntax_string),
1963 "comment" => Some(&mut self.syntax_comment),
1964 "function" => Some(&mut self.syntax_function),
1965 "type" => Some(&mut self.syntax_type),
1966 "variable" => Some(&mut self.syntax_variable),
1967 "constant" => Some(&mut self.syntax_constant),
1968 "operator" => Some(&mut self.syntax_operator),
1969 "punctuation_bracket" => Some(&mut self.syntax_punctuation_bracket),
1970 "punctuation_delimiter" => Some(&mut self.syntax_punctuation_delimiter),
1971 _ => None,
1972 },
1973 "diagnostic" => match field {
1974 "error_fg" => Some(&mut self.diagnostic_error_fg),
1975 "error_bg" => Some(&mut self.diagnostic_error_bg),
1976 "warning_fg" => Some(&mut self.diagnostic_warning_fg),
1977 "warning_bg" => Some(&mut self.diagnostic_warning_bg),
1978 "info_fg" => Some(&mut self.diagnostic_info_fg),
1979 "info_bg" => Some(&mut self.diagnostic_info_bg),
1980 "hint_fg" => Some(&mut self.diagnostic_hint_fg),
1981 "hint_bg" => Some(&mut self.diagnostic_hint_bg),
1982 _ => None,
1983 },
1984 "search" => match field {
1985 "match_bg" => Some(&mut self.search_match_bg),
1986 "match_fg" => Some(&mut self.search_match_fg),
1987 "label_bg" => Some(&mut self.search_label_bg),
1988 "label_fg" => Some(&mut self.search_label_fg),
1989 _ => None,
1990 },
1991 _ => None,
1992 }
1993 }
1994
1995 pub fn override_colors<I, K>(&mut self, overrides: I) -> usize
2000 where
2001 I: IntoIterator<Item = (K, Color)>,
2002 K: AsRef<str>,
2003 {
2004 let mut applied = 0;
2005 for (key, color) in overrides {
2006 if let Some(slot) = self.resolve_theme_key_mut(key.as_ref()) {
2007 *slot = color;
2008 applied += 1;
2009 }
2010 }
2011 applied
2012 }
2013}
2014
2015pub fn get_theme_schema() -> serde_json::Value {
2023 use schemars::schema_for;
2024 let schema = schema_for!(ThemeFile);
2025 serde_json::to_value(&schema).unwrap_or_default()
2026}
2027
2028pub fn get_builtin_themes() -> serde_json::Value {
2030 let mut map = serde_json::Map::new();
2031 for theme in BUILTIN_THEMES {
2032 map.insert(
2033 theme.name.to_string(),
2034 serde_json::Value::String(theme.json.to_string()),
2035 );
2036 }
2037 serde_json::Value::Object(map)
2038}
2039
2040#[cfg(test)]
2041mod tests {
2042 use super::*;
2043
2044 #[test]
2045 fn test_load_builtin_theme() {
2046 let dark = Theme::load_builtin(THEME_DARK).expect("Dark theme must exist");
2047 assert_eq!(dark.name, THEME_DARK);
2048
2049 let light = Theme::load_builtin(THEME_LIGHT).expect("Light theme must exist");
2050 assert_eq!(light.name, THEME_LIGHT);
2051
2052 let high_contrast =
2053 Theme::load_builtin(THEME_HIGH_CONTRAST).expect("High contrast theme must exist");
2054 assert_eq!(high_contrast.name, THEME_HIGH_CONTRAST);
2055
2056 let terminal = Theme::load_builtin(THEME_TERMINAL).expect("Terminal theme must exist");
2057 assert_eq!(terminal.name, THEME_TERMINAL);
2058 assert_eq!(terminal.editor_bg, Color::Reset);
2062 assert_eq!(terminal.editor_fg, Color::Reset);
2063 assert_eq!(terminal.terminal_bg, Color::Reset);
2064 assert!(terminal.selection_modifier.contains(Modifier::REVERSED));
2067 assert!(terminal
2068 .semantic_highlight_modifier
2069 .contains(Modifier::BOLD));
2070 }
2071
2072 #[test]
2073 fn test_modifier_def_round_trip() {
2074 let cases = [
2075 (vec!["reversed"], Modifier::REVERSED),
2076 (
2077 vec!["bold", "underlined"],
2078 Modifier::BOLD | Modifier::UNDERLINED,
2079 ),
2080 (vec!["italic", "dim"], Modifier::ITALIC | Modifier::DIM),
2081 (vec!["reverse"], Modifier::REVERSED), (vec!["underline"], Modifier::UNDERLINED), ];
2084 for (strs, expected) in cases {
2085 let def = ModifierDef(strs.iter().map(|s| s.to_string()).collect());
2086 let m: Modifier = (&def).into();
2087 assert_eq!(m, expected, "ModifierDef({:?}) -> Modifier", strs);
2088 }
2089 }
2090
2091 #[test]
2092 fn test_modifier_def_unknown_strings_are_dropped() {
2093 let def = ModifierDef(vec!["reversed".into(), "wibble".into(), "bold".into()]);
2096 let m: Modifier = (&def).into();
2097 assert_eq!(m, Modifier::REVERSED | Modifier::BOLD);
2098 }
2099
2100 #[test]
2101 fn test_themes_without_modifier_default_to_empty() {
2102 let dark = Theme::load_builtin(THEME_DARK).expect("Dark theme must exist");
2107 assert!(dark.selection_modifier.is_empty());
2108 assert!(dark.semantic_highlight_modifier.is_empty());
2109 }
2110
2111 #[test]
2112 fn test_modifier_for_bg_key_lookup() {
2113 let terminal = Theme::load_builtin(THEME_TERMINAL).expect("Terminal theme must exist");
2114 assert!(terminal
2117 .modifier_for_bg_key("editor.selection_bg")
2118 .contains(Modifier::REVERSED));
2119 assert!(terminal
2120 .modifier_for_bg_key("ui.semantic_highlight_bg")
2121 .contains(Modifier::BOLD));
2122 assert!(terminal
2125 .modifier_for_bg_key("ui.popup_selection_bg")
2126 .is_empty());
2127 assert!(terminal.modifier_for_bg_key("nonsense").is_empty());
2128 }
2129
2130 #[test]
2131 fn test_modifier_round_trip_via_theme_file() {
2132 let original = Theme::load_builtin(THEME_TERMINAL).expect("Terminal theme must exist");
2134 let file: ThemeFile = original.clone().into();
2135 let json = serde_json::to_string(&file).expect("serialize");
2136 let parsed: ThemeFile = serde_json::from_str(&json).expect("parse");
2137 let round_tripped: Theme = parsed.into();
2138 assert_eq!(
2139 round_tripped.selection_modifier,
2140 original.selection_modifier
2141 );
2142 assert_eq!(
2143 round_tripped.semantic_highlight_modifier,
2144 original.semantic_highlight_modifier
2145 );
2146 }
2147
2148 #[test]
2149 fn test_builtin_themes_match_schema() {
2150 for theme in BUILTIN_THEMES {
2151 let _: ThemeFile = serde_json::from_str(theme.json)
2152 .unwrap_or_else(|_| panic!("Theme '{}' does not match schema", theme.name));
2153 }
2154 }
2155
2156 #[test]
2157 fn test_from_json() {
2158 let json = r#"{"name":"test","editor":{},"ui":{},"search":{},"diagnostic":{},"syntax":{}}"#;
2159 let theme = Theme::from_json(json).expect("Should parse minimal theme");
2160 assert_eq!(theme.name, "test");
2161 }
2162
2163 #[test]
2175 fn test_minimal_user_theme_from_issue_1281_loads() {
2176 let json = r#"{
2178 "name": "gruvbox-light-orange",
2179 "editor": {
2180 "bg": [251, 241, 199],
2181 "fg": [60, 56, 54],
2182 "cursor": [254, 128, 25],
2183 "selection_bg": [213, 196, 161]
2184 },
2185 "syntax": {
2186 "keyword": [175, 58, 3],
2187 "string": [152, 151, 26],
2188 "comment": [146, 131, 116]
2189 }
2190}"#;
2191 let theme = Theme::from_json(json)
2192 .expect("Theme from issue #1281 should parse without `ui`/`search`/`diagnostic`");
2193 assert_eq!(theme.name, "gruvbox-light-orange");
2194
2195 assert_eq!(theme.editor_bg, Color::Rgb(251, 241, 199));
2197 assert_eq!(theme.editor_fg, Color::Rgb(60, 56, 54));
2198 assert_eq!(theme.cursor, Color::Rgb(254, 128, 25));
2199 assert_eq!(theme.selection_bg, Color::Rgb(213, 196, 161));
2200 assert_eq!(theme.syntax_keyword, Color::Rgb(175, 58, 3));
2201 assert_eq!(theme.syntax_string, Color::Rgb(152, 151, 26));
2202 assert_eq!(theme.syntax_comment, Color::Rgb(146, 131, 116));
2203
2204 let light = Theme::load_builtin(THEME_LIGHT).expect("light builtin");
2208 assert_eq!(
2209 theme.status_bar_fg, light.status_bar_fg,
2210 "ui.status_bar_fg should inherit from builtin://light when bg is bright"
2211 );
2212 assert_eq!(
2213 theme.diagnostic_error_fg, light.diagnostic_error_fg,
2214 "diagnostic.error_fg should inherit from builtin://light when bg is bright"
2215 );
2216 assert_eq!(
2217 theme.menu_bg, light.menu_bg,
2218 "ui.menu_bg should inherit from builtin://light when bg is bright"
2219 );
2220 }
2221
2222 #[test]
2225 fn test_extends_explicit_builtin_wins_over_auto_infer() {
2226 let json = r#"{
2229 "name": "explicit-light",
2230 "extends": "builtin://light",
2231 "editor": { "bg": [0, 0, 0] }
2232 }"#;
2233 let theme = Theme::from_json(json).expect("extends should resolve");
2234 let light = Theme::load_builtin(THEME_LIGHT).expect("light builtin");
2235
2236 assert_eq!(theme.editor_bg, Color::Rgb(0, 0, 0));
2238 assert_eq!(theme.menu_bg, light.menu_bg);
2240 assert_eq!(theme.tab_active_bg, light.tab_active_bg);
2241 assert_eq!(theme.diagnostic_warning_fg, light.diagnostic_warning_fg);
2242 }
2243
2244 #[test]
2249 fn test_extends_bare_builtin_name_works() {
2250 let json = r#"{ "name": "x", "extends": "high-contrast" }"#;
2251 let theme = Theme::from_json(json).expect("bare-name extends should resolve");
2252 let hc = Theme::load_builtin("high-contrast").expect("hc builtin");
2253 assert_eq!(theme.menu_bg, hc.menu_bg);
2254 }
2255
2256 #[test]
2261 fn test_extends_unknown_builtin_errors_with_helpful_message() {
2262 let json = r#"{ "name": "x", "extends": "builtin://no-such-theme" }"#;
2263 let err = Theme::from_json(json).expect_err("unknown extends must error");
2264 assert!(
2265 err.contains("no-such-theme"),
2266 "error should quote the bad value, got: {}",
2267 err
2268 );
2269 assert!(
2270 err.contains("dark") && err.contains("light"),
2271 "error should list available builtins, got: {}",
2272 err
2273 );
2274 }
2275
2276 #[test]
2280 fn test_auto_infer_dark_base_from_dark_bg() {
2281 let json = r#"{ "name": "x", "editor": { "bg": [20, 20, 30] } }"#;
2282 let theme = Theme::from_json(json).expect("should parse");
2283 let dark = Theme::load_builtin(THEME_DARK).expect("dark builtin");
2284 assert_eq!(theme.menu_bg, dark.menu_bg);
2285 assert_eq!(theme.diagnostic_error_fg, dark.diagnostic_error_fg);
2286 }
2287
2288 #[test]
2292 fn test_no_inheritance_signal_uses_hardcoded_defaults() {
2293 let json = r#"{ "name": "x" }"#;
2294 let theme = Theme::from_json(json).expect("should parse");
2295 assert_eq!(theme.editor_bg, Color::Rgb(30, 30, 30));
2298 }
2299
2300 #[test]
2304 fn test_theme_without_name_still_errors() {
2305 let json = r#"{ "editor": {} }"#;
2306 let err = Theme::from_json(json).expect_err("missing `name` must be an error");
2307 assert!(
2308 err.contains("name"),
2309 "error should mention the missing `name` field, got: {}",
2310 err
2311 );
2312 }
2313
2314 #[test]
2319 fn test_extends_overrides_compose_field_by_field() {
2320 let json = r#"{
2321 "name": "dark-with-pink-cursor",
2322 "extends": "builtin://dark",
2323 "editor": { "cursor": [255, 105, 180] }
2324 }"#;
2325 let theme = Theme::from_json(json).expect("should parse");
2326 let dark = Theme::load_builtin(THEME_DARK).expect("dark builtin");
2327
2328 assert_eq!(theme.cursor, Color::Rgb(255, 105, 180));
2330 assert_eq!(theme.editor_bg, dark.editor_bg);
2332 assert_eq!(theme.editor_fg, dark.editor_fg);
2333 assert_eq!(theme.selection_bg, dark.selection_bg);
2334 assert_eq!(theme.menu_bg, dark.menu_bg);
2336 assert_eq!(theme.syntax_keyword, dark.syntax_keyword);
2337 }
2338
2339 #[test]
2340 fn test_default_reset_color() {
2341 let color: Color = ColorDef::Named("Default".to_string()).into();
2343 assert_eq!(color, Color::Reset);
2344
2345 let color: Color = ColorDef::Named("Reset".to_string()).into();
2347 assert_eq!(color, Color::Reset);
2348 }
2349
2350 #[test]
2351 fn test_file_status_colors_fall_back_to_diagnostic_colors() {
2352 let json = r#"{
2354 "name": "test-fallback",
2355 "editor": {},
2356 "ui": {},
2357 "search": {},
2358 "diagnostic": {
2359 "error_fg": [220, 50, 47],
2360 "warning_fg": [181, 137, 0],
2361 "info_fg": [38, 139, 210],
2362 "hint_fg": [101, 123, 131]
2363 },
2364 "syntax": {}
2365 }"#;
2366 let theme = Theme::from_json(json).expect("Should parse theme without file_status keys");
2367
2368 assert_eq!(theme.file_status_added_fg, Color::Rgb(38, 139, 210));
2370 assert_eq!(theme.file_status_renamed_fg, Color::Rgb(38, 139, 210));
2371 assert_eq!(theme.file_status_modified_fg, Color::Rgb(181, 137, 0));
2373 assert_eq!(theme.file_status_deleted_fg, Color::Rgb(220, 50, 47));
2375 assert_eq!(theme.file_status_conflicted_fg, Color::Rgb(220, 50, 47));
2376 assert_eq!(theme.file_status_untracked_fg, Color::Rgb(101, 123, 131));
2378 }
2379
2380 #[test]
2381 fn test_file_status_colors_explicit_override() {
2382 let json = r#"{
2384 "name": "test-override",
2385 "editor": {},
2386 "ui": {
2387 "file_status_added_fg": [80, 250, 123],
2388 "file_status_modified_fg": [255, 184, 108]
2389 },
2390 "search": {},
2391 "diagnostic": {
2392 "info_fg": [38, 139, 210],
2393 "warning_fg": [181, 137, 0]
2394 },
2395 "syntax": {}
2396 }"#;
2397 let theme = Theme::from_json(json).expect("Should parse theme with file_status overrides");
2398
2399 assert_eq!(theme.file_status_added_fg, Color::Rgb(80, 250, 123));
2401 assert_eq!(theme.file_status_modified_fg, Color::Rgb(255, 184, 108));
2402 assert_eq!(theme.file_status_renamed_fg, Color::Rgb(38, 139, 210));
2404 }
2405
2406 #[test]
2407 fn test_file_status_colors_resolve_via_theme_key() {
2408 let json = r#"{
2409 "name": "test-resolve",
2410 "editor": {},
2411 "ui": {
2412 "file_status_added_fg": [80, 250, 123]
2413 },
2414 "search": {},
2415 "diagnostic": {
2416 "warning_fg": [181, 137, 0]
2417 },
2418 "syntax": {}
2419 }"#;
2420 let theme = Theme::from_json(json).expect("Should parse theme");
2421
2422 assert_eq!(
2424 theme.resolve_theme_key("ui.file_status_added_fg"),
2425 Some(Color::Rgb(80, 250, 123))
2426 );
2427 assert_eq!(
2428 theme.resolve_theme_key("ui.file_status_modified_fg"),
2429 Some(Color::Rgb(181, 137, 0))
2430 );
2431 }
2432
2433 #[test]
2434 fn override_colors_writes_known_keys_and_drops_unknowns() {
2435 let mut theme = Theme::load_builtin(THEME_DARK).expect("dark builtin");
2436 let applied = theme.override_colors([
2437 ("editor.bg".to_string(), Color::Rgb(10, 20, 30)),
2438 ("ui.status_bar_fg".to_string(), Color::Rgb(1, 2, 3)),
2439 ("does.not_exist".to_string(), Color::Rgb(9, 9, 9)),
2440 ("garbage_no_dot".to_string(), Color::Rgb(9, 9, 9)),
2441 ]);
2442 assert_eq!(applied, 2, "only the two valid keys should be applied");
2443 assert_eq!(
2444 theme.resolve_theme_key("editor.bg"),
2445 Some(Color::Rgb(10, 20, 30))
2446 );
2447 assert_eq!(
2448 theme.resolve_theme_key("ui.status_bar_fg"),
2449 Some(Color::Rgb(1, 2, 3))
2450 );
2451 }
2452
2453 #[test]
2454 fn resolve_theme_key_mut_matches_resolve_theme_key_domain() {
2455 let mut theme = Theme::load_builtin(THEME_DARK).expect("dark builtin");
2458 let probe = [
2459 "editor.bg",
2460 "editor.fg",
2461 "ui.status_bar_fg",
2462 "ui.tab_active_bg",
2463 "syntax.keyword",
2464 "diagnostic.error_fg",
2465 "search.match_bg",
2466 ];
2467 for key in probe {
2468 assert!(
2469 theme.resolve_theme_key(key).is_some(),
2470 "reader lost key {key}"
2471 );
2472 assert!(
2473 theme.resolve_theme_key_mut(key).is_some(),
2474 "mutator missing key {key}"
2475 );
2476 }
2477 }
2478
2479 #[test]
2480 fn test_all_builtin_themes_set_prominent_palette_indicator() {
2481 for builtin in BUILTIN_THEMES {
2488 let theme = Theme::from_json(builtin.json)
2489 .unwrap_or_else(|e| panic!("Theme '{}' failed to parse: {}", builtin.name, e));
2490 assert!(
2491 theme.status_palette_fg != theme.status_bar_fg
2492 || theme.status_palette_bg != theme.status_bar_bg,
2493 "Theme '{}' must set status_palette_fg/bg to a prominent \
2494 accent distinct from status_bar_fg/bg",
2495 builtin.name
2496 );
2497 }
2498 }
2499
2500 #[test]
2501 fn test_all_builtin_themes_have_file_status_colors() {
2502 for builtin in BUILTIN_THEMES {
2504 let theme = Theme::from_json(builtin.json)
2505 .unwrap_or_else(|e| panic!("Theme '{}' failed to parse: {}", builtin.name, e));
2506
2507 for key in &[
2509 "ui.file_status_added_fg",
2510 "ui.file_status_modified_fg",
2511 "ui.file_status_deleted_fg",
2512 "ui.file_status_renamed_fg",
2513 "ui.file_status_untracked_fg",
2514 "ui.file_status_conflicted_fg",
2515 ] {
2516 assert!(
2517 theme.resolve_theme_key(key).is_some(),
2518 "Theme '{}' missing resolution for '{}'",
2519 builtin.name,
2520 key
2521 );
2522 }
2523 }
2524 }
2525}