use crate::renderer::{default_renderer, ColorProfileKind, Renderer};
use palette::color_difference::EuclideanDistance;
use palette::{Clamp, FromColor, Hsv, Lab, Srgb};
pub trait TerminalColor {
fn token(&self, r: &Renderer) -> String;
fn rgba(&self) -> (u32, u32, u32, u32);
fn token_default(&self) -> String
where
Self: Sized,
{
self.token(default_renderer())
}
}
impl TerminalColor for &str {
fn token(&self, r: &Renderer) -> String {
Color::from(*self).token(r)
}
fn rgba(&self) -> (u32, u32, u32, u32) {
Color::from(*self).rgba()
}
}
impl TerminalColor for String {
fn token(&self, r: &Renderer) -> String {
Color::from(self.as_str()).token(r)
}
fn rgba(&self) -> (u32, u32, u32, u32) {
Color::from(self.as_str()).rgba()
}
}
pub(crate) fn rgb_to_ansi256(r: u8, g: u8, b: u8) -> u8 {
fn v2ci(v: u8) -> u8 {
if v < 48 {
0
} else if v < 115 {
1
} else {
((v as i32 - 35) / 40).clamp(0, 5) as u8
}
}
let qr = v2ci(r);
let qg = v2ci(g);
let qb = v2ci(b);
let ci = 36 * qr + 6 * qg + qb;
const I2CV: [u8; 6] = [0, 0x5f, 0x87, 0xaf, 0xd7, 0xff];
let cr = I2CV[qr as usize];
let cg = I2CV[qg as usize];
let cb = I2CV[qb as usize];
let r_f = r as f64;
let g_f = g as f64;
let b_f = b as f64;
let average = (r_f + g_f + b_f) / 3.0;
let gray_idx = if average > 238.0 {
23
} else {
((average - 3.0) / 10.0).round().clamp(0.0, 23.0) as u8
};
let gv = 8 + 10 * gray_idx;
let color_cube_dist = dist2(r, g, b, cr, cg, cb);
let gray_dist = dist2(r, g, b, gv, gv, gv);
if color_cube_dist <= gray_dist {
16 + ci
} else {
232 + gray_idx
}
}
fn dist2(r1: u8, g1: u8, b1: u8, r2: u8, g2: u8, b2: u8) -> u32 {
let dr = r1 as i32 - r2 as i32;
let dg = g1 as i32 - g2 as i32;
let db = b1 as i32 - b2 as i32;
(dr * dr + dg * dg + db * db) as u32
}
fn ansi256_to_rgb_u8(idx: u8) -> (u8, u8, u8) {
match idx {
0..=15 => ANSI16_RGB[idx as usize],
16..=231 => {
let i = idx - 16;
let r = i / 36;
let g = (i % 36) / 6;
let b = i % 6;
(
CUBE_LEVELS[r as usize],
CUBE_LEVELS[g as usize],
CUBE_LEVELS[b as usize],
)
}
232..=255 => {
let v = 8 + 10 * (idx - 232);
(v, v, v)
}
}
}
pub(crate) fn rgb_to_ansi16(r: u8, g: u8, b: u8) -> u8 {
let source_color = Srgb::new(r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0);
let source_lab = Lab::from_color(source_color.into_linear());
ANSI16_RGB
.iter()
.enumerate()
.min_by(|(_, &(r1, g1, b1)), (_, &(r2, g2, b2))| {
let p1_color = Srgb::new(r1 as f32 / 255.0, g1 as f32 / 255.0, b1 as f32 / 255.0);
let p1_lab = Lab::from_color(p1_color.into_linear());
let p2_color = Srgb::new(r2 as f32 / 255.0, g2 as f32 / 255.0, b2 as f32 / 255.0);
let p2_lab = Lab::from_color(p2_color.into_linear());
let d1 = source_lab.distance(p1_lab);
let d2 = source_lab.distance(p2_lab);
d1.total_cmp(&d2)
})
.map(|(i, _)| i as u8)
.unwrap_or(0) }
const CUBE_LEVELS: [u8; 6] = [0, 0x5f, 0x87, 0xaf, 0xd7, 0xff];
const ANSI16_RGB: [(u8, u8, u8); 16] = [
(0x00, 0x00, 0x00), (0x80, 0x00, 0x00), (0x00, 0x80, 0x00), (0x80, 0x80, 0x00), (0x00, 0x00, 0x80), (0x80, 0x00, 0x80), (0x00, 0x80, 0x80), (0xc0, 0xc0, 0xc0), (0x80, 0x80, 0x80), (0xff, 0x00, 0x00), (0x00, 0xff, 0x00), (0xff, 0xff, 0x00), (0x00, 0x00, 0xff), (0xff, 0x00, 0xff), (0x00, 0xff, 0xff), (0xff, 0xff, 0xff), ];
pub(crate) fn resolve_color_token_for_profile(s: &str, profile: ColorProfileKind) -> String {
match profile {
ColorProfileKind::NoColor => String::new(),
ColorProfileKind::TrueColor => {
if let Ok(idx) = s.parse::<u32>() {
let (r, g, b) = ansi256_to_rgb_u8((idx % 256) as u8);
return format!("#{:02x}{:02x}{:02x}", r, g, b);
}
s.to_string()
}
ColorProfileKind::ANSI256 => {
if let Ok(idx) = s.parse::<u32>() {
return ((idx % 256) as u8).to_string();
}
if let Some((r, g, b, _a)) = parse_hex_rgba(s) {
let idx = rgb_to_ansi256(r as u8, g as u8, b as u8);
idx.to_string()
} else {
s.to_string()
}
}
ColorProfileKind::ANSI => {
if let Ok(idx) = s.parse::<u32>() {
if (30..=37).contains(&idx)
|| (90..=97).contains(&idx)
|| (40..=47).contains(&idx)
|| (100..=107).contains(&idx)
{
return idx.to_string();
}
if idx <= 15 {
return idx.to_string();
}
return ((idx % 16) as u8).to_string();
}
if let Some((r, g, b, _a)) = parse_hex_rgba(s) {
let idx = rgb_to_ansi16(r as u8, g as u8, b as u8);
idx.to_string()
} else {
s.to_string()
}
}
}
}
impl Color {
pub fn token_for_renderer(&self, r: &Renderer) -> String {
resolve_color_token_for_profile(&self.0, r.color_profile())
}
}
impl TerminalColor for Color {
fn token(&self, r: &Renderer) -> String {
self.token_for_renderer(r)
}
fn rgba(&self) -> (u32, u32, u32, u32) {
if let Some((r, g, b, a)) = parse_hex_rgba(&self.0) {
(r, g, b, a)
} else if let Ok(idx) = self.0.parse::<u32>() {
let (r, g, b) = ansi256_to_rgb_u8((idx % 256) as u8);
(r as u32, g as u32, b as u32, 0xFFFF)
} else {
(0x0, 0x0, 0x0, 0xFFFF)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hex_short_and_alpha() {
assert_eq!(parse_hex_rgba("#fff"), Some((255, 255, 255, 0xFFFF)));
assert_eq!(parse_hex_rgba("#000"), Some((0, 0, 0, 0xFFFF)));
assert_eq!(
parse_hex_rgba("#abcd"),
Some((0xAA, 0xBB, 0xCC, 0xDD * 257))
);
}
#[test]
fn test_hex_long_and_alpha() {
assert_eq!(parse_hex_rgba("#ff0000"), Some((255, 0, 0, 0xFFFF)));
assert_eq!(
parse_hex_rgba("#11223344"),
Some((0x11, 0x22, 0x33, 0x44 * 257))
);
}
#[test]
fn test_ansi256_values() {
let (r59, g59, b59) = ansi256_to_rgb_u8(59);
println!("ANSI256 59 -> RGB({}, {}, {})", r59, g59, b59);
let (r240, g240, b240) = ansi256_to_rgb_u8(240);
println!("ANSI256 240 -> RGB({}, {}, {})", r240, g240, b240);
let (r238, g238, b238) = ansi256_to_rgb_u8(238);
println!("ANSI256 238 -> RGB({}, {}, {})", r238, g238, b238);
println!("Testing rgb(64,64,64):");
let dist_to_59 = dist2(64, 64, 64, r59, g59, b59);
let dist_to_240 = dist2(64, 64, 64, r240, g240, b240);
let dist_to_238 = dist2(64, 64, 64, r238, g238, b238);
println!("Distance to ANSI256 59: {}", dist_to_59);
println!("Distance to ANSI256 240: {}", dist_to_240);
println!("Distance to ANSI256 238: {}", dist_to_238);
let mut closest_idx = 0;
let mut closest_dist = u32::MAX;
for i in 0..=255 {
let (r, g, b) = ansi256_to_rgb_u8(i);
let dist = dist2(64, 64, 64, r, g, b);
if dist < closest_dist {
closest_dist = dist;
closest_idx = i;
}
}
println!(
"Closest match: ANSI256 {} with distance {}",
closest_idx, closest_dist
);
let (rclosest, gclosest, bclosest) = ansi256_to_rgb_u8(closest_idx);
println!(
"ANSI256 {} -> RGB({}, {}, {})",
closest_idx, rclosest, gclosest, bclosest
);
}
#[test]
fn test_rgb_to_ansi256_debug() {
let r = 64u8;
let g = 64u8;
let b = 64u8;
fn v2ci(v: u8) -> u8 {
if v < 48 {
0
} else if v < 115 {
1
} else {
((v as i32 - 35) / 40).clamp(0, 5) as u8
}
}
let qr = v2ci(r);
let qg = v2ci(g);
let qb = v2ci(b);
let ci = 36 * qr + 6 * qg + qb;
const I2CV: [u8; 6] = [0, 0x5f, 0x87, 0xaf, 0xd7, 0xff];
let cr = I2CV[qr as usize];
let cg = I2CV[qg as usize];
let cb = I2CV[qb as usize];
println!("Color cube: qr={}, qg={}, qb={}, ci={}", qr, qg, qb, ci);
println!("Color cube RGB: ({}, {}, {})", cr, cg, cb);
let average = (r as u32 + g as u32 + b as u32) / 3;
let gray_idx = if average > 238 {
23
} else {
((average as i32 - 3) / 10).clamp(0, 23) as u8
};
let gv = 8 + 10 * gray_idx;
println!(
"Grayscale: average={}, gray_idx={}, gv={}",
average, gray_idx, gv
);
let color_cube_dist = dist2(r, g, b, cr, cg, cb);
let gray_dist = dist2(r, g, b, gv, gv, gv);
println!(
"Distances: color_cube={}, gray={}",
color_cube_dist, gray_dist
);
let result = if color_cube_dist <= gray_dist {
16 + ci
} else {
232 + gray_idx
};
println!("Result: {} (should be closest distance)", result);
}
#[test]
fn test_rgb_to_ansi256_termenv_compatibility() {
assert_eq!(rgb_to_ansi256(255, 0, 0), 196); assert_eq!(rgb_to_ansi256(0, 255, 0), 46); assert_eq!(rgb_to_ansi256(0, 0, 255), 21);
assert_eq!(rgb_to_ansi256(128, 128, 128), 102); assert_eq!(rgb_to_ansi256(64, 64, 64), 238); assert_eq!(rgb_to_ansi256(192, 192, 192), 251);
assert_eq!(rgb_to_ansi256(95, 95, 95), 59); assert_eq!(rgb_to_ansi256(135, 135, 135), 102); }
#[test]
fn test_rgb_to_ansi16_termenv_compatibility() {
assert_eq!(rgb_to_ansi16(255, 0, 0), 9); assert_eq!(rgb_to_ansi16(0, 255, 0), 10); assert_eq!(rgb_to_ansi16(0, 0, 255), 12); assert_eq!(rgb_to_ansi16(128, 0, 0), 1); assert_eq!(rgb_to_ansi16(0, 128, 0), 2); assert_eq!(rgb_to_ansi16(0, 0, 128), 4); assert_eq!(rgb_to_ansi16(192, 192, 192), 7); assert_eq!(rgb_to_ansi16(128, 128, 128), 8); }
#[test]
fn test_color_rgba_from_numeric_index() {
let c = Color("196".to_string());
assert_eq!(c.rgba(), (255, 0, 0, 0xFFFF));
}
#[test]
fn test_adaptive_color_field_names() {
let adaptive = AdaptiveColor {
Light: "#000000",
Dark: "#ffffff",
};
let _token = adaptive.token_default();
let _rgba = adaptive.rgba();
}
#[test]
fn test_complete_color_field_names() {
let complete = CompleteColor {
TrueColor: "#FF0000".to_string(),
ANSI256: "196".to_string(),
ANSI: "10".to_string(),
};
let _token = complete.token_default();
let _rgba = complete.rgba();
}
#[test]
fn test_color_token_profile_conversion() {
use crate::renderer::{ColorProfileKind, Renderer};
let hex_red = Color("#ff0000".to_string());
let ansi_red = Color("9".to_string());
let ansi256_red = Color("196".to_string());
let mut renderer = Renderer::new();
renderer.set_color_profile(ColorProfileKind::TrueColor);
assert_eq!(hex_red.token(&renderer), "#ff0000");
renderer.set_color_profile(ColorProfileKind::ANSI256);
assert_eq!(hex_red.token(&renderer), "196");
renderer.set_color_profile(ColorProfileKind::ANSI);
assert_eq!(hex_red.token(&renderer), "9");
renderer.set_color_profile(ColorProfileKind::ANSI256);
assert_eq!(ansi256_red.token(&renderer), "196");
renderer.set_color_profile(ColorProfileKind::ANSI);
assert_eq!(ansi_red.token(&renderer), "9");
renderer.set_color_profile(ColorProfileKind::TrueColor);
assert_eq!(ansi_red.token(&renderer), "#ff0000"); }
#[test]
fn test_style_rendering_with_color_profiles() {
use crate::{
renderer::{set_default_renderer, ColorProfileKind, Renderer},
Style,
};
let input = "hello";
let test_cases = [
(ColorProfileKind::NoColor, "hello"),
(ColorProfileKind::ANSI, "\x1b[34mhello\x1b[0m"),
(ColorProfileKind::ANSI256, "\x1b[38;5;62mhello\x1b[0m"),
(
ColorProfileKind::TrueColor,
"\x1b[38;2;90;86;224mhello\x1b[0m",
),
];
for (profile, expected) in test_cases {
let mut renderer = Renderer::new();
renderer.set_color_profile(profile);
set_default_renderer(renderer);
let style = Style::new().foreground(Color("#5A56E0".to_string()));
let result = style.render(input);
assert_eq!(
result, expected,
"Profile {:?}: expected '{}', got '{}'",
profile, expected, result
);
}
}
#[test]
fn test_hex_to_color_conversion() {
let test_cases = [
("#FF0000", 0xFF0000),
("#00F", 0x0000FF),
("#6B50FF", 0x6B50FF),
("invalid color", 0x0),
("", 0x0),
];
for (input, expected) in test_cases {
let color = Color(input.to_string());
let (r, g, b, _a) = color.rgba();
let actual = (r << 16) + (g << 8) + b;
assert_eq!(
actual, expected,
"Input '{}': expected 0x{:06X}, got 0x{:06X}",
input, expected, actual
);
}
}
#[test]
fn test_comprehensive_rgba_validation() {
use crate::renderer::{set_default_renderer, ColorProfileKind, Renderer};
let basic_tests = [
(
ColorProfileKind::TrueColor,
true,
Color("#FF0000".to_string()),
0xFF0000,
),
(
ColorProfileKind::TrueColor,
true,
Color("9".to_string()),
0xFF0000,
),
(
ColorProfileKind::TrueColor,
true,
Color("21".to_string()),
0x0000FF,
),
];
for (i, (profile, dark_bg, color, expected)) in basic_tests.iter().enumerate() {
let mut renderer = Renderer::new();
renderer.set_color_profile(*profile);
renderer.set_has_dark_background(*dark_bg);
set_default_renderer(renderer);
let (r, g, b, _a) = color.rgba();
let actual = (r << 16) + (g << 8) + b;
assert_eq!(
actual,
*expected,
"Basic Test #{}: Profile {:?}, Dark: {}, Expected 0x{:06X}, Got 0x{:06X}",
i + 1,
profile,
dark_bg,
expected,
actual
);
}
let adaptive_tests = [
(
true,
AdaptiveColor {
Light: "#0000FF",
Dark: "#FF0000",
},
0xFF0000,
),
(
false,
AdaptiveColor {
Light: "#0000FF",
Dark: "#FF0000",
},
0x0000FF,
),
(
true,
AdaptiveColor {
Light: "21",
Dark: "9",
},
0xFF0000,
),
(
false,
AdaptiveColor {
Light: "21",
Dark: "9",
},
0x0000FF,
),
];
for (i, (dark_bg, color, expected)) in adaptive_tests.iter().enumerate() {
let mut renderer = Renderer::new();
renderer.set_color_profile(ColorProfileKind::TrueColor);
renderer.set_has_dark_background(*dark_bg);
set_default_renderer(renderer);
let (r, g, b, _a) = color.rgba();
let actual = (r << 16) + (g << 8) + b;
assert_eq!(
actual,
*expected,
"Adaptive Test #{}: Dark: {}, Expected 0x{:06X}, Got 0x{:06X}",
i + 1,
dark_bg,
expected,
actual
);
}
let complete_tests = [
(
ColorProfileKind::TrueColor,
CompleteColor {
TrueColor: "#FF0000".to_string(),
ANSI256: "196".to_string(),
ANSI: "10".to_string(),
},
0xFF0000,
),
(
ColorProfileKind::ANSI256,
CompleteColor {
TrueColor: "#FF0000".to_string(),
ANSI256: "196".to_string(),
ANSI: "10".to_string(),
},
0xFF0000,
),
(
ColorProfileKind::ANSI,
CompleteColor {
TrueColor: "#FF0000".to_string(),
ANSI256: "196".to_string(),
ANSI: "10".to_string(),
},
0xFF0000,
),
(
ColorProfileKind::TrueColor,
CompleteColor {
TrueColor: "".to_string(),
ANSI256: "196".to_string(),
ANSI: "10".to_string(),
},
0x000000,
),
];
for (i, (profile, color, expected)) in complete_tests.iter().enumerate() {
let mut renderer = Renderer::new();
renderer.set_color_profile(*profile);
renderer.set_has_dark_background(true);
set_default_renderer(renderer);
let (r, g, b, _a) = color.rgba();
let actual = (r << 16) + (g << 8) + b;
assert_eq!(
actual,
*expected,
"Complete Test #{}: Profile {:?}, Expected 0x{:06X}, Got 0x{:06X}",
i + 1,
profile,
expected,
actual
);
}
}
#[test]
fn test_invalid_color_handling() {
let invalid_colors = [
"",
"invalid",
"#",
"#ZZ",
"#12345", "#1234567890", "not-a-color",
"rgb(255,0,0)", ];
for invalid in invalid_colors {
let color = Color(invalid.to_string());
let (r, g, b, a) = color.rgba();
assert_eq!(
(r, g, b, a),
(0, 0, 0, 0xFFFF),
"Invalid color '{}' should return black with full alpha, got ({}, {}, {}, {})",
invalid,
r,
g,
b,
a
);
}
}
#[test]
fn test_adaptive_color_rgba_combinations() {
let adaptive = AdaptiveColor {
Light: "#FF0000", Dark: "#00FF00", };
let (r, g, b, a) = adaptive.rgba();
let is_red = (r, g, b) == (255, 0, 0);
let is_green = (r, g, b) == (0, 255, 0);
assert!(
is_red || is_green,
"AdaptiveColor RGBA should return either red (255,0,0) or green (0,255,0), got ({},{},{})",
r, g, b
);
assert_eq!(a, 0xFFFF, "Alpha should be full opacity");
}
#[test]
fn test_complete_color_rgba_combinations() {
let complete = CompleteColor {
TrueColor: "#FF0000".to_string(),
ANSI256: "46".to_string(), ANSI: "10".to_string(), };
let (r, g, b, a) = complete.rgba();
assert_eq!(
(r, g, b, a),
(255, 0, 0, 0xFFFF),
"CompleteColor RGBA should use TrueColor value"
);
let empty_complete = CompleteColor {
TrueColor: "".to_string(),
ANSI256: "196".to_string(),
ANSI: "10".to_string(),
};
let (r, g, b, a) = empty_complete.rgba();
assert_eq!(
(r, g, b, a),
(0, 0, 0, 0xFFFF),
"CompleteColor with empty TrueColor should fallback to black"
);
}
#[test]
fn test_color_utility_functions() {
assert_eq!(clamp(5, 0, 10), 5);
assert_eq!(clamp(-1, 0, 10), 0);
assert_eq!(clamp(15, 0, 10), 10);
assert_eq!(parse_hex("#ff0000"), Some((255, 0, 0, 255)));
assert_eq!(parse_hex("#f00"), Some((255, 0, 0, 255)));
assert_eq!(parse_hex("#ff0000aa"), Some((255, 0, 0, 170)));
assert_eq!(parse_hex("invalid"), None);
assert_eq!(parse_hex(""), None);
let black = Color("#000000".to_string());
let white = Color("#ffffff".to_string());
let dark_gray = Color("#404040".to_string());
let light_gray = Color("#c0c0c0".to_string());
assert!(is_dark_color(&black));
assert!(!is_dark_color(&white));
assert!(is_dark_color(&dark_gray));
assert!(!is_dark_color(&light_gray));
}
#[test]
fn test_lighten_darken() {
let red = Color("#800000".to_string());
let lighter = lighten(&red, 0.5);
let (lr, lg, lb, _) = lighter.rgba();
let (or, og, ob, _) = red.rgba();
assert!(lr >= or);
assert!(lg >= og);
assert!(lb >= ob);
let bright_red = Color("#ff0000".to_string());
let darker = darken(&bright_red, 0.3);
let (dr, dg, db, _) = darker.rgba();
let (br, bg, bb, _) = bright_red.rgba();
assert!(dr <= br);
assert!(dg <= bg);
assert!(db <= bb);
}
#[test]
fn test_alpha_adjustment() {
let red = Color("#ff0000".to_string());
let semi_transparent = alpha(&red, 0.5);
assert!(semi_transparent.0.len() == 9); assert!(semi_transparent.0.contains("7f") || semi_transparent.0.contains("80"));
}
#[test]
fn test_complementary_color() {
let red = Color("#ff0000".to_string());
let comp = complementary(&red);
let (cr, cg, cb, _) = comp.rgba();
assert!(cg > 100 || cb > 100);
assert!(cr < cg || cr < cb); }
#[test]
fn test_light_dark_function() {
let red = Color("#ff0000".to_string());
let blue = Color("#0000ff".to_string());
let dark_fn = light_dark(true);
let dark_choice = dark_fn(&red, &blue);
let (dr, dg, db, _) = dark_choice.rgba();
let (br, bg, bb, _) = blue.rgba();
assert_eq!((dr, dg, db), (br, bg, bb));
let light_fn = light_dark(false);
let light_choice = light_fn(&red, &blue);
let (lr, lg, lb, _) = light_choice.rgba();
let (rr, rg, rb, _) = red.rgba();
assert_eq!((lr, lg, lb), (rr, rg, rb)); }
#[test]
fn test_complete_function() {
use crate::renderer::ColorProfileKind;
let ansi = Color("1".to_string());
let ansi256 = Color("124".to_string());
let truecolor = Color("#ff34ac".to_string());
let complete_fn = complete(ColorProfileKind::TrueColor);
let chosen = complete_fn(&ansi, &ansi256, &truecolor);
let (cr, cg, cb, _) = chosen.rgba();
let (tr, tg, tb, _) = truecolor.rgba();
assert_eq!((cr, cg, cb), (tr, tg, tb));
let complete_fn = complete(ColorProfileKind::ANSI);
let chosen = complete_fn(&ansi, &ansi256, &truecolor);
let (cr, cg, cb, _) = chosen.rgba();
let (ar, ag, ab, _) = ansi.rgba();
assert_eq!((cr, cg, cb), (ar, ag, ab));
}
#[test]
fn test_complete_adaptive_color_combinations() {
let complete_adaptive = CompleteAdaptiveColor {
light: CompleteColor {
TrueColor: "#FF0000".to_string(), ANSI256: "196".to_string(),
ANSI: "10".to_string(),
},
dark: CompleteColor {
TrueColor: "#00FF00".to_string(), ANSI256: "46".to_string(),
ANSI: "10".to_string(),
},
};
let (r, g, b, a) = complete_adaptive.rgba();
let is_red = (r, g, b) == (255, 0, 0);
let is_green = (r, g, b) == (0, 255, 0);
assert!(
is_red || is_green,
"CompleteAdaptiveColor RGBA should return either red (255,0,0) or green (0,255,0), got ({},{},{})",
r, g, b
);
assert_eq!(a, 0xFFFF, "Alpha should be full opacity");
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct NoColor;
impl TerminalColor for NoColor {
fn token(&self, _r: &Renderer) -> String {
String::new()
}
fn rgba(&self) -> (u32, u32, u32, u32) {
(0x0, 0x0, 0x0, 0xFFFF)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Color(pub String);
impl From<&str> for Color {
fn from(s: &str) -> Self {
Color(s.to_string())
}
}
impl Color {
pub fn from_rgba(r: u8, g: u8, b: u8, a: u8) -> Self {
if a == 255 {
Color(format!("#{:02x}{:02x}{:02x}", r, g, b))
} else {
Color(format!("#{:02x}{:02x}{:02x}{:02x}", r, g, b, a))
}
}
pub fn from_rgb(r: u8, g: u8, b: u8) -> Self {
Self::from_rgba(r, g, b, 255)
}
pub fn rgba16(&self) -> (u32, u32, u32, u32) {
if let Some((r, g, b, a)) = parse_hex_rgba_8bit(&self.0) {
srgb_to_true_rgba16(Srgb::new(r as u8, g as u8, b as u8), a as u8)
} else if let Ok(idx) = self.0.parse::<u32>() {
let (r, g, b) = ansi256_to_rgb_u8((idx % 256) as u8);
srgb_to_true_rgba16(Srgb::new(r, g, b), 255)
} else {
srgb_to_true_rgba16(Srgb::new(0, 0, 0), 255)
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ANSIColor(pub u32);
impl TerminalColor for ANSIColor {
fn token(&self, _r: &Renderer) -> String {
self.0.to_string()
}
fn rgba(&self) -> (u32, u32, u32, u32) {
let (r, g, b) = ansi256_to_rgb_u8((self.0 % 256) as u8);
(r as u32, g as u32, b as u32, 0xFFFF)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[allow(non_snake_case)]
pub struct AdaptiveColor {
pub Light: &'static str,
pub Dark: &'static str,
}
impl TerminalColor for AdaptiveColor {
fn token(&self, r: &Renderer) -> String {
if r.has_dark_background() {
Color::from(self.Dark).token(r)
} else {
Color::from(self.Light).token(r)
}
}
fn rgba(&self) -> (u32, u32, u32, u32) {
let color_str = if default_renderer().has_dark_background() {
self.Dark
} else {
self.Light
};
Color::from(color_str).rgba()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[allow(non_snake_case)]
pub struct CompleteColor {
pub TrueColor: String,
pub ANSI256: String,
pub ANSI: String,
}
impl TerminalColor for CompleteColor {
fn token(&self, r: &Renderer) -> String {
match r.color_profile() {
ColorProfileKind::TrueColor => self.TrueColor.clone(),
ColorProfileKind::ANSI256 => self.ANSI256.clone(),
ColorProfileKind::ANSI => self.ANSI.clone(),
ColorProfileKind::NoColor => String::new(),
}
}
fn rgba(&self) -> (u32, u32, u32, u32) {
if !self.TrueColor.is_empty() {
Color(self.TrueColor.clone()).rgba()
} else {
(0x0, 0x0, 0x0, 0xFFFF)
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CompleteAdaptiveColor {
pub light: CompleteColor,
pub dark: CompleteColor,
}
pub const TEXT_PRIMARY: AdaptiveColor = AdaptiveColor {
Light: "#262626", Dark: "#FAFAFA", };
pub const TEXT_MUTED: AdaptiveColor = AdaptiveColor {
Light: "#737373", Dark: "#A3A3A3", };
pub const TEXT_SUBTLE: AdaptiveColor = AdaptiveColor {
Light: "#A3A3A3", Dark: "#737373", };
pub const TEXT_HEADER: AdaptiveColor = AdaptiveColor {
Light: "#171717", Dark: "#F5F5F5", };
pub const ACCENT_PRIMARY: AdaptiveColor = AdaptiveColor {
Light: "#7C3AED", Dark: "#A855F7", };
pub const ACCENT_SECONDARY: AdaptiveColor = AdaptiveColor {
Light: "#0891B2", Dark: "#06B6D4", };
pub const INTERACTIVE: AdaptiveColor = AdaptiveColor {
Light: "#DC2626", Dark: "#EF4444", };
pub const STATUS_SUCCESS: AdaptiveColor = AdaptiveColor {
Light: "#059669", Dark: "#10B981", };
pub const STATUS_WARNING: AdaptiveColor = AdaptiveColor {
Light: "#D97706", Dark: "#F59E0B", };
pub const STATUS_ERROR: AdaptiveColor = AdaptiveColor {
Light: "#DC2626", Dark: "#EF4444", };
pub const STATUS_INFO: AdaptiveColor = AdaptiveColor {
Light: "#2563EB", Dark: "#3B82F6", };
pub const SURFACE_SUBTLE: AdaptiveColor = AdaptiveColor {
Light: "#F5F5F5", Dark: "#262626", };
pub const SURFACE_ELEVATED: AdaptiveColor = AdaptiveColor {
Light: "#FFFFFF", Dark: "#171717", };
pub const BORDER_SUBTLE: AdaptiveColor = AdaptiveColor {
Light: "#E5E5E5", Dark: "#404040", };
pub const BORDER_PROMINENT: AdaptiveColor = AdaptiveColor {
Light: "#D4D4D8", Dark: "#525252", };
pub const LIST_ITEM_PRIMARY: AdaptiveColor = TEXT_PRIMARY;
pub const LIST_ITEM_SECONDARY: AdaptiveColor = TEXT_MUTED;
pub const LIST_ENUMERATOR: AdaptiveColor = ACCENT_PRIMARY;
pub const TABLE_HEADER_TEXT: AdaptiveColor = AdaptiveColor {
Light: "#FAFAFA", Dark: "#171717", };
pub const TABLE_HEADER_BG: AdaptiveColor = ACCENT_PRIMARY;
pub const TABLE_ROW_TEXT: AdaptiveColor = TEXT_PRIMARY;
pub const TABLE_ROW_EVEN_BG: AdaptiveColor = AdaptiveColor {
Light: "#F9FAFB", Dark: "#1F1F1F", };
pub const TABLE_BORDER: AdaptiveColor = BORDER_PROMINENT;
impl TerminalColor for CompleteAdaptiveColor {
fn token(&self, r: &Renderer) -> String {
if r.has_dark_background() {
self.dark.token(r)
} else {
self.light.token(r)
}
}
fn rgba(&self) -> (u32, u32, u32, u32) {
if default_renderer().has_dark_background() {
self.dark.rgba()
} else {
self.light.rgba()
}
}
}
pub(crate) fn parse_hex_rgba_8bit(s: &str) -> Option<(u32, u32, u32, u32)> {
let s = s.trim();
let hex = s.strip_prefix('#')?;
let (r, g, b, a_u8) = match hex.len() {
3 => {
let r = u8::from_str_radix(&hex[0..1], 16).ok()?;
let g = u8::from_str_radix(&hex[1..2], 16).ok()?;
let b = u8::from_str_radix(&hex[2..3], 16).ok()?;
let r = (r << 4) | r;
let g = (g << 4) | g;
let b = (b << 4) | b;
(r, g, b, 0xFF)
}
4 => {
let r = u8::from_str_radix(&hex[0..1], 16).ok()?;
let g = u8::from_str_radix(&hex[1..2], 16).ok()?;
let b = u8::from_str_radix(&hex[2..3], 16).ok()?;
let a = u8::from_str_radix(&hex[3..4], 16).ok()?;
let r = (r << 4) | r;
let g = (g << 4) | g;
let b = (b << 4) | b;
let a = (a << 4) | a;
(r, g, b, a)
}
6 => {
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()?;
(r, g, b, 0xFF)
}
8 => {
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()?;
let a = u8::from_str_radix(&hex[6..8], 16).ok()?;
(r, g, b, a)
}
_ => return None,
};
Some((r as u32, g as u32, b as u32, a_u8 as u32))
}
pub(crate) fn parse_hex_rgba(s: &str) -> Option<(u32, u32, u32, u32)> {
let s = s.trim();
let hex = s.strip_prefix('#')?;
let (r, g, b, a_u8) = match hex.len() {
3 => {
let r = u8::from_str_radix(&hex[0..1], 16).ok()?;
let g = u8::from_str_radix(&hex[1..2], 16).ok()?;
let b = u8::from_str_radix(&hex[2..3], 16).ok()?;
let r = (r << 4) | r;
let g = (g << 4) | g;
let b = (b << 4) | b;
(r, g, b, 0xFF)
}
4 => {
let r = u8::from_str_radix(&hex[0..1], 16).ok()?;
let g = u8::from_str_radix(&hex[1..2], 16).ok()?;
let b = u8::from_str_radix(&hex[2..3], 16).ok()?;
let a = u8::from_str_radix(&hex[3..4], 16).ok()?;
let r = (r << 4) | r;
let g = (g << 4) | g;
let b = (b << 4) | b;
let a = (a << 4) | a;
(r, g, b, a)
}
6 => {
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()?;
(r, g, b, 0xFF)
}
8 => {
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()?;
let a = u8::from_str_radix(&hex[6..8], 16).ok()?;
(r, g, b, a)
}
_ => return None,
};
let rgb = Srgb::new(r, g, b);
Some(srgb_to_rgba16(rgb, a_u8))
}
fn srgb_to_rgba16(rgb: Srgb<u8>, a_u8: u8) -> (u32, u32, u32, u32) {
let (r, g, b) = (rgb.red as u32, rgb.green as u32, rgb.blue as u32);
let a = a_u8 as u32;
(r, g, b, a * 257)
}
fn srgb_to_true_rgba16(rgb: Srgb<u8>, a_u8: u8) -> (u32, u32, u32, u32) {
let (r, g, b) = (
rgb.red as u32 * 257,
rgb.green as u32 * 257,
rgb.blue as u32 * 257,
);
let a = a_u8 as u32 * 257;
(r, g, b, a)
}
pub fn clamp<T: PartialOrd>(v: T, low: T, high: T) -> T {
if v < low {
low
} else if v > high {
high
} else {
v
}
}
pub fn alpha<C: TerminalColor>(color: &C, alpha_val: f64) -> Color {
let (r, g, b, _) = color.rgba();
let clamped_alpha = clamp(alpha_val, 0.0, 1.0);
let alpha_u8 = (clamped_alpha * 255.0) as u8;
let r_u8 = r as u8;
let g_u8 = g as u8;
let b_u8 = b as u8;
Color(format!(
"#{:02x}{:02x}{:02x}{:02x}",
r_u8, g_u8, b_u8, alpha_u8
))
}
pub fn lighten<C: TerminalColor>(color: &C, percent: f64) -> Color {
let (r, g, b, _a) = color.rgba();
let add = 255.0 * clamp(percent, 0.0, 1.0);
let r_u8 = r as u8;
let g_u8 = g as u8;
let b_u8 = b as u8;
Color(format!(
"#{:02x}{:02x}{:02x}",
((r_u8 as f64 + add).min(255.0)) as u8,
((g_u8 as f64 + add).min(255.0)) as u8,
((b_u8 as f64 + add).min(255.0)) as u8
))
}
pub fn darken<C: TerminalColor>(color: &C, percent: f64) -> Color {
let (r, g, b, _a) = color.rgba();
let mult = 1.0 - clamp(percent, 0.0, 1.0);
let r_u8 = r as u8;
let g_u8 = g as u8;
let b_u8 = b as u8;
Color(format!(
"#{:02x}{:02x}{:02x}",
(r_u8 as f64 * mult) as u8,
(g_u8 as f64 * mult) as u8,
(b_u8 as f64 * mult) as u8
))
}
pub fn complementary<C: TerminalColor>(color: &C) -> Color {
let (r, g, b, _a) = color.rgba();
let r_u8 = r as u8;
let g_u8 = g as u8;
let b_u8 = b as u8;
let srgb = Srgb::new(
r_u8 as f32 / 255.0,
g_u8 as f32 / 255.0,
b_u8 as f32 / 255.0,
);
let hsv: Hsv = Hsv::from_color(srgb);
let mut new_hue = hsv.hue.into_positive_degrees() + 180.0;
if new_hue >= 360.0 {
new_hue -= 360.0;
} else if new_hue < 0.0 {
new_hue += 360.0;
}
let complementary_hsv = Hsv::new(new_hue, hsv.saturation, hsv.value);
let complementary_srgb: Srgb = Srgb::from_color(complementary_hsv);
let clamped = complementary_srgb.clamp();
Color(format!(
"#{:02x}{:02x}{:02x}",
(clamped.red * 255.0) as u8,
(clamped.green * 255.0) as u8,
(clamped.blue * 255.0) as u8
))
}
pub fn is_dark_color<C: TerminalColor>(color: &C) -> bool {
let (r, g, b, _a) = color.rgba();
let luminance = 0.299 * (r as f64) + 0.587 * (g as f64) + 0.114 * (b as f64);
luminance < 127.5 }
pub type LightDarkFunc = Box<dyn Fn(&dyn TerminalColor, &dyn TerminalColor) -> Color>;
pub fn light_dark(is_dark: bool) -> LightDarkFunc {
Box::new(move |light: &dyn TerminalColor, dark: &dyn TerminalColor| {
if is_dark {
let (r, g, b, _a) = dark.rgba();
Color(format!("#{:02x}{:02x}{:02x}", r as u8, g as u8, b as u8))
} else {
let (r, g, b, _a) = light.rgba();
Color(format!("#{:02x}{:02x}{:02x}", r as u8, g as u8, b as u8))
}
})
}
pub type CompleteFunc =
Box<dyn Fn(&dyn TerminalColor, &dyn TerminalColor, &dyn TerminalColor) -> Color>;
pub fn complete(profile: ColorProfileKind) -> CompleteFunc {
Box::new(
move |ansi: &dyn TerminalColor,
ansi256: &dyn TerminalColor,
truecolor: &dyn TerminalColor| {
let chosen_color = match profile {
ColorProfileKind::ANSI => ansi,
ColorProfileKind::ANSI256 => ansi256,
ColorProfileKind::TrueColor => truecolor,
ColorProfileKind::NoColor => return Color("".to_string()),
};
let (r, g, b, _a) = chosen_color.rgba();
Color(format!("#{:02x}{:02x}{:02x}", r as u8, g as u8, b as u8))
},
)
}
pub fn parse_hex(s: &str) -> Option<(u8, u8, u8, u8)> {
let s = s.trim();
if s.is_empty() || !s.starts_with('#') {
return None;
}
let hex = &s[1..];
match hex.len() {
3 => {
let r = u8::from_str_radix(&hex[0..1], 16).ok()?;
let g = u8::from_str_radix(&hex[1..2], 16).ok()?;
let b = u8::from_str_radix(&hex[2..3], 16).ok()?;
Some(((r << 4) | r, (g << 4) | g, (b << 4) | b, 255))
}
4 => {
let r = u8::from_str_radix(&hex[0..1], 16).ok()?;
let g = u8::from_str_radix(&hex[1..2], 16).ok()?;
let b = u8::from_str_radix(&hex[2..3], 16).ok()?;
let a = u8::from_str_radix(&hex[3..4], 16).ok()?;
Some(((r << 4) | r, (g << 4) | g, (b << 4) | b, (a << 4) | a))
}
6 => {
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((r, g, b, 255))
}
8 => {
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()?;
let a = u8::from_str_radix(&hex[6..8], 16).ok()?;
Some((r, g, b, a))
}
_ => None,
}
}