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
424#[cfg(test)]
425mod tests {
426 use super::*;
427 use ratatui::style::Style;
428
429 #[test]
430 fn test_border_style_focused() {
431 let theme = Theme::gruvbox_dark();
432 let style = theme.border_style(true);
433 assert_eq!(style, Style::default().fg(theme.focus_border.to_ratatui()));
434 }
435
436 #[test]
437 fn test_border_style_unfocused() {
438 let theme = Theme::gruvbox_dark();
439 let style = theme.border_style(false);
440 assert_eq!(style, Style::default().fg(theme.border_dim.to_ratatui()));
441 }
442
443 #[test]
444 fn test_from_hex_6char() {
445 assert_eq!(
446 ThemeColor::from_string("#ff8800").unwrap(),
447 ThemeColor::Rgb(255, 136, 0)
448 );
449 }
450
451 #[test]
452 fn test_from_hex_6char_lowercase() {
453 assert_eq!(
454 ThemeColor::from_string("#abcdef").unwrap(),
455 ThemeColor::Rgb(171, 205, 239)
456 );
457 }
458
459 #[test]
460 fn test_from_hex_6char_uppercase() {
461 assert_eq!(
462 ThemeColor::from_string("#ABCDEF").unwrap(),
463 ThemeColor::Rgb(171, 205, 239)
464 );
465 }
466
467 #[test]
468 fn test_from_hex_3char() {
469 assert_eq!(
470 ThemeColor::from_string("#f80").unwrap(),
471 ThemeColor::Rgb(255, 136, 0)
472 );
473 }
474
475 #[test]
476 fn test_from_hex_3char_expansion() {
477 assert_eq!(
478 ThemeColor::from_string("#abc").unwrap(),
479 ThemeColor::Rgb(170, 187, 204)
480 );
481 }
482
483 #[test]
484 fn test_from_hex_3char_black() {
485 assert_eq!(
486 ThemeColor::from_string("#000").unwrap(),
487 ThemeColor::Rgb(0, 0, 0)
488 );
489 }
490
491 #[test]
492 fn test_from_hex_3char_white() {
493 assert_eq!(
494 ThemeColor::from_string("#fff").unwrap(),
495 ThemeColor::Rgb(255, 255, 255)
496 );
497 }
498
499 #[test]
500 fn test_from_rgb_string() {
501 assert_eq!(
502 ThemeColor::from_string("rgb(255, 128, 0)").unwrap(),
503 ThemeColor::Rgb(255, 128, 0)
504 );
505 }
506
507 #[test]
508 fn test_from_rgb_string_no_spaces() {
509 assert_eq!(
510 ThemeColor::from_string("rgb(255,128,0)").unwrap(),
511 ThemeColor::Rgb(255, 128, 0)
512 );
513 }
514
515 #[test]
516 fn test_from_rgb_string_extra_spaces() {
517 assert_eq!(
518 ThemeColor::from_string("rgb( 255 , 128 , 0 )").unwrap(),
519 ThemeColor::Rgb(255, 128, 0)
520 );
521 }
522
523 #[test]
524 fn test_from_rgb_string_min_max() {
525 assert_eq!(
526 ThemeColor::from_string("rgb(0, 255, 0)").unwrap(),
527 ThemeColor::Rgb(0, 255, 0)
528 );
529 }
530
531 #[test]
532 fn test_from_string_with_whitespace() {
533 assert_eq!(
534 ThemeColor::from_string(" #ff8800 ").unwrap(),
535 ThemeColor::Rgb(255, 136, 0)
536 );
537 }
538
539 #[test]
540 fn test_ansi_to_ratatui() {
541 assert_eq!(ThemeColor::Ansi(0).to_ratatui(), Color::Black);
543 assert_eq!(ThemeColor::Ansi(4).to_ratatui(), Color::Blue);
544 assert_eq!(ThemeColor::Ansi(7).to_ratatui(), Color::Gray);
545 assert_eq!(ThemeColor::Ansi(8).to_ratatui(), Color::DarkGray);
546 assert_eq!(ThemeColor::Ansi(15).to_ratatui(), Color::White);
547 assert_eq!(ThemeColor::Ansi(42).to_ratatui(), Color::Indexed(42));
549 assert_eq!(ThemeColor::Reset.to_ratatui(), Color::Reset);
550 }
551
552 #[test]
553 fn test_invalid_hex_length() {
554 let result = ThemeColor::from_string("#ff880");
555 assert!(result.is_err());
556 assert!(result.unwrap_err().contains("Invalid hex color length"));
557 }
558
559 #[test]
560 fn test_invalid_hex_chars() {
561 let result = ThemeColor::from_string("#gghhii");
562 assert!(result.is_err());
563 }
564
565 #[test]
566 fn test_missing_hash() {
567 let result = ThemeColor::from_string("ff8800");
568 assert!(result.is_err());
569 assert!(result.unwrap_err().contains("Invalid color format"));
570 }
571
572 #[test]
573 fn test_invalid_rgb_format() {
574 let result = ThemeColor::from_string("rgb(255, 128)");
575 assert!(result.is_err());
576 assert!(result.unwrap_err().contains("requires 3 values"));
577 }
578
579 #[test]
580 fn test_rgb_value_out_of_range() {
581 let result = ThemeColor::from_string("rgb(256, 128, 0)");
582 assert!(result.is_err());
583 }
584
585 #[test]
586 fn test_rgb_negative_value() {
587 let result = ThemeColor::from_string("rgb(-1, 128, 0)");
588 assert!(result.is_err());
589 }
590
591 #[test]
592 fn test_rgb_non_numeric() {
593 let result = ThemeColor::from_string("rgb(abc, 128, 0)");
594 assert!(result.is_err());
595 assert!(result.unwrap_err().contains("Invalid red value"));
596 }
597
598 #[test]
599 fn test_invalid_format() {
600 let result = ThemeColor::from_string("not a color");
601 assert!(result.is_err());
602 assert!(result.unwrap_err().contains("Invalid color format"));
603 }
604
605 #[test]
606 fn test_empty_string() {
607 let result = ThemeColor::from_string("");
608 assert!(result.is_err());
609 }
610
611 #[test]
612 fn test_new_constructor() {
613 assert_eq!(ThemeColor::new(255, 128, 0), ThemeColor::Rgb(255, 128, 0));
614 }
615
616 #[test]
617 fn test_to_ratatui() {
618 let color = ThemeColor::new(131, 165, 152);
619 assert_eq!(color.to_ratatui(), Color::Rgb(131, 165, 152));
620 }
621
622 #[test]
623 fn test_theme_color_serialize() {
624 #[derive(Serialize)]
625 struct Wrapper {
626 color: ThemeColor,
627 }
628 let wrapper = Wrapper {
629 color: ThemeColor::new(59, 130, 246),
630 };
631 let serialized = toml::to_string(&wrapper).unwrap();
632 assert!(serialized.contains("color = \"#3b82f6\""));
633 }
634
635 #[test]
636 fn test_theme_color_deserialize() {
637 #[derive(Deserialize)]
638 struct Wrapper {
639 color: ThemeColor,
640 }
641 let toml_str = r###"color = "#3b82f6""###;
642 let wrapper: Wrapper = toml::from_str(toml_str).unwrap();
643 assert_eq!(wrapper.color, ThemeColor::Rgb(59, 130, 246));
644 }
645
646 #[test]
647 fn test_theme_color_roundtrip() {
648 #[derive(Serialize, Deserialize)]
649 struct Wrapper {
650 color: ThemeColor,
651 }
652 let original = Wrapper {
653 color: ThemeColor::new(239, 68, 68),
654 };
655 let serialized = toml::to_string(&original).unwrap();
656 let deserialized: Wrapper = toml::from_str(&serialized).unwrap();
657 assert_eq!(original.color, deserialized.color);
658 }
659
660 #[test]
661 fn test_theme_serialize_to_toml() {
662 let theme = Theme::gruvbox_dark();
663 let toml_string = toml::to_string_pretty(&theme).unwrap();
664
665 assert!(toml_string.contains("name = \"Gruvbox Dark\""));
666 assert!(toml_string.contains("bg = \"#282828\""));
667 assert!(toml_string.contains("bg_panel = \"#32302f\""));
668 assert!(toml_string.contains("focus_border = \"#b8bb26\""));
669 assert!(toml_string.contains("color_journal_date = \"#8ec07c\""));
670 }
671
672 #[test]
673 fn test_theme_deserialize_from_toml() {
674 let toml_str = r###"
675 name = "Test Theme"
676 bg = "#282828"
677 bg_panel = "#32302f"
678 selection_bg = "#504945"
679 fg = "#ebdbb2"
680 fg_secondary = "#a89984"
681 gray = "#7c6f64"
682 selection_fg = "#fbf1c7"
683 border_dim = "#504945"
684 focus_border = "#fabd2f"
685 accent = "#fabd2f"
686 color_directory = "#83a598"
687 color_journal_date = "#8ec07c"
688 color_search_match = "#b8bb26"
689 color_tag = "#fe8019"
690 "###;
691
692 let theme: Theme = toml::from_str(toml_str).unwrap();
693 assert_eq!(theme.name, "Test Theme");
694 assert_eq!(theme.bg, ThemeColor::new(0x28, 0x28, 0x28));
695 assert_eq!(theme.focus_border, ThemeColor::new(0xfa, 0xbd, 0x2f));
696 assert_eq!(theme.color_journal_date, ThemeColor::new(0x8e, 0xc0, 0x7c));
697 }
698
699 #[test]
700 fn test_theme_roundtrip() {
701 let original = Theme::tokyo_night();
702 let toml_string = toml::to_string_pretty(&original).unwrap();
703 let deserialized: Theme = toml::from_str(&toml_string).unwrap();
704
705 assert_eq!(original.name, deserialized.name);
706 assert_eq!(original.bg, deserialized.bg);
707 assert_eq!(original.fg, deserialized.fg);
708 assert_eq!(original.focus_border, deserialized.focus_border);
709 assert_eq!(original.color_journal_date, deserialized.color_journal_date);
710 }
711
712 #[test]
713 fn test_theme_color_serialize_lowercase_hex() {
714 #[derive(Serialize)]
715 struct Wrapper {
716 color: ThemeColor,
717 }
718 let wrapper = Wrapper {
719 color: ThemeColor::new(171, 205, 239),
720 };
721 let serialized = toml::to_string(&wrapper).unwrap();
722 assert!(serialized.contains("color = \"#abcdef\""));
723 }
724
725 #[test]
726 fn test_theme_deserialize_uppercase_hex() {
727 #[derive(Deserialize)]
728 struct Wrapper {
729 color: ThemeColor,
730 }
731 let toml_str = r###"color = "#ABCDEF""###;
732 let wrapper: Wrapper = toml::from_str(toml_str).unwrap();
733 assert_eq!(wrapper.color, ThemeColor::Rgb(171, 205, 239));
734 }
735
736 #[test]
737 fn test_theme_deserialize_3char_hex() {
738 #[derive(Deserialize)]
739 struct Wrapper {
740 color: ThemeColor,
741 }
742 let toml_str = r###"color = "#abc""###;
743 let wrapper: Wrapper = toml::from_str(toml_str).unwrap();
744 assert_eq!(wrapper.color, ThemeColor::Rgb(170, 187, 204));
745 }
746
747 #[test]
748 fn test_from_ansi_index() {
749 assert_eq!(
750 ThemeColor::from_string("ansi:4").unwrap(),
751 ThemeColor::Ansi(4)
752 );
753 assert_eq!(
754 ThemeColor::from_string("ansi:255").unwrap(),
755 ThemeColor::Ansi(255)
756 );
757 }
758
759 #[test]
760 fn test_from_reset() {
761 assert_eq!(ThemeColor::from_string("reset").unwrap(), ThemeColor::Reset);
762 }
763
764 #[test]
765 fn test_all_builtin_themes_serialize() {
766 let themes = vec![
767 Theme::ansi(),
768 Theme::gruvbox_dark(),
769 Theme::gruvbox_light(),
770 Theme::catppuccin_mocha(),
771 Theme::catppuccin_latte(),
772 Theme::tokyo_night(),
773 Theme::tokyo_night_storm(),
774 Theme::solarized_dark(),
775 Theme::solarized_light(),
776 Theme::nord(),
777 ];
778 for theme in themes {
779 let toml_string = toml::to_string_pretty(&theme).unwrap();
780 let roundtrip: Theme = toml::from_str(&toml_string).unwrap();
781 assert_eq!(theme.name, roundtrip.name);
782 assert_eq!(theme.bg, roundtrip.bg);
783 }
784 }
785
786 #[test]
787 fn test_ansi_theme() {
788 let theme = Theme::ansi();
789 assert_eq!(theme.name, "ANSI");
790 assert_eq!(theme.bg, ThemeColor::Reset);
791 assert_eq!(theme.fg, ThemeColor::Reset);
792 assert_eq!(theme.selection_bg, ThemeColor::Ansi(4));
793 assert_eq!(theme.focus_border, ThemeColor::Ansi(10));
794 assert_eq!(theme.color_directory, ThemeColor::Ansi(12));
795 }
796
797 #[test]
798 fn new_decoration_fields_present_and_deserialize_default() {
799 let t = Theme::gruvbox_dark();
801 assert_eq!(
802 t.blockquote_bar,
803 ThemeColor::from_string("#fabd2f").unwrap()
804 );
805 assert_eq!(t.code_bg, ThemeColor::from_string("#32302f").unwrap());
806
807 let toml = r##"
809 name = "Old"
810 bg = "#000000"
811 bg_panel = "#111111"
812 selection_bg = "#222222"
813 fg = "#ffffff"
814 fg_secondary = "#cccccc"
815 gray = "#888888"
816 selection_fg = "#ffffff"
817 border_dim = "#333333"
818 focus_border = "#444444"
819 accent = "#55aaff"
820 color_directory = "#66ccee"
821 color_journal_date = "#77ddcc"
822 color_search_match = "#88eeaa"
823 "##;
824 let parsed: Theme = toml::from_str(toml).expect("old theme TOML must still parse");
825 assert_eq!(parsed.blockquote_bar, parsed.accent);
827 assert_eq!(parsed.code_bg, parsed.bg_panel);
828 }
829
830 #[test]
831 fn old_theme_toml_derives_new_roles_from_siblings() {
832 let toml = r##"
835 name = "Old"
836 bg = "#000000"
837 bg_panel = "#111111"
838 selection_bg = "#222222"
839 fg = "#ffffff"
840 fg_secondary = "#cccccc"
841 gray = "#888888"
842 selection_fg = "#eeeeee"
843 border_dim = "#333333"
844 focus_border = "#444444"
845 accent = "#55aaff"
846 color_directory = "#66ccee"
847 color_journal_date = "#77ddcc"
848 color_search_match = "#88eeaa"
849 "##;
850 let t: Theme = toml::from_str(toml).expect("old theme TOML must still parse");
851 assert_eq!(t.bg_hard, t.bg_panel);
852 assert_eq!(t.bg_soft, t.selection_bg);
853 assert_eq!(t.fg_bright, t.selection_fg);
854 assert_eq!(t.cursor, t.fg);
855 assert_eq!(t.red, ThemeColor::Ansi(9));
857 assert_eq!(t.green, ThemeColor::Ansi(10));
858 assert_eq!(t.yellow, ThemeColor::Ansi(11));
859 assert_eq!(t.blue, ThemeColor::Ansi(12));
860 assert_eq!(t.purple, ThemeColor::Ansi(13));
861 assert_eq!(t.aqua, ThemeColor::Ansi(14));
862 assert_eq!(t.orange, ThemeColor::Ansi(208));
863 assert_eq!(t.color_tag, t.orange);
865 }
866
867 #[test]
868 fn new_roles_roundtrip_through_toml() {
869 let original = Theme::gruvbox_dark();
870 let toml_string = toml::to_string_pretty(&original).unwrap();
871 let parsed: Theme = toml::from_str(&toml_string).unwrap();
872 assert_eq!(original, parsed);
873 }
874}