mod color;
use clap::{Args, Parser, Subcommand};
use color::Color;
use std::process;
#[derive(Parser)]
#[command(name = "grimoire_css_color_toolkit")]
#[command(about = "CSS Color Module Level 4 compliant color manipulation toolkit")]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
New(NewArgs),
FromRgb(FromRgbArgs),
FromHsl(FromHslArgs),
FromHwb(FromHwbArgs),
FromHex(FromHexArgs),
Parse(ParseArgs),
Red(ColorArgs),
Green(ColorArgs),
Blue(ColorArgs),
Alpha(ColorArgs),
Opacity(ColorArgs),
ToHsl(ColorArgs),
ToRgba(ColorArgs),
ToHex(ColorArgs),
ToNamed(ColorArgs),
Grayscale(ColorArgs),
Complement(ColorArgs),
Invert(InvertArgs),
Mix(MixArgs),
AdjustHue(AdjustHueArgs),
AdjustColor(AdjustColorArgs),
ChangeColor(ChangeColorArgs),
ScaleColor(ScaleColorArgs),
Rgba(RgbaArgs),
Lighten(AmountArgs),
Darken(AmountArgs),
Saturate(AmountArgs),
Desaturate(AmountArgs),
Opacify(AmountArgs),
FadeIn(AmountArgs),
Transparentize(AmountArgs),
FadeOut(AmountArgs),
}
#[derive(Args)]
struct NewArgs {
r: u8,
g: u8,
b: u8,
#[arg(default_value = "1.0")]
a: f32,
}
#[derive(Args)]
struct FromRgbArgs {
r: u8,
g: u8,
b: u8,
#[arg(default_value = "1.0")]
a: f32,
}
#[derive(Args)]
struct FromHslArgs {
h: f32,
s: f32,
l: f32,
#[arg(default_value = "1.0")]
a: f32,
}
#[derive(Args)]
struct FromHwbArgs {
h: f32,
w: f32,
b: f32,
#[arg(default_value = "1.0")]
a: f32,
}
#[derive(Args)]
struct FromHexArgs {
hex: String,
}
#[derive(Args)]
struct ParseArgs {
color: String,
}
#[derive(Args)]
struct ColorArgs {
color: String,
}
#[derive(Args)]
struct InvertArgs {
color: String,
#[arg(short, long)]
weight: Option<f32>,
}
#[derive(Args)]
struct MixArgs {
color1: String,
color2: String,
weight: f32,
}
#[derive(Args)]
struct AdjustHueArgs {
color: String,
degrees: f32,
}
#[derive(Args)]
struct AdjustColorArgs {
color: String,
#[arg(long)]
red_delta: Option<i32>,
#[arg(long)]
green_delta: Option<i32>,
#[arg(long)]
blue_delta: Option<i32>,
#[arg(long)]
hue_delta: Option<f32>,
#[arg(long)]
sat_delta: Option<f32>,
#[arg(long)]
light_delta: Option<f32>,
#[arg(long)]
alpha_delta: Option<f32>,
}
#[derive(Args)]
struct ChangeColorArgs {
color: String,
#[arg(long)]
red: Option<u8>,
#[arg(long)]
green: Option<u8>,
#[arg(long)]
blue: Option<u8>,
#[arg(long)]
hue: Option<f32>,
#[arg(long)]
saturation: Option<f32>,
#[arg(long)]
lightness: Option<f32>,
#[arg(long)]
alpha: Option<f32>,
}
#[derive(Args)]
struct ScaleColorArgs {
color: String,
#[arg(long)]
red_scale: Option<f32>,
#[arg(long)]
green_scale: Option<f32>,
#[arg(long)]
blue_scale: Option<f32>,
#[arg(long)]
saturation_scale: Option<f32>,
#[arg(long)]
lightness_scale: Option<f32>,
#[arg(long)]
alpha_scale: Option<f32>,
}
#[derive(Args)]
struct RgbaArgs {
color: String,
alpha: f32,
}
#[derive(Args)]
struct AmountArgs {
color: String,
amount: f32,
}
fn parse_color(color_str: &str) -> Color {
Color::try_from_str(color_str).unwrap_or_else(|| {
eprintln!("Invalid color: {color_str}");
process::exit(1);
})
}
fn main() {
let cli = Cli::parse();
match cli.command {
Commands::New(args) => {
let color = Color::new(args.r, args.g, args.b, args.a);
println!("{}", color.to_hex_string());
}
Commands::FromRgb(args) => {
let color = Color::from_rgb(args.r, args.g, args.b, args.a);
println!("{}", color.to_hex_string());
}
Commands::FromHsl(args) => {
let color = Color::from_hsl(args.h, args.s, args.l, args.a);
println!("{}", color.to_hex_string());
}
Commands::FromHwb(args) => {
let color = Color::from_hwb(args.h, args.w, args.b, args.a);
println!("{}", color.to_hex_string());
}
Commands::FromHex(args) => match Color::from_hex(&args.hex) {
Some(color) => println!("{}", color.to_hex_string()),
None => {
eprintln!("Invalid hex color: {}", args.hex);
process::exit(1);
}
},
Commands::Parse(args) => {
let color = parse_color(&args.color);
println!("{}", color.to_hex_string());
}
Commands::Red(args) => {
let color = parse_color(&args.color);
println!("{}", color.red());
}
Commands::Green(args) => {
let color = parse_color(&args.color);
println!("{}", color.green());
}
Commands::Blue(args) => {
let color = parse_color(&args.color);
println!("{}", color.blue());
}
Commands::Alpha(args) => {
let color = parse_color(&args.color);
println!("{}", color.alpha());
}
Commands::Opacity(args) => {
let color = parse_color(&args.color);
println!("{}", color.opacity());
}
Commands::ToHsl(args) => {
let color = parse_color(&args.color);
let (h, s, l) = color.to_hsl();
println!("{h} {s} {l}");
}
Commands::ToRgba(args) => {
let color = parse_color(&args.color);
let (r, g, b, a) = color.to_rgba();
println!("{r} {g} {b} {a}");
}
Commands::ToHex(args) => {
let color = parse_color(&args.color);
println!("{}", color.to_hex_string());
}
Commands::ToNamed(args) => {
let color = parse_color(&args.color);
match color.to_named_color_str() {
Some(name) => println!("{name}"),
None => process::exit(1),
}
}
Commands::Grayscale(args) => {
let color = parse_color(&args.color);
println!("{}", color.grayscale().to_hex_string());
}
Commands::Complement(args) => {
let color = parse_color(&args.color);
println!("{}", color.complement().to_hex_string());
}
Commands::Invert(args) => {
let color = parse_color(&args.color);
println!("{}", color.invert(args.weight).to_hex_string());
}
Commands::Mix(args) => {
let color1 = parse_color(&args.color1);
let color2 = parse_color(&args.color2);
println!(
"{}",
Color::mix(color1, color2, args.weight).to_hex_string()
);
}
Commands::AdjustHue(args) => {
let color = parse_color(&args.color);
println!("{}", color.adjust_hue(args.degrees).to_hex_string());
}
Commands::AdjustColor(args) => {
let color = parse_color(&args.color);
let result = color.adjust_color(
args.red_delta,
args.green_delta,
args.blue_delta,
args.hue_delta,
args.sat_delta,
args.light_delta,
args.alpha_delta,
);
println!("{}", result.to_hex_string());
}
Commands::ChangeColor(args) => {
let color = parse_color(&args.color);
let result = color.change_color(
args.red,
args.green,
args.blue,
args.hue,
args.saturation,
args.lightness,
args.alpha,
);
println!("{}", result.to_hex_string());
}
Commands::ScaleColor(args) => {
let color = parse_color(&args.color);
let result = color.scale_color(
args.red_scale,
args.green_scale,
args.blue_scale,
args.saturation_scale,
args.lightness_scale,
args.alpha_scale,
);
println!("{}", result.to_hex_string());
}
Commands::Rgba(args) => {
let color = parse_color(&args.color);
println!("{}", color.rgba(args.alpha).to_hex_string());
}
Commands::Lighten(args) => {
let color = parse_color(&args.color);
println!("{}", color.lighten(args.amount).to_hex_string());
}
Commands::Darken(args) => {
let color = parse_color(&args.color);
println!("{}", color.darken(args.amount).to_hex_string());
}
Commands::Saturate(args) => {
let color = parse_color(&args.color);
println!("{}", color.saturate(args.amount).to_hex_string());
}
Commands::Desaturate(args) => {
let color = parse_color(&args.color);
println!("{}", color.desaturate(args.amount).to_hex_string());
}
Commands::Opacify(args) => {
let color = parse_color(&args.color);
println!("{}", color.opacify(args.amount).to_hex_string());
}
Commands::FadeIn(args) => {
let color = parse_color(&args.color);
println!("{}", color.fade_in(args.amount).to_hex_string());
}
Commands::Transparentize(args) => {
let color = parse_color(&args.color);
println!("{}", color.transparentize(args.amount).to_hex_string());
}
Commands::FadeOut(args) => {
let color = parse_color(&args.color);
println!("{}", color.fade_out(args.amount).to_hex_string());
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_constructors() {
let color = Color::new(255, 0, 0, 1.0);
assert_eq!(color.to_hex_string(), "#ff0000");
let color = Color::from_rgb(0, 255, 0, 1.0);
assert_eq!(color.to_hex_string(), "#00ff00");
let color = Color::from_hsl(240.0, 100.0, 50.0, 1.0);
assert_eq!(color.to_hex_string(), "#0000ff");
let color = Color::from_hwb(120.0, 0.0, 0.0, 1.0);
assert_eq!(color.to_hex_string(), "#00ff00");
let color = Color::from_hex("#ff0000").unwrap();
assert_eq!(color.to_hex_string(), "#ff0000");
let color = Color::try_from_str("red").unwrap();
assert_eq!(color.to_hex_string(), "#ff0000");
}
#[test]
fn test_getters() {
let color = Color::new(255, 128, 64, 0.5);
assert_eq!(color.red(), 255);
assert_eq!(color.green(), 128);
assert_eq!(color.blue(), 64);
assert_eq!(color.alpha(), 0.5);
assert_eq!(color.opacity(), 0.5);
}
#[test]
fn test_conversions() {
let color = Color::new(255, 0, 0, 1.0);
let (h, s, l) = color.to_hsl();
assert_eq!(h, 0.0);
assert_eq!(s, 100.0);
assert_eq!(l, 50.0);
let (r, g, b, a) = color.to_rgba();
assert_eq!(r, 255);
assert_eq!(g, 0);
assert_eq!(b, 0);
assert_eq!(a, 1.0);
assert_eq!(color.to_hex_string(), "#ff0000");
assert_eq!(color.to_named_color_str(), Some("red"));
}
#[test]
fn test_color_operations() {
let red = Color::try_from_str("red").unwrap();
let gray = red.grayscale();
assert_eq!(gray.to_hex_string(), "#808080");
let complement = red.complement();
assert_eq!(complement.to_hex_string(), "#00ffff");
let inverted = red.invert(Some(100.0));
assert_eq!(inverted.to_hex_string(), "#00ffff");
let mixed = Color::mix(red, Color::try_from_str("blue").unwrap(), 50.0);
assert_eq!(mixed.to_hex_string(), "#800080");
let hue_adjusted = red.adjust_hue(120.0);
assert_eq!(hue_adjusted.to_hex_string(), "#00ff00");
}
#[test]
fn test_color_adjustments() {
let color = Color::new(128, 128, 128, 1.0);
let adjusted = color.adjust_color(Some(10), None, None, None, None, None, None);
assert_eq!(adjusted.red(), 138);
let changed = color.change_color(Some(255), None, None, None, None, None, None);
assert_eq!(changed.red(), 255);
let scaled = color.scale_color(Some(10.0), None, None, None, None, None);
assert!(scaled.red() > 128);
}
#[test]
fn test_alpha_operations() {
let color = Color::new(255, 0, 0, 1.0);
let with_alpha = color.rgba(0.5);
assert_eq!(with_alpha.to_hex_string(), "#ff000080");
let more_opaque = with_alpha.opacify(0.3);
assert_eq!(more_opaque.alpha(), 0.8);
let more_transparent = color.transparentize(0.5);
assert_eq!(more_transparent.alpha(), 0.5);
let fade_in = with_alpha.fade_in(0.2);
assert_eq!(fade_in.alpha(), 0.7);
let fade_out = color.fade_out(0.3);
assert_eq!(fade_out.alpha(), 0.7);
}
#[test]
fn test_lightness_adjustments() {
let red = Color::try_from_str("red").unwrap();
let lighter = red.lighten(10.0);
assert_eq!(lighter.to_hex_string(), "#ff3333");
let darker = red.darken(10.0);
assert_eq!(darker.to_hex_string(), "#cc0000");
}
#[test]
fn test_saturation_adjustments() {
let red = Color::try_from_str("red").unwrap();
let desaturated = red.desaturate(50.0);
let (_, s, _) = desaturated.to_hsl();
assert!((s - 50.0).abs() < 1.0);
let saturated = desaturated.saturate(25.0);
let (_, s, _) = saturated.to_hsl();
assert!((s - 75.0).abs() < 1.0);
}
#[test]
fn test_css_parsing() {
assert!(Color::try_from_str("rgb(255, 0, 0)").is_some());
assert!(Color::try_from_str("hsl(0, 100%, 50%)").is_some());
assert!(Color::try_from_str("hwb(0 0% 0%)").is_some());
assert!(Color::try_from_str("#ff0000").is_some());
assert!(Color::try_from_str("red").is_some());
assert!(Color::try_from_str("invalid_color").is_none());
}
#[test]
fn test_hex_variations() {
assert_eq!(Color::from_hex("#fff").unwrap().to_hex_string(), "#ffffff");
assert_eq!(
Color::from_hex("#ffffff").unwrap().to_hex_string(),
"#ffffff"
);
assert_eq!(
Color::from_hex("#ffff").unwrap().to_hex_string(),
"#ffffffff"
);
assert_eq!(
Color::from_hex("#ffffffff").unwrap().to_hex_string(),
"#ffffffff"
);
}
#[test]
fn test_named_colors() {
let red = Color::try_from_str("red").unwrap();
assert_eq!(red.to_named_color_str(), Some("red"));
let blue = Color::try_from_str("blue").unwrap();
assert_eq!(blue.to_named_color_str(), Some("blue"));
let custom = Color::new(123, 45, 67, 1.0);
assert_eq!(custom.to_named_color_str(), None);
}
#[test]
fn test_edge_cases() {
let color = Color::new(255, 255, 255, 2.0);
assert_eq!(color.alpha(), 1.0);
let color = Color::new(0, 0, 0, -1.0);
assert_eq!(color.alpha(), 0.0);
let (h, s, l) = Color::new(0, 0, 0, 1.0).to_hsl();
assert_eq!(h, 0.0);
assert_eq!(s, 0.0);
assert_eq!(l, 0.0);
}
}