1use ratatui::style::{Color, Style};
2use serde::{Deserialize, Deserializer, Serialize, Serializer};
3use std::fmt::Display;
4
5#[derive(Debug, Clone, PartialEq)]
6pub enum ThemeColor {
7 Rgb(u8, u8, u8),
8 Ansi(u8),
11 Reset,
13}
14
15impl Serialize for ThemeColor {
16 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
17 where
18 S: Serializer,
19 {
20 match self {
21 ThemeColor::Rgb(r, g, b) => {
22 serializer.serialize_str(&format!("#{:02x}{:02x}{:02x}", r, g, b))
23 }
24 ThemeColor::Ansi(n) => serializer.serialize_str(&format!("ansi:{}", n)),
25 ThemeColor::Reset => serializer.serialize_str("reset"),
26 }
27 }
28}
29
30impl<'de> Deserialize<'de> for ThemeColor {
31 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
32 where
33 D: Deserializer<'de>,
34 {
35 let s = String::deserialize(deserializer)?;
36 ThemeColor::from_string(&s).map_err(serde::de::Error::custom)
37 }
38}
39
40impl ThemeColor {
41 pub fn new(r: u8, g: u8, b: u8) -> Self {
42 ThemeColor::Rgb(r, g, b)
43 }
44
45 pub fn to_ratatui(&self) -> Color {
52 match self {
53 ThemeColor::Rgb(r, g, b) => Color::Rgb(*r, *g, *b),
54 ThemeColor::Ansi(n) => match n {
55 0 => Color::Black,
56 1 => Color::Red,
57 2 => Color::Green,
58 3 => Color::Yellow,
59 4 => Color::Blue,
60 5 => Color::Magenta,
61 6 => Color::Cyan,
62 7 => Color::Gray,
63 8 => Color::DarkGray,
64 9 => Color::LightRed,
65 10 => Color::LightGreen,
66 11 => Color::LightYellow,
67 12 => Color::LightBlue,
68 13 => Color::LightMagenta,
69 14 => Color::LightCyan,
70 15 => Color::White,
71 _ => Color::Indexed(*n),
72 },
73 ThemeColor::Reset => Color::Reset,
74 }
75 }
76
77 pub fn from_string(s: &str) -> Result<Self, String> {
84 let s = s.trim();
85
86 if s.starts_with('#') {
87 Self::from_hex(s)
88 } else if s.starts_with("rgb(") && s.ends_with(')') {
89 Self::from_rgb_string(s)
90 } else if s == "reset" {
91 Ok(ThemeColor::Reset)
92 } else if let Some(rest) = s.strip_prefix("ansi:") {
93 rest.parse::<u8>()
94 .map(ThemeColor::Ansi)
95 .map_err(|_| format!("Invalid ANSI color index: {}", rest))
96 } else {
97 Err(format!("Invalid color format: {}", s))
98 }
99 }
100
101 fn from_hex(s: &str) -> Result<Self, String> {
103 if !s.starts_with('#') {
104 return Err("Hex color must start with #".to_string());
105 }
106
107 let hex = &s[1..];
108
109 match hex.len() {
110 3 => Self::from_hex_3char(hex),
111 6 => Self::from_hex_6char(hex),
112 _ => Err(format!(
113 "Invalid hex color length: expected 3 or 6 chars, got {}",
114 hex.len()
115 )),
116 }
117 }
118
119 fn from_hex_3char(hex: &str) -> Result<Self, String> {
121 if hex.len() != 3 {
122 return Err("Expected 3 hex characters".to_string());
123 }
124
125 let r = u8::from_str_radix(&hex[0..1].repeat(2), 16)
126 .map_err(|_| format!("Invalid hex character in red component: {}", &hex[0..1]))?;
127 let g = u8::from_str_radix(&hex[1..2].repeat(2), 16)
128 .map_err(|_| format!("Invalid hex character in green component: {}", &hex[1..2]))?;
129 let b = u8::from_str_radix(&hex[2..3].repeat(2), 16)
130 .map_err(|_| format!("Invalid hex character in blue component: {}", &hex[2..3]))?;
131
132 Ok(ThemeColor::Rgb(r, g, b))
133 }
134
135 fn from_hex_6char(hex: &str) -> Result<Self, String> {
137 if hex.len() != 6 {
138 return Err("Expected 6 hex characters".to_string());
139 }
140
141 let r = u8::from_str_radix(&hex[0..2], 16)
142 .map_err(|_| format!("Invalid hex characters in red component: {}", &hex[0..2]))?;
143 let g = u8::from_str_radix(&hex[2..4], 16)
144 .map_err(|_| format!("Invalid hex characters in green component: {}", &hex[2..4]))?;
145 let b = u8::from_str_radix(&hex[4..6], 16)
146 .map_err(|_| format!("Invalid hex characters in blue component: {}", &hex[4..6]))?;
147
148 Ok(ThemeColor::Rgb(r, g, b))
149 }
150
151 fn from_rgb_string(s: &str) -> Result<Self, String> {
153 if !s.starts_with("rgb(") || !s.ends_with(')') {
154 return Err("RGB format must be rgb(r, g, b)".to_string());
155 }
156
157 let inner = &s[4..s.len() - 1];
158 let parts: Vec<&str> = inner.split(',').map(|p| p.trim()).collect();
159
160 if parts.len() != 3 {
161 return Err(format!("RGB format requires 3 values, got {}", parts.len()));
162 }
163
164 let r = parts[0]
165 .parse::<u8>()
166 .map_err(|_| format!("Invalid red value: {}", parts[0]))?;
167 let g = parts[1]
168 .parse::<u8>()
169 .map_err(|_| format!("Invalid green value: {}", parts[1]))?;
170 let b = parts[2]
171 .parse::<u8>()
172 .map_err(|_| format!("Invalid blue value: {}", parts[2]))?;
173
174 Ok(ThemeColor::Rgb(r, g, b))
175 }
176}
177
178impl Display for ThemeColor {
179 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
180 match self {
181 ThemeColor::Rgb(r, g, b) => write!(f, "rgb({},{},{})", r, g, b),
182 ThemeColor::Ansi(n) => write!(f, "ansi:{}", n),
183 ThemeColor::Reset => write!(f, "reset"),
184 }
185 }
186}
187
188#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
213pub struct Theme {
214 pub name: String,
215
216 pub bg: ThemeColor,
219 pub bg_panel: ThemeColor,
221 pub bg_selected: ThemeColor,
223
224 pub fg: ThemeColor,
227 pub fg_secondary: ThemeColor,
229 pub fg_muted: ThemeColor,
231 pub fg_selected: ThemeColor,
233
234 pub border: ThemeColor,
237 pub border_focused: ThemeColor,
239
240 pub accent: ThemeColor,
243
244 pub color_directory: ThemeColor,
247 pub color_journal_date: ThemeColor,
249 pub color_search_match: ThemeColor,
251}
252
253impl Default for Theme {
254 fn default() -> Self {
255 Self::gruvbox_dark()
256 }
257}
258
259impl Theme {
260 pub fn gruvbox_dark() -> Self {
263 Theme {
264 name: "Gruvbox Dark".to_string(),
265 bg: ThemeColor::from_string("#282828").unwrap(),
266 bg_panel: ThemeColor::from_string("#32302f").unwrap(),
267 bg_selected: ThemeColor::from_string("#504945").unwrap(),
268 fg: ThemeColor::from_string("#ebdbb2").unwrap(),
269 fg_secondary: ThemeColor::from_string("#a89984").unwrap(),
270 fg_muted: ThemeColor::from_string("#7c6f64").unwrap(),
271 fg_selected: ThemeColor::from_string("#fbf1c7").unwrap(),
272 border: ThemeColor::from_string("#504945").unwrap(),
273 border_focused: ThemeColor::from_string("#fabd2f").unwrap(),
274 accent: ThemeColor::from_string("#fabd2f").unwrap(),
275 color_directory: ThemeColor::from_string("#83a598").unwrap(),
276 color_journal_date: ThemeColor::from_string("#8ec07c").unwrap(),
277 color_search_match: ThemeColor::from_string("#b8bb26").unwrap(),
278 }
279 }
280
281 pub fn gruvbox_light() -> Self {
282 Theme {
283 name: "Gruvbox Light".to_string(),
284 bg: ThemeColor::from_string("#fbf1c7").unwrap(),
285 bg_panel: ThemeColor::from_string("#f2e5bc").unwrap(),
286 bg_selected: ThemeColor::from_string("#ebdbb2").unwrap(),
287 fg: ThemeColor::from_string("#3c3836").unwrap(),
288 fg_secondary: ThemeColor::from_string("#7c6f64").unwrap(),
289 fg_muted: ThemeColor::from_string("#a89984").unwrap(),
290 fg_selected: ThemeColor::from_string("#282828").unwrap(),
291 border: ThemeColor::from_string("#d5c4a1").unwrap(),
292 border_focused: ThemeColor::from_string("#d79921").unwrap(),
293 accent: ThemeColor::from_string("#d79921").unwrap(),
294 color_directory: ThemeColor::from_string("#458588").unwrap(),
295 color_journal_date: ThemeColor::from_string("#689d6a").unwrap(),
296 color_search_match: ThemeColor::from_string("#98971a").unwrap(),
297 }
298 }
299
300 pub fn catppuccin_mocha() -> Self {
301 Theme {
302 name: "Catppuccin Mocha".to_string(),
303 bg: ThemeColor::from_string("#1e1e2e").unwrap(),
304 bg_panel: ThemeColor::from_string("#181825").unwrap(),
305 bg_selected: ThemeColor::from_string("#313244").unwrap(),
306 fg: ThemeColor::from_string("#cdd6f4").unwrap(),
307 fg_secondary: ThemeColor::from_string("#a6adc8").unwrap(),
308 fg_muted: ThemeColor::from_string("#6c7086").unwrap(),
309 fg_selected: ThemeColor::from_string("#cdd6f4").unwrap(),
310 border: ThemeColor::from_string("#45475a").unwrap(),
311 border_focused: ThemeColor::from_string("#89b4fa").unwrap(),
312 accent: ThemeColor::from_string("#cba6f7").unwrap(),
313 color_directory: ThemeColor::from_string("#89dceb").unwrap(),
314 color_journal_date: ThemeColor::from_string("#94e2d5").unwrap(),
315 color_search_match: ThemeColor::from_string("#a6e3a1").unwrap(),
316 }
317 }
318
319 pub fn catppuccin_latte() -> Self {
320 Theme {
321 name: "Catppuccin Latte".to_string(),
322 bg: ThemeColor::from_string("#eff1f5").unwrap(),
323 bg_panel: ThemeColor::from_string("#e6e9ef").unwrap(),
324 bg_selected: ThemeColor::from_string("#ccd0da").unwrap(),
325 fg: ThemeColor::from_string("#4c4f69").unwrap(),
326 fg_secondary: ThemeColor::from_string("#6c6f85").unwrap(),
327 fg_muted: ThemeColor::from_string("#9ca0b0").unwrap(),
328 fg_selected: ThemeColor::from_string("#4c4f69").unwrap(),
329 border: ThemeColor::from_string("#ccd0da").unwrap(),
330 border_focused: ThemeColor::from_string("#1e66f5").unwrap(),
331 accent: ThemeColor::from_string("#8839ef").unwrap(),
332 color_directory: ThemeColor::from_string("#04a5e5").unwrap(),
333 color_journal_date: ThemeColor::from_string("#179299").unwrap(),
334 color_search_match: ThemeColor::from_string("#40a02b").unwrap(),
335 }
336 }
337
338 pub fn tokyo_night() -> Self {
339 Theme {
340 name: "Tokyo Night".to_string(),
341 bg: ThemeColor::from_string("#1a1b26").unwrap(),
342 bg_panel: ThemeColor::from_string("#16161e").unwrap(),
343 bg_selected: ThemeColor::from_string("#292e42").unwrap(),
344 fg: ThemeColor::from_string("#c0caf5").unwrap(),
345 fg_secondary: ThemeColor::from_string("#a9b1d6").unwrap(),
346 fg_muted: ThemeColor::from_string("#565f89").unwrap(),
347 fg_selected: ThemeColor::from_string("#c0caf5").unwrap(),
348 border: ThemeColor::from_string("#3b4261").unwrap(),
349 border_focused: ThemeColor::from_string("#7aa2f7").unwrap(),
350 accent: ThemeColor::from_string("#7aa2f7").unwrap(),
351 color_directory: ThemeColor::from_string("#7dcfff").unwrap(),
352 color_journal_date: ThemeColor::from_string("#73daca").unwrap(),
353 color_search_match: ThemeColor::from_string("#9ece6a").unwrap(),
354 }
355 }
356
357 pub fn tokyo_night_storm() -> Self {
358 Theme {
359 name: "Tokyo Night Storm".to_string(),
360 bg: ThemeColor::from_string("#24283b").unwrap(),
361 bg_panel: ThemeColor::from_string("#1f2335").unwrap(),
362 bg_selected: ThemeColor::from_string("#364a82").unwrap(),
363 fg: ThemeColor::from_string("#c0caf5").unwrap(),
364 fg_secondary: ThemeColor::from_string("#a9b1d6").unwrap(),
365 fg_muted: ThemeColor::from_string("#565f89").unwrap(),
366 fg_selected: ThemeColor::from_string("#c0caf5").unwrap(),
367 border: ThemeColor::from_string("#3b4261").unwrap(),
368 border_focused: ThemeColor::from_string("#7aa2f7").unwrap(),
369 accent: ThemeColor::from_string("#bb9af7").unwrap(),
370 color_directory: ThemeColor::from_string("#7dcfff").unwrap(),
371 color_journal_date: ThemeColor::from_string("#73daca").unwrap(),
372 color_search_match: ThemeColor::from_string("#9ece6a").unwrap(),
373 }
374 }
375
376 pub fn solarized_dark() -> Self {
377 Theme {
378 name: "Solarized Dark".to_string(),
379 bg: ThemeColor::from_string("#002b36").unwrap(),
380 bg_panel: ThemeColor::from_string("#073642").unwrap(),
381 bg_selected: ThemeColor::from_string("#586e75").unwrap(),
382 fg: ThemeColor::from_string("#839496").unwrap(),
383 fg_secondary: ThemeColor::from_string("#657b83").unwrap(),
384 fg_muted: ThemeColor::from_string("#586e75").unwrap(),
385 fg_selected: ThemeColor::from_string("#eee8d5").unwrap(),
386 border: ThemeColor::from_string("#073642").unwrap(),
387 border_focused: ThemeColor::from_string("#268bd2").unwrap(),
388 accent: ThemeColor::from_string("#268bd2").unwrap(),
389 color_directory: ThemeColor::from_string("#2aa198").unwrap(),
390 color_journal_date: ThemeColor::from_string("#859900").unwrap(),
391 color_search_match: ThemeColor::from_string("#b58900").unwrap(),
392 }
393 }
394
395 pub fn solarized_light() -> Self {
396 Theme {
397 name: "Solarized Light".to_string(),
398 bg: ThemeColor::from_string("#fdf6e3").unwrap(),
399 bg_panel: ThemeColor::from_string("#eee8d5").unwrap(),
400 bg_selected: ThemeColor::from_string("#93a1a1").unwrap(),
401 fg: ThemeColor::from_string("#657b83").unwrap(),
402 fg_secondary: ThemeColor::from_string("#839496").unwrap(),
403 fg_muted: ThemeColor::from_string("#93a1a1").unwrap(),
404 fg_selected: ThemeColor::from_string("#073642").unwrap(),
405 border: ThemeColor::from_string("#eee8d5").unwrap(),
406 border_focused: ThemeColor::from_string("#268bd2").unwrap(),
407 accent: ThemeColor::from_string("#268bd2").unwrap(),
408 color_directory: ThemeColor::from_string("#2aa198").unwrap(),
409 color_journal_date: ThemeColor::from_string("#859900").unwrap(),
410 color_search_match: ThemeColor::from_string("#b58900").unwrap(),
411 }
412 }
413
414 pub fn border_style(&self, focused: bool) -> Style {
416 if focused {
417 Style::default().fg(self.border_focused.to_ratatui())
418 } else {
419 Style::default().fg(self.border.to_ratatui())
420 }
421 }
422
423 pub fn base_style(&self) -> Style {
425 Style::default()
426 .fg(self.fg.to_ratatui())
427 .bg(self.bg.to_ratatui())
428 }
429
430 pub fn panel_style(&self) -> Style {
432 Style::default()
433 .fg(self.fg.to_ratatui())
434 .bg(self.bg_panel.to_ratatui())
435 }
436
437 pub fn nord() -> Self {
438 Theme {
439 name: "Nord".to_string(),
440 bg: ThemeColor::from_string("#2e3440").unwrap(),
441 bg_panel: ThemeColor::from_string("#3b4252").unwrap(),
442 bg_selected: ThemeColor::from_string("#434c5e").unwrap(),
443 fg: ThemeColor::from_string("#eceff4").unwrap(),
444 fg_secondary: ThemeColor::from_string("#d8dee9").unwrap(),
445 fg_muted: ThemeColor::from_string("#4c566a").unwrap(),
446 fg_selected: ThemeColor::from_string("#eceff4").unwrap(),
447 border: ThemeColor::from_string("#434c5e").unwrap(),
448 border_focused: ThemeColor::from_string("#81a1c1").unwrap(),
449 accent: ThemeColor::from_string("#88c0d0").unwrap(),
450 color_directory: ThemeColor::from_string("#81a1c1").unwrap(),
451 color_journal_date: ThemeColor::from_string("#8fbcbb").unwrap(),
452 color_search_match: ThemeColor::from_string("#a3be8c").unwrap(),
453 }
454 }
455
456 pub fn ansi() -> Self {
462 Theme {
463 name: "ANSI".to_string(),
464 bg: ThemeColor::Reset,
465 bg_panel: ThemeColor::Reset,
466 bg_selected: ThemeColor::Ansi(4), fg: ThemeColor::Reset,
468 fg_secondary: ThemeColor::Ansi(7), fg_muted: ThemeColor::Ansi(8), fg_selected: ThemeColor::Ansi(15), border: ThemeColor::Ansi(8), border_focused: ThemeColor::Ansi(6), accent: ThemeColor::Ansi(6), color_directory: ThemeColor::Ansi(12), color_journal_date: ThemeColor::Ansi(10), color_search_match: ThemeColor::Ansi(11), }
478 }
479}
480
481#[cfg(test)]
482mod tests {
483 use super::*;
484 use ratatui::style::Style;
485
486 #[test]
487 fn test_border_style_focused() {
488 let theme = Theme::gruvbox_dark();
489 let style = theme.border_style(true);
490 assert_eq!(
491 style,
492 Style::default().fg(theme.border_focused.to_ratatui())
493 );
494 }
495
496 #[test]
497 fn test_border_style_unfocused() {
498 let theme = Theme::gruvbox_dark();
499 let style = theme.border_style(false);
500 assert_eq!(style, Style::default().fg(theme.border.to_ratatui()));
501 }
502
503 #[test]
504 fn test_from_hex_6char() {
505 assert_eq!(
506 ThemeColor::from_string("#ff8800").unwrap(),
507 ThemeColor::Rgb(255, 136, 0)
508 );
509 }
510
511 #[test]
512 fn test_from_hex_6char_lowercase() {
513 assert_eq!(
514 ThemeColor::from_string("#abcdef").unwrap(),
515 ThemeColor::Rgb(171, 205, 239)
516 );
517 }
518
519 #[test]
520 fn test_from_hex_6char_uppercase() {
521 assert_eq!(
522 ThemeColor::from_string("#ABCDEF").unwrap(),
523 ThemeColor::Rgb(171, 205, 239)
524 );
525 }
526
527 #[test]
528 fn test_from_hex_3char() {
529 assert_eq!(
530 ThemeColor::from_string("#f80").unwrap(),
531 ThemeColor::Rgb(255, 136, 0)
532 );
533 }
534
535 #[test]
536 fn test_from_hex_3char_expansion() {
537 assert_eq!(
538 ThemeColor::from_string("#abc").unwrap(),
539 ThemeColor::Rgb(170, 187, 204)
540 );
541 }
542
543 #[test]
544 fn test_from_hex_3char_black() {
545 assert_eq!(
546 ThemeColor::from_string("#000").unwrap(),
547 ThemeColor::Rgb(0, 0, 0)
548 );
549 }
550
551 #[test]
552 fn test_from_hex_3char_white() {
553 assert_eq!(
554 ThemeColor::from_string("#fff").unwrap(),
555 ThemeColor::Rgb(255, 255, 255)
556 );
557 }
558
559 #[test]
560 fn test_from_rgb_string() {
561 assert_eq!(
562 ThemeColor::from_string("rgb(255, 128, 0)").unwrap(),
563 ThemeColor::Rgb(255, 128, 0)
564 );
565 }
566
567 #[test]
568 fn test_from_rgb_string_no_spaces() {
569 assert_eq!(
570 ThemeColor::from_string("rgb(255,128,0)").unwrap(),
571 ThemeColor::Rgb(255, 128, 0)
572 );
573 }
574
575 #[test]
576 fn test_from_rgb_string_extra_spaces() {
577 assert_eq!(
578 ThemeColor::from_string("rgb( 255 , 128 , 0 )").unwrap(),
579 ThemeColor::Rgb(255, 128, 0)
580 );
581 }
582
583 #[test]
584 fn test_from_rgb_string_min_max() {
585 assert_eq!(
586 ThemeColor::from_string("rgb(0, 255, 0)").unwrap(),
587 ThemeColor::Rgb(0, 255, 0)
588 );
589 }
590
591 #[test]
592 fn test_from_string_with_whitespace() {
593 assert_eq!(
594 ThemeColor::from_string(" #ff8800 ").unwrap(),
595 ThemeColor::Rgb(255, 136, 0)
596 );
597 }
598
599 #[test]
600 fn test_ansi_to_ratatui() {
601 assert_eq!(ThemeColor::Ansi(0).to_ratatui(), Color::Black);
603 assert_eq!(ThemeColor::Ansi(4).to_ratatui(), Color::Blue);
604 assert_eq!(ThemeColor::Ansi(7).to_ratatui(), Color::Gray);
605 assert_eq!(ThemeColor::Ansi(8).to_ratatui(), Color::DarkGray);
606 assert_eq!(ThemeColor::Ansi(15).to_ratatui(), Color::White);
607 assert_eq!(ThemeColor::Ansi(42).to_ratatui(), Color::Indexed(42));
609 assert_eq!(ThemeColor::Reset.to_ratatui(), Color::Reset);
610 }
611
612 #[test]
613 fn test_invalid_hex_length() {
614 let result = ThemeColor::from_string("#ff880");
615 assert!(result.is_err());
616 assert!(result.unwrap_err().contains("Invalid hex color length"));
617 }
618
619 #[test]
620 fn test_invalid_hex_chars() {
621 let result = ThemeColor::from_string("#gghhii");
622 assert!(result.is_err());
623 }
624
625 #[test]
626 fn test_missing_hash() {
627 let result = ThemeColor::from_string("ff8800");
628 assert!(result.is_err());
629 assert!(result.unwrap_err().contains("Invalid color format"));
630 }
631
632 #[test]
633 fn test_invalid_rgb_format() {
634 let result = ThemeColor::from_string("rgb(255, 128)");
635 assert!(result.is_err());
636 assert!(result.unwrap_err().contains("requires 3 values"));
637 }
638
639 #[test]
640 fn test_rgb_value_out_of_range() {
641 let result = ThemeColor::from_string("rgb(256, 128, 0)");
642 assert!(result.is_err());
643 }
644
645 #[test]
646 fn test_rgb_negative_value() {
647 let result = ThemeColor::from_string("rgb(-1, 128, 0)");
648 assert!(result.is_err());
649 }
650
651 #[test]
652 fn test_rgb_non_numeric() {
653 let result = ThemeColor::from_string("rgb(abc, 128, 0)");
654 assert!(result.is_err());
655 assert!(result.unwrap_err().contains("Invalid red value"));
656 }
657
658 #[test]
659 fn test_invalid_format() {
660 let result = ThemeColor::from_string("not a color");
661 assert!(result.is_err());
662 assert!(result.unwrap_err().contains("Invalid color format"));
663 }
664
665 #[test]
666 fn test_empty_string() {
667 let result = ThemeColor::from_string("");
668 assert!(result.is_err());
669 }
670
671 #[test]
672 fn test_new_constructor() {
673 assert_eq!(ThemeColor::new(255, 128, 0), ThemeColor::Rgb(255, 128, 0));
674 }
675
676 #[test]
677 fn test_to_ratatui() {
678 let color = ThemeColor::new(131, 165, 152);
679 assert_eq!(color.to_ratatui(), Color::Rgb(131, 165, 152));
680 }
681
682 #[test]
683 fn test_theme_color_serialize() {
684 #[derive(Serialize)]
685 struct Wrapper {
686 color: ThemeColor,
687 }
688 let wrapper = Wrapper {
689 color: ThemeColor::new(59, 130, 246),
690 };
691 let serialized = toml::to_string(&wrapper).unwrap();
692 assert!(serialized.contains("color = \"#3b82f6\""));
693 }
694
695 #[test]
696 fn test_theme_color_deserialize() {
697 #[derive(Deserialize)]
698 struct Wrapper {
699 color: ThemeColor,
700 }
701 let toml_str = r###"color = "#3b82f6""###;
702 let wrapper: Wrapper = toml::from_str(toml_str).unwrap();
703 assert_eq!(wrapper.color, ThemeColor::Rgb(59, 130, 246));
704 }
705
706 #[test]
707 fn test_theme_color_roundtrip() {
708 #[derive(Serialize, Deserialize)]
709 struct Wrapper {
710 color: ThemeColor,
711 }
712 let original = Wrapper {
713 color: ThemeColor::new(239, 68, 68),
714 };
715 let serialized = toml::to_string(&original).unwrap();
716 let deserialized: Wrapper = toml::from_str(&serialized).unwrap();
717 assert_eq!(original.color, deserialized.color);
718 }
719
720 #[test]
721 fn test_theme_serialize_to_toml() {
722 let theme = Theme::gruvbox_dark();
723 let toml_string = toml::to_string_pretty(&theme).unwrap();
724
725 assert!(toml_string.contains("name = \"Gruvbox Dark\""));
726 assert!(toml_string.contains("bg = \"#282828\""));
727 assert!(toml_string.contains("bg_panel = \"#32302f\""));
728 assert!(toml_string.contains("border_focused = \"#fabd2f\""));
729 assert!(toml_string.contains("color_journal_date = \"#8ec07c\""));
730 }
731
732 #[test]
733 fn test_theme_deserialize_from_toml() {
734 let toml_str = r###"
735 name = "Test Theme"
736 bg = "#282828"
737 bg_panel = "#32302f"
738 bg_selected = "#504945"
739 fg = "#ebdbb2"
740 fg_secondary = "#a89984"
741 fg_muted = "#7c6f64"
742 fg_selected = "#fbf1c7"
743 border = "#504945"
744 border_focused = "#fabd2f"
745 accent = "#fabd2f"
746 color_directory = "#83a598"
747 color_journal_date = "#8ec07c"
748 color_search_match = "#b8bb26"
749 "###;
750
751 let theme: Theme = toml::from_str(toml_str).unwrap();
752 assert_eq!(theme.name, "Test Theme");
753 assert_eq!(theme.bg, ThemeColor::new(0x28, 0x28, 0x28));
754 assert_eq!(theme.border_focused, ThemeColor::new(0xfa, 0xbd, 0x2f));
755 assert_eq!(theme.color_journal_date, ThemeColor::new(0x8e, 0xc0, 0x7c));
756 }
757
758 #[test]
759 fn test_theme_roundtrip() {
760 let original = Theme::tokyo_night();
761 let toml_string = toml::to_string_pretty(&original).unwrap();
762 let deserialized: Theme = toml::from_str(&toml_string).unwrap();
763
764 assert_eq!(original.name, deserialized.name);
765 assert_eq!(original.bg, deserialized.bg);
766 assert_eq!(original.fg, deserialized.fg);
767 assert_eq!(original.border_focused, deserialized.border_focused);
768 assert_eq!(original.color_journal_date, deserialized.color_journal_date);
769 }
770
771 #[test]
772 fn test_theme_color_serialize_lowercase_hex() {
773 #[derive(Serialize)]
774 struct Wrapper {
775 color: ThemeColor,
776 }
777 let wrapper = Wrapper {
778 color: ThemeColor::new(171, 205, 239),
779 };
780 let serialized = toml::to_string(&wrapper).unwrap();
781 assert!(serialized.contains("color = \"#abcdef\""));
782 }
783
784 #[test]
785 fn test_theme_deserialize_uppercase_hex() {
786 #[derive(Deserialize)]
787 struct Wrapper {
788 color: ThemeColor,
789 }
790 let toml_str = r###"color = "#ABCDEF""###;
791 let wrapper: Wrapper = toml::from_str(toml_str).unwrap();
792 assert_eq!(wrapper.color, ThemeColor::Rgb(171, 205, 239));
793 }
794
795 #[test]
796 fn test_theme_deserialize_3char_hex() {
797 #[derive(Deserialize)]
798 struct Wrapper {
799 color: ThemeColor,
800 }
801 let toml_str = r###"color = "#abc""###;
802 let wrapper: Wrapper = toml::from_str(toml_str).unwrap();
803 assert_eq!(wrapper.color, ThemeColor::Rgb(170, 187, 204));
804 }
805
806 #[test]
807 fn test_from_ansi_index() {
808 assert_eq!(
809 ThemeColor::from_string("ansi:4").unwrap(),
810 ThemeColor::Ansi(4)
811 );
812 assert_eq!(
813 ThemeColor::from_string("ansi:255").unwrap(),
814 ThemeColor::Ansi(255)
815 );
816 }
817
818 #[test]
819 fn test_from_reset() {
820 assert_eq!(ThemeColor::from_string("reset").unwrap(), ThemeColor::Reset);
821 }
822
823 #[test]
824 fn test_all_builtin_themes_serialize() {
825 let themes = vec![
826 Theme::ansi(),
827 Theme::gruvbox_dark(),
828 Theme::gruvbox_light(),
829 Theme::catppuccin_mocha(),
830 Theme::catppuccin_latte(),
831 Theme::tokyo_night(),
832 Theme::tokyo_night_storm(),
833 Theme::solarized_dark(),
834 Theme::solarized_light(),
835 Theme::nord(),
836 ];
837 for theme in themes {
838 let toml_string = toml::to_string_pretty(&theme).unwrap();
839 let roundtrip: Theme = toml::from_str(&toml_string).unwrap();
840 assert_eq!(theme.name, roundtrip.name);
841 assert_eq!(theme.bg, roundtrip.bg);
842 }
843 }
844
845 #[test]
846 fn test_ansi_theme() {
847 let theme = Theme::ansi();
848 assert_eq!(theme.name, "ANSI");
849 assert_eq!(theme.bg, ThemeColor::Reset);
850 assert_eq!(theme.fg, ThemeColor::Reset);
851 assert_eq!(theme.bg_selected, ThemeColor::Ansi(4));
852 assert_eq!(theme.border_focused, ThemeColor::Ansi(6));
853 assert_eq!(theme.color_directory, ThemeColor::Ansi(12));
854 }
855}