use ratatui::style::Color;
use crate::foundation::color::perceptual_distance;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ColorLevel {
TrueColor,
Ansi256,
Monochrome,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ColorCapabilities {
pub level: ColorLevel,
pub color_enabled: bool,
}
impl ColorCapabilities {
pub fn resolve(
no_color: bool,
term: Option<&str>,
colorterm: Option<&str>,
force_color: bool,
) -> Self {
if no_color && !force_color {
return Self::monochrome();
}
if term.is_some_and(|value| value.eq_ignore_ascii_case("dumb")) && !force_color {
return Self::monochrome();
}
let truecolor = colorterm.is_some_and(|value| {
value.eq_ignore_ascii_case("truecolor") || value.eq_ignore_ascii_case("24bit")
});
let ansi256 = term.is_some_and(|value| value.to_ascii_lowercase().contains("256color"));
let level = if truecolor {
ColorLevel::TrueColor
} else if ansi256 || force_color {
ColorLevel::Ansi256
} else {
ColorLevel::Monochrome
};
Self {
level,
color_enabled: level != ColorLevel::Monochrome,
}
}
pub const fn monochrome() -> Self {
Self {
level: ColorLevel::Monochrome,
color_enabled: false,
}
}
pub fn best_color(self, target: (u8, u8, u8)) -> Color {
match self.level {
ColorLevel::TrueColor => Color::Rgb(target.0, target.1, target.2),
ColorLevel::Ansi256 => Color::Indexed(nearest_xterm_index(target)),
ColorLevel::Monochrome => Color::Reset,
}
}
}
fn nearest_xterm_index(target: (u8, u8, u8)) -> u8 {
xterm_fixed_colors()
.min_by(|(_, left), (_, right)| {
perceptual_distance(*left, target)
.partial_cmp(&perceptual_distance(*right, target))
.unwrap_or(std::cmp::Ordering::Equal)
})
.map_or(16, |(index, _)| index)
}
fn xterm_fixed_colors() -> impl Iterator<Item = (u8, (u8, u8, u8))> {
let cube = (0u8..6).flat_map(|r| {
(0u8..6).flat_map(move |g| {
(0u8..6).map(move |b| {
let index = 16 + 36 * r + 6 * g + b;
(
index,
(xterm_component(r), xterm_component(g), xterm_component(b)),
)
})
})
});
let grayscale = (0u8..24).map(|offset| {
let value = 8 + offset * 10;
(232 + offset, (value, value, value))
});
cube.chain(grayscale)
}
const fn xterm_component(value: u8) -> u8 {
if value == 0 {
0
} else {
55 + value * 40
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn truecolor_is_not_quantized() {
let capabilities =
ColorCapabilities::resolve(false, Some("xterm-256color"), Some("truecolor"), false);
assert_eq!(capabilities.level, ColorLevel::TrueColor);
assert_eq!(
capabilities.best_color((12, 34, 56)),
Color::Rgb(12, 34, 56)
);
}
#[test]
fn ansi256_uses_a_stable_index() {
let capabilities = ColorCapabilities::resolve(false, Some("xterm-256color"), None, false);
assert_eq!(capabilities.level, ColorLevel::Ansi256);
assert_eq!(capabilities.best_color((255, 0, 0)), Color::Indexed(196));
assert_eq!(
capabilities.best_color((128, 128, 128)),
Color::Indexed(244)
);
}
#[test]
fn no_color_disables_authored_colors() {
let capabilities =
ColorCapabilities::resolve(true, Some("xterm-256color"), Some("truecolor"), false);
assert_eq!(capabilities, ColorCapabilities::monochrome());
assert_eq!(capabilities.best_color((255, 0, 0)), Color::Reset);
}
#[test]
fn dumb_terminal_is_monochrome_unless_forced() {
assert_eq!(
ColorCapabilities::resolve(false, Some("dumb"), None, false),
ColorCapabilities::monochrome()
);
assert_eq!(
ColorCapabilities::resolve(false, Some("dumb"), None, true).level,
ColorLevel::Ansi256
);
}
}