1use ratatui::style::Color;
7use serde::{Deserialize, Deserializer};
8use std::fmt;
9
10#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct ColorValue(pub Color);
15
16impl ColorValue {
17 #[must_use]
19 pub fn color(&self) -> Color {
20 self.0
21 }
22}
23
24impl Default for ColorValue {
25 fn default() -> Self {
26 Self(Color::Reset)
27 }
28}
29
30impl fmt::Display for ColorValue {
31 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32 write!(f, "{:?}", self.0)
33 }
34}
35
36fn parse_color(s: &str) -> Color {
40 let s = s.trim();
41
42 if let Some(hex) = s.strip_prefix('#') {
44 if hex.len() == 6
45 && let (Ok(r), Ok(g), Ok(b)) = (
46 u8::from_str_radix(&hex[0..2], 16),
47 u8::from_str_radix(&hex[2..4], 16),
48 u8::from_str_radix(&hex[4..6], 16),
49 )
50 {
51 return Color::Rgb(r, g, b);
52 }
53 tracing::warn!(color = s, "invalid hex color, using Reset");
54 return Color::Reset;
55 }
56
57 match s.to_lowercase().as_str() {
59 "black" => Color::Black,
60 "red" => Color::Red,
61 "green" => Color::Green,
62 "yellow" => Color::Yellow,
63 "blue" => Color::Blue,
64 "magenta" => Color::Magenta,
65 "cyan" => Color::Cyan,
66 "white" => Color::White,
67 "gray" | "grey" => Color::Gray,
68 "dark_gray" | "dark_grey" | "darkgray" | "darkgrey" => Color::DarkGray,
69 "light_red" | "lightred" => Color::LightRed,
70 "light_green" | "lightgreen" => Color::LightGreen,
71 "light_yellow" | "lightyellow" => Color::LightYellow,
72 "light_blue" | "lightblue" => Color::LightBlue,
73 "light_magenta" | "lightmagenta" => Color::LightMagenta,
74 "light_cyan" | "lightcyan" => Color::LightCyan,
75 "reset" => Color::Reset,
76 _ => {
77 tracing::warn!(color = s, "unknown color name, using Reset");
78 Color::Reset
79 }
80 }
81}
82
83impl<'de> Deserialize<'de> for ColorValue {
84 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
85 where
86 D: Deserializer<'de>,
87 {
88 let s = String::deserialize(deserializer)?;
89 Ok(Self(parse_color(&s)))
90 }
91}
92
93#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
97#[serde(default)]
98pub struct ColorPalette {
99 pub foreground: ColorValue,
101 pub background: ColorValue,
103 pub accent: ColorValue,
105 pub error: ColorValue,
107 pub warning: ColorValue,
109 pub success: ColorValue,
111 pub info: ColorValue,
113 pub selected_bg: ColorValue,
115 pub selected_fg: ColorValue,
117 pub border: ColorValue,
119 pub muted: ColorValue,
121}
122
123impl Default for ColorPalette {
124 fn default() -> Self {
125 Self {
126 foreground: ColorValue(Color::Rgb(0xd8, 0xd8, 0xd8)),
127 background: ColorValue(Color::Rgb(0x18, 0x18, 0x18)),
128 accent: ColorValue(Color::Rgb(0x6a, 0x9f, 0xb5)),
129 error: ColorValue(Color::Rgb(0xac, 0x42, 0x42)),
130 warning: ColorValue(Color::Rgb(0xf4, 0xbf, 0x75)),
131 success: ColorValue(Color::Rgb(0x90, 0xa9, 0x59)),
132 info: ColorValue(Color::Rgb(0x75, 0xb5, 0xaa)),
133 selected_bg: ColorValue(Color::Rgb(0x38, 0x38, 0x38)),
134 selected_fg: ColorValue(Color::Rgb(0xf8, 0xf8, 0xf8)),
135 border: ColorValue(Color::Rgb(0x58, 0x58, 0x58)),
136 muted: ColorValue(Color::Rgb(0x6b, 0x6b, 0x6b)),
137 }
138 }
139}
140
141#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
143#[serde(default)]
144pub struct ThemeConfig {
145 pub colors: ColorPalette,
147}
148
149#[cfg(test)]
150#[allow(clippy::expect_used)]
151mod tests {
152 use super::*;
153
154 #[test]
155 fn parse_hex_color() {
156 let color = parse_color("#6a9fb5");
157 assert_eq!(color, Color::Rgb(106, 159, 181));
158 }
159
160 #[test]
161 fn parse_named_color() {
162 assert_eq!(parse_color("red"), Color::Red);
163 assert_eq!(parse_color("blue"), Color::Blue);
164 assert_eq!(parse_color("green"), Color::Green);
165 assert_eq!(parse_color("dark_gray"), Color::DarkGray);
166 }
167
168 #[test]
169 fn parse_invalid_color_falls_back() {
170 assert_eq!(parse_color("not_a_color"), Color::Reset);
171 assert_eq!(parse_color("#xyz"), Color::Reset);
172 }
173
174 #[test]
175 fn default_palette_has_dark_theme() {
176 let palette = ColorPalette::default();
177 assert_eq!(palette.background, ColorValue(Color::Rgb(0x18, 0x18, 0x18)));
178 assert_eq!(palette.accent, ColorValue(Color::Rgb(0x6a, 0x9f, 0xb5)));
179 }
180
181 #[test]
182 fn deserialize_theme_from_toml() {
183 let toml_str = r##"
184 [colors]
185 accent = "#ff0000"
186 error = "red"
187 "##;
188 let theme: ThemeConfig = toml::from_str(toml_str).expect("parse theme");
189 assert_eq!(theme.colors.accent, ColorValue(Color::Rgb(255, 0, 0)));
190 assert_eq!(theme.colors.error, ColorValue(Color::Red));
191 assert_eq!(
193 theme.colors.background,
194 ColorValue(Color::Rgb(0x18, 0x18, 0x18))
195 );
196 }
197
198 #[test]
199 fn color_value_display() {
200 let cv = ColorValue(Color::Red);
201 let display = cv.to_string();
202 assert!(!display.is_empty());
203 }
204}