rcpufetch 0.0.5

[ALPHA] A rusty crossplatform, but simple CLI binutil for reading CPU information.
//! Color scheme handling for `-c`/`--color`, matching cpufetch's named schemes and its
//! custom `R,G,B:R,G,B:R,G,B:R,G,B:R,G,B` format (3 logo colors + 2 text colors).

use crate::cla::Style;

#[derive(Debug, Clone, Copy)]
pub struct Rgb(pub u8, pub u8, pub u8);

impl Rgb {
    pub fn fg(self) -> String {
        format!("\x1b[38;2;{};{};{}m", self.0, self.1, self.2)
    }
}

#[derive(Debug, Clone, Copy)]
pub struct Colors {
    pub logo: [Rgb; 3],
    pub label: Rgb,
    pub value: Rgb,
}

pub const RESET: &str = "\x1b[0m";

const fn c(r: u8, g: u8, b: u8) -> Rgb {
    Rgb(r, g, b)
}

impl Colors {
    fn from_five(v: [Rgb; 5]) -> Colors {
        Colors {
            logo: [v[0], v[1], v[2]],
            label: v[3],
            value: v[4],
        }
    }

    pub fn named(scheme: &str) -> Option<Colors> {
        Some(Self::from_five(match scheme.to_lowercase().as_str() {
            "intel" => [
                c(0, 113, 197),
                c(0, 113, 197),
                c(0, 113, 197),
                c(0, 113, 197),
                c(255, 255, 255),
            ],
            "intel-new" => [
                c(0, 133, 199),
                c(0, 133, 199),
                c(0, 133, 199),
                c(0, 133, 199),
                c(255, 255, 255),
            ],
            "amd" => [
                c(0, 158, 82),
                c(0, 158, 82),
                c(0, 158, 82),
                c(0, 158, 82),
                c(255, 255, 255),
            ],
            "ibm" => [
                c(15, 98, 254),
                c(15, 98, 254),
                c(15, 98, 254),
                c(15, 98, 254),
                c(255, 255, 255),
            ],
            "arm" => [
                c(0, 145, 189),
                c(0, 145, 189),
                c(0, 145, 189),
                c(0, 145, 189),
                c(255, 255, 255),
            ],
            "rockchip" => [
                c(0, 174, 239),
                c(0, 174, 239),
                c(0, 174, 239),
                c(0, 174, 239),
                c(255, 255, 255),
            ],
            "sifive" => [
                c(83, 61, 209),
                c(83, 61, 209),
                c(83, 61, 209),
                c(83, 61, 209),
                c(255, 255, 255),
            ],
            _ => return None,
        }))
    }

    /// Parse the custom `R,G,B:R,G,B:R,G,B:R,G,B:R,G,B` format.
    pub fn custom(spec: &str) -> Result<Colors, String> {
        let groups: Vec<&str> = spec.split(':').collect();
        if groups.len() != 5 {
            return Err(format!(
                "Error: invalid --color value '{}': expected 5 colon-separated R,G,B triplets",
                spec
            ));
        }
        let mut out = [Rgb(0, 0, 0); 5];
        for (i, g) in groups.iter().enumerate() {
            let parts: Vec<&str> = g.split(',').collect();
            if parts.len() != 3 {
                return Err(format!(
                    "Error: invalid --color triplet '{}': expected R,G,B",
                    g
                ));
            }
            let mut rgb = [0u8; 3];
            for (j, p) in parts.iter().enumerate() {
                rgb[j] = p
                    .trim()
                    .parse::<u16>()
                    .ok()
                    .filter(|v| *v <= 255)
                    .ok_or_else(|| {
                        format!("Error: invalid color component '{}' (must be 0-255)", p)
                    })? as u8;
            }
            out[i] = Rgb(rgb[0], rgb[1], rgb[2]);
        }
        Ok(Self::from_five(out))
    }

    pub fn for_vendor(vendor: &str) -> Colors {
        let key = match vendor {
            v if v.eq_ignore_ascii_case("Intel") => "intel-new",
            v if v.eq_ignore_ascii_case("AMD") => "amd",
            v if v.eq_ignore_ascii_case("IBM") => "ibm",
            v if v.eq_ignore_ascii_case("ARM") => "arm",
            v if v.eq_ignore_ascii_case("Rockchip") => "rockchip",
            v if v.eq_ignore_ascii_case("SiFive") => "sifive",
            _ => "arm",
        };
        Self::named(key).unwrap()
    }

    pub fn legacy() -> Colors {
        let none = Rgb(255, 255, 255);
        Colors {
            logo: [none; 3],
            label: none,
            value: none,
        }
    }
}

/// Resolve the effective style/colors from CLI args + detected vendor, honoring the
/// `NO_COLOR` convention (https://no-color.org/) like upstream cpufetch does.
pub fn resolve(
    style: Style,
    color_arg: Option<&str>,
    vendor: &str,
) -> Result<(Style, Colors, bool), String> {
    let no_color_env = std::env::var_os("NO_COLOR").is_some_and(|v| !v.is_empty());
    let effective_style = if no_color_env { Style::Legacy } else { style };

    if effective_style == Style::Legacy {
        return Ok((effective_style, Colors::legacy(), false));
    }

    let colors = match color_arg {
        Some(spec) => Colors::named(spec)
            .map(Ok)
            .unwrap_or_else(|| Colors::custom(spec))?,
        None => Colors::for_vendor(vendor),
    };
    Ok((effective_style, colors, true))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn custom_color_parses_five_triplets() {
        let c = Colors::custom("1,2,3:4,5,6:7,8,9:10,11,12:13,14,15").unwrap();
        assert_eq!((c.logo[0].0, c.logo[0].1, c.logo[0].2), (1, 2, 3));
        assert_eq!((c.logo[2].0, c.logo[2].1, c.logo[2].2), (7, 8, 9));
        assert_eq!((c.label.0, c.label.1, c.label.2), (10, 11, 12));
        assert_eq!((c.value.0, c.value.1, c.value.2), (13, 14, 15));
    }

    #[test]
    fn custom_color_rejects_wrong_group_count() {
        assert!(Colors::custom("1,2,3:4,5,6").is_err());
    }

    #[test]
    fn custom_color_rejects_out_of_range_component() {
        assert!(Colors::custom("1,2,300:4,5,6:7,8,9:10,11,12:13,14,15").is_err());
    }

    #[test]
    fn named_scheme_is_case_insensitive() {
        assert!(Colors::named("INTEL").is_some());
        assert!(Colors::named("not-a-scheme").is_none());
    }

    #[test]
    fn legacy_style_disables_color_regardless_of_arg() {
        let (style, _, colored) = resolve(Style::Legacy, Some("intel"), "Intel").unwrap();
        assert_eq!(style, Style::Legacy);
        assert!(!colored);
    }
}