1use owo_colors::{OwoColorize, Rgb};
2
3pub fn parse_color(input: &str) -> Option<Rgb> {
8 let s = input.trim();
9
10 if s.starts_with('#') || s.len() == 6 {
12 let hex = s.strip_prefix('#').unwrap_or(s);
13 if hex.len() == 6 {
14 if let (Ok(r), Ok(g), Ok(b)) = (
15 u8::from_str_radix(&hex[0..2], 16),
16 u8::from_str_radix(&hex[2..4], 16),
17 u8::from_str_radix(&hex[4..6], 16),
18 ) {
19 return Some(Rgb(r, g, b));
20 }
21 }
22 }
23
24 match s.to_lowercase().as_str() {
26 "black" => Some(Rgb(0, 0, 0)),
27 "red" => Some(Rgb(255, 0, 0)),
28 "green" => Some(Rgb(0, 255, 0)),
29 "yellow" => Some(Rgb(255, 255, 0)),
30 "blue" => Some(Rgb(0, 0, 255)),
31 "magenta" => Some(Rgb(255, 0, 255)),
32 "cyan" => Some(Rgb(0, 255, 255)),
33 "white" => Some(Rgb(255, 255, 255)),
34 "bright_black" | "grey" | "gray" => Some(Rgb(128, 128, 128)),
35 "bright_red" => Some(Rgb(255, 128, 128)),
36 "bright_green" => Some(Rgb(128, 255, 128)),
37 "bright_yellow" => Some(Rgb(255, 255, 128)),
38 "bright_blue" => Some(Rgb(128, 128, 255)),
39 "bright_magenta" => Some(Rgb(255, 128, 255)),
40 "bright_cyan" => Some(Rgb(128, 255, 255)),
41 "bright_white" => Some(Rgb(255, 255, 255)),
42 _ => None,
43 }
44}
45
46#[derive(Debug, Clone)]
51pub struct Theme {
52 pub name: String,
54 pub label_color: Rgb,
56 pub value_color: Rgb,
58 pub accent_color: Rgb,
60 pub title_color: Rgb,
62 pub separator_color: Rgb,
64}
65
66impl Default for Theme {
67 fn default() -> Self {
68 Self {
69 name: "neutral".to_string(),
70 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), }
76 }
77}
78
79impl Theme {
80 pub fn neutral() -> Self {
82 Self::default()
83 }
84
85 pub fn new_default() -> Self {
87 Self::neutral()
88 }
89
90 pub fn dark() -> Self {
92 Self {
93 name: "dark".to_string(),
94 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), }
100 }
101
102 pub fn light() -> Self {
104 Self {
105 name: "light".to_string(),
106 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), }
112 }
113
114 pub fn catppuccin_latte() -> Self {
118 Self {
119 name: "catppuccin-latte".to_string(),
120 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), }
126 }
127
128 pub fn catppuccin_frappe() -> Self {
130 Self {
131 name: "catppuccin-frappe".to_string(),
132 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), }
138 }
139
140 pub fn catppuccin_macchiato() -> Self {
142 Self {
143 name: "catppuccin-macchiato".to_string(),
144 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), }
150 }
151
152 pub fn catppuccin_mocha() -> Self {
154 Self {
155 name: "catppuccin-mocha".to_string(),
156 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), }
162 }
163
164 pub fn solarized_light() -> Self {
166 Self {
167 name: "solarized-light".to_string(),
168 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), }
174 }
175
176 pub fn solarized_dark() -> Self {
178 Self {
179 name: "solarized-dark".to_string(),
180 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), }
186 }
187
188 pub fn from_name(name: &str) -> Self {
190 match name.to_lowercase().replace('_', "-").as_str() {
191 "dark" => Self::dark(),
192 "light" => Self::light(),
193 "neutral" | "default" => Self::neutral(), "custom" => Self::default(),
195 "auto" => Self::detect_system_theme(),
196 "catppuccin-latte" | "catppuccin_latte" | "latte" => Self::catppuccin_latte(),
197 "catppuccin-frappe" | "catppuccin_frappe" | "frappe" => Self::catppuccin_frappe(),
198 "catppuccin-macchiato" | "catppuccin_macchiato" | "macchiato" => {
199 Self::catppuccin_macchiato()
200 }
201 "catppuccin-mocha" | "catppuccin_mocha" | "mocha" => Self::catppuccin_mocha(),
202 "solarized-light" | "solarized_light" => Self::solarized_light(),
203 "solarized-dark" | "solarized_dark" => Self::solarized_dark(),
204 _ => Self::default(),
205 }
206 }
207
208 pub fn detect_system_theme() -> Self {
212 if let Some(config_dir) = dirs::config_dir() {
214 let gtk_settings = config_dir.join("gtk-3.0").join("settings.ini");
215 if gtk_settings.exists() {
216 if let Ok(contents) = std::fs::read_to_string(>k_settings) {
217 for line in contents.lines() {
218 if line.to_lowercase().contains("prefer-dark-theme") {
219 if line.to_lowercase().contains("true") {
220 return Self::dark();
221 } else {
222 return Self::light();
223 }
224 }
225 }
226 }
227 }
228 }
229 Self::dark()
231 }
232
233 pub fn with_custom_overrides(base: Self, custom: &crate::config::CustomTheme) -> Self {
235 let mut theme = base;
236
237 if let Some(color) = &custom.label_color {
238 if let Some(c) = parse_color(color) {
239 theme.label_color = c;
240 }
241 }
242 if let Some(color) = &custom.value_color {
243 if let Some(c) = parse_color(color) {
244 theme.value_color = c;
245 }
246 }
247 if let Some(color) = &custom.accent_color {
248 if let Some(c) = parse_color(color) {
249 theme.accent_color = c;
250 }
251 }
252 if let Some(color) = &custom.title_color {
253 if let Some(c) = parse_color(color) {
254 theme.title_color = c;
255 }
256 }
257 if let Some(color) = &custom.separator_color {
258 if let Some(c) = parse_color(color) {
259 theme.separator_color = c;
260 }
261 }
262
263 theme
264 }
265
266 pub fn color_label(&self, text: &str) -> String {
268 text.color(self.label_color).to_string()
269 }
270
271 pub fn color_value(&self, text: &str) -> String {
273 text.color(self.value_color).to_string()
274 }
275
276 pub fn color_accent(&self, text: &str) -> String {
278 text.color(self.accent_color).to_string()
279 }
280
281 pub fn color_separator(&self, text: &str) -> String {
283 text.color(self.separator_color).to_string()
284 }
285}
286
287#[cfg(test)]
288mod tests {
289 use super::*;
290
291 #[test]
292 fn test_neutral_theme() {
293 let theme = Theme::neutral();
294 assert_eq!(theme.name, "neutral");
295 assert_eq!(theme.label_color, Rgb(0, 255, 255));
296 assert_eq!(theme.value_color, Rgb(255, 255, 255));
297 assert_eq!(theme.accent_color, Rgb(0, 255, 0));
298 assert_eq!(theme.title_color, Rgb(255, 255, 0));
299 }
300
301 #[test]
302 fn test_dark_theme() {
303 let theme = Theme::dark();
304 assert_eq!(theme.name, "dark");
305 assert_eq!(theme.label_color, Rgb(128, 128, 255));
306 assert_eq!(theme.title_color, Rgb(255, 255, 128));
307 }
308
309 #[test]
310 fn test_light_theme() {
311 let theme = Theme::light();
312 assert_eq!(theme.name, "light");
313 assert_eq!(theme.label_color, Rgb(0, 0, 255));
314 assert_eq!(theme.title_color, Rgb(255, 255, 128));
315 }
316
317 #[test]
318 fn test_new_default() {
319 let theme = Theme::new_default();
320 assert_eq!(theme.name, "neutral");
321 }
322}