aboutsummaryrefslogtreecommitdiff
path: root/src/config/theme.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/config/theme.rs')
-rw-r--r--src/config/theme.rs163
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());
+ }
+}