rizzoo 0.6.9

a cross platform color generation tool implementing Material 3 Expressive
use clap::Parser;
use std::path::PathBuf;

use crate::color::types::Rgb;
use crate::config::{Contrast, PreferMode, Style};

#[derive(Parser, Debug, Clone)]
#[command(
    name = "rizzoo",
    version = env!("CARGO_PKG_VERSION"),
    about = "a cross platform color generation tool implementing Material 3 Expressive",
)]
pub struct Cli {
    #[arg(
        short = 'i',
        long = "image",
        value_name = "PATH",
        conflicts_with = "image_url",
        help = "image file / path as the color source"
    )]
    pub image: Option<PathBuf>,

    #[arg(
        short = 'u', long = "image-url", value_name = "URL",
        conflicts_with_all = ["image", "reload_scheme"],
        help = "url of an image to use as the color source"
    )]
    pub image_url: Option<String>,

    #[arg(
        short = 'c', long = "color", value_name = "HEX",
        conflicts_with_all = ["image", "image_url", "reload_scheme"],
        help = "hex color to use as the color source"
    )]
    pub color: Option<String>,

    #[arg(
        short = 'P', long = "pick", value_name = "N",
        value_parser = clap::value_parser!(u8).range(0..5),
        conflicts_with = "prefer",
        help = "explicitly choose the most optimal source color 0-3"
    )]
    pub pick: Option<u8>,

    #[arg(
        short = 'e',
        long = "prefer",
        value_name = "MODE",
        conflicts_with = "pick",
        help = "auto-pick the source color based on..."
    )]
    pub prefer: Option<PreferMode>,

    #[arg(
        long = "open-picker",
        default_value_t = false,
        conflicts_with = "pick",
        help = "open the interactive color picker"
    )]
    pub open_picker: bool,

    #[arg(
        short = 'r',
        long = "render",
        default_value_t = false,
        help = "fill template files with colors"
    )]
    pub render: bool,

    #[arg(
        short = 'o',
        long = "output",
        default_value_t = false,
        help = "write all processed templates to output"
    )]
    pub output: bool,

    #[arg(
        long = "output-only",
        value_name = "APP",
        help = "write specific processed template to output"
    )]
    pub output_to: Option<String>,

    #[arg(
        short = 'p',
        long = "preview",
        default_value_t = false,
        conflicts_with = "silent",
        help = "print palette table"
    )]
    pub preview: bool,

    #[arg(
        short = 'n',
        long = "dry-run",
        default_value_t = false,
        help = "render but skip writing or updating files anywhere"
    )]
    pub dry_run: bool,

    #[arg(
        short = 'q', long = "silent",
        default_value_t = false,
        conflicts_with_all = ["preview"],
        help = "suppress process outputs"
    )]
    pub silent: bool,

    #[arg(
        short = 'S',
        long = "style",
        value_name = "STYLE",
        default_value = "tonal-spot",
        help = "choose a Material 3 style"
    )]
    pub style: Style,

    #[arg(
        long = "light",
        default_value_t = false,
        help = "generate light variant colors"
    )]
    pub light: bool,

    #[arg(
        short = 't',
        long = "contrast",
        value_name = "LEVEL",
        default_value = "standard",
        help = "choose color contrast levels"
    )]
    pub contrast: Contrast,

    #[arg(
        short = 'b',
        long = "blend-style",
        value_name = "STYLE",
        help = "blend with a second Material 3 style"
    )]
    pub blend_style: Option<Style>,

    #[arg(
        long = "blend-ratio",
        value_name = "RATIO",
        default_value_t = 0.5,
        help = "blend ratio 0.1 – 0.9 ( requires --blend-style )"
    )]
    pub blend_ratio: f64,

    #[arg(
        short = 'w',
        long = "wallpaper",
        default_value_t = false,
        help = "set as desktop wallpaper"
    )]
    pub wallpaper: bool,

    #[arg(
        short = 'R',
        long = "reload-scheme",
        default_value_t = false,
        conflicts_with_all = ["image", "image_url"],
        help = "reload last generated color scheme"
    )]
    pub reload_scheme: bool,

    #[arg(
        short = 'W',
        long = "watch",
        default_value_t = false,
        help = "watch relevant files and re-render on change"
    )]
    pub watch: bool,

    #[arg(
        long = "init",
        default_value_t = false,
        help = "generate a default configuration file ( warns if already existing )"
    )]
    pub init: bool,

    #[arg(
        long = "init-overwrite",
        default_value_t = false,
        conflicts_with = "init",
        help = "overwrite configuration file with defaults"
    )]
    pub init_overwrite: bool,
}

impl Cli {
    pub fn validate(&self) -> Result<(), String> {
        if self.init || self.init_overwrite {
            return Ok(());
        }
        let has_image = self.image.is_some() || self.image_url.is_some();
        if !has_image
            && self.color.is_none()
            && !self.reload_scheme
            && !self.output
            && self.output_to.is_none()
            && !self.render
            && !self.preview
        {
            return Err(
                "what to do?\n [-i image, -u url, -c color, -R reload-scheme, -m fill templates, -l output apps, -p preview]".into()
            );
        }
        if self.prefer.is_some() && !has_image {
            return Err("--prefer needs an image (-i or -u)".into());
        }
        if let Some(ref hex) = self.color {
            if Rgb::from_hex(hex).is_none() {
                return Err(format!("invalid format --color '#{hex}'"));
            }
        }
        if self.blend_ratio < 0.1 || self.blend_ratio > 0.9 {
            return Err("--blend-ratio must be between 0.1 and 0.9".into());
        }
        Ok(())
    }
}