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}
790
791fn default_search_match_bg() -> ColorDef {
793 ColorDef::Rgb(100, 100, 20)
794}
795fn default_search_match_fg() -> ColorDef {
796 ColorDef::Rgb(255, 255, 255)
797}
798
799#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
801pub struct DiagnosticColors {
802 #[serde(default = "default_diagnostic_error_fg")]
804 pub error_fg: ColorDef,
805 #[serde(default = "default_diagnostic_error_bg")]
807 pub error_bg: ColorDef,
808 #[serde(default = "default_diagnostic_warning_fg")]
810 pub warning_fg: ColorDef,
811 #[serde(default = "default_diagnostic_warning_bg")]
813 pub warning_bg: ColorDef,
814 #[serde(default = "default_diagnostic_info_fg")]
816 pub info_fg: ColorDef,
817 #[serde(default = "default_diagnostic_info_bg")]
819 pub info_bg: ColorDef,
820 #[serde(default = "default_diagnostic_hint_fg")]
822 pub hint_fg: ColorDef,
823 #[serde(default = "default_diagnostic_hint_bg")]
825 pub hint_bg: ColorDef,
826}
827
828fn default_diagnostic_error_fg() -> ColorDef {
830 ColorDef::Named("Red".to_string())
831}
832fn default_diagnostic_error_bg() -> ColorDef {
833 ColorDef::Rgb(60, 20, 20)
834}
835fn default_diagnostic_warning_fg() -> ColorDef {
836 ColorDef::Named("Yellow".to_string())
837}
838fn default_diagnostic_warning_bg() -> ColorDef {
839 ColorDef::Rgb(60, 50, 0)
840}
841fn default_diagnostic_info_fg() -> ColorDef {
842 ColorDef::Named("Blue".to_string())
843}
844fn default_diagnostic_info_bg() -> ColorDef {
845 ColorDef::Rgb(0, 30, 60)
846}
847fn default_diagnostic_hint_fg() -> ColorDef {
848 ColorDef::Named("Gray".to_string())
849}
850fn default_diagnostic_hint_bg() -> ColorDef {
851 ColorDef::Rgb(30, 30, 30)
852}
853
854#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
856pub struct SyntaxColors {
857 #[serde(default = "default_syntax_keyword")]
859 pub keyword: ColorDef,
860 #[serde(default = "default_syntax_string")]
862 pub string: ColorDef,
863 #[serde(default = "default_syntax_comment")]
865 pub comment: ColorDef,
866 #[serde(default = "default_syntax_function")]
868 pub function: ColorDef,
869 #[serde(rename = "type", default = "default_syntax_type")]
871 pub type_: ColorDef,
872 #[serde(default = "default_syntax_variable")]
874 pub variable: ColorDef,
875 #[serde(default = "default_syntax_constant")]
877 pub constant: ColorDef,
878 #[serde(default = "default_syntax_operator")]
880 pub operator: ColorDef,
881 #[serde(default = "default_syntax_punctuation_bracket")]
883 pub punctuation_bracket: ColorDef,
884 #[serde(default = "default_syntax_punctuation_delimiter")]
886 pub punctuation_delimiter: ColorDef,
887}
888
889fn default_syntax_keyword() -> ColorDef {
891 ColorDef::Rgb(86, 156, 214)
892}
893fn default_syntax_string() -> ColorDef {
894 ColorDef::Rgb(206, 145, 120)
895}
896fn default_syntax_comment() -> ColorDef {
897 ColorDef::Rgb(106, 153, 85)
898}
899fn default_syntax_function() -> ColorDef {
900 ColorDef::Rgb(220, 220, 170)
901}
902fn default_syntax_type() -> ColorDef {
903 ColorDef::Rgb(78, 201, 176)
904}
905fn default_syntax_variable() -> ColorDef {
906 ColorDef::Rgb(156, 220, 254)
907}
908fn default_syntax_constant() -> ColorDef {
909 ColorDef::Rgb(79, 193, 255)
910}
911fn default_syntax_operator() -> ColorDef {
912 ColorDef::Rgb(212, 212, 212)
913}
914fn default_syntax_punctuation_bracket() -> ColorDef {
915 ColorDef::Rgb(212, 212, 212) }
917fn default_syntax_punctuation_delimiter() -> ColorDef {
918 ColorDef::Rgb(212, 212, 212) }
920
921#[derive(Debug, Clone)]
923pub struct Theme {
924 pub name: String,
926
927 pub editor_bg: Color,
929 pub editor_fg: Color,
930 pub cursor: Color,
931 pub inactive_cursor: Color,
932 pub selection_bg: Color,
933 pub current_line_bg: Color,
934 pub line_number_fg: Color,
935 pub line_number_bg: Color,
936
937 pub after_eof_bg: Color,
939
940 pub ruler_bg: Color,
942
943 pub whitespace_indicator_fg: Color,
945
946 pub diff_add_bg: Color,
948 pub diff_remove_bg: Color,
949 pub diff_modify_bg: Color,
950 pub diff_add_highlight_bg: Color,
952 pub diff_remove_highlight_bg: Color,
954
955 pub tab_active_fg: Color,
957 pub tab_active_bg: Color,
958 pub tab_inactive_fg: Color,
959 pub tab_inactive_bg: Color,
960 pub tab_separator_bg: Color,
961 pub tab_close_hover_fg: Color,
962 pub tab_hover_bg: Color,
963
964 pub menu_bg: Color,
966 pub menu_fg: Color,
967 pub menu_active_bg: Color,
968 pub menu_active_fg: Color,
969 pub menu_dropdown_bg: Color,
970 pub menu_dropdown_fg: Color,
971 pub menu_highlight_bg: Color,
972 pub menu_highlight_fg: Color,
973 pub menu_border_fg: Color,
974 pub menu_separator_fg: Color,
975 pub menu_hover_bg: Color,
976 pub menu_hover_fg: Color,
977 pub menu_disabled_fg: Color,
978 pub menu_disabled_bg: Color,
979
980 pub status_bar_fg: Color,
981 pub status_bar_bg: Color,
982 pub prompt_fg: Color,
983 pub prompt_bg: Color,
984 pub prompt_selection_fg: Color,
985 pub prompt_selection_bg: Color,
986
987 pub popup_border_fg: Color,
988 pub popup_bg: Color,
989 pub popup_selection_bg: Color,
990 pub popup_selection_fg: Color,
991 pub popup_text_fg: Color,
992
993 pub suggestion_bg: Color,
994 pub suggestion_selected_bg: Color,
995
996 pub help_bg: Color,
997 pub help_fg: Color,
998 pub help_key_fg: Color,
999 pub help_separator_fg: Color,
1000
1001 pub help_indicator_fg: Color,
1002 pub help_indicator_bg: Color,
1003
1004 pub inline_code_bg: Color,
1006
1007 pub split_separator_fg: Color,
1008 pub split_separator_hover_fg: Color,
1009
1010 pub scrollbar_track_fg: Color,
1012 pub scrollbar_thumb_fg: Color,
1013 pub scrollbar_track_hover_fg: Color,
1014 pub scrollbar_thumb_hover_fg: Color,
1015
1016 pub compose_margin_bg: Color,
1018
1019 pub semantic_highlight_bg: Color,
1021
1022 pub terminal_bg: Color,
1024 pub terminal_fg: Color,
1025
1026 pub status_warning_indicator_bg: Color,
1028 pub status_warning_indicator_fg: Color,
1029 pub status_error_indicator_bg: Color,
1030 pub status_error_indicator_fg: Color,
1031 pub status_warning_indicator_hover_bg: Color,
1032 pub status_warning_indicator_hover_fg: Color,
1033 pub status_error_indicator_hover_bg: Color,
1034 pub status_error_indicator_hover_fg: Color,
1035
1036 pub tab_drop_zone_bg: Color,
1038 pub tab_drop_zone_border: Color,
1039
1040 pub settings_selected_bg: Color,
1042 pub settings_selected_fg: Color,
1043
1044 pub file_status_added_fg: Color,
1046 pub file_status_modified_fg: Color,
1047 pub file_status_deleted_fg: Color,
1048 pub file_status_renamed_fg: Color,
1049 pub file_status_untracked_fg: Color,
1050 pub file_status_conflicted_fg: Color,
1051
1052 pub search_match_bg: Color,
1054 pub search_match_fg: Color,
1055
1056 pub diagnostic_error_fg: Color,
1058 pub diagnostic_error_bg: Color,
1059 pub diagnostic_warning_fg: Color,
1060 pub diagnostic_warning_bg: Color,
1061 pub diagnostic_info_fg: Color,
1062 pub diagnostic_info_bg: Color,
1063 pub diagnostic_hint_fg: Color,
1064 pub diagnostic_hint_bg: Color,
1065
1066 pub syntax_keyword: Color,
1068 pub syntax_string: Color,
1069 pub syntax_comment: Color,
1070 pub syntax_function: Color,
1071 pub syntax_type: Color,
1072 pub syntax_variable: Color,
1073 pub syntax_constant: Color,
1074 pub syntax_operator: Color,
1075 pub syntax_punctuation_bracket: Color,
1076 pub syntax_punctuation_delimiter: Color,
1077}
1078
1079impl From<ThemeFile> for Theme {
1080 fn from(file: ThemeFile) -> Self {
1081 Self {
1082 name: file.name,
1083 editor_bg: file.editor.bg.clone().into(),
1084 editor_fg: file.editor.fg.into(),
1085 cursor: file.editor.cursor.into(),
1086 inactive_cursor: file.editor.inactive_cursor.into(),
1087 selection_bg: file.editor.selection_bg.into(),
1088 current_line_bg: file.editor.current_line_bg.into(),
1089 line_number_fg: file.editor.line_number_fg.into(),
1090 line_number_bg: file.editor.line_number_bg.into(),
1091 after_eof_bg: file
1094 .editor
1095 .after_eof_bg
1096 .clone()
1097 .map(|c| c.into())
1098 .unwrap_or_else(|| shade_toward_contrast(file.editor.bg.clone().into(), 10)),
1099 ruler_bg: file.editor.ruler_bg.into(),
1100 whitespace_indicator_fg: file.editor.whitespace_indicator_fg.into(),
1101 diff_add_bg: file.editor.diff_add_bg.clone().into(),
1102 diff_remove_bg: file.editor.diff_remove_bg.clone().into(),
1103 diff_modify_bg: file.editor.diff_modify_bg.into(),
1104 diff_add_highlight_bg: file
1106 .editor
1107 .diff_add_highlight_bg
1108 .map(|c| c.into())
1109 .unwrap_or_else(|| brighten_color(file.editor.diff_add_bg.into(), 40)),
1110 diff_remove_highlight_bg: file
1111 .editor
1112 .diff_remove_highlight_bg
1113 .map(|c| c.into())
1114 .unwrap_or_else(|| brighten_color(file.editor.diff_remove_bg.into(), 40)),
1115 tab_active_fg: file.ui.tab_active_fg.into(),
1116 tab_active_bg: file.ui.tab_active_bg.into(),
1117 tab_inactive_fg: file.ui.tab_inactive_fg.into(),
1118 tab_inactive_bg: file.ui.tab_inactive_bg.into(),
1119 tab_separator_bg: file.ui.tab_separator_bg.into(),
1120 tab_close_hover_fg: file.ui.tab_close_hover_fg.into(),
1121 tab_hover_bg: file.ui.tab_hover_bg.into(),
1122 menu_bg: file.ui.menu_bg.into(),
1123 menu_fg: file.ui.menu_fg.into(),
1124 menu_active_bg: file.ui.menu_active_bg.into(),
1125 menu_active_fg: file.ui.menu_active_fg.into(),
1126 menu_dropdown_bg: file.ui.menu_dropdown_bg.into(),
1127 menu_dropdown_fg: file.ui.menu_dropdown_fg.into(),
1128 menu_highlight_bg: file.ui.menu_highlight_bg.into(),
1129 menu_highlight_fg: file.ui.menu_highlight_fg.into(),
1130 menu_border_fg: file.ui.menu_border_fg.into(),
1131 menu_separator_fg: file.ui.menu_separator_fg.into(),
1132 menu_hover_bg: file.ui.menu_hover_bg.into(),
1133 menu_hover_fg: file.ui.menu_hover_fg.into(),
1134 menu_disabled_fg: file.ui.menu_disabled_fg.into(),
1135 menu_disabled_bg: file.ui.menu_disabled_bg.into(),
1136 status_bar_fg: file.ui.status_bar_fg.into(),
1137 status_bar_bg: file.ui.status_bar_bg.into(),
1138 prompt_fg: file.ui.prompt_fg.into(),
1139 prompt_bg: file.ui.prompt_bg.into(),
1140 prompt_selection_fg: file.ui.prompt_selection_fg.into(),
1141 prompt_selection_bg: file.ui.prompt_selection_bg.into(),
1142 popup_border_fg: file.ui.popup_border_fg.into(),
1143 popup_bg: file.ui.popup_bg.into(),
1144 popup_selection_bg: file.ui.popup_selection_bg.into(),
1145 popup_selection_fg: file.ui.popup_selection_fg.into(),
1146 popup_text_fg: file.ui.popup_text_fg.into(),
1147 suggestion_bg: file.ui.suggestion_bg.into(),
1148 suggestion_selected_bg: file.ui.suggestion_selected_bg.into(),
1149 help_bg: file.ui.help_bg.into(),
1150 help_fg: file.ui.help_fg.into(),
1151 help_key_fg: file.ui.help_key_fg.into(),
1152 help_separator_fg: file.ui.help_separator_fg.into(),
1153 help_indicator_fg: file.ui.help_indicator_fg.into(),
1154 help_indicator_bg: file.ui.help_indicator_bg.into(),
1155 inline_code_bg: file.ui.inline_code_bg.into(),
1156 split_separator_fg: file.ui.split_separator_fg.into(),
1157 split_separator_hover_fg: file.ui.split_separator_hover_fg.into(),
1158 scrollbar_track_fg: file.ui.scrollbar_track_fg.into(),
1159 scrollbar_thumb_fg: file.ui.scrollbar_thumb_fg.into(),
1160 scrollbar_track_hover_fg: file.ui.scrollbar_track_hover_fg.into(),
1161 scrollbar_thumb_hover_fg: file.ui.scrollbar_thumb_hover_fg.into(),
1162 compose_margin_bg: file.ui.compose_margin_bg.into(),
1163 semantic_highlight_bg: file.ui.semantic_highlight_bg.into(),
1164 terminal_bg: file.ui.terminal_bg.into(),
1165 terminal_fg: file.ui.terminal_fg.into(),
1166 status_warning_indicator_bg: file.ui.status_warning_indicator_bg.into(),
1167 status_warning_indicator_fg: file.ui.status_warning_indicator_fg.into(),
1168 status_error_indicator_bg: file.ui.status_error_indicator_bg.into(),
1169 status_error_indicator_fg: file.ui.status_error_indicator_fg.into(),
1170 status_warning_indicator_hover_bg: file.ui.status_warning_indicator_hover_bg.into(),
1171 status_warning_indicator_hover_fg: file.ui.status_warning_indicator_hover_fg.into(),
1172 status_error_indicator_hover_bg: file.ui.status_error_indicator_hover_bg.into(),
1173 status_error_indicator_hover_fg: file.ui.status_error_indicator_hover_fg.into(),
1174 tab_drop_zone_bg: file.ui.tab_drop_zone_bg.into(),
1175 tab_drop_zone_border: file.ui.tab_drop_zone_border.into(),
1176 settings_selected_bg: file.ui.settings_selected_bg.into(),
1177 settings_selected_fg: file.ui.settings_selected_fg.into(),
1178 file_status_added_fg: file
1179 .ui
1180 .file_status_added_fg
1181 .map(|c| c.into())
1182 .unwrap_or_else(|| file.diagnostic.info_fg.clone().into()),
1183 file_status_modified_fg: file
1184 .ui
1185 .file_status_modified_fg
1186 .map(|c| c.into())
1187 .unwrap_or_else(|| file.diagnostic.warning_fg.clone().into()),
1188 file_status_deleted_fg: file
1189 .ui
1190 .file_status_deleted_fg
1191 .map(|c| c.into())
1192 .unwrap_or_else(|| file.diagnostic.error_fg.clone().into()),
1193 file_status_renamed_fg: file
1194 .ui
1195 .file_status_renamed_fg
1196 .map(|c| c.into())
1197 .unwrap_or_else(|| file.diagnostic.info_fg.clone().into()),
1198 file_status_untracked_fg: file
1199 .ui
1200 .file_status_untracked_fg
1201 .map(|c| c.into())
1202 .unwrap_or_else(|| file.diagnostic.hint_fg.clone().into()),
1203 file_status_conflicted_fg: file
1204 .ui
1205 .file_status_conflicted_fg
1206 .map(|c| c.into())
1207 .unwrap_or_else(|| file.diagnostic.error_fg.clone().into()),
1208 search_match_bg: file.search.match_bg.into(),
1209 search_match_fg: file.search.match_fg.into(),
1210 diagnostic_error_fg: file.diagnostic.error_fg.into(),
1211 diagnostic_error_bg: file.diagnostic.error_bg.into(),
1212 diagnostic_warning_fg: file.diagnostic.warning_fg.into(),
1213 diagnostic_warning_bg: file.diagnostic.warning_bg.into(),
1214 diagnostic_info_fg: file.diagnostic.info_fg.into(),
1215 diagnostic_info_bg: file.diagnostic.info_bg.into(),
1216 diagnostic_hint_fg: file.diagnostic.hint_fg.into(),
1217 diagnostic_hint_bg: file.diagnostic.hint_bg.into(),
1218 syntax_keyword: file.syntax.keyword.into(),
1219 syntax_string: file.syntax.string.into(),
1220 syntax_comment: file.syntax.comment.into(),
1221 syntax_function: file.syntax.function.into(),
1222 syntax_type: file.syntax.type_.into(),
1223 syntax_variable: file.syntax.variable.into(),
1224 syntax_constant: file.syntax.constant.into(),
1225 syntax_operator: file.syntax.operator.into(),
1226 syntax_punctuation_bracket: file.syntax.punctuation_bracket.into(),
1227 syntax_punctuation_delimiter: file.syntax.punctuation_delimiter.into(),
1228 }
1229 }
1230}
1231
1232impl From<Theme> for ThemeFile {
1233 fn from(theme: Theme) -> Self {
1234 Self {
1235 name: theme.name,
1236 editor: EditorColors {
1237 bg: theme.editor_bg.into(),
1238 fg: theme.editor_fg.into(),
1239 cursor: theme.cursor.into(),
1240 inactive_cursor: theme.inactive_cursor.into(),
1241 selection_bg: theme.selection_bg.into(),
1242 current_line_bg: theme.current_line_bg.into(),
1243 line_number_fg: theme.line_number_fg.into(),
1244 line_number_bg: theme.line_number_bg.into(),
1245 diff_add_bg: theme.diff_add_bg.into(),
1246 diff_remove_bg: theme.diff_remove_bg.into(),
1247 diff_add_highlight_bg: Some(theme.diff_add_highlight_bg.into()),
1248 diff_remove_highlight_bg: Some(theme.diff_remove_highlight_bg.into()),
1249 diff_modify_bg: theme.diff_modify_bg.into(),
1250 ruler_bg: theme.ruler_bg.into(),
1251 whitespace_indicator_fg: theme.whitespace_indicator_fg.into(),
1252 after_eof_bg: Some(theme.after_eof_bg.into()),
1253 },
1254 ui: UiColors {
1255 tab_active_fg: theme.tab_active_fg.into(),
1256 tab_active_bg: theme.tab_active_bg.into(),
1257 tab_inactive_fg: theme.tab_inactive_fg.into(),
1258 tab_inactive_bg: theme.tab_inactive_bg.into(),
1259 tab_separator_bg: theme.tab_separator_bg.into(),
1260 tab_close_hover_fg: theme.tab_close_hover_fg.into(),
1261 tab_hover_bg: theme.tab_hover_bg.into(),
1262 menu_bg: theme.menu_bg.into(),
1263 menu_fg: theme.menu_fg.into(),
1264 menu_active_bg: theme.menu_active_bg.into(),
1265 menu_active_fg: theme.menu_active_fg.into(),
1266 menu_dropdown_bg: theme.menu_dropdown_bg.into(),
1267 menu_dropdown_fg: theme.menu_dropdown_fg.into(),
1268 menu_highlight_bg: theme.menu_highlight_bg.into(),
1269 menu_highlight_fg: theme.menu_highlight_fg.into(),
1270 menu_border_fg: theme.menu_border_fg.into(),
1271 menu_separator_fg: theme.menu_separator_fg.into(),
1272 menu_hover_bg: theme.menu_hover_bg.into(),
1273 menu_hover_fg: theme.menu_hover_fg.into(),
1274 menu_disabled_fg: theme.menu_disabled_fg.into(),
1275 menu_disabled_bg: theme.menu_disabled_bg.into(),
1276 status_bar_fg: theme.status_bar_fg.into(),
1277 status_bar_bg: theme.status_bar_bg.into(),
1278 prompt_fg: theme.prompt_fg.into(),
1279 prompt_bg: theme.prompt_bg.into(),
1280 prompt_selection_fg: theme.prompt_selection_fg.into(),
1281 prompt_selection_bg: theme.prompt_selection_bg.into(),
1282 popup_border_fg: theme.popup_border_fg.into(),
1283 popup_bg: theme.popup_bg.into(),
1284 popup_selection_bg: theme.popup_selection_bg.into(),
1285 popup_selection_fg: theme.popup_selection_fg.into(),
1286 popup_text_fg: theme.popup_text_fg.into(),
1287 suggestion_bg: theme.suggestion_bg.into(),
1288 suggestion_selected_bg: theme.suggestion_selected_bg.into(),
1289 help_bg: theme.help_bg.into(),
1290 help_fg: theme.help_fg.into(),
1291 help_key_fg: theme.help_key_fg.into(),
1292 help_separator_fg: theme.help_separator_fg.into(),
1293 help_indicator_fg: theme.help_indicator_fg.into(),
1294 help_indicator_bg: theme.help_indicator_bg.into(),
1295 inline_code_bg: theme.inline_code_bg.into(),
1296 split_separator_fg: theme.split_separator_fg.into(),
1297 split_separator_hover_fg: theme.split_separator_hover_fg.into(),
1298 scrollbar_track_fg: theme.scrollbar_track_fg.into(),
1299 scrollbar_thumb_fg: theme.scrollbar_thumb_fg.into(),
1300 scrollbar_track_hover_fg: theme.scrollbar_track_hover_fg.into(),
1301 scrollbar_thumb_hover_fg: theme.scrollbar_thumb_hover_fg.into(),
1302 compose_margin_bg: theme.compose_margin_bg.into(),
1303 semantic_highlight_bg: theme.semantic_highlight_bg.into(),
1304 terminal_bg: theme.terminal_bg.into(),
1305 terminal_fg: theme.terminal_fg.into(),
1306 status_warning_indicator_bg: theme.status_warning_indicator_bg.into(),
1307 status_warning_indicator_fg: theme.status_warning_indicator_fg.into(),
1308 status_error_indicator_bg: theme.status_error_indicator_bg.into(),
1309 status_error_indicator_fg: theme.status_error_indicator_fg.into(),
1310 status_warning_indicator_hover_bg: theme.status_warning_indicator_hover_bg.into(),
1311 status_warning_indicator_hover_fg: theme.status_warning_indicator_hover_fg.into(),
1312 status_error_indicator_hover_bg: theme.status_error_indicator_hover_bg.into(),
1313 status_error_indicator_hover_fg: theme.status_error_indicator_hover_fg.into(),
1314 tab_drop_zone_bg: theme.tab_drop_zone_bg.into(),
1315 tab_drop_zone_border: theme.tab_drop_zone_border.into(),
1316 settings_selected_bg: theme.settings_selected_bg.into(),
1317 settings_selected_fg: theme.settings_selected_fg.into(),
1318 file_status_added_fg: Some(theme.file_status_added_fg.into()),
1319 file_status_modified_fg: Some(theme.file_status_modified_fg.into()),
1320 file_status_deleted_fg: Some(theme.file_status_deleted_fg.into()),
1321 file_status_renamed_fg: Some(theme.file_status_renamed_fg.into()),
1322 file_status_untracked_fg: Some(theme.file_status_untracked_fg.into()),
1323 file_status_conflicted_fg: Some(theme.file_status_conflicted_fg.into()),
1324 },
1325 search: SearchColors {
1326 match_bg: theme.search_match_bg.into(),
1327 match_fg: theme.search_match_fg.into(),
1328 },
1329 diagnostic: DiagnosticColors {
1330 error_fg: theme.diagnostic_error_fg.into(),
1331 error_bg: theme.diagnostic_error_bg.into(),
1332 warning_fg: theme.diagnostic_warning_fg.into(),
1333 warning_bg: theme.diagnostic_warning_bg.into(),
1334 info_fg: theme.diagnostic_info_fg.into(),
1335 info_bg: theme.diagnostic_info_bg.into(),
1336 hint_fg: theme.diagnostic_hint_fg.into(),
1337 hint_bg: theme.diagnostic_hint_bg.into(),
1338 },
1339 syntax: SyntaxColors {
1340 keyword: theme.syntax_keyword.into(),
1341 string: theme.syntax_string.into(),
1342 comment: theme.syntax_comment.into(),
1343 function: theme.syntax_function.into(),
1344 type_: theme.syntax_type.into(),
1345 variable: theme.syntax_variable.into(),
1346 constant: theme.syntax_constant.into(),
1347 operator: theme.syntax_operator.into(),
1348 punctuation_bracket: theme.syntax_punctuation_bracket.into(),
1349 punctuation_delimiter: theme.syntax_punctuation_delimiter.into(),
1350 },
1351 }
1352 }
1353}
1354
1355impl Theme {
1356 pub fn is_light(&self) -> bool {
1362 if let Some((r, g, b)) = color_to_rgb(self.editor_bg) {
1363 let lum = 0.2126 * (r as f64 / 255.0)
1365 + 0.7152 * (g as f64 / 255.0)
1366 + 0.0722 * (b as f64 / 255.0);
1367 lum > 0.5
1368 } else {
1369 false
1370 }
1371 }
1372
1373 pub fn load_builtin(name: &str) -> Option<Self> {
1375 BUILTIN_THEMES
1376 .iter()
1377 .find(|t| t.name == name)
1378 .and_then(|t| serde_json::from_str::<ThemeFile>(t.json).ok())
1379 .map(|tf| tf.into())
1380 }
1381
1382 pub fn from_json(json: &str) -> Result<Self, String> {
1384 let theme_file: ThemeFile =
1385 serde_json::from_str(json).map_err(|e| format!("Failed to parse theme JSON: {}", e))?;
1386 Ok(theme_file.into())
1387 }
1388
1389 pub fn resolve_theme_key(&self, key: &str) -> Option<Color> {
1400 let parts: Vec<&str> = key.split('.').collect();
1402 if parts.len() != 2 {
1403 return None;
1404 }
1405
1406 let (section, field) = (parts[0], parts[1]);
1407
1408 match section {
1409 "editor" => match field {
1410 "bg" => Some(self.editor_bg),
1411 "fg" => Some(self.editor_fg),
1412 "cursor" => Some(self.cursor),
1413 "inactive_cursor" => Some(self.inactive_cursor),
1414 "selection_bg" => Some(self.selection_bg),
1415 "current_line_bg" => Some(self.current_line_bg),
1416 "line_number_fg" => Some(self.line_number_fg),
1417 "line_number_bg" => Some(self.line_number_bg),
1418 "diff_add_bg" => Some(self.diff_add_bg),
1419 "diff_remove_bg" => Some(self.diff_remove_bg),
1420 "diff_modify_bg" => Some(self.diff_modify_bg),
1421 "ruler_bg" => Some(self.ruler_bg),
1422 "whitespace_indicator_fg" => Some(self.whitespace_indicator_fg),
1423 _ => None,
1424 },
1425 "ui" => match field {
1426 "tab_active_fg" => Some(self.tab_active_fg),
1427 "tab_active_bg" => Some(self.tab_active_bg),
1428 "tab_inactive_fg" => Some(self.tab_inactive_fg),
1429 "tab_inactive_bg" => Some(self.tab_inactive_bg),
1430 "status_bar_fg" => Some(self.status_bar_fg),
1431 "status_bar_bg" => Some(self.status_bar_bg),
1432 "prompt_fg" => Some(self.prompt_fg),
1433 "prompt_bg" => Some(self.prompt_bg),
1434 "prompt_selection_fg" => Some(self.prompt_selection_fg),
1435 "prompt_selection_bg" => Some(self.prompt_selection_bg),
1436 "popup_bg" => Some(self.popup_bg),
1437 "popup_border_fg" => Some(self.popup_border_fg),
1438 "popup_selection_bg" => Some(self.popup_selection_bg),
1439 "popup_selection_fg" => Some(self.popup_selection_fg),
1440 "popup_text_fg" => Some(self.popup_text_fg),
1441 "menu_bg" => Some(self.menu_bg),
1442 "menu_fg" => Some(self.menu_fg),
1443 "menu_active_bg" => Some(self.menu_active_bg),
1444 "menu_active_fg" => Some(self.menu_active_fg),
1445 "help_bg" => Some(self.help_bg),
1446 "help_fg" => Some(self.help_fg),
1447 "help_key_fg" => Some(self.help_key_fg),
1448 "split_separator_fg" => Some(self.split_separator_fg),
1449 "scrollbar_thumb_fg" => Some(self.scrollbar_thumb_fg),
1450 "semantic_highlight_bg" => Some(self.semantic_highlight_bg),
1451 "file_status_added_fg" => Some(self.file_status_added_fg),
1452 "file_status_modified_fg" => Some(self.file_status_modified_fg),
1453 "file_status_deleted_fg" => Some(self.file_status_deleted_fg),
1454 "file_status_renamed_fg" => Some(self.file_status_renamed_fg),
1455 "file_status_untracked_fg" => Some(self.file_status_untracked_fg),
1456 "file_status_conflicted_fg" => Some(self.file_status_conflicted_fg),
1457 _ => None,
1458 },
1459 "syntax" => match field {
1460 "keyword" => Some(self.syntax_keyword),
1461 "string" => Some(self.syntax_string),
1462 "comment" => Some(self.syntax_comment),
1463 "function" => Some(self.syntax_function),
1464 "type" => Some(self.syntax_type),
1465 "variable" => Some(self.syntax_variable),
1466 "constant" => Some(self.syntax_constant),
1467 "operator" => Some(self.syntax_operator),
1468 "punctuation_bracket" => Some(self.syntax_punctuation_bracket),
1469 "punctuation_delimiter" => Some(self.syntax_punctuation_delimiter),
1470 _ => None,
1471 },
1472 "diagnostic" => match field {
1473 "error_fg" => Some(self.diagnostic_error_fg),
1474 "error_bg" => Some(self.diagnostic_error_bg),
1475 "warning_fg" => Some(self.diagnostic_warning_fg),
1476 "warning_bg" => Some(self.diagnostic_warning_bg),
1477 "info_fg" => Some(self.diagnostic_info_fg),
1478 "info_bg" => Some(self.diagnostic_info_bg),
1479 "hint_fg" => Some(self.diagnostic_hint_fg),
1480 "hint_bg" => Some(self.diagnostic_hint_bg),
1481 _ => None,
1482 },
1483 "search" => match field {
1484 "match_bg" => Some(self.search_match_bg),
1485 "match_fg" => Some(self.search_match_fg),
1486 _ => None,
1487 },
1488 _ => None,
1489 }
1490 }
1491
1492 pub fn resolve_theme_key_mut(&mut self, key: &str) -> Option<&mut Color> {
1496 let parts: Vec<&str> = key.split('.').collect();
1497 if parts.len() != 2 {
1498 return None;
1499 }
1500 let (section, field) = (parts[0], parts[1]);
1501 match section {
1502 "editor" => match field {
1503 "bg" => Some(&mut self.editor_bg),
1504 "fg" => Some(&mut self.editor_fg),
1505 "cursor" => Some(&mut self.cursor),
1506 "inactive_cursor" => Some(&mut self.inactive_cursor),
1507 "selection_bg" => Some(&mut self.selection_bg),
1508 "current_line_bg" => Some(&mut self.current_line_bg),
1509 "line_number_fg" => Some(&mut self.line_number_fg),
1510 "line_number_bg" => Some(&mut self.line_number_bg),
1511 "diff_add_bg" => Some(&mut self.diff_add_bg),
1512 "diff_remove_bg" => Some(&mut self.diff_remove_bg),
1513 "diff_modify_bg" => Some(&mut self.diff_modify_bg),
1514 "ruler_bg" => Some(&mut self.ruler_bg),
1515 "whitespace_indicator_fg" => Some(&mut self.whitespace_indicator_fg),
1516 _ => None,
1517 },
1518 "ui" => match field {
1519 "tab_active_fg" => Some(&mut self.tab_active_fg),
1520 "tab_active_bg" => Some(&mut self.tab_active_bg),
1521 "tab_inactive_fg" => Some(&mut self.tab_inactive_fg),
1522 "tab_inactive_bg" => Some(&mut self.tab_inactive_bg),
1523 "status_bar_fg" => Some(&mut self.status_bar_fg),
1524 "status_bar_bg" => Some(&mut self.status_bar_bg),
1525 "prompt_fg" => Some(&mut self.prompt_fg),
1526 "prompt_bg" => Some(&mut self.prompt_bg),
1527 "prompt_selection_fg" => Some(&mut self.prompt_selection_fg),
1528 "prompt_selection_bg" => Some(&mut self.prompt_selection_bg),
1529 "popup_bg" => Some(&mut self.popup_bg),
1530 "popup_border_fg" => Some(&mut self.popup_border_fg),
1531 "popup_selection_bg" => Some(&mut self.popup_selection_bg),
1532 "popup_selection_fg" => Some(&mut self.popup_selection_fg),
1533 "popup_text_fg" => Some(&mut self.popup_text_fg),
1534 "menu_bg" => Some(&mut self.menu_bg),
1535 "menu_fg" => Some(&mut self.menu_fg),
1536 "menu_active_bg" => Some(&mut self.menu_active_bg),
1537 "menu_active_fg" => Some(&mut self.menu_active_fg),
1538 "help_bg" => Some(&mut self.help_bg),
1539 "help_fg" => Some(&mut self.help_fg),
1540 "help_key_fg" => Some(&mut self.help_key_fg),
1541 "split_separator_fg" => Some(&mut self.split_separator_fg),
1542 "scrollbar_thumb_fg" => Some(&mut self.scrollbar_thumb_fg),
1543 "semantic_highlight_bg" => Some(&mut self.semantic_highlight_bg),
1544 "file_status_added_fg" => Some(&mut self.file_status_added_fg),
1545 "file_status_modified_fg" => Some(&mut self.file_status_modified_fg),
1546 "file_status_deleted_fg" => Some(&mut self.file_status_deleted_fg),
1547 "file_status_renamed_fg" => Some(&mut self.file_status_renamed_fg),
1548 "file_status_untracked_fg" => Some(&mut self.file_status_untracked_fg),
1549 "file_status_conflicted_fg" => Some(&mut self.file_status_conflicted_fg),
1550 _ => None,
1551 },
1552 "syntax" => match field {
1553 "keyword" => Some(&mut self.syntax_keyword),
1554 "string" => Some(&mut self.syntax_string),
1555 "comment" => Some(&mut self.syntax_comment),
1556 "function" => Some(&mut self.syntax_function),
1557 "type" => Some(&mut self.syntax_type),
1558 "variable" => Some(&mut self.syntax_variable),
1559 "constant" => Some(&mut self.syntax_constant),
1560 "operator" => Some(&mut self.syntax_operator),
1561 "punctuation_bracket" => Some(&mut self.syntax_punctuation_bracket),
1562 "punctuation_delimiter" => Some(&mut self.syntax_punctuation_delimiter),
1563 _ => None,
1564 },
1565 "diagnostic" => match field {
1566 "error_fg" => Some(&mut self.diagnostic_error_fg),
1567 "error_bg" => Some(&mut self.diagnostic_error_bg),
1568 "warning_fg" => Some(&mut self.diagnostic_warning_fg),
1569 "warning_bg" => Some(&mut self.diagnostic_warning_bg),
1570 "info_fg" => Some(&mut self.diagnostic_info_fg),
1571 "info_bg" => Some(&mut self.diagnostic_info_bg),
1572 "hint_fg" => Some(&mut self.diagnostic_hint_fg),
1573 "hint_bg" => Some(&mut self.diagnostic_hint_bg),
1574 _ => None,
1575 },
1576 "search" => match field {
1577 "match_bg" => Some(&mut self.search_match_bg),
1578 "match_fg" => Some(&mut self.search_match_fg),
1579 _ => None,
1580 },
1581 _ => None,
1582 }
1583 }
1584
1585 pub fn override_colors<I, K>(&mut self, overrides: I) -> usize
1590 where
1591 I: IntoIterator<Item = (K, Color)>,
1592 K: AsRef<str>,
1593 {
1594 let mut applied = 0;
1595 for (key, color) in overrides {
1596 if let Some(slot) = self.resolve_theme_key_mut(key.as_ref()) {
1597 *slot = color;
1598 applied += 1;
1599 }
1600 }
1601 applied
1602 }
1603}
1604
1605pub fn get_theme_schema() -> serde_json::Value {
1613 use schemars::schema_for;
1614 let schema = schema_for!(ThemeFile);
1615 serde_json::to_value(&schema).unwrap_or_default()
1616}
1617
1618pub fn get_builtin_themes() -> serde_json::Value {
1620 let mut map = serde_json::Map::new();
1621 for theme in BUILTIN_THEMES {
1622 map.insert(
1623 theme.name.to_string(),
1624 serde_json::Value::String(theme.json.to_string()),
1625 );
1626 }
1627 serde_json::Value::Object(map)
1628}
1629
1630#[cfg(test)]
1631mod tests {
1632 use super::*;
1633
1634 #[test]
1635 fn test_load_builtin_theme() {
1636 let dark = Theme::load_builtin(THEME_DARK).expect("Dark theme must exist");
1637 assert_eq!(dark.name, THEME_DARK);
1638
1639 let light = Theme::load_builtin(THEME_LIGHT).expect("Light theme must exist");
1640 assert_eq!(light.name, THEME_LIGHT);
1641
1642 let high_contrast =
1643 Theme::load_builtin(THEME_HIGH_CONTRAST).expect("High contrast theme must exist");
1644 assert_eq!(high_contrast.name, THEME_HIGH_CONTRAST);
1645 }
1646
1647 #[test]
1648 fn test_builtin_themes_match_schema() {
1649 for theme in BUILTIN_THEMES {
1650 let _: ThemeFile = serde_json::from_str(theme.json)
1651 .unwrap_or_else(|_| panic!("Theme '{}' does not match schema", theme.name));
1652 }
1653 }
1654
1655 #[test]
1656 fn test_from_json() {
1657 let json = r#"{"name":"test","editor":{},"ui":{},"search":{},"diagnostic":{},"syntax":{}}"#;
1658 let theme = Theme::from_json(json).expect("Should parse minimal theme");
1659 assert_eq!(theme.name, "test");
1660 }
1661
1662 #[test]
1663 fn test_default_reset_color() {
1664 let color: Color = ColorDef::Named("Default".to_string()).into();
1666 assert_eq!(color, Color::Reset);
1667
1668 let color: Color = ColorDef::Named("Reset".to_string()).into();
1670 assert_eq!(color, Color::Reset);
1671 }
1672
1673 #[test]
1674 fn test_file_status_colors_fall_back_to_diagnostic_colors() {
1675 let json = r#"{
1677 "name": "test-fallback",
1678 "editor": {},
1679 "ui": {},
1680 "search": {},
1681 "diagnostic": {
1682 "error_fg": [220, 50, 47],
1683 "warning_fg": [181, 137, 0],
1684 "info_fg": [38, 139, 210],
1685 "hint_fg": [101, 123, 131]
1686 },
1687 "syntax": {}
1688 }"#;
1689 let theme = Theme::from_json(json).expect("Should parse theme without file_status keys");
1690
1691 assert_eq!(theme.file_status_added_fg, Color::Rgb(38, 139, 210));
1693 assert_eq!(theme.file_status_renamed_fg, Color::Rgb(38, 139, 210));
1694 assert_eq!(theme.file_status_modified_fg, Color::Rgb(181, 137, 0));
1696 assert_eq!(theme.file_status_deleted_fg, Color::Rgb(220, 50, 47));
1698 assert_eq!(theme.file_status_conflicted_fg, Color::Rgb(220, 50, 47));
1699 assert_eq!(theme.file_status_untracked_fg, Color::Rgb(101, 123, 131));
1701 }
1702
1703 #[test]
1704 fn test_file_status_colors_explicit_override() {
1705 let json = r#"{
1707 "name": "test-override",
1708 "editor": {},
1709 "ui": {
1710 "file_status_added_fg": [80, 250, 123],
1711 "file_status_modified_fg": [255, 184, 108]
1712 },
1713 "search": {},
1714 "diagnostic": {
1715 "info_fg": [38, 139, 210],
1716 "warning_fg": [181, 137, 0]
1717 },
1718 "syntax": {}
1719 }"#;
1720 let theme = Theme::from_json(json).expect("Should parse theme with file_status overrides");
1721
1722 assert_eq!(theme.file_status_added_fg, Color::Rgb(80, 250, 123));
1724 assert_eq!(theme.file_status_modified_fg, Color::Rgb(255, 184, 108));
1725 assert_eq!(theme.file_status_renamed_fg, Color::Rgb(38, 139, 210));
1727 }
1728
1729 #[test]
1730 fn test_file_status_colors_resolve_via_theme_key() {
1731 let json = r#"{
1732 "name": "test-resolve",
1733 "editor": {},
1734 "ui": {
1735 "file_status_added_fg": [80, 250, 123]
1736 },
1737 "search": {},
1738 "diagnostic": {
1739 "warning_fg": [181, 137, 0]
1740 },
1741 "syntax": {}
1742 }"#;
1743 let theme = Theme::from_json(json).expect("Should parse theme");
1744
1745 assert_eq!(
1747 theme.resolve_theme_key("ui.file_status_added_fg"),
1748 Some(Color::Rgb(80, 250, 123))
1749 );
1750 assert_eq!(
1751 theme.resolve_theme_key("ui.file_status_modified_fg"),
1752 Some(Color::Rgb(181, 137, 0))
1753 );
1754 }
1755
1756 #[test]
1757 fn override_colors_writes_known_keys_and_drops_unknowns() {
1758 let mut theme = Theme::load_builtin(THEME_DARK).expect("dark builtin");
1759 let applied = theme.override_colors([
1760 ("editor.bg".to_string(), Color::Rgb(10, 20, 30)),
1761 ("ui.status_bar_fg".to_string(), Color::Rgb(1, 2, 3)),
1762 ("does.not_exist".to_string(), Color::Rgb(9, 9, 9)),
1763 ("garbage_no_dot".to_string(), Color::Rgb(9, 9, 9)),
1764 ]);
1765 assert_eq!(applied, 2, "only the two valid keys should be applied");
1766 assert_eq!(
1767 theme.resolve_theme_key("editor.bg"),
1768 Some(Color::Rgb(10, 20, 30))
1769 );
1770 assert_eq!(
1771 theme.resolve_theme_key("ui.status_bar_fg"),
1772 Some(Color::Rgb(1, 2, 3))
1773 );
1774 }
1775
1776 #[test]
1777 fn resolve_theme_key_mut_matches_resolve_theme_key_domain() {
1778 let mut theme = Theme::load_builtin(THEME_DARK).expect("dark builtin");
1781 let probe = [
1782 "editor.bg",
1783 "editor.fg",
1784 "ui.status_bar_fg",
1785 "ui.tab_active_bg",
1786 "syntax.keyword",
1787 "diagnostic.error_fg",
1788 "search.match_bg",
1789 ];
1790 for key in probe {
1791 assert!(
1792 theme.resolve_theme_key(key).is_some(),
1793 "reader lost key {key}"
1794 );
1795 assert!(
1796 theme.resolve_theme_key_mut(key).is_some(),
1797 "mutator missing key {key}"
1798 );
1799 }
1800 }
1801
1802 #[test]
1803 fn test_all_builtin_themes_have_file_status_colors() {
1804 for builtin in BUILTIN_THEMES {
1806 let theme = Theme::from_json(builtin.json)
1807 .unwrap_or_else(|e| panic!("Theme '{}' failed to parse: {}", builtin.name, e));
1808
1809 for key in &[
1811 "ui.file_status_added_fg",
1812 "ui.file_status_modified_fg",
1813 "ui.file_status_deleted_fg",
1814 "ui.file_status_renamed_fg",
1815 "ui.file_status_untracked_fg",
1816 "ui.file_status_conflicted_fg",
1817 ] {
1818 assert!(
1819 theme.resolve_theme_key(key).is_some(),
1820 "Theme '{}' missing resolution for '{}'",
1821 builtin.name,
1822 key
1823 );
1824 }
1825 }
1826 }
1827}