rizzoo 0.6.9

a cross platform color generation tool implementing Material 3 Expressive
use crate::color::types::Rgb;
use serde::Deserialize;
use std::collections::HashMap;
use std::path::Path;

#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default, clap::ValueEnum)]
#[serde(rename_all = "kebab-case")]
pub enum Style {
    #[default]
    TonalSpot,
    Neutral,
    Vibrant,
    Expressive,
    Rainbow,
    FruitSalad,
    Monochrome,
    Fidelity,
    Content,
}

impl Style {
    pub fn as_str(&self) -> &'static str {
        match self {
            Self::TonalSpot => "tonal-spot",
            Self::Neutral => "neutral",
            Self::Vibrant => "vibrant",
            Self::Expressive => "expressive",
            Self::Rainbow => "rainbow",
            Self::FruitSalad => "fruit-salad",
            Self::Monochrome => "monochrome",
            Self::Fidelity => "fidelity",
            Self::Content => "content",
        }
    }
}

impl std::fmt::Display for Style {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.as_str())
    }
}

#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default, clap::ValueEnum)]
#[serde(rename_all = "kebab-case")]
pub enum Contrast {
    #[default]
    Standard,
    Medium,
    High,
}

impl Contrast {
    pub fn as_str(&self) -> &'static str {
        match self {
            Self::Standard => "standard",
            Self::Medium => "medium",
            Self::High => "high",
        }
    }

    pub fn multiplier(&self) -> f64 {
        match self {
            Self::High => 1.0,
            Self::Medium => 0.5,
            Self::Standard => 0.0,
        }
    }
}

impl std::fmt::Display for Contrast {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.as_str())
    }
}

#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
#[serde(rename_all = "kebab-case")]
pub enum PreferMode {
    Darkness,
    Lightness,
    Saturation,
}

impl std::fmt::Display for PreferMode {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Darkness => write!(f, "darkness"),
            Self::Lightness => write!(f, "lightness"),
            Self::Saturation => write!(f, "saturation"),
        }
    }
}

#[derive(Deserialize, Debug, Clone)]
pub struct CustomColor {
    pub color: String,
    #[serde(default = "is_blend")]
    pub blend: bool,
}

fn is_blend() -> bool {
    true
}

#[derive(Deserialize, Debug, Clone, Default)]
pub struct WallpaperConfiguration {
    #[serde(default)]
    pub set: Option<bool>,
    #[serde(default)]
    pub command: Option<String>,
}

#[derive(Deserialize, Debug, Clone)]
pub struct TemplateConfiguration {
    pub template: String,
    pub output: String,
    pub post_hook: Option<String>,
    #[serde(default = "is_template_configuration")]
    pub enabled: bool,
}

fn is_template_configuration() -> bool {
    true
}

#[derive(Deserialize, Debug, Clone, Default)]
pub struct Configuration {
    #[serde(default)]
    pub style: Option<Style>,
    #[serde(default)]
    pub light: Option<bool>,
    #[serde(default)]
    pub contrast: Option<Contrast>,
    #[serde(default)]
    pub pick: Option<u8>,
    #[serde(default)]
    pub prefer: Option<PreferMode>,
    #[serde(default)]
    pub custom_colors: Option<HashMap<String, CustomColor>>,
    #[serde(default)]
    pub wallpaper: Option<WallpaperConfiguration>,
    #[serde(default, flatten)]
    pub templates: HashMap<String, TemplateConfiguration>,
}

impl Configuration {
    pub fn harmonize_custom_colors(&self, seed_color: &Rgb) -> HashMap<String, Rgb> {
        let Some(ref custom) = self.custom_colors else {
            return HashMap::new();
        };
        let mut out = HashMap::new();
        for (name, cfg) in custom {
            let Some(mut custom) = Rgb::from_hex(&cfg.color) else {
                continue;
            };
            if cfg.blend {
                custom = crate::color::blend::harmonize_color(&custom, seed_color);
            }
            out.insert(name.clone(), custom);
        }
        out
    }

    pub fn load(path: &Path) -> Self {
        if !path.exists() {
            return Self::default();
        }
        let content = match std::fs::read_to_string(path) {
            Ok(c) => c,
            Err(e) => {
                log::warn!("failed to read {}: {e}", path.display());
                return Self::default();
            }
        };
        match toml::from_str::<Configuration>(&content) {
            Ok(cfg) => cfg,
            Err(e) => {
                log::warn!("failed to parse {}: {e}", path.display());
                Self::default()
            }
        }
    }

    pub fn merge(&self, cli: &crate::cli::Cli) -> crate::cli::Cli {
        let mut c = cli.clone();
        if c.style == Style::TonalSpot {
            if let Some(style) = self.style {
                c.style = style;
            }
        }
        if !c.light {
            if let Some(l) = self.light {
                c.light = l;
            }
        }
        if c.contrast == Contrast::Standard {
            if let Some(ct) = self.contrast {
                c.contrast = ct;
            }
        }
        if c.pick.is_none() {
            if let Some(p) = self.pick {
                c.pick = Some(p);
            }
        }
        if c.prefer.is_none() {
            if let Some(pr) = self.prefer {
                if c.image.is_some() || c.image_url.is_some() {
                    c.prefer = Some(pr);
                }
            }
        }
        c
    }
}