1use ratatui::style::Color;
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";
17
18pub struct BuiltinTheme {
20 pub name: &'static str,
21 pub pack: &'static str,
23 pub json: &'static str,
24}
25
26include!(concat!(env!("OUT_DIR"), "/builtin_themes.rs"));
28
29#[derive(Debug, Clone, PartialEq, Eq)]
31pub struct ThemeInfo {
32 pub name: String,
34 pub pack: String,
36 pub key: String,
44}
45
46impl ThemeInfo {
47 pub fn new(name: impl Into<String>, pack: impl Into<String>) -> Self {
50 let name = name.into();
51 let pack = pack.into();
52 let key = if pack.is_empty() {
53 name.clone()
54 } else {
55 format!("{}/{}", pack, name)
56 };
57 Self { name, pack, key }
58 }
59
60 pub fn with_key(
62 name: impl Into<String>,
63 pack: impl Into<String>,
64 key: impl Into<String>,
65 ) -> Self {
66 Self {
67 name: name.into(),
68 pack: pack.into(),
69 key: key.into(),
70 }
71 }
72
73 pub fn display_name(&self) -> String {
75 if self.pack.is_empty() {
76 self.name.clone()
77 } else {
78 format!("{} ({})", self.name, self.pack)
79 }
80 }
81}
82
83pub fn color_to_rgb(color: Color) -> Option<(u8, u8, u8)> {
86 match color {
87 Color::Rgb(r, g, b) => Some((r, g, b)),
88 Color::White => Some((255, 255, 255)),
89 Color::Black => Some((0, 0, 0)),
90 Color::Red => Some((205, 0, 0)),
91 Color::Green => Some((0, 205, 0)),
92 Color::Blue => Some((0, 0, 238)),
93 Color::Yellow => Some((205, 205, 0)),
94 Color::Magenta => Some((205, 0, 205)),
95 Color::Cyan => Some((0, 205, 205)),
96 Color::Gray => Some((229, 229, 229)),
97 Color::DarkGray => Some((127, 127, 127)),
98 Color::LightRed => Some((255, 0, 0)),
99 Color::LightGreen => Some((0, 255, 0)),
100 Color::LightBlue => Some((92, 92, 255)),
101 Color::LightYellow => Some((255, 255, 0)),
102 Color::LightMagenta => Some((255, 0, 255)),
103 Color::LightCyan => Some((0, 255, 255)),
104 Color::Reset | Color::Indexed(_) => None,
105 }
106}
107
108pub fn brighten_color(color: Color, amount: u8) -> Color {
111 if let Some((r, g, b)) = color_to_rgb(color) {
112 Color::Rgb(
113 r.saturating_add(amount),
114 g.saturating_add(amount),
115 b.saturating_add(amount),
116 )
117 } else {
118 color
119 }
120}
121
122pub fn shade_toward_contrast(color: Color, amount: u8) -> Color {
130 if let Some((r, g, b)) = color_to_rgb(color) {
131 let avg = (u16::from(r) + u16::from(g) + u16::from(b)) / 3;
132 if avg < 128 {
133 Color::Rgb(
134 r.saturating_add(amount),
135 g.saturating_add(amount),
136 b.saturating_add(amount),
137 )
138 } else {
139 Color::Rgb(
140 r.saturating_sub(amount),
141 g.saturating_sub(amount),
142 b.saturating_sub(amount),
143 )
144 }
145 } else {
146 color
147 }
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
152#[serde(untagged)]
153pub enum ColorDef {
154 Rgb(u8, u8, u8),
156 Named(String),
158}
159
160impl From<ColorDef> for Color {
161 fn from(def: ColorDef) -> Self {
162 match def {
163 ColorDef::Rgb(r, g, b) => Color::Rgb(r, g, b),
164 ColorDef::Named(name) => match name.as_str() {
165 "Black" => Color::Black,
166 "Red" => Color::Red,
167 "Green" => Color::Green,
168 "Yellow" => Color::Yellow,
169 "Blue" => Color::Blue,
170 "Magenta" => Color::Magenta,
171 "Cyan" => Color::Cyan,
172 "Gray" => Color::Gray,
173 "DarkGray" => Color::DarkGray,
174 "LightRed" => Color::LightRed,
175 "LightGreen" => Color::LightGreen,
176 "LightYellow" => Color::LightYellow,
177 "LightBlue" => Color::LightBlue,
178 "LightMagenta" => Color::LightMagenta,
179 "LightCyan" => Color::LightCyan,
180 "White" => Color::White,
181 "Default" | "Reset" => Color::Reset,
183 _ => Color::White, },
185 }
186 }
187}
188
189pub fn named_color_from_str(name: &str) -> Option<Color> {
192 match name {
193 "Black" => Some(Color::Black),
194 "Red" => Some(Color::Red),
195 "Green" => Some(Color::Green),
196 "Yellow" => Some(Color::Yellow),
197 "Blue" => Some(Color::Blue),
198 "Magenta" => Some(Color::Magenta),
199 "Cyan" => Some(Color::Cyan),
200 "Gray" => Some(Color::Gray),
201 "DarkGray" => Some(Color::DarkGray),
202 "LightRed" => Some(Color::LightRed),
203 "LightGreen" => Some(Color::LightGreen),
204 "LightYellow" => Some(Color::LightYellow),
205 "LightBlue" => Some(Color::LightBlue),
206 "LightMagenta" => Some(Color::LightMagenta),
207 "LightCyan" => Some(Color::LightCyan),
208 "White" => Some(Color::White),
209 "Default" | "Reset" => Some(Color::Reset),
210 _ => None,
211 }
212}
213
214impl From<Color> for ColorDef {
215 fn from(color: Color) -> Self {
216 match color {
217 Color::Rgb(r, g, b) => ColorDef::Rgb(r, g, b),
218 Color::White => ColorDef::Named("White".to_string()),
219 Color::Black => ColorDef::Named("Black".to_string()),
220 Color::Red => ColorDef::Named("Red".to_string()),
221 Color::Green => ColorDef::Named("Green".to_string()),
222 Color::Blue => ColorDef::Named("Blue".to_string()),
223 Color::Yellow => ColorDef::Named("Yellow".to_string()),
224 Color::Magenta => ColorDef::Named("Magenta".to_string()),
225 Color::Cyan => ColorDef::Named("Cyan".to_string()),
226 Color::Gray => ColorDef::Named("Gray".to_string()),
227 Color::DarkGray => ColorDef::Named("DarkGray".to_string()),
228 Color::LightRed => ColorDef::Named("LightRed".to_string()),
229 Color::LightGreen => ColorDef::Named("LightGreen".to_string()),
230 Color::LightBlue => ColorDef::Named("LightBlue".to_string()),
231 Color::LightYellow => ColorDef::Named("LightYellow".to_string()),
232 Color::LightMagenta => ColorDef::Named("LightMagenta".to_string()),
233 Color::LightCyan => ColorDef::Named("LightCyan".to_string()),
234 Color::Reset => ColorDef::Named("Default".to_string()),
235 Color::Indexed(_) => {
236 if let Some((r, g, b)) = color_to_rgb(color) {
238 ColorDef::Rgb(r, g, b)
239 } else {
240 ColorDef::Named("Default".to_string())
241 }
242 }
243 }
244 }
245}
246
247#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
249pub struct ThemeFile {
250 pub name: String,
252 pub editor: EditorColors,
254 pub ui: UiColors,
256 pub search: SearchColors,
258 pub diagnostic: DiagnosticColors,
260 pub syntax: SyntaxColors,
262}
263
264#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
266pub struct EditorColors {
267 #[serde(default = "default_editor_bg")]
269 pub bg: ColorDef,
270 #[serde(default = "default_editor_fg")]
272 pub fg: ColorDef,
273 #[serde(default = "default_cursor")]
275 pub cursor: ColorDef,
276 #[serde(default = "default_inactive_cursor")]
278 pub inactive_cursor: ColorDef,
279 #[serde(default = "default_selection_bg")]
281 pub selection_bg: ColorDef,
282 #[serde(default = "default_current_line_bg")]
284 pub current_line_bg: ColorDef,
285 #[serde(default = "default_line_number_fg")]
287 pub line_number_fg: ColorDef,
288 #[serde(default = "default_line_number_bg")]
290 pub line_number_bg: ColorDef,
291 #[serde(default = "default_diff_add_bg")]
293 pub diff_add_bg: ColorDef,
294 #[serde(default = "default_diff_remove_bg")]
296 pub diff_remove_bg: ColorDef,
297 #[serde(default)]
300 pub diff_add_highlight_bg: Option<ColorDef>,
301 #[serde(default)]
304 pub diff_remove_highlight_bg: Option<ColorDef>,
305 #[serde(default = "default_diff_modify_bg")]
307 pub diff_modify_bg: ColorDef,
308 #[serde(default = "default_ruler_bg")]
310 pub ruler_bg: ColorDef,
311 #[serde(default = "default_whitespace_indicator_fg")]
313 pub whitespace_indicator_fg: ColorDef,
314 #[serde(default)]
319 pub after_eof_bg: Option<ColorDef>,
320}
321
322fn default_editor_bg() -> ColorDef {
324 ColorDef::Rgb(30, 30, 30)
325}
326fn default_editor_fg() -> ColorDef {
327 ColorDef::Rgb(212, 212, 212)
328}
329fn default_cursor() -> ColorDef {
330 ColorDef::Rgb(255, 255, 255)
331}
332fn default_inactive_cursor() -> ColorDef {
333 ColorDef::Named("DarkGray".to_string())
334}
335fn default_selection_bg() -> ColorDef {
336 ColorDef::Rgb(38, 79, 120)
337}
338fn default_current_line_bg() -> ColorDef {
339 ColorDef::Rgb(40, 40, 40)
340}
341fn default_line_number_fg() -> ColorDef {
342 ColorDef::Rgb(100, 100, 100)
343}
344fn default_line_number_bg() -> ColorDef {
345 ColorDef::Rgb(30, 30, 30)
346}
347fn default_diff_add_bg() -> ColorDef {
348 ColorDef::Rgb(35, 60, 35) }
350fn default_diff_remove_bg() -> ColorDef {
351 ColorDef::Rgb(70, 35, 35) }
353fn default_diff_modify_bg() -> ColorDef {
354 ColorDef::Rgb(40, 38, 30) }
356fn default_ruler_bg() -> ColorDef {
357 ColorDef::Rgb(50, 50, 50) }
359fn default_whitespace_indicator_fg() -> ColorDef {
360 ColorDef::Rgb(70, 70, 70) }
362
363#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
365pub struct UiColors {
366 #[serde(default = "default_tab_active_fg")]
368 pub tab_active_fg: ColorDef,
369 #[serde(default = "default_tab_active_bg")]
371 pub tab_active_bg: ColorDef,
372 #[serde(default = "default_tab_inactive_fg")]
374 pub tab_inactive_fg: ColorDef,
375 #[serde(default = "default_tab_inactive_bg")]
377 pub tab_inactive_bg: ColorDef,
378 #[serde(default = "default_tab_separator_bg")]
380 pub tab_separator_bg: ColorDef,
381 #[serde(default = "default_tab_close_hover_fg")]
383 pub tab_close_hover_fg: ColorDef,
384 #[serde(default = "default_tab_hover_bg")]
386 pub tab_hover_bg: ColorDef,
387 #[serde(default = "default_menu_bg")]
389 pub menu_bg: ColorDef,
390 #[serde(default = "default_menu_fg")]
392 pub menu_fg: ColorDef,
393 #[serde(default = "default_menu_active_bg")]
395 pub menu_active_bg: ColorDef,
396 #[serde(default = "default_menu_active_fg")]
398 pub menu_active_fg: ColorDef,
399 #[serde(default = "default_menu_dropdown_bg")]
401 pub menu_dropdown_bg: ColorDef,
402 #[serde(default = "default_menu_dropdown_fg")]
404 pub menu_dropdown_fg: ColorDef,
405 #[serde(default = "default_menu_highlight_bg")]
407 pub menu_highlight_bg: ColorDef,
408 #[serde(default = "default_menu_highlight_fg")]
410 pub menu_highlight_fg: ColorDef,
411 #[serde(default = "default_menu_border_fg")]
413 pub menu_border_fg: ColorDef,
414 #[serde(default = "default_menu_separator_fg")]
416 pub menu_separator_fg: ColorDef,
417 #[serde(default = "default_menu_hover_bg")]
419 pub menu_hover_bg: ColorDef,
420 #[serde(default = "default_menu_hover_fg")]
422 pub menu_hover_fg: ColorDef,
423 #[serde(default = "default_menu_disabled_fg")]
425 pub menu_disabled_fg: ColorDef,
426 #[serde(default = "default_menu_disabled_bg")]
428 pub menu_disabled_bg: ColorDef,
429 #[serde(default = "default_status_bar_fg")]
431 pub status_bar_fg: ColorDef,
432 #[serde(default = "default_status_bar_bg")]
434 pub status_bar_bg: ColorDef,
435 #[serde(default = "default_prompt_fg")]
437 pub prompt_fg: ColorDef,
438 #[serde(default = "default_prompt_bg")]
440 pub prompt_bg: ColorDef,
441 #[serde(default = "default_prompt_selection_fg")]
443 pub prompt_selection_fg: ColorDef,
444 #[serde(default = "default_prompt_selection_bg")]
446 pub prompt_selection_bg: ColorDef,
447 #[serde(default = "default_popup_border_fg")]
449 pub popup_border_fg: ColorDef,
450 #[serde(default = "default_popup_bg")]
452 pub popup_bg: ColorDef,
453 #[serde(default = "default_popup_selection_bg")]
455 pub popup_selection_bg: ColorDef,
456 #[serde(default = "default_popup_selection_fg")]
458 pub popup_selection_fg: ColorDef,
459 #[serde(default = "default_popup_text_fg")]
461 pub popup_text_fg: ColorDef,
462 #[serde(default = "default_suggestion_bg")]
464 pub suggestion_bg: ColorDef,
465 #[serde(default = "default_suggestion_selected_bg")]
467 pub suggestion_selected_bg: ColorDef,
468 #[serde(default = "default_help_bg")]
470 pub help_bg: ColorDef,
471 #[serde(default = "default_help_fg")]
473 pub help_fg: ColorDef,
474 #[serde(default = "default_help_key_fg")]
476 pub help_key_fg: ColorDef,
477 #[serde(default = "default_help_separator_fg")]
479 pub help_separator_fg: ColorDef,
480 #[serde(default = "default_help_indicator_fg")]
482 pub help_indicator_fg: ColorDef,
483 #[serde(default = "default_help_indicator_bg")]
485 pub help_indicator_bg: ColorDef,
486 #[serde(default = "default_inline_code_bg")]
488 pub inline_code_bg: ColorDef,
489 #[serde(default = "default_split_separator_fg")]
491 pub split_separator_fg: ColorDef,
492 #[serde(default = "default_split_separator_hover_fg")]
494 pub split_separator_hover_fg: ColorDef,
495 #[serde(default = "default_scrollbar_track_fg")]
497 pub scrollbar_track_fg: ColorDef,
498 #[serde(default = "default_scrollbar_thumb_fg")]
500 pub scrollbar_thumb_fg: ColorDef,
501 #[serde(default = "default_scrollbar_track_hover_fg")]
503 pub scrollbar_track_hover_fg: ColorDef,
504 #[serde(default = "default_scrollbar_thumb_hover_fg")]
506 pub scrollbar_thumb_hover_fg: ColorDef,
507 #[serde(default = "default_compose_margin_bg")]
509 pub compose_margin_bg: ColorDef,
510 #[serde(default = "default_semantic_highlight_bg")]
512 pub semantic_highlight_bg: ColorDef,
513 #[serde(default = "default_terminal_bg")]
515 pub terminal_bg: ColorDef,
516 #[serde(default = "default_terminal_fg")]
518 pub terminal_fg: ColorDef,
519 #[serde(default = "default_status_warning_indicator_bg")]
521 pub status_warning_indicator_bg: ColorDef,
522 #[serde(default = "default_status_warning_indicator_fg")]
524 pub status_warning_indicator_fg: ColorDef,
525 #[serde(default = "default_status_error_indicator_bg")]
527 pub status_error_indicator_bg: ColorDef,
528 #[serde(default = "default_status_error_indicator_fg")]
530 pub status_error_indicator_fg: ColorDef,
531 #[serde(default = "default_status_warning_indicator_hover_bg")]
533 pub status_warning_indicator_hover_bg: ColorDef,
534 #[serde(default = "default_status_warning_indicator_hover_fg")]
536 pub status_warning_indicator_hover_fg: ColorDef,
537 #[serde(default = "default_status_error_indicator_hover_bg")]
539 pub status_error_indicator_hover_bg: ColorDef,
540 #[serde(default = "default_status_error_indicator_hover_fg")]
542 pub status_error_indicator_hover_fg: ColorDef,
543 #[serde(default = "default_tab_drop_zone_bg")]
545 pub tab_drop_zone_bg: ColorDef,
546 #[serde(default = "default_tab_drop_zone_border")]
548 pub tab_drop_zone_border: ColorDef,
549 #[serde(default = "default_settings_selected_bg")]
551 pub settings_selected_bg: ColorDef,
552 #[serde(default = "default_settings_selected_fg")]
554 pub settings_selected_fg: ColorDef,
555 #[serde(default)]
557 pub file_status_added_fg: Option<ColorDef>,
558 #[serde(default)]
560 pub file_status_modified_fg: Option<ColorDef>,
561 #[serde(default)]
563 pub file_status_deleted_fg: Option<ColorDef>,
564 #[serde(default)]
566 pub file_status_renamed_fg: Option<ColorDef>,
567 #[serde(default)]
569 pub file_status_untracked_fg: Option<ColorDef>,
570 #[serde(default)]
572 pub file_status_conflicted_fg: Option<ColorDef>,
573}
574
575fn default_tab_active_fg() -> ColorDef {
578 ColorDef::Named("Yellow".to_string())
579}
580fn default_tab_active_bg() -> ColorDef {
581 ColorDef::Named("Blue".to_string())
582}
583fn default_tab_inactive_fg() -> ColorDef {
584 ColorDef::Named("White".to_string())
585}
586fn default_tab_inactive_bg() -> ColorDef {
587 ColorDef::Named("DarkGray".to_string())
588}
589fn default_tab_separator_bg() -> ColorDef {
590 ColorDef::Named("Black".to_string())
591}
592fn default_tab_close_hover_fg() -> ColorDef {
593 ColorDef::Rgb(255, 100, 100) }
595fn default_tab_hover_bg() -> ColorDef {
596 ColorDef::Rgb(70, 70, 75) }
598
599fn default_menu_bg() -> ColorDef {
601 ColorDef::Rgb(60, 60, 65)
602}
603fn default_menu_fg() -> ColorDef {
604 ColorDef::Rgb(220, 220, 220)
605}
606fn default_menu_active_bg() -> ColorDef {
607 ColorDef::Rgb(60, 60, 60)
608}
609fn default_menu_active_fg() -> ColorDef {
610 ColorDef::Rgb(255, 255, 255)
611}
612fn default_menu_dropdown_bg() -> ColorDef {
613 ColorDef::Rgb(50, 50, 50)
614}
615fn default_menu_dropdown_fg() -> ColorDef {
616 ColorDef::Rgb(220, 220, 220)
617}
618fn default_menu_highlight_bg() -> ColorDef {
619 ColorDef::Rgb(70, 130, 180)
620}
621fn default_menu_highlight_fg() -> ColorDef {
622 ColorDef::Rgb(255, 255, 255)
623}
624fn default_menu_border_fg() -> ColorDef {
625 ColorDef::Rgb(100, 100, 100)
626}
627fn default_menu_separator_fg() -> ColorDef {
628 ColorDef::Rgb(80, 80, 80)
629}
630fn default_menu_hover_bg() -> ColorDef {
631 ColorDef::Rgb(55, 55, 55)
632}
633fn default_menu_hover_fg() -> ColorDef {
634 ColorDef::Rgb(255, 255, 255)
635}
636fn default_menu_disabled_fg() -> ColorDef {
637 ColorDef::Rgb(100, 100, 100) }
639fn default_menu_disabled_bg() -> ColorDef {
640 ColorDef::Rgb(50, 50, 50) }
642fn default_status_bar_fg() -> ColorDef {
644 ColorDef::Named("White".to_string())
645}
646fn default_status_bar_bg() -> ColorDef {
647 ColorDef::Named("DarkGray".to_string())
648}
649
650fn default_prompt_fg() -> ColorDef {
652 ColorDef::Named("White".to_string())
653}
654fn default_prompt_bg() -> ColorDef {
655 ColorDef::Named("Black".to_string())
656}
657fn default_prompt_selection_fg() -> ColorDef {
658 ColorDef::Named("White".to_string())
659}
660fn default_prompt_selection_bg() -> ColorDef {
661 ColorDef::Rgb(58, 79, 120)
662}
663
664fn default_popup_border_fg() -> ColorDef {
666 ColorDef::Named("Gray".to_string())
667}
668fn default_popup_bg() -> ColorDef {
669 ColorDef::Rgb(30, 30, 30)
670}
671fn default_popup_selection_bg() -> ColorDef {
672 ColorDef::Rgb(58, 79, 120)
673}
674fn default_popup_selection_fg() -> ColorDef {
675 ColorDef::Rgb(255, 255, 255) }
677fn default_popup_text_fg() -> ColorDef {
678 ColorDef::Named("White".to_string())
679}
680
681fn default_suggestion_bg() -> ColorDef {
683 ColorDef::Rgb(30, 30, 30)
684}
685fn default_suggestion_selected_bg() -> ColorDef {
686 ColorDef::Rgb(58, 79, 120)
687}
688
689fn default_help_bg() -> ColorDef {
691 ColorDef::Named("Black".to_string())
692}
693fn default_help_fg() -> ColorDef {
694 ColorDef::Named("White".to_string())
695}
696fn default_help_key_fg() -> ColorDef {
697 ColorDef::Named("Cyan".to_string())
698}
699fn default_help_separator_fg() -> ColorDef {
700 ColorDef::Named("DarkGray".to_string())
701}
702fn default_help_indicator_fg() -> ColorDef {
703 ColorDef::Named("Red".to_string())
704}
705fn default_help_indicator_bg() -> ColorDef {
706 ColorDef::Named("Black".to_string())
707}
708
709fn default_inline_code_bg() -> ColorDef {
710 ColorDef::Named("DarkGray".to_string())
711}
712
713fn default_split_separator_fg() -> ColorDef {
715 ColorDef::Rgb(100, 100, 100)
716}
717fn default_split_separator_hover_fg() -> ColorDef {
718 ColorDef::Rgb(100, 149, 237) }
720fn default_scrollbar_track_fg() -> ColorDef {
721 ColorDef::Named("DarkGray".to_string())
722}
723fn default_scrollbar_thumb_fg() -> ColorDef {
724 ColorDef::Named("Gray".to_string())
725}
726fn default_scrollbar_track_hover_fg() -> ColorDef {
727 ColorDef::Named("Gray".to_string())
728}
729fn default_scrollbar_thumb_hover_fg() -> ColorDef {
730 ColorDef::Named("White".to_string())
731}
732fn default_compose_margin_bg() -> ColorDef {
733 ColorDef::Rgb(18, 18, 18) }
735fn default_semantic_highlight_bg() -> ColorDef {
736 ColorDef::Rgb(60, 60, 80) }
738fn default_terminal_bg() -> ColorDef {
739 ColorDef::Named("Default".to_string()) }
741fn default_terminal_fg() -> ColorDef {
742 ColorDef::Named("Default".to_string()) }
744fn default_status_warning_indicator_bg() -> ColorDef {
745 ColorDef::Rgb(181, 137, 0) }
747fn default_status_warning_indicator_fg() -> ColorDef {
748 ColorDef::Rgb(0, 0, 0) }
750fn default_status_error_indicator_bg() -> ColorDef {
751 ColorDef::Rgb(220, 50, 47) }
753fn default_status_error_indicator_fg() -> ColorDef {
754 ColorDef::Rgb(255, 255, 255) }
756fn default_status_warning_indicator_hover_bg() -> ColorDef {
757 ColorDef::Rgb(211, 167, 30) }
759fn default_status_warning_indicator_hover_fg() -> ColorDef {
760 ColorDef::Rgb(0, 0, 0) }
762fn default_status_error_indicator_hover_bg() -> ColorDef {
763 ColorDef::Rgb(250, 80, 77) }
765fn default_status_error_indicator_hover_fg() -> ColorDef {
766 ColorDef::Rgb(255, 255, 255) }
768fn default_tab_drop_zone_bg() -> ColorDef {
769 ColorDef::Rgb(70, 130, 180) }
771fn default_tab_drop_zone_border() -> ColorDef {
772 ColorDef::Rgb(100, 149, 237) }
774fn default_settings_selected_bg() -> ColorDef {
775 ColorDef::Rgb(60, 60, 70) }
777fn default_settings_selected_fg() -> ColorDef {
778 ColorDef::Rgb(255, 255, 255) }
780#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
782pub struct SearchColors {
783 #[serde(default = "default_search_match_bg")]
785 pub match_bg: ColorDef,
786 #[serde(default = "default_search_match_fg")]
788 pub match_fg: ColorDef,
789 #[serde(default = "default_search_label_bg")]
793 pub label_bg: ColorDef,
794 #[serde(default = "default_search_label_fg")]
798 pub label_fg: ColorDef,
799}
800
801fn default_search_match_bg() -> ColorDef {
803 ColorDef::Rgb(100, 100, 20)
804}
805fn default_search_match_fg() -> ColorDef {
806 ColorDef::Rgb(255, 255, 255)
807}
808fn default_search_label_bg() -> ColorDef {
813 ColorDef::Rgb(199, 78, 189)
814}
815fn default_search_label_fg() -> ColorDef {
816 ColorDef::Rgb(255, 255, 255)
817}
818
819#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
821pub struct DiagnosticColors {
822 #[serde(default = "default_diagnostic_error_fg")]
824 pub error_fg: ColorDef,
825 #[serde(default = "default_diagnostic_error_bg")]
827 pub error_bg: ColorDef,
828 #[serde(default = "default_diagnostic_warning_fg")]
830 pub warning_fg: ColorDef,
831 #[serde(default = "default_diagnostic_warning_bg")]
833 pub warning_bg: ColorDef,
834 #[serde(default = "default_diagnostic_info_fg")]
836 pub info_fg: ColorDef,
837 #[serde(default = "default_diagnostic_info_bg")]
839 pub info_bg: ColorDef,
840 #[serde(default = "default_diagnostic_hint_fg")]
842 pub hint_fg: ColorDef,
843 #[serde(default = "default_diagnostic_hint_bg")]
845 pub hint_bg: ColorDef,
846}
847
848fn default_diagnostic_error_fg() -> ColorDef {
850 ColorDef::Named("Red".to_string())
851}
852fn default_diagnostic_error_bg() -> ColorDef {
853 ColorDef::Rgb(60, 20, 20)
854}
855fn default_diagnostic_warning_fg() -> ColorDef {
856 ColorDef::Named("Yellow".to_string())
857}
858fn default_diagnostic_warning_bg() -> ColorDef {
859 ColorDef::Rgb(60, 50, 0)
860}
861fn default_diagnostic_info_fg() -> ColorDef {
862 ColorDef::Named("Blue".to_string())
863}
864fn default_diagnostic_info_bg() -> ColorDef {
865 ColorDef::Rgb(0, 30, 60)
866}
867fn default_diagnostic_hint_fg() -> ColorDef {
868 ColorDef::Named("Gray".to_string())
869}
870fn default_diagnostic_hint_bg() -> ColorDef {
871 ColorDef::Rgb(30, 30, 30)
872}
873
874#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
876pub struct SyntaxColors {
877 #[serde(default = "default_syntax_keyword")]
879 pub keyword: ColorDef,
880 #[serde(default = "default_syntax_string")]
882 pub string: ColorDef,
883 #[serde(default = "default_syntax_comment")]
885 pub comment: ColorDef,
886 #[serde(default = "default_syntax_function")]
888 pub function: ColorDef,
889 #[serde(rename = "type", default = "default_syntax_type")]
891 pub type_: ColorDef,
892 #[serde(default = "default_syntax_variable")]
894 pub variable: ColorDef,
895 #[serde(default = "default_syntax_constant")]
897 pub constant: ColorDef,
898 #[serde(default = "default_syntax_operator")]
900 pub operator: ColorDef,
901 #[serde(default = "default_syntax_punctuation_bracket")]
903 pub punctuation_bracket: ColorDef,
904 #[serde(default = "default_syntax_punctuation_delimiter")]
906 pub punctuation_delimiter: ColorDef,
907}
908
909fn default_syntax_keyword() -> ColorDef {
911 ColorDef::Rgb(86, 156, 214)
912}
913fn default_syntax_string() -> ColorDef {
914 ColorDef::Rgb(206, 145, 120)
915}
916fn default_syntax_comment() -> ColorDef {
917 ColorDef::Rgb(106, 153, 85)
918}
919fn default_syntax_function() -> ColorDef {
920 ColorDef::Rgb(220, 220, 170)
921}
922fn default_syntax_type() -> ColorDef {
923 ColorDef::Rgb(78, 201, 176)
924}
925fn default_syntax_variable() -> ColorDef {
926 ColorDef::Rgb(156, 220, 254)
927}
928fn default_syntax_constant() -> ColorDef {
929 ColorDef::Rgb(79, 193, 255)
930}
931fn default_syntax_operator() -> ColorDef {
932 ColorDef::Rgb(212, 212, 212)
933}
934fn default_syntax_punctuation_bracket() -> ColorDef {
935 ColorDef::Rgb(212, 212, 212) }
937fn default_syntax_punctuation_delimiter() -> ColorDef {
938 ColorDef::Rgb(212, 212, 212) }
940
941#[derive(Debug, Clone)]
943pub struct Theme {
944 pub name: String,
946
947 pub editor_bg: Color,
949 pub editor_fg: Color,
950 pub cursor: Color,
951 pub inactive_cursor: Color,
952 pub selection_bg: Color,
953 pub current_line_bg: Color,
954 pub line_number_fg: Color,
955 pub line_number_bg: Color,
956
957 pub after_eof_bg: Color,
959
960 pub ruler_bg: Color,
962
963 pub whitespace_indicator_fg: Color,
965
966 pub diff_add_bg: Color,
968 pub diff_remove_bg: Color,
969 pub diff_modify_bg: Color,
970 pub diff_add_highlight_bg: Color,
972 pub diff_remove_highlight_bg: Color,
974
975 pub tab_active_fg: Color,
977 pub tab_active_bg: Color,
978 pub tab_inactive_fg: Color,
979 pub tab_inactive_bg: Color,
980 pub tab_separator_bg: Color,
981 pub tab_close_hover_fg: Color,
982 pub tab_hover_bg: Color,
983
984 pub menu_bg: Color,
986 pub menu_fg: Color,
987 pub menu_active_bg: Color,
988 pub menu_active_fg: Color,
989 pub menu_dropdown_bg: Color,
990 pub menu_dropdown_fg: Color,
991 pub menu_highlight_bg: Color,
992 pub menu_highlight_fg: Color,
993 pub menu_border_fg: Color,
994 pub menu_separator_fg: Color,
995 pub menu_hover_bg: Color,
996 pub menu_hover_fg: Color,
997 pub menu_disabled_fg: Color,
998 pub menu_disabled_bg: Color,
999
1000 pub status_bar_fg: Color,
1001 pub status_bar_bg: Color,
1002 pub prompt_fg: Color,
1003 pub prompt_bg: Color,
1004 pub prompt_selection_fg: Color,
1005 pub prompt_selection_bg: Color,
1006
1007 pub popup_border_fg: Color,
1008 pub popup_bg: Color,
1009 pub popup_selection_bg: Color,
1010 pub popup_selection_fg: Color,
1011 pub popup_text_fg: Color,
1012
1013 pub suggestion_bg: Color,
1014 pub suggestion_selected_bg: Color,
1015
1016 pub help_bg: Color,
1017 pub help_fg: Color,
1018 pub help_key_fg: Color,
1019 pub help_separator_fg: Color,
1020
1021 pub help_indicator_fg: Color,
1022 pub help_indicator_bg: Color,
1023
1024 pub inline_code_bg: Color,
1026
1027 pub split_separator_fg: Color,
1028 pub split_separator_hover_fg: Color,
1029
1030 pub scrollbar_track_fg: Color,
1032 pub scrollbar_thumb_fg: Color,
1033 pub scrollbar_track_hover_fg: Color,
1034 pub scrollbar_thumb_hover_fg: Color,
1035
1036 pub compose_margin_bg: Color,
1038
1039 pub semantic_highlight_bg: Color,
1041
1042 pub terminal_bg: Color,
1044 pub terminal_fg: Color,
1045
1046 pub status_warning_indicator_bg: Color,
1048 pub status_warning_indicator_fg: Color,
1049 pub status_error_indicator_bg: Color,
1050 pub status_error_indicator_fg: Color,
1051 pub status_warning_indicator_hover_bg: Color,
1052 pub status_warning_indicator_hover_fg: Color,
1053 pub status_error_indicator_hover_bg: Color,
1054 pub status_error_indicator_hover_fg: Color,
1055
1056 pub tab_drop_zone_bg: Color,
1058 pub tab_drop_zone_border: Color,
1059
1060 pub settings_selected_bg: Color,
1062 pub settings_selected_fg: Color,
1063
1064 pub file_status_added_fg: Color,
1066 pub file_status_modified_fg: Color,
1067 pub file_status_deleted_fg: Color,
1068 pub file_status_renamed_fg: Color,
1069 pub file_status_untracked_fg: Color,
1070 pub file_status_conflicted_fg: Color,
1071
1072 pub search_match_bg: Color,
1074 pub search_match_fg: Color,
1075 pub search_label_bg: Color,
1076 pub search_label_fg: Color,
1077
1078 pub diagnostic_error_fg: Color,
1080 pub diagnostic_error_bg: Color,
1081 pub diagnostic_warning_fg: Color,
1082 pub diagnostic_warning_bg: Color,
1083 pub diagnostic_info_fg: Color,
1084 pub diagnostic_info_bg: Color,
1085 pub diagnostic_hint_fg: Color,
1086 pub diagnostic_hint_bg: Color,
1087
1088 pub syntax_keyword: Color,
1090 pub syntax_string: Color,
1091 pub syntax_comment: Color,
1092 pub syntax_function: Color,
1093 pub syntax_type: Color,
1094 pub syntax_variable: Color,
1095 pub syntax_constant: Color,
1096 pub syntax_operator: Color,
1097 pub syntax_punctuation_bracket: Color,
1098 pub syntax_punctuation_delimiter: Color,
1099}
1100
1101impl From<ThemeFile> for Theme {
1102 fn from(file: ThemeFile) -> Self {
1103 Self {
1104 name: file.name,
1105 editor_bg: file.editor.bg.clone().into(),
1106 editor_fg: file.editor.fg.into(),
1107 cursor: file.editor.cursor.into(),
1108 inactive_cursor: file.editor.inactive_cursor.into(),
1109 selection_bg: file.editor.selection_bg.into(),
1110 current_line_bg: file.editor.current_line_bg.into(),
1111 line_number_fg: file.editor.line_number_fg.into(),
1112 line_number_bg: file.editor.line_number_bg.into(),
1113 after_eof_bg: file
1116 .editor
1117 .after_eof_bg
1118 .clone()
1119 .map(|c| c.into())
1120 .unwrap_or_else(|| shade_toward_contrast(file.editor.bg.clone().into(), 10)),
1121 ruler_bg: file.editor.ruler_bg.into(),
1122 whitespace_indicator_fg: file.editor.whitespace_indicator_fg.into(),
1123 diff_add_bg: file.editor.diff_add_bg.clone().into(),
1124 diff_remove_bg: file.editor.diff_remove_bg.clone().into(),
1125 diff_modify_bg: file.editor.diff_modify_bg.into(),
1126 diff_add_highlight_bg: file
1128 .editor
1129 .diff_add_highlight_bg
1130 .map(|c| c.into())
1131 .unwrap_or_else(|| brighten_color(file.editor.diff_add_bg.into(), 40)),
1132 diff_remove_highlight_bg: file
1133 .editor
1134 .diff_remove_highlight_bg
1135 .map(|c| c.into())
1136 .unwrap_or_else(|| brighten_color(file.editor.diff_remove_bg.into(), 40)),
1137 tab_active_fg: file.ui.tab_active_fg.into(),
1138 tab_active_bg: file.ui.tab_active_bg.into(),
1139 tab_inactive_fg: file.ui.tab_inactive_fg.into(),
1140 tab_inactive_bg: file.ui.tab_inactive_bg.into(),
1141 tab_separator_bg: file.ui.tab_separator_bg.into(),
1142 tab_close_hover_fg: file.ui.tab_close_hover_fg.into(),
1143 tab_hover_bg: file.ui.tab_hover_bg.into(),
1144 menu_bg: file.ui.menu_bg.into(),
1145 menu_fg: file.ui.menu_fg.into(),
1146 menu_active_bg: file.ui.menu_active_bg.into(),
1147 menu_active_fg: file.ui.menu_active_fg.into(),
1148 menu_dropdown_bg: file.ui.menu_dropdown_bg.into(),
1149 menu_dropdown_fg: file.ui.menu_dropdown_fg.into(),
1150 menu_highlight_bg: file.ui.menu_highlight_bg.into(),
1151 menu_highlight_fg: file.ui.menu_highlight_fg.into(),
1152 menu_border_fg: file.ui.menu_border_fg.into(),
1153 menu_separator_fg: file.ui.menu_separator_fg.into(),
1154 menu_hover_bg: file.ui.menu_hover_bg.into(),
1155 menu_hover_fg: file.ui.menu_hover_fg.into(),
1156 menu_disabled_fg: file.ui.menu_disabled_fg.into(),
1157 menu_disabled_bg: file.ui.menu_disabled_bg.into(),
1158 status_bar_fg: file.ui.status_bar_fg.into(),
1159 status_bar_bg: file.ui.status_bar_bg.into(),
1160 prompt_fg: file.ui.prompt_fg.into(),
1161 prompt_bg: file.ui.prompt_bg.into(),
1162 prompt_selection_fg: file.ui.prompt_selection_fg.into(),
1163 prompt_selection_bg: file.ui.prompt_selection_bg.into(),
1164 popup_border_fg: file.ui.popup_border_fg.into(),
1165 popup_bg: file.ui.popup_bg.into(),
1166 popup_selection_bg: file.ui.popup_selection_bg.into(),
1167 popup_selection_fg: file.ui.popup_selection_fg.into(),
1168 popup_text_fg: file.ui.popup_text_fg.into(),
1169 suggestion_bg: file.ui.suggestion_bg.into(),
1170 suggestion_selected_bg: file.ui.suggestion_selected_bg.into(),
1171 help_bg: file.ui.help_bg.into(),
1172 help_fg: file.ui.help_fg.into(),
1173 help_key_fg: file.ui.help_key_fg.into(),
1174 help_separator_fg: file.ui.help_separator_fg.into(),
1175 help_indicator_fg: file.ui.help_indicator_fg.into(),
1176 help_indicator_bg: file.ui.help_indicator_bg.into(),
1177 inline_code_bg: file.ui.inline_code_bg.into(),
1178 split_separator_fg: file.ui.split_separator_fg.into(),
1179 split_separator_hover_fg: file.ui.split_separator_hover_fg.into(),
1180 scrollbar_track_fg: file.ui.scrollbar_track_fg.into(),
1181 scrollbar_thumb_fg: file.ui.scrollbar_thumb_fg.into(),
1182 scrollbar_track_hover_fg: file.ui.scrollbar_track_hover_fg.into(),
1183 scrollbar_thumb_hover_fg: file.ui.scrollbar_thumb_hover_fg.into(),
1184 compose_margin_bg: file.ui.compose_margin_bg.into(),
1185 semantic_highlight_bg: file.ui.semantic_highlight_bg.into(),
1186 terminal_bg: file.ui.terminal_bg.into(),
1187 terminal_fg: file.ui.terminal_fg.into(),
1188 status_warning_indicator_bg: file.ui.status_warning_indicator_bg.into(),
1189 status_warning_indicator_fg: file.ui.status_warning_indicator_fg.into(),
1190 status_error_indicator_bg: file.ui.status_error_indicator_bg.into(),
1191 status_error_indicator_fg: file.ui.status_error_indicator_fg.into(),
1192 status_warning_indicator_hover_bg: file.ui.status_warning_indicator_hover_bg.into(),
1193 status_warning_indicator_hover_fg: file.ui.status_warning_indicator_hover_fg.into(),
1194 status_error_indicator_hover_bg: file.ui.status_error_indicator_hover_bg.into(),
1195 status_error_indicator_hover_fg: file.ui.status_error_indicator_hover_fg.into(),
1196 tab_drop_zone_bg: file.ui.tab_drop_zone_bg.into(),
1197 tab_drop_zone_border: file.ui.tab_drop_zone_border.into(),
1198 settings_selected_bg: file.ui.settings_selected_bg.into(),
1199 settings_selected_fg: file.ui.settings_selected_fg.into(),
1200 file_status_added_fg: file
1201 .ui
1202 .file_status_added_fg
1203 .map(|c| c.into())
1204 .unwrap_or_else(|| file.diagnostic.info_fg.clone().into()),
1205 file_status_modified_fg: file
1206 .ui
1207 .file_status_modified_fg
1208 .map(|c| c.into())
1209 .unwrap_or_else(|| file.diagnostic.warning_fg.clone().into()),
1210 file_status_deleted_fg: file
1211 .ui
1212 .file_status_deleted_fg
1213 .map(|c| c.into())
1214 .unwrap_or_else(|| file.diagnostic.error_fg.clone().into()),
1215 file_status_renamed_fg: file
1216 .ui
1217 .file_status_renamed_fg
1218 .map(|c| c.into())
1219 .unwrap_or_else(|| file.diagnostic.info_fg.clone().into()),
1220 file_status_untracked_fg: file
1221 .ui
1222 .file_status_untracked_fg
1223 .map(|c| c.into())
1224 .unwrap_or_else(|| file.diagnostic.hint_fg.clone().into()),
1225 file_status_conflicted_fg: file
1226 .ui
1227 .file_status_conflicted_fg
1228 .map(|c| c.into())
1229 .unwrap_or_else(|| file.diagnostic.error_fg.clone().into()),
1230 search_match_bg: file.search.match_bg.into(),
1231 search_match_fg: file.search.match_fg.into(),
1232 search_label_bg: file.search.label_bg.into(),
1233 search_label_fg: file.search.label_fg.into(),
1234 diagnostic_error_fg: file.diagnostic.error_fg.into(),
1235 diagnostic_error_bg: file.diagnostic.error_bg.into(),
1236 diagnostic_warning_fg: file.diagnostic.warning_fg.into(),
1237 diagnostic_warning_bg: file.diagnostic.warning_bg.into(),
1238 diagnostic_info_fg: file.diagnostic.info_fg.into(),
1239 diagnostic_info_bg: file.diagnostic.info_bg.into(),
1240 diagnostic_hint_fg: file.diagnostic.hint_fg.into(),
1241 diagnostic_hint_bg: file.diagnostic.hint_bg.into(),
1242 syntax_keyword: file.syntax.keyword.into(),
1243 syntax_string: file.syntax.string.into(),
1244 syntax_comment: file.syntax.comment.into(),
1245 syntax_function: file.syntax.function.into(),
1246 syntax_type: file.syntax.type_.into(),
1247 syntax_variable: file.syntax.variable.into(),
1248 syntax_constant: file.syntax.constant.into(),
1249 syntax_operator: file.syntax.operator.into(),
1250 syntax_punctuation_bracket: file.syntax.punctuation_bracket.into(),
1251 syntax_punctuation_delimiter: file.syntax.punctuation_delimiter.into(),
1252 }
1253 }
1254}
1255
1256impl From<Theme> for ThemeFile {
1257 fn from(theme: Theme) -> Self {
1258 Self {
1259 name: theme.name,
1260 editor: EditorColors {
1261 bg: theme.editor_bg.into(),
1262 fg: theme.editor_fg.into(),
1263 cursor: theme.cursor.into(),
1264 inactive_cursor: theme.inactive_cursor.into(),
1265 selection_bg: theme.selection_bg.into(),
1266 current_line_bg: theme.current_line_bg.into(),
1267 line_number_fg: theme.line_number_fg.into(),
1268 line_number_bg: theme.line_number_bg.into(),
1269 diff_add_bg: theme.diff_add_bg.into(),
1270 diff_remove_bg: theme.diff_remove_bg.into(),
1271 diff_add_highlight_bg: Some(theme.diff_add_highlight_bg.into()),
1272 diff_remove_highlight_bg: Some(theme.diff_remove_highlight_bg.into()),
1273 diff_modify_bg: theme.diff_modify_bg.into(),
1274 ruler_bg: theme.ruler_bg.into(),
1275 whitespace_indicator_fg: theme.whitespace_indicator_fg.into(),
1276 after_eof_bg: Some(theme.after_eof_bg.into()),
1277 },
1278 ui: UiColors {
1279 tab_active_fg: theme.tab_active_fg.into(),
1280 tab_active_bg: theme.tab_active_bg.into(),
1281 tab_inactive_fg: theme.tab_inactive_fg.into(),
1282 tab_inactive_bg: theme.tab_inactive_bg.into(),
1283 tab_separator_bg: theme.tab_separator_bg.into(),
1284 tab_close_hover_fg: theme.tab_close_hover_fg.into(),
1285 tab_hover_bg: theme.tab_hover_bg.into(),
1286 menu_bg: theme.menu_bg.into(),
1287 menu_fg: theme.menu_fg.into(),
1288 menu_active_bg: theme.menu_active_bg.into(),
1289 menu_active_fg: theme.menu_active_fg.into(),
1290 menu_dropdown_bg: theme.menu_dropdown_bg.into(),
1291 menu_dropdown_fg: theme.menu_dropdown_fg.into(),
1292 menu_highlight_bg: theme.menu_highlight_bg.into(),
1293 menu_highlight_fg: theme.menu_highlight_fg.into(),
1294 menu_border_fg: theme.menu_border_fg.into(),
1295 menu_separator_fg: theme.menu_separator_fg.into(),
1296 menu_hover_bg: theme.menu_hover_bg.into(),
1297 menu_hover_fg: theme.menu_hover_fg.into(),
1298 menu_disabled_fg: theme.menu_disabled_fg.into(),
1299 menu_disabled_bg: theme.menu_disabled_bg.into(),
1300 status_bar_fg: theme.status_bar_fg.into(),
1301 status_bar_bg: theme.status_bar_bg.into(),
1302 prompt_fg: theme.prompt_fg.into(),
1303 prompt_bg: theme.prompt_bg.into(),
1304 prompt_selection_fg: theme.prompt_selection_fg.into(),
1305 prompt_selection_bg: theme.prompt_selection_bg.into(),
1306 popup_border_fg: theme.popup_border_fg.into(),
1307 popup_bg: theme.popup_bg.into(),
1308 popup_selection_bg: theme.popup_selection_bg.into(),
1309 popup_selection_fg: theme.popup_selection_fg.into(),
1310 popup_text_fg: theme.popup_text_fg.into(),
1311 suggestion_bg: theme.suggestion_bg.into(),
1312 suggestion_selected_bg: theme.suggestion_selected_bg.into(),
1313 help_bg: theme.help_bg.into(),
1314 help_fg: theme.help_fg.into(),
1315 help_key_fg: theme.help_key_fg.into(),
1316 help_separator_fg: theme.help_separator_fg.into(),
1317 help_indicator_fg: theme.help_indicator_fg.into(),
1318 help_indicator_bg: theme.help_indicator_bg.into(),
1319 inline_code_bg: theme.inline_code_bg.into(),
1320 split_separator_fg: theme.split_separator_fg.into(),
1321 split_separator_hover_fg: theme.split_separator_hover_fg.into(),
1322 scrollbar_track_fg: theme.scrollbar_track_fg.into(),
1323 scrollbar_thumb_fg: theme.scrollbar_thumb_fg.into(),
1324 scrollbar_track_hover_fg: theme.scrollbar_track_hover_fg.into(),
1325 scrollbar_thumb_hover_fg: theme.scrollbar_thumb_hover_fg.into(),
1326 compose_margin_bg: theme.compose_margin_bg.into(),
1327 semantic_highlight_bg: theme.semantic_highlight_bg.into(),
1328 terminal_bg: theme.terminal_bg.into(),
1329 terminal_fg: theme.terminal_fg.into(),
1330 status_warning_indicator_bg: theme.status_warning_indicator_bg.into(),
1331 status_warning_indicator_fg: theme.status_warning_indicator_fg.into(),
1332 status_error_indicator_bg: theme.status_error_indicator_bg.into(),
1333 status_error_indicator_fg: theme.status_error_indicator_fg.into(),
1334 status_warning_indicator_hover_bg: theme.status_warning_indicator_hover_bg.into(),
1335 status_warning_indicator_hover_fg: theme.status_warning_indicator_hover_fg.into(),
1336 status_error_indicator_hover_bg: theme.status_error_indicator_hover_bg.into(),
1337 status_error_indicator_hover_fg: theme.status_error_indicator_hover_fg.into(),
1338 tab_drop_zone_bg: theme.tab_drop_zone_bg.into(),
1339 tab_drop_zone_border: theme.tab_drop_zone_border.into(),
1340 settings_selected_bg: theme.settings_selected_bg.into(),
1341 settings_selected_fg: theme.settings_selected_fg.into(),
1342 file_status_added_fg: Some(theme.file_status_added_fg.into()),
1343 file_status_modified_fg: Some(theme.file_status_modified_fg.into()),
1344 file_status_deleted_fg: Some(theme.file_status_deleted_fg.into()),
1345 file_status_renamed_fg: Some(theme.file_status_renamed_fg.into()),
1346 file_status_untracked_fg: Some(theme.file_status_untracked_fg.into()),
1347 file_status_conflicted_fg: Some(theme.file_status_conflicted_fg.into()),
1348 },
1349 search: SearchColors {
1350 match_bg: theme.search_match_bg.into(),
1351 match_fg: theme.search_match_fg.into(),
1352 label_bg: theme.search_label_bg.into(),
1353 label_fg: theme.search_label_fg.into(),
1354 },
1355 diagnostic: DiagnosticColors {
1356 error_fg: theme.diagnostic_error_fg.into(),
1357 error_bg: theme.diagnostic_error_bg.into(),
1358 warning_fg: theme.diagnostic_warning_fg.into(),
1359 warning_bg: theme.diagnostic_warning_bg.into(),
1360 info_fg: theme.diagnostic_info_fg.into(),
1361 info_bg: theme.diagnostic_info_bg.into(),
1362 hint_fg: theme.diagnostic_hint_fg.into(),
1363 hint_bg: theme.diagnostic_hint_bg.into(),
1364 },
1365 syntax: SyntaxColors {
1366 keyword: theme.syntax_keyword.into(),
1367 string: theme.syntax_string.into(),
1368 comment: theme.syntax_comment.into(),
1369 function: theme.syntax_function.into(),
1370 type_: theme.syntax_type.into(),
1371 variable: theme.syntax_variable.into(),
1372 constant: theme.syntax_constant.into(),
1373 operator: theme.syntax_operator.into(),
1374 punctuation_bracket: theme.syntax_punctuation_bracket.into(),
1375 punctuation_delimiter: theme.syntax_punctuation_delimiter.into(),
1376 },
1377 }
1378 }
1379}
1380
1381impl Theme {
1382 pub fn is_light(&self) -> bool {
1388 if let Some((r, g, b)) = color_to_rgb(self.editor_bg) {
1389 let lum = 0.2126 * (r as f64 / 255.0)
1391 + 0.7152 * (g as f64 / 255.0)
1392 + 0.0722 * (b as f64 / 255.0);
1393 lum > 0.5
1394 } else {
1395 false
1396 }
1397 }
1398
1399 pub fn load_builtin(name: &str) -> Option<Self> {
1401 BUILTIN_THEMES
1402 .iter()
1403 .find(|t| t.name == name)
1404 .and_then(|t| serde_json::from_str::<ThemeFile>(t.json).ok())
1405 .map(|tf| tf.into())
1406 }
1407
1408 pub fn from_json(json: &str) -> Result<Self, String> {
1410 let theme_file: ThemeFile =
1411 serde_json::from_str(json).map_err(|e| format!("Failed to parse theme JSON: {}", e))?;
1412 Ok(theme_file.into())
1413 }
1414
1415 pub fn resolve_theme_key(&self, key: &str) -> Option<Color> {
1426 let parts: Vec<&str> = key.split('.').collect();
1428 if parts.len() != 2 {
1429 return None;
1430 }
1431
1432 let (section, field) = (parts[0], parts[1]);
1433
1434 match section {
1435 "editor" => match field {
1436 "bg" => Some(self.editor_bg),
1437 "fg" => Some(self.editor_fg),
1438 "cursor" => Some(self.cursor),
1439 "inactive_cursor" => Some(self.inactive_cursor),
1440 "selection_bg" => Some(self.selection_bg),
1441 "current_line_bg" => Some(self.current_line_bg),
1442 "line_number_fg" => Some(self.line_number_fg),
1443 "line_number_bg" => Some(self.line_number_bg),
1444 "diff_add_bg" => Some(self.diff_add_bg),
1445 "diff_remove_bg" => Some(self.diff_remove_bg),
1446 "diff_modify_bg" => Some(self.diff_modify_bg),
1447 "ruler_bg" => Some(self.ruler_bg),
1448 "whitespace_indicator_fg" => Some(self.whitespace_indicator_fg),
1449 _ => None,
1450 },
1451 "ui" => match field {
1452 "tab_active_fg" => Some(self.tab_active_fg),
1453 "tab_active_bg" => Some(self.tab_active_bg),
1454 "tab_inactive_fg" => Some(self.tab_inactive_fg),
1455 "tab_inactive_bg" => Some(self.tab_inactive_bg),
1456 "status_bar_fg" => Some(self.status_bar_fg),
1457 "status_bar_bg" => Some(self.status_bar_bg),
1458 "prompt_fg" => Some(self.prompt_fg),
1459 "prompt_bg" => Some(self.prompt_bg),
1460 "prompt_selection_fg" => Some(self.prompt_selection_fg),
1461 "prompt_selection_bg" => Some(self.prompt_selection_bg),
1462 "popup_bg" => Some(self.popup_bg),
1463 "popup_border_fg" => Some(self.popup_border_fg),
1464 "popup_selection_bg" => Some(self.popup_selection_bg),
1465 "popup_selection_fg" => Some(self.popup_selection_fg),
1466 "popup_text_fg" => Some(self.popup_text_fg),
1467 "menu_bg" => Some(self.menu_bg),
1468 "menu_fg" => Some(self.menu_fg),
1469 "menu_active_bg" => Some(self.menu_active_bg),
1470 "menu_active_fg" => Some(self.menu_active_fg),
1471 "help_bg" => Some(self.help_bg),
1472 "help_fg" => Some(self.help_fg),
1473 "help_key_fg" => Some(self.help_key_fg),
1474 "split_separator_fg" => Some(self.split_separator_fg),
1475 "scrollbar_thumb_fg" => Some(self.scrollbar_thumb_fg),
1476 "semantic_highlight_bg" => Some(self.semantic_highlight_bg),
1477 "file_status_added_fg" => Some(self.file_status_added_fg),
1478 "file_status_modified_fg" => Some(self.file_status_modified_fg),
1479 "file_status_deleted_fg" => Some(self.file_status_deleted_fg),
1480 "file_status_renamed_fg" => Some(self.file_status_renamed_fg),
1481 "file_status_untracked_fg" => Some(self.file_status_untracked_fg),
1482 "file_status_conflicted_fg" => Some(self.file_status_conflicted_fg),
1483 _ => None,
1484 },
1485 "syntax" => match field {
1486 "keyword" => Some(self.syntax_keyword),
1487 "string" => Some(self.syntax_string),
1488 "comment" => Some(self.syntax_comment),
1489 "function" => Some(self.syntax_function),
1490 "type" => Some(self.syntax_type),
1491 "variable" => Some(self.syntax_variable),
1492 "constant" => Some(self.syntax_constant),
1493 "operator" => Some(self.syntax_operator),
1494 "punctuation_bracket" => Some(self.syntax_punctuation_bracket),
1495 "punctuation_delimiter" => Some(self.syntax_punctuation_delimiter),
1496 _ => None,
1497 },
1498 "diagnostic" => match field {
1499 "error_fg" => Some(self.diagnostic_error_fg),
1500 "error_bg" => Some(self.diagnostic_error_bg),
1501 "warning_fg" => Some(self.diagnostic_warning_fg),
1502 "warning_bg" => Some(self.diagnostic_warning_bg),
1503 "info_fg" => Some(self.diagnostic_info_fg),
1504 "info_bg" => Some(self.diagnostic_info_bg),
1505 "hint_fg" => Some(self.diagnostic_hint_fg),
1506 "hint_bg" => Some(self.diagnostic_hint_bg),
1507 _ => None,
1508 },
1509 "search" => match field {
1510 "match_bg" => Some(self.search_match_bg),
1511 "match_fg" => Some(self.search_match_fg),
1512 "label_bg" => Some(self.search_label_bg),
1513 "label_fg" => Some(self.search_label_fg),
1514 _ => None,
1515 },
1516 _ => None,
1517 }
1518 }
1519
1520 pub fn resolve_theme_key_mut(&mut self, key: &str) -> Option<&mut Color> {
1524 let parts: Vec<&str> = key.split('.').collect();
1525 if parts.len() != 2 {
1526 return None;
1527 }
1528 let (section, field) = (parts[0], parts[1]);
1529 match section {
1530 "editor" => match field {
1531 "bg" => Some(&mut self.editor_bg),
1532 "fg" => Some(&mut self.editor_fg),
1533 "cursor" => Some(&mut self.cursor),
1534 "inactive_cursor" => Some(&mut self.inactive_cursor),
1535 "selection_bg" => Some(&mut self.selection_bg),
1536 "current_line_bg" => Some(&mut self.current_line_bg),
1537 "line_number_fg" => Some(&mut self.line_number_fg),
1538 "line_number_bg" => Some(&mut self.line_number_bg),
1539 "diff_add_bg" => Some(&mut self.diff_add_bg),
1540 "diff_remove_bg" => Some(&mut self.diff_remove_bg),
1541 "diff_modify_bg" => Some(&mut self.diff_modify_bg),
1542 "ruler_bg" => Some(&mut self.ruler_bg),
1543 "whitespace_indicator_fg" => Some(&mut self.whitespace_indicator_fg),
1544 _ => None,
1545 },
1546 "ui" => match field {
1547 "tab_active_fg" => Some(&mut self.tab_active_fg),
1548 "tab_active_bg" => Some(&mut self.tab_active_bg),
1549 "tab_inactive_fg" => Some(&mut self.tab_inactive_fg),
1550 "tab_inactive_bg" => Some(&mut self.tab_inactive_bg),
1551 "status_bar_fg" => Some(&mut self.status_bar_fg),
1552 "status_bar_bg" => Some(&mut self.status_bar_bg),
1553 "prompt_fg" => Some(&mut self.prompt_fg),
1554 "prompt_bg" => Some(&mut self.prompt_bg),
1555 "prompt_selection_fg" => Some(&mut self.prompt_selection_fg),
1556 "prompt_selection_bg" => Some(&mut self.prompt_selection_bg),
1557 "popup_bg" => Some(&mut self.popup_bg),
1558 "popup_border_fg" => Some(&mut self.popup_border_fg),
1559 "popup_selection_bg" => Some(&mut self.popup_selection_bg),
1560 "popup_selection_fg" => Some(&mut self.popup_selection_fg),
1561 "popup_text_fg" => Some(&mut self.popup_text_fg),
1562 "menu_bg" => Some(&mut self.menu_bg),
1563 "menu_fg" => Some(&mut self.menu_fg),
1564 "menu_active_bg" => Some(&mut self.menu_active_bg),
1565 "menu_active_fg" => Some(&mut self.menu_active_fg),
1566 "help_bg" => Some(&mut self.help_bg),
1567 "help_fg" => Some(&mut self.help_fg),
1568 "help_key_fg" => Some(&mut self.help_key_fg),
1569 "split_separator_fg" => Some(&mut self.split_separator_fg),
1570 "scrollbar_thumb_fg" => Some(&mut self.scrollbar_thumb_fg),
1571 "semantic_highlight_bg" => Some(&mut self.semantic_highlight_bg),
1572 "file_status_added_fg" => Some(&mut self.file_status_added_fg),
1573 "file_status_modified_fg" => Some(&mut self.file_status_modified_fg),
1574 "file_status_deleted_fg" => Some(&mut self.file_status_deleted_fg),
1575 "file_status_renamed_fg" => Some(&mut self.file_status_renamed_fg),
1576 "file_status_untracked_fg" => Some(&mut self.file_status_untracked_fg),
1577 "file_status_conflicted_fg" => Some(&mut self.file_status_conflicted_fg),
1578 _ => None,
1579 },
1580 "syntax" => match field {
1581 "keyword" => Some(&mut self.syntax_keyword),
1582 "string" => Some(&mut self.syntax_string),
1583 "comment" => Some(&mut self.syntax_comment),
1584 "function" => Some(&mut self.syntax_function),
1585 "type" => Some(&mut self.syntax_type),
1586 "variable" => Some(&mut self.syntax_variable),
1587 "constant" => Some(&mut self.syntax_constant),
1588 "operator" => Some(&mut self.syntax_operator),
1589 "punctuation_bracket" => Some(&mut self.syntax_punctuation_bracket),
1590 "punctuation_delimiter" => Some(&mut self.syntax_punctuation_delimiter),
1591 _ => None,
1592 },
1593 "diagnostic" => match field {
1594 "error_fg" => Some(&mut self.diagnostic_error_fg),
1595 "error_bg" => Some(&mut self.diagnostic_error_bg),
1596 "warning_fg" => Some(&mut self.diagnostic_warning_fg),
1597 "warning_bg" => Some(&mut self.diagnostic_warning_bg),
1598 "info_fg" => Some(&mut self.diagnostic_info_fg),
1599 "info_bg" => Some(&mut self.diagnostic_info_bg),
1600 "hint_fg" => Some(&mut self.diagnostic_hint_fg),
1601 "hint_bg" => Some(&mut self.diagnostic_hint_bg),
1602 _ => None,
1603 },
1604 "search" => match field {
1605 "match_bg" => Some(&mut self.search_match_bg),
1606 "match_fg" => Some(&mut self.search_match_fg),
1607 "label_bg" => Some(&mut self.search_label_bg),
1608 "label_fg" => Some(&mut self.search_label_fg),
1609 _ => None,
1610 },
1611 _ => None,
1612 }
1613 }
1614
1615 pub fn override_colors<I, K>(&mut self, overrides: I) -> usize
1620 where
1621 I: IntoIterator<Item = (K, Color)>,
1622 K: AsRef<str>,
1623 {
1624 let mut applied = 0;
1625 for (key, color) in overrides {
1626 if let Some(slot) = self.resolve_theme_key_mut(key.as_ref()) {
1627 *slot = color;
1628 applied += 1;
1629 }
1630 }
1631 applied
1632 }
1633}
1634
1635pub fn get_theme_schema() -> serde_json::Value {
1643 use schemars::schema_for;
1644 let schema = schema_for!(ThemeFile);
1645 serde_json::to_value(&schema).unwrap_or_default()
1646}
1647
1648pub fn get_builtin_themes() -> serde_json::Value {
1650 let mut map = serde_json::Map::new();
1651 for theme in BUILTIN_THEMES {
1652 map.insert(
1653 theme.name.to_string(),
1654 serde_json::Value::String(theme.json.to_string()),
1655 );
1656 }
1657 serde_json::Value::Object(map)
1658}
1659
1660#[cfg(test)]
1661mod tests {
1662 use super::*;
1663
1664 #[test]
1665 fn test_load_builtin_theme() {
1666 let dark = Theme::load_builtin(THEME_DARK).expect("Dark theme must exist");
1667 assert_eq!(dark.name, THEME_DARK);
1668
1669 let light = Theme::load_builtin(THEME_LIGHT).expect("Light theme must exist");
1670 assert_eq!(light.name, THEME_LIGHT);
1671
1672 let high_contrast =
1673 Theme::load_builtin(THEME_HIGH_CONTRAST).expect("High contrast theme must exist");
1674 assert_eq!(high_contrast.name, THEME_HIGH_CONTRAST);
1675 }
1676
1677 #[test]
1678 fn test_builtin_themes_match_schema() {
1679 for theme in BUILTIN_THEMES {
1680 let _: ThemeFile = serde_json::from_str(theme.json)
1681 .unwrap_or_else(|_| panic!("Theme '{}' does not match schema", theme.name));
1682 }
1683 }
1684
1685 #[test]
1686 fn test_from_json() {
1687 let json = r#"{"name":"test","editor":{},"ui":{},"search":{},"diagnostic":{},"syntax":{}}"#;
1688 let theme = Theme::from_json(json).expect("Should parse minimal theme");
1689 assert_eq!(theme.name, "test");
1690 }
1691
1692 #[test]
1693 fn test_default_reset_color() {
1694 let color: Color = ColorDef::Named("Default".to_string()).into();
1696 assert_eq!(color, Color::Reset);
1697
1698 let color: Color = ColorDef::Named("Reset".to_string()).into();
1700 assert_eq!(color, Color::Reset);
1701 }
1702
1703 #[test]
1704 fn test_file_status_colors_fall_back_to_diagnostic_colors() {
1705 let json = r#"{
1707 "name": "test-fallback",
1708 "editor": {},
1709 "ui": {},
1710 "search": {},
1711 "diagnostic": {
1712 "error_fg": [220, 50, 47],
1713 "warning_fg": [181, 137, 0],
1714 "info_fg": [38, 139, 210],
1715 "hint_fg": [101, 123, 131]
1716 },
1717 "syntax": {}
1718 }"#;
1719 let theme = Theme::from_json(json).expect("Should parse theme without file_status keys");
1720
1721 assert_eq!(theme.file_status_added_fg, Color::Rgb(38, 139, 210));
1723 assert_eq!(theme.file_status_renamed_fg, Color::Rgb(38, 139, 210));
1724 assert_eq!(theme.file_status_modified_fg, Color::Rgb(181, 137, 0));
1726 assert_eq!(theme.file_status_deleted_fg, Color::Rgb(220, 50, 47));
1728 assert_eq!(theme.file_status_conflicted_fg, Color::Rgb(220, 50, 47));
1729 assert_eq!(theme.file_status_untracked_fg, Color::Rgb(101, 123, 131));
1731 }
1732
1733 #[test]
1734 fn test_file_status_colors_explicit_override() {
1735 let json = r#"{
1737 "name": "test-override",
1738 "editor": {},
1739 "ui": {
1740 "file_status_added_fg": [80, 250, 123],
1741 "file_status_modified_fg": [255, 184, 108]
1742 },
1743 "search": {},
1744 "diagnostic": {
1745 "info_fg": [38, 139, 210],
1746 "warning_fg": [181, 137, 0]
1747 },
1748 "syntax": {}
1749 }"#;
1750 let theme = Theme::from_json(json).expect("Should parse theme with file_status overrides");
1751
1752 assert_eq!(theme.file_status_added_fg, Color::Rgb(80, 250, 123));
1754 assert_eq!(theme.file_status_modified_fg, Color::Rgb(255, 184, 108));
1755 assert_eq!(theme.file_status_renamed_fg, Color::Rgb(38, 139, 210));
1757 }
1758
1759 #[test]
1760 fn test_file_status_colors_resolve_via_theme_key() {
1761 let json = r#"{
1762 "name": "test-resolve",
1763 "editor": {},
1764 "ui": {
1765 "file_status_added_fg": [80, 250, 123]
1766 },
1767 "search": {},
1768 "diagnostic": {
1769 "warning_fg": [181, 137, 0]
1770 },
1771 "syntax": {}
1772 }"#;
1773 let theme = Theme::from_json(json).expect("Should parse theme");
1774
1775 assert_eq!(
1777 theme.resolve_theme_key("ui.file_status_added_fg"),
1778 Some(Color::Rgb(80, 250, 123))
1779 );
1780 assert_eq!(
1781 theme.resolve_theme_key("ui.file_status_modified_fg"),
1782 Some(Color::Rgb(181, 137, 0))
1783 );
1784 }
1785
1786 #[test]
1787 fn override_colors_writes_known_keys_and_drops_unknowns() {
1788 let mut theme = Theme::load_builtin(THEME_DARK).expect("dark builtin");
1789 let applied = theme.override_colors([
1790 ("editor.bg".to_string(), Color::Rgb(10, 20, 30)),
1791 ("ui.status_bar_fg".to_string(), Color::Rgb(1, 2, 3)),
1792 ("does.not_exist".to_string(), Color::Rgb(9, 9, 9)),
1793 ("garbage_no_dot".to_string(), Color::Rgb(9, 9, 9)),
1794 ]);
1795 assert_eq!(applied, 2, "only the two valid keys should be applied");
1796 assert_eq!(
1797 theme.resolve_theme_key("editor.bg"),
1798 Some(Color::Rgb(10, 20, 30))
1799 );
1800 assert_eq!(
1801 theme.resolve_theme_key("ui.status_bar_fg"),
1802 Some(Color::Rgb(1, 2, 3))
1803 );
1804 }
1805
1806 #[test]
1807 fn resolve_theme_key_mut_matches_resolve_theme_key_domain() {
1808 let mut theme = Theme::load_builtin(THEME_DARK).expect("dark builtin");
1811 let probe = [
1812 "editor.bg",
1813 "editor.fg",
1814 "ui.status_bar_fg",
1815 "ui.tab_active_bg",
1816 "syntax.keyword",
1817 "diagnostic.error_fg",
1818 "search.match_bg",
1819 ];
1820 for key in probe {
1821 assert!(
1822 theme.resolve_theme_key(key).is_some(),
1823 "reader lost key {key}"
1824 );
1825 assert!(
1826 theme.resolve_theme_key_mut(key).is_some(),
1827 "mutator missing key {key}"
1828 );
1829 }
1830 }
1831
1832 #[test]
1833 fn test_all_builtin_themes_have_file_status_colors() {
1834 for builtin in BUILTIN_THEMES {
1836 let theme = Theme::from_json(builtin.json)
1837 .unwrap_or_else(|e| panic!("Theme '{}' failed to parse: {}", builtin.name, e));
1838
1839 for key in &[
1841 "ui.file_status_added_fg",
1842 "ui.file_status_modified_fg",
1843 "ui.file_status_deleted_fg",
1844 "ui.file_status_renamed_fg",
1845 "ui.file_status_untracked_fg",
1846 "ui.file_status_conflicted_fg",
1847 ] {
1848 assert!(
1849 theme.resolve_theme_key(key).is_some(),
1850 "Theme '{}' missing resolution for '{}'",
1851 builtin.name,
1852 key
1853 );
1854 }
1855 }
1856 }
1857}