use compact_str::CompactString;
use crate::errors::{Result, RichError};
use crate::markup::py_repr_str;
#[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 {
self.hex_name().into()
}
pub(crate) fn hex_name(&self) -> CompactString {
const DIGITS: &[u8; 16] = b"0123456789abcdef";
let mut hex = *b"#000000";
for (i, value) in [self.red, self.green, self.blue].into_iter().enumerate() {
hex[1 + 2 * i] = DIGITS[usize::from(value >> 4)];
hex[2 + 2 * i] = DIGITS[usize::from(value & 0xf)];
}
CompactString::from_utf8(hex).unwrap_or_default()
}
pub fn rgb(&self) -> String {
format!("rgb({},{},{})", self.red, self.green, self.blue)
}
pub fn normalized(&self) -> (f64, f64, f64) {
(
self.red as f64 / 255.0,
self.green as f64 / 255.0,
self.blue as f64 / 255.0,
)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Color {
pub name: crate::CompactString,
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: CompactString::const_new("default"),
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.into(),
kind,
number: Some(number),
triplet: None,
}
}
pub fn from_ansi(number: u8) -> Self {
Color {
name: compact_str::format_compact!("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_name(),
kind: ColorType::Truecolor,
number: None,
triplet: Some(triplet),
}
}
pub fn parse(color: &str) -> Result<Self> {
let original = color;
let color = color.to_lowercase();
let color = color.trim();
let invalid =
|| RichError::ColorParse(format!("{} is not a valid color", py_repr_str(original)));
if color == "default" {
return Ok(Color::default_color());
}
if let Some(number) = crate::color_names::ansi_color_number(color) {
return Ok(Color::named(color, number));
}
if let Some(hex) = color.strip_prefix('#') {
if hex.len() != 6 || !hex.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'f')) {
return Err(invalid());
}
return Ok(Color {
name: color.into(),
kind: ColorType::Truecolor,
number: None,
triplet: Some(parse_rgb_hex(hex)),
});
}
if let Some(digits) = color
.strip_prefix("color(")
.and_then(|s| s.strip_suffix(')'))
{
if digits.is_empty() || digits.len() > 3 || !digits.bytes().all(|b| b.is_ascii_digit())
{
return Err(invalid());
}
let number: u16 = digits.parse().map_err(|_| invalid())?;
if number > 255 {
return Err(RichError::ColorParse(format!(
"color number must be <= 255 in {}",
py_repr_str(color)
)));
}
return Ok(Color {
name: color.into(),
kind: if number < 16 {
ColorType::Standard
} else {
ColorType::EightBit
},
number: Some(number as u8),
triplet: None,
});
}
if let Some(inner) = color.strip_prefix("rgb(").and_then(|s| s.strip_suffix(')')) {
if inner.is_empty()
|| !inner
.chars()
.all(|c| c.is_ascii_digit() || c == ',' || c.is_whitespace())
{
return Err(invalid());
}
let components: Vec<&str> = inner.split(',').collect();
if components.len() != 3 {
return Err(RichError::ColorParse(format!(
"expected three components in {}",
py_repr_str(original)
)));
}
let mut values = [0u8; 3];
for (slot, component) in values.iter_mut().zip(&components) {
let digits = component.trim();
if digits.is_empty() || digits.contains(char::is_whitespace) {
return Err(RichError::ColorParse(format!(
"invalid literal for int() with base 10: {}",
py_repr_str(component)
)));
}
match digits.parse::<u16>() {
Ok(value) if value <= 255 => *slot = value as u8,
_ => {
return Err(RichError::ColorParse(format!(
"color components must be <= 255 in {}",
py_repr_str(original)
)));
}
}
}
return Ok(Color {
name: color.into(),
kind: ColorType::Truecolor,
number: None,
triplet: Some(ColorTriplet::new(values[0], values[1], values[2])),
});
}
Err(invalid())
}
pub fn from_triplet(triplet: ColorTriplet) -> Self {
Color {
name: triplet.hex_name(),
kind: ColorType::Truecolor,
number: None,
triplet: Some(triplet),
}
}
pub fn system(&self) -> ColorSystem {
match self.kind {
ColorType::Default | ColorType::Standard => ColorSystem::Standard,
ColorType::EightBit => ColorSystem::EightBit,
ColorType::Truecolor => ColorSystem::Truecolor,
ColorType::Windows => ColorSystem::Windows,
}
}
pub fn is_system_defined(&self) -> bool {
!matches!(
self.system(),
ColorSystem::EightBit | ColorSystem::Truecolor
)
}
pub fn get_truecolor_for(
&self,
theme: Option<&crate::terminal_theme::TerminalTheme>,
foreground: bool,
) -> ColorTriplet {
let theme = theme.unwrap_or(&crate::terminal_theme::DEFAULT_TERMINAL_THEME);
theme.resolve(self, foreground)
}
pub fn rich_text(&self) -> crate::text::Text {
let kind = match self.kind {
ColorType::Default => "default",
ColorType::Standard => "standard",
ColorType::EightBit => "eight_bit",
ColorType::Truecolor => "truecolor",
ColorType::Windows => "windows",
};
let mut text =
crate::text::Text::new(format!("<color {} ({kind})", py_repr_str(&self.name)));
let dot = crate::style::Style::from_color(Some(self.clone()), None);
text.append("⬤", Some(dot.into()));
text.append(" >", None);
text
}
pub fn get_truecolor(&self) -> Option<ColorTriplet> {
match self.kind {
ColorType::Default => None,
ColorType::Truecolor => self.triplet,
ColorType::Standard => self.number.map(|n| ANSI_BASE_PALETTE[n as usize]),
ColorType::Windows => self.number.map(|n| WINDOWS_PALETTE[(n & 15) 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 {
let same_system = matches!(
(self.kind, system),
(ColorType::Standard, ColorSystem::Standard)
| (ColorType::EightBit, ColorSystem::EightBit)
| (ColorType::Truecolor, ColorSystem::Truecolor)
| (ColorType::Windows, ColorSystem::Windows)
);
if self.kind == ColorType::Default || same_system {
return self.clone();
}
let is_truecolor = self.kind == ColorType::Truecolor;
let source_triplet = || {
if is_truecolor {
self.triplet
} else {
self.number.map(eight_bit_triplet)
}
};
match system {
ColorSystem::EightBit if is_truecolor => {
let Some(triplet) = self.triplet else {
return self.clone();
};
Color {
name: self.name.clone(),
kind: ColorType::EightBit,
number: Some(truecolor_to_eight_bit(triplet)),
triplet: None,
}
}
ColorSystem::Standard => {
let Some(triplet) = source_triplet() else {
return self.clone();
};
Color {
name: self.name.clone(),
kind: ColorType::Standard,
number: Some(match_color(&STANDARD_PALETTE, triplet)),
triplet: None,
}
}
ColorSystem::Windows => {
if !is_truecolor && let Some(number) = self.number.filter(|&number| number < 16) {
return Color {
name: self.name.clone(),
kind: ColorType::Windows,
number: Some(number),
triplet: None,
};
}
let Some(triplet) = source_triplet() else {
return self.clone();
};
Color {
name: self.name.clone(),
kind: ColorType::Windows,
number: Some(match_color(&WINDOWS_PALETTE, triplet)),
triplet: None,
}
}
_ => self.clone(),
}
}
}
pub fn parse_rgb_hex(hex_color: &str) -> ColorTriplet {
assert_eq!(hex_color.len(), 6, "must be 6 characters");
let channel = |range: std::ops::Range<usize>| {
u8::from_str_radix(&hex_color[range], 16).expect("hex digits")
};
ColorTriplet::new(channel(0..2), channel(2..4), channel(4..6))
}
pub fn blend_rgb(color1: ColorTriplet, color2: ColorTriplet, cross_fade: f64) -> ColorTriplet {
let mix = |a: u8, b: u8| (a as f64 + (b as f64 - a as f64) * cross_fade) as u8;
ColorTriplet::new(
mix(color1.red, color2.red),
mix(color1.green, color2.green),
mix(color1.blue, color2.blue),
)
}
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),
];
pub const WINDOWS_PALETTE: [ColorTriplet; 16] = [
ColorTriplet::new(12, 12, 12),
ColorTriplet::new(197, 15, 31),
ColorTriplet::new(19, 161, 14),
ColorTriplet::new(193, 156, 0),
ColorTriplet::new(0, 55, 218),
ColorTriplet::new(136, 23, 152),
ColorTriplet::new(58, 150, 221),
ColorTriplet::new(204, 204, 204),
ColorTriplet::new(118, 118, 118),
ColorTriplet::new(231, 72, 86),
ColorTriplet::new(22, 198, 12),
ColorTriplet::new(249, 241, 165),
ColorTriplet::new(59, 120, 255),
ColorTriplet::new(180, 0, 158),
ColorTriplet::new(97, 214, 214),
ColorTriplet::new(242, 242, 242),
];
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
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Palette {
colors: Vec<ColorTriplet>,
}
impl Palette {
pub fn new(colors: Vec<ColorTriplet>) -> Self {
Palette { colors }
}
pub fn get(&self, number: usize) -> Option<ColorTriplet> {
self.colors.get(number).copied()
}
pub fn colors(&self) -> &[ColorTriplet] {
&self.colors
}
pub fn len(&self) -> usize {
self.colors.len()
}
pub fn is_empty(&self) -> bool {
self.colors.is_empty()
}
pub fn match_color(&self, color: ColorTriplet) -> usize {
assert!(!self.colors.is_empty(), "match on an empty palette");
match_color(&self.colors, color) as usize
}
}
impl std::ops::Index<usize> for Palette {
type Output = ColorTriplet;
fn index(&self, number: usize) -> &ColorTriplet {
&self.colors[number]
}
}
impl crate::protocol::Renderable for Palette {
fn rich_render(
&self,
console: &crate::console::Console,
options: &crate::console::ConsoleOptions,
) -> Vec<crate::segment::Segment> {
self.table().rich_render(console, options)
}
fn measure(
&self,
console: &crate::console::Console,
options: &crate::console::ConsoleOptions,
) -> crate::measure::Measurement {
self.table().measure(console, options)
}
}
impl Palette {
fn table(&self) -> crate::table::Table {
use crate::table::{Cell, Table};
use crate::text::Text;
let mut table = Table::new()
.title("Palette")
.caption(format!("{} colors", self.colors.len()))
.highlight(true)
.caption_justify(crate::console::Justify::Right);
table.add_column("index");
table.add_column("RGB");
table.add_column("Color");
for (index, color) in self.colors.iter().enumerate() {
let swatch = crate::style::Style::from_color(None, Some(Color::from_triplet(*color)));
table.add_row_cells(vec![
Cell::Markup(index.to_string()),
Cell::Markup(format!("({}, {}, {})", color.red, color.green, color.blue)),
Cell::Text(Text::styled(" ".repeat(16), swatch)),
]);
}
table
}
}
impl crate::protocol::Renderable for Color {
fn rich_render(
&self,
console: &crate::console::Console,
options: &crate::console::ConsoleOptions,
) -> Vec<crate::segment::Segment> {
self.rich_text().rich_render(console, options)
}
fn measure(
&self,
console: &crate::console::Console,
options: &crate::console::ConsoleOptions,
) -> crate::measure::Measurement {
self.rich_text().measure(console, options)
}
fn printed_text(&self) -> Option<crate::text::Text> {
Some(self.rich_text())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hex_matches_format() {
for value in 0..=255u8 {
let other = value.wrapping_mul(97).wrapping_add(13);
for (r, g, b) in [(value, other, 0), (other, value, 255), (0, other, value)] {
assert_eq!(
ColorTriplet::new(r, g, b).hex(),
format!("#{r:02x}{g:02x}{b:02x}")
);
}
}
}
#[test]
fn windows_downgrade_uses_the_windows_palette() {
let grey = Color::parse("#808080")
.unwrap()
.downgrade(ColorSystem::Windows);
assert_eq!((grey.kind, grey.number), (ColorType::Windows, Some(8)));
let low = Color::parse("color(9)")
.unwrap()
.downgrade(ColorSystem::Windows);
assert_eq!((low.kind, low.number), (ColorType::Windows, Some(9)));
assert!(Color::parse("grey").is_err());
assert!(Color::parse("gray").is_err());
}
#[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));
}
}