diff options
Diffstat (limited to 'src/config/theme.rs')
| -rw-r--r-- | src/config/theme.rs | 163 |
1 files changed, 163 insertions, 0 deletions
diff --git a/src/config/theme.rs b/src/config/theme.rs new file mode 100644 index 00000000..9d641862 --- /dev/null +++ b/src/config/theme.rs @@ -0,0 +1,163 @@ +use serde::Deserialize; +use tracing::warn; + +/// Theme configuration: pick a built-in or override individual tokens. +/// +/// ```toml +/// [theme] +/// name = "tokyo-night" # built-in: catppuccin, tokyo-night, dracula, nord, etc. +/// +/// [theme.custom] # override individual tokens on top of the base +/// accent = "#f5c2e7" +/// red = "#ff6188" +/// ``` +#[derive(Debug, Default, Deserialize)] +#[serde(default)] +pub struct ThemeConfig { + /// Built-in theme name. Default: "catppuccin". + pub name: Option<String>, + /// Custom overrides — applied on top of the selected base theme. + pub custom: Option<CustomThemeColors>, +} + +/// Per-token color overrides. All fields optional — only set what you want to change. +#[derive(Debug, Default, Deserialize)] +#[serde(default)] +pub struct CustomThemeColors { + pub accent: Option<String>, + pub panel_bg: Option<String>, + pub surface0: Option<String>, + pub surface1: Option<String>, + pub surface_dim: Option<String>, + pub overlay0: Option<String>, + pub overlay1: Option<String>, + pub text: Option<String>, + pub subtext0: Option<String>, + pub mauve: Option<String>, + pub green: Option<String>, + pub yellow: Option<String>, + pub red: Option<String>, + pub blue: Option<String>, + pub teal: Option<String>, + pub peach: Option<String>, +} + +/// Parse a color string into a ratatui Color. +/// Supports: hex (#rrggbb, #rgb), named colors, rgb(r,g,b), and reset aliases. +pub fn parse_color(s: &str) -> ratatui::style::Color { + use ratatui::style::Color; + let s = s.trim().to_lowercase(); + + match s.as_str() { + "reset" | "default" | "none" | "transparent" => return Color::Reset, + _ => {} + } + + if let Some(hex) = s.strip_prefix('#') { + 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 Color::Rgb(r, g, b); + } + } else if hex.len() == 3 { + let chars: Vec<u8> = hex + .chars() + .filter_map(|c| u8::from_str_radix(&c.to_string(), 16).ok()) + .collect(); + if chars.len() == 3 { + return Color::Rgb(chars[0] * 17, chars[1] * 17, chars[2] * 17); + } + } + } + + if let Some(inner) = s.strip_prefix("rgb(").and_then(|s| s.strip_suffix(')')) { + let parts: Vec<&str> = inner.split(',').collect(); + if parts.len() == 3 { + if let (Ok(r), Ok(g), Ok(b)) = ( + parts[0].trim().parse::<u8>(), + parts[1].trim().parse::<u8>(), + parts[2].trim().parse::<u8>(), + ) { + return Color::Rgb(r, g, b); + } + } + } + + match s.as_str() { + "black" => Color::Black, + "red" => Color::Red, + "green" => Color::Green, + "yellow" => Color::Yellow, + "blue" => Color::Blue, + "magenta" | "purple" => Color::Magenta, + "cyan" => Color::Cyan, + "white" => Color::White, + "gray" | "grey" => Color::Gray, + "darkgray" | "darkgrey" => Color::DarkGray, + "lightred" => Color::LightRed, + "lightgreen" => Color::LightGreen, + "lightyellow" => Color::LightYellow, + "lightblue" => Color::LightBlue, + "lightmagenta" => Color::LightMagenta, + "lightcyan" => Color::LightCyan, + _ => { + warn!(color = s, "unknown color, defaulting to cyan"); + Color::Cyan + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::config::Config; + + #[test] + fn theme_name_parses() { + let toml = r#" +[theme] +name = "dracula" +"#; + let config: Config = toml::from_str(toml).unwrap(); + assert_eq!(config.theme.name.as_deref(), Some("dracula")); + } + + #[test] + fn parse_color_accepts_reset_aliases() { + use ratatui::style::Color; + + for value in ["reset", "default", "none", "transparent"] { + assert_eq!(parse_color(value), Color::Reset, "value: {value}"); + } + } + + #[test] + fn theme_custom_overrides_parse() { + let toml = r##" +[theme] +name = "nord" + +[theme.custom] +panel_bg = "#1e1e2e" +accent = "#ff79c6" +red = "rgb(255, 85, 85)" +"##; + let config: Config = toml::from_str(toml).unwrap(); + assert_eq!(config.theme.name.as_deref(), Some("nord")); + let custom = config.theme.custom.as_ref().unwrap(); + assert_eq!(custom.panel_bg.as_deref(), Some("#1e1e2e")); + assert_eq!(custom.accent.as_deref(), Some("#ff79c6")); + assert_eq!(custom.red.as_deref(), Some("rgb(255, 85, 85)")); + assert!(custom.green.is_none()); + } + + #[test] + fn theme_defaults_when_missing() { + let config: Config = toml::from_str("").unwrap(); + assert!(config.theme.name.is_none()); + assert!(config.theme.custom.is_none()); + } +} |
