use clap::Parser;
const RESET: &str = "\x1b[m";
const FG_BLACK: &str = "\x1b[38;2;0;0;0m";
const FG_GREY: &str = "\x1b[38;5;250m";
#[derive(Debug, Parser)]
#[clap(about = "Display terminal colors.")]
#[clap(author = "https://ariel.ninja")]
#[clap(version)]
#[clap(disable_version_flag = true)]
struct Args {
#[arg(short = 'H', long, default_value_t = 16)]
hues: u8,
#[arg(short = 'V', long, default_value_t = 4)]
values: u8,
#[arg(short = 'S', long, default_value_t = 4)]
saturations: u8,
#[arg(short, long)]
resolution: Option<u8>,
#[arg(short, long, default_value_t = 0.0)]
offset: f64,
#[arg(short, long, value_enum, default_value_t = DisplayOptions::Rgb)]
display: DisplayOptions,
#[arg(long, default_value_t = 50.0)]
dark: f64,
#[arg(short = 'D', long, default_value_t = 5.0)]
dark_factor: f64,
#[arg(short, long)]
legend: bool,
#[arg(long, action = clap::ArgAction::Version)]
version: (),
}
#[derive(Debug, Copy, Clone, PartialEq, clap::ValueEnum)]
enum DisplayOptions {
Rgb,
Ansi,
Lum,
None,
}
fn main() {
let mut args = Args::parse();
if let Some(resolution) = args.resolution {
args.values = resolution;
args.saturations = resolution;
}
let hues = range(args.hues + 1, 0, 1, args.offset / 360.0);
let values = range(args.values + 2, 1, 1, 0.0);
let mut saturations = range(args.saturations + 2, 1, 1, 0.0);
saturations.reverse();
let is_table = !values.is_empty() || !saturations.is_empty();
let legend = args.legend && args.display != DisplayOptions::Ansi;
if legend && is_table {
for v in &values {
print!(" {:>3}% v ", (v * 100.0).round());
}
print!(" val/sat ");
for s in &saturations {
print!(" {:>3}% s ", (s * 100.0).round());
}
println!();
}
for h in hues {
values.iter().for_each(|v| {
Color::from_hsv(h, 1.0, *v).print(args.display, args.dark, args.dark_factor);
});
Color::from_hsv(h, 1.0, 1.0).print(args.display, args.dark, args.dark_factor);
saturations.iter().for_each(|s| {
Color::from_hsv(h, *s, 1.0).print(args.display, args.dark, args.dark_factor);
});
if legend {
print!(" hue: {}", (h * 360.0).round());
}
println!();
}
if let DisplayOptions::Ansi = args.display {
for i in 0..16 {
if i % 8 == 0 {
println!()
}
let foreground = if i == 0 { FG_GREY } else { FG_BLACK };
let background = format!("\x1b[48;5;{i}m");
print!("{background}{foreground}{i:^9}{RESET}");
}
println!();
for i in 232..255 {
if (i - 232) % 8 == 0 {
println!()
}
let foreground = if i <= 237 { FG_GREY } else { FG_BLACK };
let background = format!("\x1b[48;5;{i}m");
print!("{background}{foreground}{i:^9}{RESET}");
}
println!();
}
}
fn range(resolution: u8, truncate_head: u8, truncate_tail: u8, offset: f64) -> Vec<f64> {
if resolution
.saturating_sub(truncate_head)
.saturating_sub(truncate_tail)
== 0
{
return Vec::new();
}
let factor = 1.0 / f64::from(resolution.saturating_sub(1));
(truncate_head..resolution.saturating_sub(truncate_tail))
.map(|i| (f64::from(i) * factor + offset) % 1.0)
.collect()
}
#[derive(Debug, Copy, Clone)]
struct Color(f64, f64, f64);
impl Color {
fn from_hsv(h: f64, s: f64, v: f64) -> Self {
let h = h * 360.0;
let c = v * s;
let x = c * (1.0 - f64::abs((h / 60.0) % 2.0 - 1.0));
let m = v - c;
let (r_, g_, b_) = if h < 60.0 {
(c, x, 0.0)
} else if h < 120.0 {
(x, c, 0.0)
} else if h < 180.0 {
(0.0, c, x)
} else if h < 240.0 {
(0.0, x, c)
} else if h < 300.0 {
(x, 0.0, c)
} else {
(c, 0.0, x)
};
let r = r_ + m;
let g = g_ + m;
let b = b_ + m;
Self(r, g, b)
}
fn as_bytes(&self) -> (u8, u8, u8) {
(
(self.0 * 255.0).round() as u8,
(self.1 * 255.0).round() as u8,
(self.2 * 255.0).round() as u8,
)
}
fn eic_luminosity(&self) -> f64 {
0.2126 * self.0 + 0.7152 * self.1 + 0.0722 * self.2
}
fn nearest_ansi_color_code(&self) -> u8 {
let (r, g, b) = self.as_bytes();
let r = (r / 32).min(5);
let g = (g / 32).min(5);
let b = (b / 32).min(5);
16 + 36 * r + 6 * g + b
}
fn display_hex(&self) -> String {
let (r, g, b) = self.as_bytes();
format!("{r:02X}{g:02X}{b:02X}")
}
fn bg(&self) -> String {
let (r, g, b) = self.as_bytes();
format!("\x1b[48;2;{r};{g};{b}m")
}
fn fg(&self) -> String {
let (r, g, b) = self.as_bytes();
format!("\x1b[38;2;{r};{g};{b}m")
}
fn print(&self, display: DisplayOptions, dark_threshold: f64, dark_factor: f64) {
let luminosity = self.eic_luminosity();
let dark = dark_threshold / 100.0;
let fgv = if luminosity > dark {
(1.0 - luminosity).powf(dark_factor) } else {
luminosity.powf(1.0 / dark_factor) };
let foreground = Color::from_hsv(0.0, 0.0, fgv).fg();
let (background, text) = match display {
DisplayOptions::Ansi => {
let color_code = self.nearest_ansi_color_code();
let background = format!("\x1b[48;5;{}m", color_code);
let text = format!("{:^6}", color_code);
(background, text)
}
DisplayOptions::Rgb => (self.bg(), self.display_hex()),
DisplayOptions::Lum => (self.bg(), format!("{:>3}%", (luminosity * 100.0).round())),
DisplayOptions::None => (self.bg(), String::new()),
};
print!("{background}{foreground}{text:^9}{RESET}");
}
}