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