1use ratatui::style::{Color, Style};
2use serde::{Deserialize, Deserializer, Serialize, Serializer};
3use std::fmt::Display;
4
5mod builtin;
7pub mod color_depth;
9
10#[derive(Debug, Clone, PartialEq)]
11pub enum ThemeColor {
12 Rgb(u8, u8, u8),
13 Ansi(u8),
16 Reset,
18}
19
20impl Serialize for ThemeColor {
21 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
22 where
23 S: Serializer,
24 {
25 match self {
26 ThemeColor::Rgb(r, g, b) => {
27 serializer.serialize_str(&format!("#{:02x}{:02x}{:02x}", r, g, b))
28 }
29 ThemeColor::Ansi(n) => serializer.serialize_str(&format!("ansi:{}", n)),
30 ThemeColor::Reset => serializer.serialize_str("reset"),
31 }
32 }
33}
34
35impl<'de> Deserialize<'de> for ThemeColor {
36 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
37 where
38 D: Deserializer<'de>,
39 {
40 let s = String::deserialize(deserializer)?;
41 ThemeColor::from_string(&s).map_err(serde::de::Error::custom)
42 }
43}
44
45impl ThemeColor {
46 pub fn new(r: u8, g: u8, b: u8) -> Self {
47 ThemeColor::Rgb(r, g, b)
48 }
49
50 pub fn to_ratatui(&self) -> Color {
57 match self {
58 ThemeColor::Rgb(r, g, b) => Color::Rgb(*r, *g, *b),
59 ThemeColor::Ansi(n) => match n {
60 0 => Color::Black,
61 1 => Color::Red,
62 2 => Color::Green,
63 3 => Color::Yellow,
64 4 => Color::Blue,
65 5 => Color::Magenta,
66 6 => Color::Cyan,
67 7 => Color::Gray,
68 8 => Color::DarkGray,
69 9 => Color::LightRed,
70 10 => Color::LightGreen,
71 11 => Color::LightYellow,
72 12 => Color::LightBlue,
73 13 => Color::LightMagenta,
74 14 => Color::LightCyan,
75 15 => Color::White,
76 _ => Color::Indexed(*n),
77 },
78 ThemeColor::Reset => Color::Reset,
79 }
80 }
81
82 pub fn from_string(s: &str) -> Result<Self, String> {
89 let s = s.trim();
90
91 if s.starts_with('#') {
92 Self::from_hex(s)
93 } else if s.starts_with("rgb(") && s.ends_with(')') {
94 Self::from_rgb_string(s)
95 } else if s == "reset" {
96 Ok(ThemeColor::Reset)
97 } else if let Some(rest) = s.strip_prefix("ansi:") {
98 rest.parse::<u8>()
99 .map(ThemeColor::Ansi)
100 .map_err(|_| format!("Invalid ANSI color index: {}", rest))
101 } else {
102 Err(format!("Invalid color format: {}", s))
103 }
104 }
105
106 fn from_hex(s: &str) -> Result<Self, String> {
108 if !s.starts_with('#') {
109 return Err("Hex color must start with #".to_string());
110 }
111
112 let hex = &s[1..];
113
114 match hex.len() {
115 3 => Self::from_hex_3char(hex),
116 6 => Self::from_hex_6char(hex),
117 _ => Err(format!(
118 "Invalid hex color length: expected 3 or 6 chars, got {}",
119 hex.len()
120 )),
121 }
122 }
123
124 fn from_hex_3char(hex: &str) -> Result<Self, String> {
126 if hex.len() != 3 {
127 return Err("Expected 3 hex characters".to_string());
128 }
129
130 let r = u8::from_str_radix(&hex[0..1].repeat(2), 16)
131 .map_err(|_| format!("Invalid hex character in red component: {}", &hex[0..1]))?;
132 let g = u8::from_str_radix(&hex[1..2].repeat(2), 16)
133 .map_err(|_| format!("Invalid hex character in green component: {}", &hex[1..2]))?;
134 let b = u8::from_str_radix(&hex[2..3].repeat(2), 16)
135 .map_err(|_| format!("Invalid hex character in blue component: {}", &hex[2..3]))?;
136
137 Ok(ThemeColor::Rgb(r, g, b))
138 }
139
140 fn from_hex_6char(hex: &str) -> Result<Self, String> {
142 if hex.len() != 6 {
143 return Err("Expected 6 hex characters".to_string());
144 }
145
146 let r = u8::from_str_radix(&hex[0..2], 16)
147 .map_err(|_| format!("Invalid hex characters in red component: {}", &hex[0..2]))?;
148 let g = u8::from_str_radix(&hex[2..4], 16)
149 .map_err(|_| format!("Invalid hex characters in green component: {}", &hex[2..4]))?;
150 let b = u8::from_str_radix(&hex[4..6], 16)
151 .map_err(|_| format!("Invalid hex characters in blue component: {}", &hex[4..6]))?;
152
153 Ok(ThemeColor::Rgb(r, g, b))
154 }
155
156 fn from_rgb_string(s: &str) -> Result<Self, String> {
158 if !s.starts_with("rgb(") || !s.ends_with(')') {
159 return Err("RGB format must be rgb(r, g, b)".to_string());
160 }
161
162 let inner = &s[4..s.len() - 1];
163 let parts: Vec<&str> = inner.split(',').map(|p| p.trim()).collect();
164
165 if parts.len() != 3 {
166 return Err(format!("RGB format requires 3 values, got {}", parts.len()));
167 }
168
169 let r = parts[0]
170 .parse::<u8>()
171 .map_err(|_| format!("Invalid red value: {}", parts[0]))?;
172 let g = parts[1]
173 .parse::<u8>()
174 .map_err(|_| format!("Invalid green value: {}", parts[1]))?;
175 let b = parts[2]
176 .parse::<u8>()
177 .map_err(|_| format!("Invalid blue value: {}", parts[2]))?;
178
179 Ok(ThemeColor::Rgb(r, g, b))
180 }
181}
182
183impl Display for ThemeColor {
184 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
185 match self {
186 ThemeColor::Rgb(r, g, b) => write!(f, "rgb({},{},{})", r, g, b),
187 ThemeColor::Ansi(n) => write!(f, "ansi:{}", n),
188 ThemeColor::Reset => write!(f, "reset"),
189 }
190 }
191}
192
193#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
235#[serde(from = "ThemeToml")]
236pub struct Theme {
237 pub name: String,
238
239 pub bg: ThemeColor,
242 pub bg_hard: ThemeColor,
244 pub bg_soft: ThemeColor,
246 pub bg_panel: ThemeColor,
248 pub selection_bg: ThemeColor,
250
251 pub fg: ThemeColor,
254 pub fg_bright: ThemeColor,
256 pub fg_secondary: ThemeColor,
258 pub gray: ThemeColor,
260 pub selection_fg: ThemeColor,
262
263 pub border_dim: ThemeColor,
266 pub focus_border: ThemeColor,
268
269 pub accent: ThemeColor,
272 pub cursor: ThemeColor,
274
275 pub red: ThemeColor,
278 pub green: ThemeColor,
280 pub yellow: ThemeColor,
282 pub blue: ThemeColor,
284 pub purple: ThemeColor,
286 pub aqua: ThemeColor,
288 pub orange: ThemeColor,
290
291 pub color_directory: ThemeColor,
294 pub color_journal_date: ThemeColor,
296 pub color_search_match: ThemeColor,
298 pub color_tag: ThemeColor,
300 pub blockquote_bar: ThemeColor,
302 pub code_bg: ThemeColor,
305 pub color_replace_preview: ThemeColor,
311}
312
313#[derive(Deserialize)]
320struct ThemeToml {
321 name: String,
322 bg: ThemeColor,
323 bg_hard: Option<ThemeColor>,
324 bg_soft: Option<ThemeColor>,
325 bg_panel: ThemeColor,
326 selection_bg: ThemeColor,
327 fg: ThemeColor,
328 fg_bright: Option<ThemeColor>,
329 fg_secondary: ThemeColor,
330 gray: ThemeColor,
331 selection_fg: ThemeColor,
332 border_dim: ThemeColor,
333 focus_border: ThemeColor,
334 accent: ThemeColor,
335 cursor: Option<ThemeColor>,
336 red: Option<ThemeColor>,
337 green: Option<ThemeColor>,
338 yellow: Option<ThemeColor>,
339 blue: Option<ThemeColor>,
340 purple: Option<ThemeColor>,
341 aqua: Option<ThemeColor>,
342 orange: Option<ThemeColor>,
343 color_directory: ThemeColor,
344 color_journal_date: ThemeColor,
345 color_search_match: ThemeColor,
346 color_tag: Option<ThemeColor>,
347 blockquote_bar: Option<ThemeColor>,
348 code_bg: Option<ThemeColor>,
349 color_replace_preview: Option<ThemeColor>,
350}
351
352impl From<ThemeToml> for Theme {
353 fn from(t: ThemeToml) -> Self {
354 let orange = t.orange.unwrap_or(ThemeColor::Ansi(208));
355 Theme {
356 name: t.name,
357 bg_hard: t.bg_hard.unwrap_or_else(|| t.bg_panel.clone()),
358 bg_soft: t.bg_soft.unwrap_or_else(|| t.selection_bg.clone()),
359 fg_bright: t.fg_bright.unwrap_or_else(|| t.selection_fg.clone()),
360 cursor: t.cursor.unwrap_or_else(|| t.fg.clone()),
361 red: t.red.unwrap_or(ThemeColor::Ansi(9)),
362 green: t.green.unwrap_or(ThemeColor::Ansi(10)),
363 yellow: t.yellow.unwrap_or(ThemeColor::Ansi(11)),
364 blue: t.blue.unwrap_or(ThemeColor::Ansi(12)),
365 purple: t.purple.unwrap_or(ThemeColor::Ansi(13)),
366 aqua: t.aqua.unwrap_or(ThemeColor::Ansi(14)),
367 color_tag: t.color_tag.unwrap_or_else(|| orange.clone()),
368 blockquote_bar: t.blockquote_bar.unwrap_or_else(|| t.accent.clone()),
369 code_bg: t.code_bg.unwrap_or_else(|| t.bg_panel.clone()),
370 color_replace_preview: t.color_replace_preview.unwrap_or_else(|| t.accent.clone()),
375 orange,
376 bg: t.bg,
377 bg_panel: t.bg_panel,
378 selection_bg: t.selection_bg,
379 fg: t.fg,
380 fg_secondary: t.fg_secondary,
381 gray: t.gray,
382 selection_fg: t.selection_fg,
383 border_dim: t.border_dim,
384 focus_border: t.focus_border,
385 accent: t.accent,
386 color_directory: t.color_directory,
387 color_journal_date: t.color_journal_date,
388 color_search_match: t.color_search_match,
389 }
390 }
391}
392
393impl Default for Theme {
394 fn default() -> Self {
395 Self::gruvbox_dark()
396 }
397}
398
399impl Theme {
400 pub fn border_style(&self, focused: bool) -> Style {
402 if focused {
403 Style::default().fg(self.focus_border.to_ratatui())
404 } else {
405 Style::default().fg(self.border_dim.to_ratatui())
406 }
407 }
408
409 pub fn base_style(&self) -> Style {
411 Style::default()
412 .fg(self.fg.to_ratatui())
413 .bg(self.bg.to_ratatui())
414 }
415
416 pub fn panel_style(&self) -> Style {
418 Style::default()
419 .fg(self.fg.to_ratatui())
420 .bg(self.bg_panel.to_ratatui())
421 }
422
423 pub fn action(&self) -> Style {
428 Style::default().fg(self.blue.to_ratatui())
429 }
430
431 pub fn hover(&self) -> Style {
435 Style::default()
436 .fg(self.bg_hard.to_ratatui())
437 .bg(self.blue.to_ratatui())
438 }
439}
440
441#[cfg(test)]
442mod tests {
443 use super::*;
444 use ratatui::style::Style;
445
446 #[test]
447 fn test_border_style_focused() {
448 let theme = Theme::gruvbox_dark();
449 let style = theme.border_style(true);
450 assert_eq!(style, Style::default().fg(theme.focus_border.to_ratatui()));
451 }
452
453 #[test]
454 fn test_border_style_unfocused() {
455 let theme = Theme::gruvbox_dark();
456 let style = theme.border_style(false);
457 assert_eq!(style, Style::default().fg(theme.border_dim.to_ratatui()));
458 }
459
460 #[test]
461 fn test_from_hex_6char() {
462 assert_eq!(
463 ThemeColor::from_string("#ff8800").unwrap(),
464 ThemeColor::Rgb(255, 136, 0)
465 );
466 }
467
468 #[test]
469 fn test_from_hex_6char_lowercase() {
470 assert_eq!(
471 ThemeColor::from_string("#abcdef").unwrap(),
472 ThemeColor::Rgb(171, 205, 239)
473 );
474 }
475
476 #[test]
477 fn test_from_hex_6char_uppercase() {
478 assert_eq!(
479 ThemeColor::from_string("#ABCDEF").unwrap(),
480 ThemeColor::Rgb(171, 205, 239)
481 );
482 }
483
484 #[test]
485 fn test_from_hex_3char() {
486 assert_eq!(
487 ThemeColor::from_string("#f80").unwrap(),
488 ThemeColor::Rgb(255, 136, 0)
489 );
490 }
491
492 #[test]
493 fn test_from_hex_3char_expansion() {
494 assert_eq!(
495 ThemeColor::from_string("#abc").unwrap(),
496 ThemeColor::Rgb(170, 187, 204)
497 );
498 }
499
500 #[test]
501 fn test_from_hex_3char_black() {
502 assert_eq!(
503 ThemeColor::from_string("#000").unwrap(),
504 ThemeColor::Rgb(0, 0, 0)
505 );
506 }
507
508 #[test]
509 fn test_from_hex_3char_white() {
510 assert_eq!(
511 ThemeColor::from_string("#fff").unwrap(),
512 ThemeColor::Rgb(255, 255, 255)
513 );
514 }
515
516 #[test]
517 fn test_from_rgb_string() {
518 assert_eq!(
519 ThemeColor::from_string("rgb(255, 128, 0)").unwrap(),
520 ThemeColor::Rgb(255, 128, 0)
521 );
522 }
523
524 #[test]
525 fn test_from_rgb_string_no_spaces() {
526 assert_eq!(
527 ThemeColor::from_string("rgb(255,128,0)").unwrap(),
528 ThemeColor::Rgb(255, 128, 0)
529 );
530 }
531
532 #[test]
533 fn test_from_rgb_string_extra_spaces() {
534 assert_eq!(
535 ThemeColor::from_string("rgb( 255 , 128 , 0 )").unwrap(),
536 ThemeColor::Rgb(255, 128, 0)
537 );
538 }
539
540 #[test]
541 fn test_from_rgb_string_min_max() {
542 assert_eq!(
543 ThemeColor::from_string("rgb(0, 255, 0)").unwrap(),
544 ThemeColor::Rgb(0, 255, 0)
545 );
546 }
547
548 #[test]
549 fn test_from_string_with_whitespace() {
550 assert_eq!(
551 ThemeColor::from_string(" #ff8800 ").unwrap(),
552 ThemeColor::Rgb(255, 136, 0)
553 );
554 }
555
556 #[test]
557 fn test_ansi_to_ratatui() {
558 assert_eq!(ThemeColor::Ansi(0).to_ratatui(), Color::Black);
560 assert_eq!(ThemeColor::Ansi(4).to_ratatui(), Color::Blue);
561 assert_eq!(ThemeColor::Ansi(7).to_ratatui(), Color::Gray);
562 assert_eq!(ThemeColor::Ansi(8).to_ratatui(), Color::DarkGray);
563 assert_eq!(ThemeColor::Ansi(15).to_ratatui(), Color::White);
564 assert_eq!(ThemeColor::Ansi(42).to_ratatui(), Color::Indexed(42));
566 assert_eq!(ThemeColor::Reset.to_ratatui(), Color::Reset);
567 }
568
569 #[test]
570 fn test_invalid_hex_length() {
571 let result = ThemeColor::from_string("#ff880");
572 assert!(result.is_err());
573 assert!(result.unwrap_err().contains("Invalid hex color length"));
574 }
575
576 #[test]
577 fn test_invalid_hex_chars() {
578 let result = ThemeColor::from_string("#gghhii");
579 assert!(result.is_err());
580 }
581
582 #[test]
583 fn test_missing_hash() {
584 let result = ThemeColor::from_string("ff8800");
585 assert!(result.is_err());
586 assert!(result.unwrap_err().contains("Invalid color format"));
587 }
588
589 #[test]
590 fn test_invalid_rgb_format() {
591 let result = ThemeColor::from_string("rgb(255, 128)");
592 assert!(result.is_err());
593 assert!(result.unwrap_err().contains("requires 3 values"));
594 }
595
596 #[test]
597 fn test_rgb_value_out_of_range() {
598 let result = ThemeColor::from_string("rgb(256, 128, 0)");
599 assert!(result.is_err());
600 }
601
602 #[test]
603 fn test_rgb_negative_value() {
604 let result = ThemeColor::from_string("rgb(-1, 128, 0)");
605 assert!(result.is_err());
606 }
607
608 #[test]
609 fn test_rgb_non_numeric() {
610 let result = ThemeColor::from_string("rgb(abc, 128, 0)");
611 assert!(result.is_err());
612 assert!(result.unwrap_err().contains("Invalid red value"));
613 }
614
615 #[test]
616 fn test_invalid_format() {
617 let result = ThemeColor::from_string("not a color");
618 assert!(result.is_err());
619 assert!(result.unwrap_err().contains("Invalid color format"));
620 }
621
622 #[test]
623 fn test_empty_string() {
624 let result = ThemeColor::from_string("");
625 assert!(result.is_err());
626 }
627
628 #[test]
629 fn test_new_constructor() {
630 assert_eq!(ThemeColor::new(255, 128, 0), ThemeColor::Rgb(255, 128, 0));
631 }
632
633 #[test]
634 fn test_to_ratatui() {
635 let color = ThemeColor::new(131, 165, 152);
636 assert_eq!(color.to_ratatui(), Color::Rgb(131, 165, 152));
637 }
638
639 #[test]
640 fn test_theme_color_serialize() {
641 #[derive(Serialize)]
642 struct Wrapper {
643 color: ThemeColor,
644 }
645 let wrapper = Wrapper {
646 color: ThemeColor::new(59, 130, 246),
647 };
648 let serialized = toml::to_string(&wrapper).unwrap();
649 assert!(serialized.contains("color = \"#3b82f6\""));
650 }
651
652 #[test]
653 fn test_theme_color_deserialize() {
654 #[derive(Deserialize)]
655 struct Wrapper {
656 color: ThemeColor,
657 }
658 let toml_str = r###"color = "#3b82f6""###;
659 let wrapper: Wrapper = toml::from_str(toml_str).unwrap();
660 assert_eq!(wrapper.color, ThemeColor::Rgb(59, 130, 246));
661 }
662
663 #[test]
664 fn test_theme_color_roundtrip() {
665 #[derive(Serialize, Deserialize)]
666 struct Wrapper {
667 color: ThemeColor,
668 }
669 let original = Wrapper {
670 color: ThemeColor::new(239, 68, 68),
671 };
672 let serialized = toml::to_string(&original).unwrap();
673 let deserialized: Wrapper = toml::from_str(&serialized).unwrap();
674 assert_eq!(original.color, deserialized.color);
675 }
676
677 #[test]
678 fn test_theme_serialize_to_toml() {
679 let theme = Theme::gruvbox_dark();
680 let toml_string = toml::to_string_pretty(&theme).unwrap();
681
682 assert!(toml_string.contains("name = \"Gruvbox Dark\""));
683 assert!(toml_string.contains("bg = \"#282828\""));
684 assert!(toml_string.contains("bg_panel = \"#32302f\""));
685 assert!(toml_string.contains("focus_border = \"#b8bb26\""));
686 assert!(toml_string.contains("color_journal_date = \"#8ec07c\""));
687 }
688
689 #[test]
690 fn test_theme_deserialize_from_toml() {
691 let toml_str = r###"
692 name = "Test Theme"
693 bg = "#282828"
694 bg_panel = "#32302f"
695 selection_bg = "#504945"
696 fg = "#ebdbb2"
697 fg_secondary = "#a89984"
698 gray = "#7c6f64"
699 selection_fg = "#fbf1c7"
700 border_dim = "#504945"
701 focus_border = "#fabd2f"
702 accent = "#fabd2f"
703 color_directory = "#83a598"
704 color_journal_date = "#8ec07c"
705 color_search_match = "#b8bb26"
706 color_tag = "#fe8019"
707 "###;
708
709 let theme: Theme = toml::from_str(toml_str).unwrap();
710 assert_eq!(theme.name, "Test Theme");
711 assert_eq!(theme.bg, ThemeColor::new(0x28, 0x28, 0x28));
712 assert_eq!(theme.focus_border, ThemeColor::new(0xfa, 0xbd, 0x2f));
713 assert_eq!(theme.color_journal_date, ThemeColor::new(0x8e, 0xc0, 0x7c));
714 }
715
716 #[test]
717 fn test_theme_roundtrip() {
718 let original = Theme::tokyo_night();
719 let toml_string = toml::to_string_pretty(&original).unwrap();
720 let deserialized: Theme = toml::from_str(&toml_string).unwrap();
721
722 assert_eq!(original.name, deserialized.name);
723 assert_eq!(original.bg, deserialized.bg);
724 assert_eq!(original.fg, deserialized.fg);
725 assert_eq!(original.focus_border, deserialized.focus_border);
726 assert_eq!(original.color_journal_date, deserialized.color_journal_date);
727 }
728
729 #[test]
730 fn test_theme_color_serialize_lowercase_hex() {
731 #[derive(Serialize)]
732 struct Wrapper {
733 color: ThemeColor,
734 }
735 let wrapper = Wrapper {
736 color: ThemeColor::new(171, 205, 239),
737 };
738 let serialized = toml::to_string(&wrapper).unwrap();
739 assert!(serialized.contains("color = \"#abcdef\""));
740 }
741
742 #[test]
743 fn test_theme_deserialize_uppercase_hex() {
744 #[derive(Deserialize)]
745 struct Wrapper {
746 color: ThemeColor,
747 }
748 let toml_str = r###"color = "#ABCDEF""###;
749 let wrapper: Wrapper = toml::from_str(toml_str).unwrap();
750 assert_eq!(wrapper.color, ThemeColor::Rgb(171, 205, 239));
751 }
752
753 #[test]
754 fn test_theme_deserialize_3char_hex() {
755 #[derive(Deserialize)]
756 struct Wrapper {
757 color: ThemeColor,
758 }
759 let toml_str = r###"color = "#abc""###;
760 let wrapper: Wrapper = toml::from_str(toml_str).unwrap();
761 assert_eq!(wrapper.color, ThemeColor::Rgb(170, 187, 204));
762 }
763
764 #[test]
765 fn test_from_ansi_index() {
766 assert_eq!(
767 ThemeColor::from_string("ansi:4").unwrap(),
768 ThemeColor::Ansi(4)
769 );
770 assert_eq!(
771 ThemeColor::from_string("ansi:255").unwrap(),
772 ThemeColor::Ansi(255)
773 );
774 }
775
776 #[test]
777 fn test_from_reset() {
778 assert_eq!(ThemeColor::from_string("reset").unwrap(), ThemeColor::Reset);
779 }
780
781 #[test]
782 fn test_all_builtin_themes_serialize() {
783 let themes = vec![
784 Theme::ansi(),
785 Theme::gruvbox_dark(),
786 Theme::gruvbox_light(),
787 Theme::catppuccin_mocha(),
788 Theme::catppuccin_latte(),
789 Theme::tokyo_night(),
790 Theme::tokyo_night_storm(),
791 Theme::solarized_dark(),
792 Theme::solarized_light(),
793 Theme::nord(),
794 ];
795 for theme in themes {
796 let toml_string = toml::to_string_pretty(&theme).unwrap();
797 let roundtrip: Theme = toml::from_str(&toml_string).unwrap();
798 assert_eq!(theme.name, roundtrip.name);
799 assert_eq!(theme.bg, roundtrip.bg);
800 }
801 }
802
803 #[test]
804 fn test_ansi_theme() {
805 let theme = Theme::ansi();
806 assert_eq!(theme.name, "ANSI");
807 assert_eq!(theme.bg, ThemeColor::Reset);
808 assert_eq!(theme.fg, ThemeColor::Reset);
809 assert_eq!(theme.selection_bg, ThemeColor::Ansi(4));
810 assert_eq!(theme.focus_border, ThemeColor::Ansi(10));
811 assert_eq!(theme.color_directory, ThemeColor::Ansi(12));
812 }
813
814 #[test]
815 fn new_decoration_fields_present_and_deserialize_default() {
816 let t = Theme::gruvbox_dark();
818 assert_eq!(
819 t.blockquote_bar,
820 ThemeColor::from_string("#fabd2f").unwrap()
821 );
822 assert_eq!(t.code_bg, ThemeColor::from_string("#32302f").unwrap());
823
824 let toml = r##"
826 name = "Old"
827 bg = "#000000"
828 bg_panel = "#111111"
829 selection_bg = "#222222"
830 fg = "#ffffff"
831 fg_secondary = "#cccccc"
832 gray = "#888888"
833 selection_fg = "#ffffff"
834 border_dim = "#333333"
835 focus_border = "#444444"
836 accent = "#55aaff"
837 color_directory = "#66ccee"
838 color_journal_date = "#77ddcc"
839 color_search_match = "#88eeaa"
840 "##;
841 let parsed: Theme = toml::from_str(toml).expect("old theme TOML must still parse");
842 assert_eq!(parsed.blockquote_bar, parsed.accent);
844 assert_eq!(parsed.code_bg, parsed.bg_panel);
845 }
846
847 #[test]
848 fn old_theme_toml_derives_new_roles_from_siblings() {
849 let toml = r##"
852 name = "Old"
853 bg = "#000000"
854 bg_panel = "#111111"
855 selection_bg = "#222222"
856 fg = "#ffffff"
857 fg_secondary = "#cccccc"
858 gray = "#888888"
859 selection_fg = "#eeeeee"
860 border_dim = "#333333"
861 focus_border = "#444444"
862 accent = "#55aaff"
863 color_directory = "#66ccee"
864 color_journal_date = "#77ddcc"
865 color_search_match = "#88eeaa"
866 "##;
867 let t: Theme = toml::from_str(toml).expect("old theme TOML must still parse");
868 assert_eq!(t.bg_hard, t.bg_panel);
869 assert_eq!(t.bg_soft, t.selection_bg);
870 assert_eq!(t.fg_bright, t.selection_fg);
871 assert_eq!(t.cursor, t.fg);
872 assert_eq!(t.red, ThemeColor::Ansi(9));
874 assert_eq!(t.green, ThemeColor::Ansi(10));
875 assert_eq!(t.yellow, ThemeColor::Ansi(11));
876 assert_eq!(t.blue, ThemeColor::Ansi(12));
877 assert_eq!(t.purple, ThemeColor::Ansi(13));
878 assert_eq!(t.aqua, ThemeColor::Ansi(14));
879 assert_eq!(t.orange, ThemeColor::Ansi(208));
880 assert_eq!(t.color_tag, t.orange);
882 }
883
884 #[test]
885 fn new_roles_roundtrip_through_toml() {
886 let original = Theme::gruvbox_dark();
887 let toml_string = toml::to_string_pretty(&original).unwrap();
888 let parsed: Theme = toml::from_str(&toml_string).unwrap();
889 assert_eq!(original, parsed);
890 }
891}