1use owo_colors::{OwoColorize, Rgb};
2
3pub fn parse_color(input: &str) -> Option<Rgb> {
6 let s = input.trim();
7
8 if s.starts_with('#') || s.len() == 6 {
10 let hex = s.strip_prefix('#').unwrap_or(s);
11 if hex.len() == 6 {
12 if let (Ok(r), Ok(g), Ok(b)) = (
13 u8::from_str_radix(&hex[0..2], 16),
14 u8::from_str_radix(&hex[2..4], 16),
15 u8::from_str_radix(&hex[4..6], 16),
16 ) {
17 return Some(Rgb(r, g, b));
18 }
19 }
20 }
21
22 match s.to_lowercase().as_str() {
24 "black" => Some(Rgb(0, 0, 0)),
25 "red" => Some(Rgb(255, 0, 0)),
26 "green" => Some(Rgb(0, 255, 0)),
27 "yellow" => Some(Rgb(255, 255, 0)),
28 "blue" => Some(Rgb(0, 0, 255)),
29 "magenta" => Some(Rgb(255, 0, 255)),
30 "cyan" => Some(Rgb(0, 255, 255)),
31 "white" => Some(Rgb(255, 255, 255)),
32 "bright_black" | "grey" | "gray" => Some(Rgb(128, 128, 128)),
33 "bright_red" => Some(Rgb(255, 128, 128)),
34 "bright_green" => Some(Rgb(128, 255, 128)),
35 "bright_yellow" => Some(Rgb(255, 255, 128)),
36 "bright_blue" => Some(Rgb(128, 128, 255)),
37 "bright_magenta" => Some(Rgb(255, 128, 255)),
38 "bright_cyan" => Some(Rgb(128, 255, 255)),
39 "bright_white" => Some(Rgb(255, 255, 255)),
40 _ => None,
41 }
42}
43
44#[derive(Debug, Clone)]
45pub struct Theme {
46 pub name: String,
47 pub label_color: Rgb,
48 pub value_color: Rgb,
49 pub accent_color: Rgb,
50 pub title_color: Rgb,
51 pub separator_color: Rgb,
52}
53
54impl Default for Theme {
55 fn default() -> Self {
56 Self {
57 name: "neutral".to_string(),
58 label_color: Rgb(0, 255, 255), value_color: Rgb(255, 255, 255), accent_color: Rgb(0, 255, 0), title_color: Rgb(255, 255, 0), separator_color: Rgb(128, 128, 128), }
64 }
65}
66
67impl Theme {
68 pub fn neutral() -> Self {
69 Self::default()
70 }
71
72 pub fn new_default() -> Self {
74 Self::neutral()
75 }
76
77 pub fn dark() -> Self {
78 Self {
79 name: "dark".to_string(),
80 label_color: Rgb(128, 128, 255), value_color: Rgb(255, 255, 255), accent_color: Rgb(128, 255, 128), title_color: Rgb(255, 255, 128), separator_color: Rgb(128, 128, 128), }
86 }
87
88 pub fn light() -> Self {
89 Self {
90 name: "light".to_string(),
91 label_color: Rgb(0, 0, 255), value_color: Rgb(0, 0, 0), accent_color: Rgb(0, 255, 0), title_color: Rgb(255, 255, 128), separator_color: Rgb(0, 0, 0), }
97 }
98
99 pub fn catppuccin_latte() -> Self {
103 Self {
104 name: "catppuccin-latte".to_string(),
105 label_color: Rgb(30, 102, 245), value_color: Rgb(76, 79, 105), accent_color: Rgb(64, 160, 43), title_color: Rgb(223, 142, 29), separator_color: Rgb(140, 143, 161), }
111 }
112
113 pub fn catppuccin_frappe() -> Self {
115 Self {
116 name: "catppuccin-frappe".to_string(),
117 label_color: Rgb(137, 180, 250), value_color: Rgb(198, 208, 245), accent_color: Rgb(166, 227, 161), title_color: Rgb(249, 226, 175), separator_color: Rgb(98, 104, 128), }
123 }
124
125 pub fn catppuccin_macchiato() -> Self {
127 Self {
128 name: "catppuccin-macchiato".to_string(),
129 label_color: Rgb(138, 173, 244), value_color: Rgb(202, 211, 245), accent_color: Rgb(166, 218, 149), title_color: Rgb(238, 212, 159), separator_color: Rgb(91, 96, 120), }
135 }
136
137 pub fn catppuccin_mocha() -> Self {
139 Self {
140 name: "catppuccin-mocha".to_string(),
141 label_color: Rgb(137, 180, 250), value_color: Rgb(205, 214, 244), accent_color: Rgb(245, 194, 231), title_color: Rgb(249, 226, 175), separator_color: Rgb(108, 112, 134), }
147 }
148
149 pub fn solarized_light() -> Self {
151 Self {
152 name: "solarized-light".to_string(),
153 label_color: Rgb(38, 139, 210), value_color: Rgb(101, 123, 131), accent_color: Rgb(181, 137, 0), title_color: Rgb(203, 75, 22), separator_color: Rgb(147, 161, 161), }
159 }
160
161 pub fn solarized_dark() -> Self {
163 Self {
164 name: "solarized-dark".to_string(),
165 label_color: Rgb(38, 139, 210), value_color: Rgb(131, 148, 150), accent_color: Rgb(181, 137, 0), title_color: Rgb(203, 75, 22), separator_color: Rgb(147, 161, 161), }
171 }
172
173 pub fn from_name(name: &str) -> Self {
174 match name.to_lowercase().replace('_', "-").as_str() {
175 "dark" => Self::dark(),
176 "light" => Self::light(),
177 "neutral" | "default" => Self::neutral(), "custom" => Self::default(),
179 "auto" => Self::detect_system_theme(),
180 "catppuccin-latte" | "catppuccin_latte" | "latte" => Self::catppuccin_latte(),
181 "catppuccin-frappe" | "catppuccin_frappe" | "frappe" => Self::catppuccin_frappe(),
182 "catppuccin-macchiato" | "catppuccin_macchiato" | "macchiato" => {
183 Self::catppuccin_macchiato()
184 }
185 "catppuccin-mocha" | "catppuccin_mocha" | "mocha" => Self::catppuccin_mocha(),
186 "solarized-light" | "solarized_light" => Self::solarized_light(),
187 "solarized-dark" | "solarized_dark" => Self::solarized_dark(),
188 _ => Self::default(),
189 }
190 }
191
192 pub fn detect_system_theme() -> Self {
194 if let Some(config_dir) = dirs::config_dir() {
196 let gtk_settings = config_dir.join("gtk-3.0").join("settings.ini");
197 if gtk_settings.exists() {
198 if let Ok(contents) = std::fs::read_to_string(>k_settings) {
199 for line in contents.lines() {
200 if line.to_lowercase().contains("prefer-dark-theme") {
201 if line.to_lowercase().contains("true") {
202 return Self::dark();
203 } else {
204 return Self::light();
205 }
206 }
207 }
208 }
209 }
210 }
211 Self::dark()
213 }
214
215 pub fn with_custom_overrides(base: Self, custom: &crate::config::CustomTheme) -> Self {
217 let mut theme = base;
218
219 if let Some(color) = &custom.label_color {
220 if let Some(c) = parse_color(color) {
221 theme.label_color = c;
222 }
223 }
224 if let Some(color) = &custom.value_color {
225 if let Some(c) = parse_color(color) {
226 theme.value_color = c;
227 }
228 }
229 if let Some(color) = &custom.accent_color {
230 if let Some(c) = parse_color(color) {
231 theme.accent_color = c;
232 }
233 }
234 if let Some(color) = &custom.title_color {
235 if let Some(c) = parse_color(color) {
236 theme.title_color = c;
237 }
238 }
239 if let Some(color) = &custom.separator_color {
240 if let Some(c) = parse_color(color) {
241 theme.separator_color = c;
242 }
243 }
244
245 theme
246 }
247
248 pub fn color_label(&self, text: &str) -> String {
249 text.color(self.label_color).to_string()
250 }
251
252 pub fn color_value(&self, text: &str) -> String {
253 text.color(self.value_color).to_string()
254 }
255
256 pub fn color_accent(&self, text: &str) -> String {
257 text.color(self.accent_color).to_string()
258 }
259
260 pub fn color_separator(&self, text: &str) -> String {
261 text.color(self.separator_color).to_string()
262 }
263}
264
265#[cfg(test)]
266mod tests {
267 use super::*;
268
269 #[test]
270 fn test_neutral_theme() {
271 let theme = Theme::neutral();
272 assert_eq!(theme.name, "neutral");
273 assert_eq!(theme.label_color, Rgb(0, 255, 255));
274 assert_eq!(theme.value_color, Rgb(255, 255, 255));
275 assert_eq!(theme.accent_color, Rgb(0, 255, 0));
276 assert_eq!(theme.title_color, Rgb(255, 255, 0));
277 }
278
279 #[test]
280 fn test_dark_theme() {
281 let theme = Theme::dark();
282 assert_eq!(theme.name, "dark");
283 assert_eq!(theme.label_color, Rgb(128, 128, 255));
284 assert_eq!(theme.title_color, Rgb(255, 255, 128));
285 }
286
287 #[test]
288 fn test_light_theme() {
289 let theme = Theme::light();
290 assert_eq!(theme.name, "light");
291 assert_eq!(theme.label_color, Rgb(0, 0, 255));
292 assert_eq!(theme.title_color, Rgb(255, 255, 128));
293 }
294
295 #[test]
296 fn test_new_default() {
297 let theme = Theme::new_default();
298 assert_eq!(theme.name, "neutral");
299 }
300}