use crate::utils::terminal_caps::ColorCapability;
const COLOR_CUBE_LEVELS: [u8; 6] = [0, 95, 135, 175, 215, 255];
#[inline]
const fn color_distance_squared(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);
#[allow(clippy::cast_sign_loss)]
let result = (dr * dr + dg * dg + db * db) as u32;
result
}
#[inline]
#[must_use]
pub fn rgb_to_ansi256(r: u8, g: u8, b: u8) -> u8 {
let cube_r = find_closest_cube_level(r);
let cube_g = find_closest_cube_level(g);
let cube_b = find_closest_cube_level(b);
let cube_index = 16 + 36 * cube_r + 6 * cube_g + cube_b;
let cube_color = (
COLOR_CUBE_LEVELS[cube_r as usize],
COLOR_CUBE_LEVELS[cube_g as usize],
COLOR_CUBE_LEVELS[cube_b as usize],
);
let cube_distance = color_distance_squared(r, g, b, cube_color.0, cube_color.1, cube_color.2);
let gray_avg = (u16::from(r) + u16::from(g) + u16::from(b)) / 3;
#[allow(clippy::cast_possible_truncation)]
let gray_avg_u8 = gray_avg as u8;
let gray_index = find_closest_gray_index(gray_avg_u8);
let gray_value = gray_index_to_rgb(gray_index);
let gray_distance = color_distance_squared(r, g, b, gray_value, gray_value, gray_value);
if gray_distance < cube_distance {
gray_index
} else {
cube_index
}
}
#[inline]
const fn find_closest_cube_level(value: u8) -> u8 {
match value {
0..=47 => 0, 48..=114 => 1, 115..=154 => 2, 155..=194 => 3, 195..=234 => 4, 235..=255 => 5, }
}
#[inline]
const fn find_closest_gray_index(gray: u8) -> u8 {
if gray < 4 {
232
} else if gray > 243 {
255
} else {
let offset = gray.saturating_sub(8);
let index = (offset + 5) / 10; let capped = if index > 23 { 23 } else { index };
232 + capped
}
}
#[inline]
const fn gray_index_to_rgb(index: u8) -> u8 {
8 + 10 * (index.saturating_sub(232))
}
#[inline]
#[must_use]
pub fn rgb_to_ansi16(r: u8, g: u8, b: u8) -> u8 {
let r_high = r > 127;
let g_high = g > 127;
let b_high = b > 127;
let max_channel = r.max(g).max(b);
let is_bright = max_channel > 191;
let base = match (r_high, g_high, b_high) {
(false, false, false) => 0, (true, false, false) => 1, (false, true, false) => 2, (true, true, false) => 3, (false, false, true) => 4, (true, false, true) => 5, (false, true, true) => 6, (true, true, true) => 7, };
if is_bright && base > 0 {
base + 8
} else if is_bright && base == 0 {
8
} else {
base
}
}
#[inline]
#[must_use]
pub fn rgb_to_truecolor_escape(r: u8, g: u8, b: u8) -> String {
format!("\x1b[38;2;{r};{g};{b}m")
}
#[inline]
#[must_use]
pub fn rgb_to_truecolor_bg_escape(r: u8, g: u8, b: u8) -> String {
format!("\x1b[48;2;{r};{g};{b}m")
}
#[inline]
#[must_use]
pub fn ansi256_fg_escape(index: u8) -> String {
format!("\x1b[38;5;{index}m")
}
#[inline]
#[must_use]
pub fn ansi256_bg_escape(index: u8) -> String {
format!("\x1b[48;5;{index}m")
}
#[inline]
#[must_use]
pub fn ansi16_fg_escape(code: u8) -> String {
if code < 8 {
format!("\x1b[3{code}m")
} else {
format!("\x1b[9{}m", code - 8)
}
}
#[inline]
#[must_use]
pub fn ansi16_bg_escape(code: u8) -> String {
if code < 8 {
format!("\x1b[4{code}m")
} else {
format!("\x1b[10{}m", code - 8)
}
}
#[inline]
#[must_use]
pub const fn color_reset() -> &'static str {
"\x1b[0m"
}
#[inline]
#[must_use]
pub fn rgb_to_terminal_color(r: u8, g: u8, b: u8, capability: ColorCapability) -> String {
match capability {
ColorCapability::TrueColor => rgb_to_truecolor_escape(r, g, b),
ColorCapability::Ansi256 => ansi256_fg_escape(rgb_to_ansi256(r, g, b)),
ColorCapability::Ansi16 => ansi16_fg_escape(rgb_to_ansi16(r, g, b)),
ColorCapability::Monochrome => String::new(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rgb_to_ansi256_pure_red() {
assert_eq!(rgb_to_ansi256(255, 0, 0), 196);
}
#[test]
fn test_rgb_to_ansi256_pure_green() {
assert_eq!(rgb_to_ansi256(0, 255, 0), 46);
}
#[test]
fn test_rgb_to_ansi256_pure_blue() {
assert_eq!(rgb_to_ansi256(0, 0, 255), 21);
}
#[test]
fn test_rgb_to_ansi256_black() {
assert_eq!(rgb_to_ansi256(0, 0, 0), 16);
}
#[test]
fn test_rgb_to_ansi256_white() {
assert_eq!(rgb_to_ansi256(255, 255, 255), 231);
}
#[test]
fn test_rgb_to_ansi256_gray() {
let result = rgb_to_ansi256(128, 128, 128);
assert!(
result >= 232,
"Expected grayscale index (232-255), got {result}"
);
}
#[test]
fn test_rgb_to_ansi256_gray_exact_match() {
assert_eq!(rgb_to_ansi256(128, 128, 128), 244);
}
#[test]
fn test_rgb_to_ansi256_dark_gray() {
let result = rgb_to_ansi256(64, 64, 64);
assert!(
result >= 232,
"Expected grayscale index, got {result}"
);
}
#[test]
fn test_rgb_to_ansi256_light_gray() {
let result = rgb_to_ansi256(192, 192, 192);
assert!(
result >= 232,
"Expected grayscale index, got {result}"
);
}
#[test]
fn test_rgb_to_ansi256_yellow() {
assert_eq!(rgb_to_ansi256(255, 255, 0), 226);
}
#[test]
fn test_rgb_to_ansi256_cyan() {
assert_eq!(rgb_to_ansi256(0, 255, 255), 51);
}
#[test]
fn test_rgb_to_ansi256_magenta() {
assert_eq!(rgb_to_ansi256(255, 0, 255), 201);
}
#[test]
fn test_rgb_to_ansi16_bright_red() {
assert_eq!(rgb_to_ansi16(255, 0, 0), 9);
}
#[test]
fn test_rgb_to_ansi16_bright_green() {
assert_eq!(rgb_to_ansi16(0, 255, 0), 10);
}
#[test]
fn test_rgb_to_ansi16_bright_blue() {
assert_eq!(rgb_to_ansi16(0, 0, 255), 12);
}
#[test]
fn test_rgb_to_ansi16_black() {
assert_eq!(rgb_to_ansi16(0, 0, 0), 0);
}
#[test]
fn test_rgb_to_ansi16_white() {
assert_eq!(rgb_to_ansi16(255, 255, 255), 15);
}
#[test]
fn test_rgb_to_ansi16_dark_red() {
assert_eq!(rgb_to_ansi16(128, 0, 0), 1);
}
#[test]
fn test_rgb_to_ansi16_dark_green() {
assert_eq!(rgb_to_ansi16(0, 128, 0), 2);
}
#[test]
fn test_rgb_to_ansi16_dark_blue() {
assert_eq!(rgb_to_ansi16(0, 0, 128), 4);
}
#[test]
fn test_rgb_to_ansi16_bright_yellow() {
assert_eq!(rgb_to_ansi16(255, 255, 0), 11);
}
#[test]
fn test_rgb_to_ansi16_bright_cyan() {
assert_eq!(rgb_to_ansi16(0, 255, 255), 14);
}
#[test]
fn test_rgb_to_ansi16_bright_magenta() {
assert_eq!(rgb_to_ansi16(255, 0, 255), 13);
}
#[test]
fn test_rgb_to_ansi16_dark_gray() {
assert_eq!(rgb_to_ansi16(64, 64, 64), 0);
}
#[test]
fn test_rgb_to_ansi16_light_gray() {
assert_eq!(rgb_to_ansi16(192, 192, 192), 15);
}
#[test]
fn test_rgb_to_ansi16_mid_gray() {
assert_eq!(rgb_to_ansi16(128, 128, 128), 7);
}
#[test]
fn test_rgb_to_truecolor_escape_format() {
let escape = rgb_to_truecolor_escape(255, 128, 0);
assert_eq!(escape, "\x1b[38;2;255;128;0m");
}
#[test]
fn test_rgb_to_truecolor_escape_black() {
let escape = rgb_to_truecolor_escape(0, 0, 0);
assert_eq!(escape, "\x1b[38;2;0;0;0m");
}
#[test]
fn test_rgb_to_truecolor_escape_white() {
let escape = rgb_to_truecolor_escape(255, 255, 255);
assert_eq!(escape, "\x1b[38;2;255;255;255m");
}
#[test]
fn test_rgb_to_truecolor_bg_escape_format() {
let escape = rgb_to_truecolor_bg_escape(255, 128, 0);
assert_eq!(escape, "\x1b[48;2;255;128;0m");
}
#[test]
fn test_truecolor_fg_vs_bg_different() {
let fg = rgb_to_truecolor_escape(255, 0, 0);
let bg = rgb_to_truecolor_bg_escape(255, 0, 0);
assert_ne!(fg, bg);
assert!(fg.contains("38;2;"));
assert!(bg.contains("48;2;"));
}
#[test]
fn test_rgb_to_terminal_color_truecolor() {
let escape = rgb_to_terminal_color(255, 128, 0, ColorCapability::TrueColor);
assert_eq!(escape, "\x1b[38;2;255;128;0m");
}
#[test]
fn test_rgb_to_terminal_color_ansi256() {
let escape = rgb_to_terminal_color(255, 128, 0, ColorCapability::Ansi256);
assert!(escape.starts_with("\x1b[38;5;"));
}
#[test]
fn test_rgb_to_terminal_color_ansi16() {
let escape = rgb_to_terminal_color(255, 0, 0, ColorCapability::Ansi16);
assert_eq!(escape, "\x1b[91m");
}
#[test]
fn test_rgb_to_terminal_color_monochrome() {
let escape = rgb_to_terminal_color(255, 128, 0, ColorCapability::Monochrome);
assert_eq!(escape, "");
}
#[test]
fn test_ansi256_bg_escape_format() {
let escape = ansi256_bg_escape(196);
assert_eq!(escape, "\x1b[48;5;196m");
}
#[test]
fn test_ansi16_bg_escape_dark() {
let escape = ansi16_bg_escape(1);
assert_eq!(escape, "\x1b[41m");
}
#[test]
fn test_ansi16_bg_escape_bright() {
let escape = ansi16_bg_escape(9);
assert_eq!(escape, "\x1b[101m");
}
#[test]
fn test_fg_vs_bg_escape_different() {
let fg256 = ansi256_fg_escape(196);
let bg256 = ansi256_bg_escape(196);
assert_ne!(fg256, bg256);
let fg16 = ansi16_fg_escape(9);
let bg16 = ansi16_bg_escape(9);
assert_ne!(fg16, bg16);
}
#[test]
fn test_color_reset_format() {
assert_eq!(color_reset(), "\x1b[0m");
}
#[test]
fn test_color_reset_is_static() {
let reset1 = color_reset();
let reset2 = color_reset();
assert_eq!(reset1.as_ptr(), reset2.as_ptr());
}
#[test]
fn test_edge_case_rgb_000() {
let ansi256 = rgb_to_ansi256(0, 0, 0);
let ansi16 = rgb_to_ansi16(0, 0, 0);
assert_eq!(ansi256, 16); assert_eq!(ansi16, 0); }
#[test]
fn test_edge_case_rgb_255() {
let ansi256 = rgb_to_ansi256(255, 255, 255);
let ansi16 = rgb_to_ansi16(255, 255, 255);
assert_eq!(ansi256, 231); assert_eq!(ansi16, 15); }
#[test]
fn test_edge_case_rgb_128() {
let ansi256 = rgb_to_ansi256(128, 128, 128);
let ansi16 = rgb_to_ansi16(128, 128, 128);
assert_eq!(ansi256, 244); assert_eq!(ansi16, 7); }
#[test]
fn test_primary_colors_accuracy() {
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(0, 255, 255), 51);
assert_eq!(rgb_to_ansi256(255, 0, 255), 201);
assert_eq!(rgb_to_ansi256(255, 255, 0), 226);
}
#[test]
fn test_ansi16_all_base_colors() {
assert_eq!(rgb_to_ansi16(0, 0, 0), 0); assert_eq!(rgb_to_ansi16(255, 0, 0), 9); assert_eq!(rgb_to_ansi16(0, 255, 0), 10); assert_eq!(rgb_to_ansi16(255, 255, 0), 11); assert_eq!(rgb_to_ansi16(0, 0, 255), 12); assert_eq!(rgb_to_ansi16(255, 0, 255), 13); assert_eq!(rgb_to_ansi16(0, 255, 255), 14); assert_eq!(rgb_to_ansi16(255, 255, 255), 15);
assert_eq!(rgb_to_ansi16(191, 0, 0), 1); assert_eq!(rgb_to_ansi16(0, 191, 0), 2); assert_eq!(rgb_to_ansi16(0, 0, 191), 4); }
#[test]
fn test_ansi16_escape_all_colors() {
for code in 0..8 {
let escape = ansi16_fg_escape(code);
assert!(escape.starts_with("\x1b[3"));
assert!(escape.ends_with("m"));
}
for code in 8..16 {
let escape = ansi16_fg_escape(code);
assert!(escape.starts_with("\x1b[9"));
assert!(escape.ends_with("m"));
}
}
#[test]
fn test_grayscale_ramp_boundaries() {
let dark = rgb_to_ansi256(8, 8, 8);
assert!(dark >= 232, "Very dark gray should be in grayscale ramp");
let light = rgb_to_ansi256(238, 238, 238);
assert!(light >= 232, "Very light gray should be in grayscale ramp");
}
#[test]
fn test_color_cube_boundaries() {
let below_95 = rgb_to_ansi256(94, 0, 0);
let at_95 = rgb_to_ansi256(95, 0, 0);
assert!(below_95 >= 16);
assert!(at_95 >= 16);
}
#[test]
fn test_conversion_deterministic() {
for _ in 0..100 {
assert_eq!(rgb_to_ansi256(128, 64, 192), rgb_to_ansi256(128, 64, 192));
assert_eq!(rgb_to_ansi16(128, 64, 192), rgb_to_ansi16(128, 64, 192));
}
}
}