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