use owo_colors::{OwoColorize, Rgb};
pub fn parse_color(input: &str) -> Option<Rgb> {
let s = input.trim();
if s.starts_with('#') || s.len() == 6 {
let hex = s.strip_prefix('#').unwrap_or(s);
if hex.len() == 6 {
if let (Ok(r), Ok(g), Ok(b)) = (
u8::from_str_radix(&hex[0..2], 16),
u8::from_str_radix(&hex[2..4], 16),
u8::from_str_radix(&hex[4..6], 16),
) {
return Some(Rgb(r, g, b));
}
}
}
match s.to_lowercase().as_str() {
"black" => Some(Rgb(0, 0, 0)),
"red" => Some(Rgb(255, 0, 0)),
"green" => Some(Rgb(0, 255, 0)),
"yellow" => Some(Rgb(255, 255, 0)),
"blue" => Some(Rgb(0, 0, 255)),
"magenta" => Some(Rgb(255, 0, 255)),
"cyan" => Some(Rgb(0, 255, 255)),
"white" => Some(Rgb(255, 255, 255)),
"bright_black" | "grey" | "gray" => Some(Rgb(128, 128, 128)),
"bright_red" => Some(Rgb(255, 128, 128)),
"bright_green" => Some(Rgb(128, 255, 128)),
"bright_yellow" => Some(Rgb(255, 255, 128)),
"bright_blue" => Some(Rgb(128, 128, 255)),
"bright_magenta" => Some(Rgb(255, 128, 255)),
"bright_cyan" => Some(Rgb(128, 255, 255)),
"bright_white" => Some(Rgb(255, 255, 255)),
_ => None,
}
}
#[derive(Debug, Clone)]
pub struct Theme {
pub name: String,
pub label_color: Rgb,
pub value_color: Rgb,
pub accent_color: Rgb,
pub title_color: Rgb,
pub separator_color: Rgb,
}
impl Default for Theme {
fn default() -> Self {
Self {
name: "neutral".to_string(),
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), }
}
}
impl Theme {
pub fn neutral() -> Self {
Self::default()
}
pub fn new_default() -> Self {
Self::neutral()
}
pub fn dark() -> Self {
Self {
name: "dark".to_string(),
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), }
}
pub fn light() -> Self {
Self {
name: "light".to_string(),
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), }
}
pub fn catppuccin_latte() -> Self {
Self {
name: "catppuccin-latte".to_string(),
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), }
}
pub fn catppuccin_frappe() -> Self {
Self {
name: "catppuccin-frappe".to_string(),
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), }
}
pub fn catppuccin_macchiato() -> Self {
Self {
name: "catppuccin-macchiato".to_string(),
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), }
}
pub fn catppuccin_mocha() -> Self {
Self {
name: "catppuccin-mocha".to_string(),
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), }
}
pub fn solarized_light() -> Self {
Self {
name: "solarized-light".to_string(),
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), }
}
pub fn solarized_dark() -> Self {
Self {
name: "solarized-dark".to_string(),
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), }
}
pub fn from_name(name: &str) -> Self {
match name.to_lowercase().replace('_', "-").as_str() {
"dark" => Self::dark(),
"light" => Self::light(),
"neutral" | "default" => Self::neutral(), "custom" => Self::default(),
"auto" => Self::detect_system_theme(),
"catppuccin-latte" | "catppuccin_latte" | "latte" => Self::catppuccin_latte(),
"catppuccin-frappe" | "catppuccin_frappe" | "frappe" => Self::catppuccin_frappe(),
"catppuccin-macchiato" | "catppuccin_macchiato" | "macchiato" => {
Self::catppuccin_macchiato()
}
"catppuccin-mocha" | "catppuccin_mocha" | "mocha" => Self::catppuccin_mocha(),
"solarized-light" | "solarized_light" => Self::solarized_light(),
"solarized-dark" | "solarized_dark" => Self::solarized_dark(),
_ => Self::default(),
}
}
pub fn detect_system_theme() -> Self {
if let Some(config_dir) = dirs::config_dir() {
let gtk_settings = config_dir.join("gtk-3.0").join("settings.ini");
if gtk_settings.exists() {
if let Ok(contents) = std::fs::read_to_string(>k_settings) {
for line in contents.lines() {
if line.to_lowercase().contains("prefer-dark-theme") {
if line.to_lowercase().contains("true") {
return Self::dark();
} else {
return Self::light();
}
}
}
}
}
}
Self::dark()
}
pub fn with_custom_overrides(base: Self, custom: &crate::config::CustomTheme) -> Self {
let mut theme = base;
if let Some(color) = &custom.label_color {
if let Some(c) = parse_color(color) {
theme.label_color = c;
}
}
if let Some(color) = &custom.value_color {
if let Some(c) = parse_color(color) {
theme.value_color = c;
}
}
if let Some(color) = &custom.accent_color {
if let Some(c) = parse_color(color) {
theme.accent_color = c;
}
}
if let Some(color) = &custom.title_color {
if let Some(c) = parse_color(color) {
theme.title_color = c;
}
}
if let Some(color) = &custom.separator_color {
if let Some(c) = parse_color(color) {
theme.separator_color = c;
}
}
theme
}
pub fn color_label(&self, text: &str) -> String {
text.color(self.label_color).to_string()
}
pub fn color_value(&self, text: &str) -> String {
text.color(self.value_color).to_string()
}
pub fn color_accent(&self, text: &str) -> String {
text.color(self.accent_color).to_string()
}
pub fn color_separator(&self, text: &str) -> String {
text.color(self.separator_color).to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_neutral_theme() {
let theme = Theme::neutral();
assert_eq!(theme.name, "neutral");
assert_eq!(theme.label_color, Rgb(0, 255, 255));
assert_eq!(theme.value_color, Rgb(255, 255, 255));
assert_eq!(theme.accent_color, Rgb(0, 255, 0));
assert_eq!(theme.title_color, Rgb(255, 255, 0));
}
#[test]
fn test_dark_theme() {
let theme = Theme::dark();
assert_eq!(theme.name, "dark");
assert_eq!(theme.label_color, Rgb(128, 128, 255));
assert_eq!(theme.title_color, Rgb(255, 255, 128));
}
#[test]
fn test_light_theme() {
let theme = Theme::light();
assert_eq!(theme.name, "light");
assert_eq!(theme.label_color, Rgb(0, 0, 255));
assert_eq!(theme.title_color, Rgb(255, 255, 128));
}
#[test]
fn test_new_default() {
let theme = Theme::new_default();
assert_eq!(theme.name, "neutral");
}
}