use crate::errors::{Result, RichError};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum ColorSystem {
Standard,
EightBit,
Truecolor,
Windows,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ColorType {
Default,
Standard,
EightBit,
Truecolor,
Windows,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ColorTriplet {
pub red: u8,
pub green: u8,
pub blue: u8,
}
impl ColorTriplet {
pub const fn new(red: u8, green: u8, blue: u8) -> Self {
Self { red, green, blue }
}
pub fn hex(&self) -> String {
format!("#{:02x}{:02x}{:02x}", self.red, self.green, self.blue)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Color {
pub name: String,
pub kind: ColorType,
pub number: Option<u8>,
pub triplet: Option<ColorTriplet>,
}
impl Color {
pub fn is_default(&self) -> bool {
self.kind == ColorType::Default
}
pub fn default_color() -> Self {
Color {
name: "default".to_string(),
kind: ColorType::Default,
number: None,
triplet: None,
}
}
fn named(name: &str, number: u8) -> Self {
let kind = if number < 16 {
ColorType::Standard
} else {
ColorType::EightBit
};
Color {
name: name.to_string(),
kind,
number: Some(number),
triplet: None,
}
}
pub fn from_ansi(number: u8) -> Self {
Color {
name: format!("color({number})"),
kind: if number < 16 {
ColorType::Standard
} else {
ColorType::EightBit
},
number: Some(number),
triplet: None,
}
}
pub fn from_rgb(red: u8, green: u8, blue: u8) -> Self {
let triplet = ColorTriplet::new(red, green, blue);
Color {
name: triplet.hex(),
kind: ColorType::Truecolor,
number: None,
triplet: Some(triplet),
}
}
pub fn parse(color: &str) -> Result<Self> {
let original = color.trim();
let lower = original.to_ascii_lowercase();
if lower == "default" {
return Ok(Color::default_color());
}
if let Some(number) = crate::color_names::ansi_color_number(&lower) {
return Ok(Color::named(&lower, number));
}
if let Some(hex) = lower.strip_prefix('#') {
let triplet = parse_hex(hex)
.ok_or_else(|| RichError::ColorParse(format!("invalid hex color {original:?}")))?;
return Ok(Color {
name: lower.clone(),
kind: ColorType::Truecolor,
number: None,
triplet: Some(triplet),
});
}
if let Some(inner) = lower.strip_prefix("rgb(").and_then(|s| s.strip_suffix(')')) {
let triplet = parse_rgb(inner)
.ok_or_else(|| RichError::ColorParse(format!("invalid rgb color {original:?}")))?;
return Ok(Color {
name: lower.clone(),
kind: ColorType::Truecolor,
number: None,
triplet: Some(triplet),
});
}
if let Some(inner) = lower
.strip_prefix("color(")
.and_then(|s| s.strip_suffix(')'))
{
let n: u16 = inner
.trim()
.parse()
.map_err(|_| RichError::ColorParse(format!("invalid color number {original:?}")))?;
if n > 255 {
return Err(RichError::ColorParse(format!(
"color number must be <= 255, not {n}"
)));
}
return Ok(Color {
name: lower.clone(),
kind: if n < 16 {
ColorType::Standard
} else {
ColorType::EightBit
},
number: Some(n as u8),
triplet: None,
});
}
Err(RichError::ColorParse(format!(
"{original:?} is not a valid color"
)))
}
pub fn get_truecolor(&self) -> Option<ColorTriplet> {
match self.kind {
ColorType::Default => None,
ColorType::Truecolor => self.triplet,
ColorType::Standard | ColorType::Windows => {
self.number.map(|n| ANSI_BASE_PALETTE[n as usize])
}
ColorType::EightBit => self.number.map(eight_bit_triplet),
}
}
pub fn ansi_codes(&self, foreground: bool) -> Vec<String> {
match self.kind {
ColorType::Default => vec![if foreground { "39" } else { "49" }.to_string()],
ColorType::Windows | ColorType::Standard => {
let number = self.number.unwrap_or(0);
let (fore, back) = if number < 8 { (30, 40) } else { (82, 92) };
vec![(if foreground { fore } else { back } + number as u32).to_string()]
}
ColorType::EightBit => {
let number = self.number.unwrap_or(0);
vec![
if foreground { "38" } else { "48" }.to_string(),
"5".to_string(),
number.to_string(),
]
}
ColorType::Truecolor => {
let t = self.triplet.unwrap_or(ColorTriplet::new(0, 0, 0));
vec![
if foreground { "38" } else { "48" }.to_string(),
"2".to_string(),
t.red.to_string(),
t.green.to_string(),
t.blue.to_string(),
]
}
}
}
pub fn downgrade(&self, system: ColorSystem) -> Color {
if self.kind == ColorType::Default {
return self.clone();
}
let target_rank = match system {
ColorSystem::Standard | ColorSystem::Windows => 0,
ColorSystem::EightBit => 1,
ColorSystem::Truecolor => 2,
};
let self_rank = match self.kind {
ColorType::Standard | ColorType::Windows => 0,
ColorType::EightBit => 1,
ColorType::Truecolor => 2,
ColorType::Default => return self.clone(),
};
if self_rank <= target_rank {
return self.clone();
}
let triplet = match self.get_truecolor() {
Some(t) => t,
None => return self.clone(),
};
match target_rank {
1 => Color {
name: self.name.clone(),
kind: ColorType::EightBit,
number: Some(truecolor_to_eight_bit(triplet)),
triplet: None,
},
_ => {
let number = match_color(&STANDARD_PALETTE, triplet);
Color {
name: self.name.clone(),
kind: ColorType::Standard,
number: Some(number),
triplet: None,
}
}
}
}
}
fn parse_hex(hex: &str) -> Option<ColorTriplet> {
if hex.len() != 6 || !hex.bytes().all(|b| b.is_ascii_hexdigit()) {
return None;
}
let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
Some(ColorTriplet::new(r, g, b))
}
fn parse_rgb(inner: &str) -> Option<ColorTriplet> {
let parts: Vec<&str> = inner.split(',').map(str::trim).collect();
if parts.len() != 3 {
return None;
}
let r = parts[0].parse().ok()?;
let g = parts[1].parse().ok()?;
let b = parts[2].parse().ok()?;
Some(ColorTriplet::new(r, g, b))
}
pub const ANSI_BASE_PALETTE: [ColorTriplet; 16] = [
ColorTriplet::new(0, 0, 0),
ColorTriplet::new(128, 0, 0),
ColorTriplet::new(0, 128, 0),
ColorTriplet::new(128, 128, 0),
ColorTriplet::new(0, 0, 128),
ColorTriplet::new(128, 0, 128),
ColorTriplet::new(0, 128, 128),
ColorTriplet::new(192, 192, 192),
ColorTriplet::new(128, 128, 128),
ColorTriplet::new(255, 0, 0),
ColorTriplet::new(0, 255, 0),
ColorTriplet::new(255, 255, 0),
ColorTriplet::new(0, 0, 255),
ColorTriplet::new(255, 0, 255),
ColorTriplet::new(0, 255, 255),
ColorTriplet::new(255, 255, 255),
];
pub const STANDARD_PALETTE: [ColorTriplet; 16] = [
ColorTriplet::new(0, 0, 0),
ColorTriplet::new(170, 0, 0),
ColorTriplet::new(0, 170, 0),
ColorTriplet::new(170, 85, 0),
ColorTriplet::new(0, 0, 170),
ColorTriplet::new(170, 0, 170),
ColorTriplet::new(0, 170, 170),
ColorTriplet::new(170, 170, 170),
ColorTriplet::new(85, 85, 85),
ColorTriplet::new(255, 85, 85),
ColorTriplet::new(85, 255, 85),
ColorTriplet::new(255, 255, 85),
ColorTriplet::new(85, 85, 255),
ColorTriplet::new(255, 85, 255),
ColorTriplet::new(85, 255, 255),
ColorTriplet::new(255, 255, 255),
];
static EIGHT_BIT_PALETTE: [ColorTriplet; 256] = build_eight_bit_palette();
const fn build_eight_bit_palette() -> [ColorTriplet; 256] {
let mut palette = [ColorTriplet::new(0, 0, 0); 256];
let mut i = 0;
while i < 16 {
palette[i] = ANSI_BASE_PALETTE[i];
i += 1;
}
let levels = [0u8, 95, 135, 175, 215, 255];
let mut r = 0;
while r < 6 {
let mut g = 0;
while g < 6 {
let mut b = 0;
while b < 6 {
let index = 16 + 36 * r + 6 * g + b;
palette[index] = ColorTriplet::new(levels[r], levels[g], levels[b]);
b += 1;
}
g += 1;
}
r += 1;
}
let mut n = 0;
while n < 24 {
let value = 8 + 10 * n as u8;
palette[232 + n] = ColorTriplet::new(value, value, value);
n += 1;
}
palette
}
fn eight_bit_triplet(number: u8) -> ColorTriplet {
EIGHT_BIT_PALETTE[number as usize]
}
fn rgb_to_ls(red: f64, green: f64, blue: f64) -> (f64, f64) {
let max = red.max(green).max(blue);
let min = red.min(green).min(blue);
let lightness = (max + min) / 2.0;
if max == min {
return (lightness, 0.0);
}
let saturation = if lightness <= 0.5 {
(max - min) / (max + min)
} else {
(max - min) / (2.0 - max - min)
};
(lightness, saturation)
}
fn truecolor_to_eight_bit(color: ColorTriplet) -> u8 {
let (red, green, blue) = (
color.red as f64 / 255.0,
color.green as f64 / 255.0,
color.blue as f64 / 255.0,
);
let (lightness, saturation) = rgb_to_ls(red, green, blue);
if saturation < 0.15 {
let gray = (lightness * 25.0).round_ties_even() as i64;
return match gray {
0 => 16,
25 => 231,
other => (231 + other) as u8,
};
}
let axis = |component: u8| -> f64 {
let value = component as f64;
if value < 95.0 {
value / 95.0
} else {
1.0 + (value - 95.0) / 40.0
}
};
let six_red = axis(color.red).round_ties_even();
let six_green = axis(color.green).round_ties_even();
let six_blue = axis(color.blue).round_ties_even();
(16.0 + 36.0 * six_red + 6.0 * six_green + six_blue) as u8
}
fn match_color(palette: &[ColorTriplet], color: ColorTriplet) -> u8 {
let (red1, green1, blue1) = (color.red as i64, color.green as i64, color.blue as i64);
let mut best_index = 0usize;
let mut best_distance = i64::MAX;
for (index, candidate) in palette.iter().enumerate() {
let (red2, green2, blue2) = (
candidate.red as i64,
candidate.green as i64,
candidate.blue as i64,
);
let red_mean = (red1 + red2) / 2;
let red = red1 - red2;
let green = green1 - green2;
let blue = blue1 - blue2;
let distance = (((512 + red_mean) * red * red) >> 8)
+ 4 * green * green
+ (((767 - red_mean) * blue * blue) >> 8);
if distance < best_distance {
best_distance = distance;
best_index = index;
}
}
best_index as u8
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_standard_name() {
let c = Color::parse("red").unwrap();
assert_eq!(c.kind, ColorType::Standard);
assert_eq!(c.number, Some(1));
assert_eq!(c.ansi_codes(true), vec!["31"]);
assert_eq!(c.ansi_codes(false), vec!["41"]);
}
#[test]
fn extended_name_is_eight_bit() {
let c = Color::parse("orange1").unwrap();
assert_eq!(c.kind, ColorType::EightBit);
assert_eq!(c.number, Some(214));
assert_eq!(c.ansi_codes(true), vec!["38", "5", "214"]);
}
#[test]
fn bright_color_uses_high_intensity_sgr() {
let c = Color::parse("bright_red").unwrap();
assert_eq!(c.number, Some(9));
assert_eq!(c.ansi_codes(true), vec!["91"]);
}
#[test]
fn parses_hex_truecolor() {
let c = Color::parse("#ff8800").unwrap();
assert_eq!(c.kind, ColorType::Truecolor);
assert_eq!(c.triplet, Some(ColorTriplet::new(0xff, 0x88, 0x00)));
assert_eq!(c.ansi_codes(true), vec!["38", "2", "255", "136", "0"]);
}
#[test]
fn downgrade_to_standard_matches_upstream() {
let standard = |hex: &str| {
let down = Color::parse(hex).unwrap().downgrade(ColorSystem::Standard);
assert_eq!(down.kind, ColorType::Standard);
down.number
};
assert_eq!(standard("#ff0000"), Some(1));
assert_eq!(standard("#00ff00"), Some(2));
assert_eq!(standard("#0000ff"), Some(4));
assert_eq!(standard("#ffffff"), Some(15));
assert_eq!(standard("#808080"), Some(7));
assert_eq!(standard("#ff8800"), Some(9));
}
#[test]
fn downgrade_to_eight_bit_matches_upstream() {
let eight_bit = |hex: &str| {
let down = Color::parse(hex).unwrap().downgrade(ColorSystem::EightBit);
assert_eq!(down.kind, ColorType::EightBit);
down.number
};
assert_eq!(eight_bit("#ff0000"), Some(196));
assert_eq!(eight_bit("#00ff00"), Some(46));
assert_eq!(eight_bit("#0000ff"), Some(21));
assert_eq!(eight_bit("#ff8800"), Some(208));
assert_eq!(eight_bit("#ffffff"), Some(231));
assert_eq!(eight_bit("#808080"), Some(244));
}
#[test]
fn eight_bit_palette_cube_is_correct() {
assert_eq!(eight_bit_triplet(196), ColorTriplet::new(255, 0, 0));
}
}