use crossterm::style::Color as CrosstermColor;
use presentar_core::Color;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ColorMode {
#[default]
TrueColor,
Color256,
Color16,
Mono,
}
impl ColorMode {
#[must_use]
pub fn detect() -> Self {
Self::detect_with_env(std::env::var("COLORTERM").ok(), std::env::var("TERM").ok())
}
#[must_use]
#[allow(clippy::needless_pass_by_value)]
pub fn detect_with_env(colorterm: Option<String>, term: Option<String>) -> Self {
if let Some(ref ct) = colorterm {
if ct == "truecolor" || ct == "24bit" {
return Self::TrueColor;
}
}
match term.as_deref() {
Some(t) if t.contains("256color") => Self::Color256,
Some(t) if t.contains("color") || t.contains("xterm") => Self::Color16,
Some("dumb") | None => Self::Mono,
_ => Self::Color16,
}
}
#[must_use]
pub fn to_crossterm(&self, color: Color) -> CrosstermColor {
debug_assert!(color.r >= 0.0 && color.r <= 1.0, "r must be in 0.0-1.0");
debug_assert!(color.g >= 0.0 && color.g <= 1.0, "g must be in 0.0-1.0");
debug_assert!(color.b >= 0.0 && color.b <= 1.0, "b must be in 0.0-1.0");
debug_assert!(color.a >= 0.0 && color.a <= 1.0, "a must be in 0.0-1.0");
if color.a == 0.0 {
return CrosstermColor::Reset;
}
let r = (color.r * 255.0).round() as u8;
let g = (color.g * 255.0).round() as u8;
let b = (color.b * 255.0).round() as u8;
match self {
Self::TrueColor => CrosstermColor::Rgb { r, g, b },
Self::Color256 => CrosstermColor::AnsiValue(Self::rgb_to_256(r, g, b)),
Self::Color16 => Self::rgb_to_16(r, g, b),
Self::Mono => CrosstermColor::White,
}
}
fn rgb_to_256(r: u8, g: u8, b: u8) -> u8 {
if r == g && g == b {
if r < 8 {
return 16; }
if r > 248 {
return 231; }
return 232 + ((r - 8) / 10).min(23);
}
let r_idx = (u16::from(r) * 5 / 255) as u8;
let g_idx = (u16::from(g) * 5 / 255) as u8;
let b_idx = (u16::from(b) * 5 / 255) as u8;
16 + 36 * r_idx + 6 * g_idx + b_idx
}
fn rgb_to_16(r: u8, g: u8, b: u8) -> CrosstermColor {
let luminance = (u32::from(r) * 299 + u32::from(g) * 587 + u32::from(b) * 114) / 1000;
let bright = luminance > 127;
let max = r.max(g).max(b);
let threshold = max / 2;
let has_r = r > threshold;
let has_g = g > threshold;
let has_b = b > threshold;
match (has_r, has_g, has_b, bright) {
(false, false, false, false) => CrosstermColor::Black,
(false, false, false, true) => CrosstermColor::DarkGrey,
(true, false, false, false) => CrosstermColor::DarkRed,
(true, false, false, true) => CrosstermColor::Red,
(false, true, false, false) => CrosstermColor::DarkGreen,
(false, true, false, true) => CrosstermColor::Green,
(true, true, false, false) => CrosstermColor::DarkYellow,
(true, true, false, true) => CrosstermColor::Yellow,
(false, false, true, false) => CrosstermColor::DarkBlue,
(false, false, true, true) => CrosstermColor::Blue,
(true, false, true, false) => CrosstermColor::DarkMagenta,
(true, false, true, true) => CrosstermColor::Magenta,
(false, true, true, false) => CrosstermColor::DarkCyan,
(false, true, true, true) => CrosstermColor::Cyan,
(true, true, true, false) => CrosstermColor::Grey,
(true, true, true, true) => CrosstermColor::White,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_color_mode_default() {
assert_eq!(ColorMode::default(), ColorMode::TrueColor);
}
#[test]
fn test_truecolor_conversion() {
let mode = ColorMode::TrueColor;
let color = Color::new(0.5, 0.25, 0.75, 1.0);
let result = mode.to_crossterm(color);
assert_eq!(
result,
CrosstermColor::Rgb {
r: 128,
g: 64,
b: 191
}
);
}
#[test]
fn test_256_grayscale() {
assert_eq!(ColorMode::rgb_to_256(0, 0, 0), 16);
assert_eq!(ColorMode::rgb_to_256(255, 255, 255), 231);
let mid = ColorMode::rgb_to_256(128, 128, 128);
assert!(mid >= 232);
}
#[test]
fn test_256_grayscale_near_black() {
assert_eq!(ColorMode::rgb_to_256(5, 5, 5), 16);
}
#[test]
fn test_256_grayscale_ramp() {
let gray50 = ColorMode::rgb_to_256(50, 50, 50);
assert!(gray50 >= 232);
let gray100 = ColorMode::rgb_to_256(100, 100, 100);
assert!(gray100 >= 232);
let gray200 = ColorMode::rgb_to_256(200, 200, 200);
assert!(gray200 >= 232);
}
#[test]
fn test_256_color_cube() {
let red = ColorMode::rgb_to_256(255, 0, 0);
assert!(red >= 16 && red <= 231);
let green = ColorMode::rgb_to_256(0, 255, 0);
assert!(green >= 16 && green <= 231);
let blue = ColorMode::rgb_to_256(0, 0, 255);
assert!(blue >= 16 && blue <= 231);
let magenta = ColorMode::rgb_to_256(255, 0, 255);
assert!(magenta >= 16 && magenta <= 231);
}
#[test]
fn test_16_color_mapping() {
assert_eq!(ColorMode::rgb_to_16(0, 0, 0), CrosstermColor::Black);
assert_eq!(ColorMode::rgb_to_16(255, 255, 255), CrosstermColor::White);
assert!(matches!(
ColorMode::rgb_to_16(255, 0, 0),
CrosstermColor::Red | CrosstermColor::DarkRed
));
}
#[test]
fn test_16_color_green() {
let result = ColorMode::rgb_to_16(0, 255, 0);
assert!(matches!(
result,
CrosstermColor::Green | CrosstermColor::DarkGreen
));
}
#[test]
fn test_16_color_blue() {
let result = ColorMode::rgb_to_16(0, 0, 255);
assert!(matches!(
result,
CrosstermColor::Blue | CrosstermColor::DarkBlue
));
}
#[test]
fn test_16_color_yellow() {
let result = ColorMode::rgb_to_16(255, 255, 0);
assert!(matches!(
result,
CrosstermColor::Yellow | CrosstermColor::DarkYellow
));
}
#[test]
fn test_16_color_cyan() {
let result = ColorMode::rgb_to_16(0, 255, 255);
assert!(matches!(
result,
CrosstermColor::Cyan | CrosstermColor::DarkCyan
));
}
#[test]
fn test_16_color_magenta() {
let result = ColorMode::rgb_to_16(255, 0, 255);
assert!(matches!(
result,
CrosstermColor::Magenta | CrosstermColor::DarkMagenta
));
}
#[test]
fn test_16_color_dark_gray() {
let result = ColorMode::rgb_to_16(50, 50, 50);
assert!(matches!(
result,
CrosstermColor::Black | CrosstermColor::DarkGrey | CrosstermColor::Grey
));
}
#[test]
fn test_16_color_gray() {
let result = ColorMode::rgb_to_16(192, 192, 192);
assert!(matches!(
result,
CrosstermColor::Grey | CrosstermColor::White
));
}
#[test]
fn test_mono_conversion() {
let mode = ColorMode::Mono;
assert_eq!(mode.to_crossterm(Color::RED), CrosstermColor::White);
assert_eq!(mode.to_crossterm(Color::BLUE), CrosstermColor::White);
assert_eq!(mode.to_crossterm(Color::GREEN), CrosstermColor::White);
}
#[test]
fn test_transparent_returns_reset() {
for mode in [
ColorMode::TrueColor,
ColorMode::Color256,
ColorMode::Color16,
ColorMode::Mono,
] {
assert_eq!(
mode.to_crossterm(Color::TRANSPARENT),
CrosstermColor::Reset,
"Mode {:?} should return Reset for TRANSPARENT",
mode
);
let zero_alpha = Color::new(1.0, 0.5, 0.25, 0.0);
assert_eq!(
mode.to_crossterm(zero_alpha),
CrosstermColor::Reset,
"Mode {:?} should return Reset for any color with alpha=0",
mode
);
}
}
#[test]
fn test_256_conversion() {
let mode = ColorMode::Color256;
let result = mode.to_crossterm(Color::new(0.5, 0.25, 0.75, 1.0));
assert!(matches!(result, CrosstermColor::AnsiValue(_)));
}
#[test]
fn test_16_conversion() {
let mode = ColorMode::Color16;
let result = mode.to_crossterm(Color::RED);
assert!(matches!(
result,
CrosstermColor::Red | CrosstermColor::DarkRed
));
}
#[test]
fn test_color_mode_eq() {
assert_eq!(ColorMode::TrueColor, ColorMode::TrueColor);
assert_eq!(ColorMode::Color256, ColorMode::Color256);
assert_eq!(ColorMode::Color16, ColorMode::Color16);
assert_eq!(ColorMode::Mono, ColorMode::Mono);
assert_ne!(ColorMode::TrueColor, ColorMode::Color256);
}
#[test]
fn test_color_mode_clone() {
let mode = ColorMode::Color256;
let cloned = mode;
assert_eq!(mode, cloned);
}
#[test]
fn test_color_mode_debug() {
let mode = ColorMode::TrueColor;
assert!(format!("{:?}", mode).contains("TrueColor"));
}
#[test]
fn test_16_color_dim_colors() {
let dark_red = ColorMode::rgb_to_16(128, 0, 0);
assert!(matches!(
dark_red,
CrosstermColor::Red | CrosstermColor::DarkRed
));
let dark_green = ColorMode::rgb_to_16(0, 100, 0);
assert!(matches!(
dark_green,
CrosstermColor::Green | CrosstermColor::DarkGreen
));
let dark_blue = ColorMode::rgb_to_16(0, 0, 128);
assert!(matches!(
dark_blue,
CrosstermColor::Blue | CrosstermColor::DarkBlue
));
}
#[test]
fn test_256_near_white() {
let near_white = ColorMode::rgb_to_256(250, 250, 250);
assert_eq!(near_white, 231);
}
#[test]
fn test_256_mixed_colors() {
let orange = ColorMode::rgb_to_256(255, 128, 0);
assert!(orange >= 16 && orange <= 231);
let purple = ColorMode::rgb_to_256(128, 0, 255);
assert!(purple >= 16 && purple <= 231);
let teal = ColorMode::rgb_to_256(0, 128, 128);
assert!(teal >= 16 && teal <= 231);
}
#[test]
fn test_color_mode_detect() {
let mode = ColorMode::detect();
assert!(matches!(
mode,
ColorMode::TrueColor | ColorMode::Color256 | ColorMode::Color16 | ColorMode::Mono
));
}
#[test]
fn test_16_color_all_dark_variants() {
let dark_red = ColorMode::rgb_to_16(180, 20, 20);
assert!(matches!(
dark_red,
CrosstermColor::DarkRed | CrosstermColor::Red
));
let dark_green = ColorMode::rgb_to_16(20, 150, 20);
assert!(matches!(
dark_green,
CrosstermColor::DarkGreen | CrosstermColor::Green
));
let dark_blue = ColorMode::rgb_to_16(20, 20, 180);
assert!(matches!(
dark_blue,
CrosstermColor::DarkBlue | CrosstermColor::Blue
));
let dark_yellow = ColorMode::rgb_to_16(150, 150, 20);
assert!(matches!(
dark_yellow,
CrosstermColor::DarkYellow | CrosstermColor::Yellow
));
let dark_cyan = ColorMode::rgb_to_16(20, 150, 150);
assert!(matches!(
dark_cyan,
CrosstermColor::DarkCyan | CrosstermColor::Cyan
));
let dark_magenta = ColorMode::rgb_to_16(150, 20, 150);
assert!(matches!(
dark_magenta,
CrosstermColor::DarkMagenta | CrosstermColor::Magenta
));
}
#[test]
fn test_16_color_bright_variants() {
let bright_red = ColorMode::rgb_to_16(255, 50, 50);
assert!(!matches!(bright_red, CrosstermColor::Black));
let bright_green = ColorMode::rgb_to_16(50, 255, 50);
assert!(!matches!(bright_green, CrosstermColor::Black));
let bright_blue = ColorMode::rgb_to_16(50, 50, 255);
assert!(!matches!(bright_blue, CrosstermColor::Black));
}
#[test]
fn test_16_color_dark_grey_explicit() {
let dark_grey = ColorMode::rgb_to_16(80, 80, 80);
assert!(matches!(
dark_grey,
CrosstermColor::DarkGrey | CrosstermColor::Black | CrosstermColor::Grey
));
}
#[test]
fn test_to_crossterm_edge_values() {
let mode = ColorMode::TrueColor;
let black = mode.to_crossterm(Color::new(0.0, 0.0, 0.0, 1.0));
assert_eq!(black, CrosstermColor::Rgb { r: 0, g: 0, b: 0 });
let white = mode.to_crossterm(Color::new(1.0, 1.0, 1.0, 1.0));
assert_eq!(
white,
CrosstermColor::Rgb {
r: 255,
g: 255,
b: 255
}
);
}
#[test]
fn test_256_grayscale_boundary() {
assert_eq!(ColorMode::rgb_to_256(7, 7, 7), 16); assert_eq!(ColorMode::rgb_to_256(8, 8, 8), 232); assert_eq!(ColorMode::rgb_to_256(249, 249, 249), 231); }
#[test]
fn test_256_color_cube_corners() {
let c000 = ColorMode::rgb_to_256(1, 1, 2); assert!(c000 >= 16 && c000 <= 231);
let c555 = ColorMode::rgb_to_256(254, 254, 255); assert!(c555 >= 16 && c555 <= 231);
}
#[test]
fn test_color16_to_crossterm() {
let mode = ColorMode::Color16;
let red = mode.to_crossterm(Color::RED);
assert!(matches!(red, CrosstermColor::Red | CrosstermColor::DarkRed));
let green = mode.to_crossterm(Color::GREEN);
assert!(matches!(
green,
CrosstermColor::Green | CrosstermColor::DarkGreen
));
let blue = mode.to_crossterm(Color::BLUE);
assert!(matches!(
blue,
CrosstermColor::Blue | CrosstermColor::DarkBlue
));
let black = mode.to_crossterm(Color::BLACK);
assert!(matches!(black, CrosstermColor::Black));
let white = mode.to_crossterm(Color::WHITE);
assert!(matches!(white, CrosstermColor::White));
}
#[test]
fn test_color256_grayscale_through_mode() {
let mode = ColorMode::Color256;
let black = mode.to_crossterm(Color::BLACK);
assert!(matches!(black, CrosstermColor::AnsiValue(16)));
let gray = mode.to_crossterm(Color::new(0.5, 0.5, 0.5, 1.0));
if let CrosstermColor::AnsiValue(v) = gray {
assert!(v >= 232 || (v >= 16 && v <= 231));
}
}
#[test]
fn test_rgb_to_256_extensive() {
for r in [0, 51, 102, 153, 204, 255] {
for g in [0, 51, 102, 153, 204, 255] {
for b in [0, 51, 102, 153, 204, 255] {
let result = ColorMode::rgb_to_256(r, g, b);
assert!(result <= 255);
}
}
}
}
#[test]
fn test_rgb_to_16_extensive() {
for r in [0, 64, 128, 192, 255] {
for g in [0, 64, 128, 192, 255] {
for b in [0, 64, 128, 192, 255] {
let result = ColorMode::rgb_to_16(r, g, b);
let _ = format!("{:?}", result);
}
}
}
}
#[test]
fn test_to_crossterm_all_modes() {
let test_colors = [
Color::BLACK,
Color::WHITE,
Color::RED,
Color::GREEN,
Color::BLUE,
Color::new(0.5, 0.5, 0.5, 1.0),
Color::new(0.25, 0.75, 0.5, 1.0),
];
for mode in [
ColorMode::TrueColor,
ColorMode::Color256,
ColorMode::Color16,
ColorMode::Mono,
] {
for color in &test_colors {
let result = mode.to_crossterm(*color);
let _ = format!("{:?}", result);
}
}
}
#[test]
fn test_grayscale_ramp_comprehensive() {
for gray in 0..=255 {
let result = ColorMode::rgb_to_256(gray, gray, gray);
assert!(result == 16 || result == 231 || (result >= 232 && result <= 255));
}
}
#[test]
fn test_detect_returns_valid() {
let mode = ColorMode::detect();
match mode {
ColorMode::TrueColor => assert!(true),
ColorMode::Color256 => assert!(true),
ColorMode::Color16 => assert!(true),
ColorMode::Mono => assert!(true),
}
}
#[test]
fn test_color_mode_copy() {
let mode1 = ColorMode::TrueColor;
let mode2 = mode1; assert_eq!(mode1, mode2);
}
#[test]
fn test_detect_colorterm_truecolor() {
let mode = ColorMode::detect_with_env(Some("truecolor".to_string()), None);
assert_eq!(mode, ColorMode::TrueColor);
}
#[test]
fn test_detect_colorterm_24bit() {
let mode = ColorMode::detect_with_env(Some("24bit".to_string()), None);
assert_eq!(mode, ColorMode::TrueColor);
}
#[test]
fn test_detect_colorterm_other_falls_through() {
let mode = ColorMode::detect_with_env(
Some("other".to_string()),
Some("xterm-256color".to_string()),
);
assert_eq!(mode, ColorMode::Color256);
}
#[test]
fn test_detect_term_256color() {
let mode = ColorMode::detect_with_env(None, Some("xterm-256color".to_string()));
assert_eq!(mode, ColorMode::Color256);
let mode2 = ColorMode::detect_with_env(None, Some("screen-256color".to_string()));
assert_eq!(mode2, ColorMode::Color256);
}
#[test]
fn test_detect_term_xterm() {
let mode = ColorMode::detect_with_env(None, Some("xterm".to_string()));
assert_eq!(mode, ColorMode::Color16);
}
#[test]
fn test_detect_term_color() {
let mode = ColorMode::detect_with_env(None, Some("linux-color".to_string()));
assert_eq!(mode, ColorMode::Color16);
}
#[test]
fn test_detect_term_dumb() {
let mode = ColorMode::detect_with_env(None, Some("dumb".to_string()));
assert_eq!(mode, ColorMode::Mono);
}
#[test]
fn test_detect_term_none() {
let mode = ColorMode::detect_with_env(None, None);
assert_eq!(mode, ColorMode::Mono);
}
#[test]
fn test_detect_term_unknown() {
let mode = ColorMode::detect_with_env(None, Some("vt100".to_string()));
assert_eq!(mode, ColorMode::Color16);
}
#[test]
fn test_detect_colorterm_priority() {
let mode =
ColorMode::detect_with_env(Some("truecolor".to_string()), Some("dumb".to_string()));
assert_eq!(mode, ColorMode::TrueColor);
}
#[test]
fn test_detect_colorterm_empty_string() {
let mode = ColorMode::detect_with_env(Some("".to_string()), None);
assert_eq!(mode, ColorMode::Mono);
}
#[test]
fn test_detect_term_various() {
assert_eq!(
ColorMode::detect_with_env(None, Some("rxvt-256color".to_string())),
ColorMode::Color256
);
assert_eq!(
ColorMode::detect_with_env(None, Some("screen".to_string())),
ColorMode::Color16
);
assert_eq!(
ColorMode::detect_with_env(None, Some("ansi".to_string())),
ColorMode::Color16
);
}
#[test]
fn test_detect_colorterm_with_term_fallback() {
let mode =
ColorMode::detect_with_env(Some("something".to_string()), Some("xterm".to_string()));
assert_eq!(mode, ColorMode::Color16);
}
#[test]
fn test_to_crossterm_comprehensive() {
let colors = [
Color::new(0.0, 0.0, 0.0, 1.0),
Color::new(1.0, 1.0, 1.0, 1.0),
Color::new(1.0, 0.0, 0.0, 1.0),
Color::new(0.0, 1.0, 0.0, 1.0),
Color::new(0.0, 0.0, 1.0, 1.0),
Color::new(0.5, 0.5, 0.5, 1.0),
Color::new(0.25, 0.5, 0.75, 1.0),
Color::new(0.1, 0.2, 0.3, 1.0),
];
for color in colors {
for mode in [
ColorMode::TrueColor,
ColorMode::Color256,
ColorMode::Color16,
ColorMode::Mono,
] {
let _ = mode.to_crossterm(color);
}
}
}
#[test]
fn test_rgb_to_256_boundary_values() {
for v in [0, 51, 102, 153, 204, 255] {
let _ = ColorMode::rgb_to_256(v, 0, 0);
let _ = ColorMode::rgb_to_256(0, v, 0);
let _ = ColorMode::rgb_to_256(0, 0, v);
}
}
#[test]
fn test_rgb_to_16_all_combinations() {
let test_cases = [
(0, 0, 0), (50, 50, 50), (128, 0, 0), (255, 0, 0), (0, 128, 0), (0, 255, 0), (128, 128, 0), (255, 255, 0), (0, 0, 128), (0, 0, 255), (128, 0, 128), (255, 0, 255), (0, 128, 128), (0, 255, 255), (192, 192, 192), (255, 255, 255), ];
for (r, g, b) in test_cases {
let _ = ColorMode::rgb_to_16(r, g, b);
}
}
#[test]
fn test_color_lerp_boundary() {
let c1 = Color::RED;
let c2 = Color::BLUE;
let _ = c1.lerp(&c2, 0.0);
let _ = c1.lerp(&c2, 1.0);
let _ = c1.lerp(&c2, 0.5);
}
#[test]
fn test_detect_original_still_works() {
let _ = ColorMode::detect();
}
}