use ratatui::style::{Color, Style};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ColorDepth {
Truecolor,
X256,
X16,
}
pub fn detect_color_depth() -> ColorDepth {
if let Ok(force) = std::env::var("CHABEAU_COLOR") {
match force.trim().to_ascii_lowercase().as_str() {
"truecolor" | "24bit" | "24-bit" => return ColorDepth::Truecolor,
"256" | "x256" | "256color" => return ColorDepth::X256,
"16" | "ansi" | "x16" => return ColorDepth::X16,
_ => {}
}
}
if let Ok(colorterm) = std::env::var("COLORTERM") {
let s = colorterm.to_ascii_lowercase();
if s.contains("truecolor") || s.contains("24bit") || s.contains("24-bit") {
return ColorDepth::Truecolor;
}
}
if let Ok(term) = std::env::var("TERM") {
let s = term.to_ascii_lowercase();
if s.contains("256color") {
return ColorDepth::X256;
}
}
ColorDepth::X16
}
pub fn quantize_color(color: Color, depth: ColorDepth) -> Color {
match depth {
ColorDepth::Truecolor => color,
ColorDepth::X256 => quantize_color_256(color),
ColorDepth::X16 => quantize_color_16(color),
}
}
pub fn quantize_style(mut style: Style, depth: ColorDepth) -> Style {
if let Some(fg) = style.fg {
style.fg = Some(quantize_color(fg, depth));
}
if let Some(bg) = style.bg {
style.bg = Some(quantize_color(bg, depth));
}
if let Some(uc) = style.underline_color {
style.underline_color = Some(quantize_color(uc, depth));
}
style
}
pub fn quantize_theme_if_needed(
mut theme: crate::ui::theme::Theme,
depth: ColorDepth,
) -> crate::ui::theme::Theme {
if depth == ColorDepth::Truecolor {
return theme;
}
theme.background_color = quantize_color(theme.background_color, depth);
theme.user_prefix_style = quantize_style(theme.user_prefix_style, depth);
theme.user_text_style = quantize_style(theme.user_text_style, depth);
theme.assistant_text_style = quantize_style(theme.assistant_text_style, depth);
theme.system_text_style = quantize_style(theme.system_text_style, depth);
theme.error_text_style = quantize_style(theme.error_text_style, depth);
theme.app_messages.info.prefix_style =
quantize_style(theme.app_messages.info.prefix_style, depth);
theme.app_messages.info.text_style = quantize_style(theme.app_messages.info.text_style, depth);
theme.app_messages.warning.prefix_style =
quantize_style(theme.app_messages.warning.prefix_style, depth);
theme.app_messages.warning.text_style =
quantize_style(theme.app_messages.warning.text_style, depth);
theme.app_messages.error.prefix_style =
quantize_style(theme.app_messages.error.prefix_style, depth);
theme.app_messages.error.text_style =
quantize_style(theme.app_messages.error.text_style, depth);
theme.title_style = quantize_style(theme.title_style, depth);
theme.streaming_indicator_style = quantize_style(theme.streaming_indicator_style, depth);
theme.selection_highlight_style = quantize_style(theme.selection_highlight_style, depth);
theme.input_border_style = quantize_style(theme.input_border_style, depth);
theme.input_title_style = quantize_style(theme.input_title_style, depth);
theme.input_text_style = quantize_style(theme.input_text_style, depth);
theme.input_cursor_style = quantize_style(theme.input_cursor_style, depth);
theme.input_cursor_line_style = quantize_style(theme.input_cursor_line_style, depth);
theme.input_cursor_color = theme
.input_cursor_color
.map(|color| quantize_color(color, depth));
theme.md_h1 = theme.md_h1.map(|s| quantize_style(s, depth));
theme.md_h2 = theme.md_h2.map(|s| quantize_style(s, depth));
theme.md_h3 = theme.md_h3.map(|s| quantize_style(s, depth));
theme.md_h4 = theme.md_h4.map(|s| quantize_style(s, depth));
theme.md_h5 = theme.md_h5.map(|s| quantize_style(s, depth));
theme.md_h6 = theme.md_h6.map(|s| quantize_style(s, depth));
theme.md_paragraph = theme.md_paragraph.map(|s| quantize_style(s, depth));
theme.md_inline_code = theme.md_inline_code.map(|s| quantize_style(s, depth));
theme.md_link = theme.md_link.map(|s| quantize_style(s, depth));
theme.md_rule = theme.md_rule.map(|s| quantize_style(s, depth));
theme.md_blockquote_text = theme.md_blockquote_text.map(|s| quantize_style(s, depth));
theme.md_list_marker = theme.md_list_marker.map(|s| quantize_style(s, depth));
theme.md_codeblock_text = theme.md_codeblock_text.map(|s| quantize_style(s, depth));
theme.md_codeblock_bg = theme.md_codeblock_bg.map(|c| quantize_color(c, depth));
theme
}
pub fn quantize_theme_for_current_terminal(
theme: crate::ui::theme::Theme,
) -> crate::ui::theme::Theme {
let depth = detect_color_depth();
quantize_theme_if_needed(theme, depth)
}
pub fn color_to_rgb(color: Color) -> Option<(u8, u8, u8)> {
match color {
Color::Rgb(r, g, b) => Some((r, g, b)),
Color::Black => Some((0, 0, 0)),
Color::Red => Some((205, 0, 0)),
Color::Green => Some((0, 205, 0)),
Color::Yellow => Some((205, 205, 0)),
Color::Blue => Some((0, 0, 205)),
Color::Magenta => Some((205, 0, 205)),
Color::Cyan => Some((0, 205, 205)),
Color::Gray => Some((192, 192, 192)),
Color::DarkGray => Some((128, 128, 128)),
Color::LightRed => Some((255, 0, 0)),
Color::LightGreen => Some((0, 255, 0)),
Color::LightYellow => Some((255, 255, 0)),
Color::LightBlue => Some((92, 92, 255)),
Color::LightMagenta => Some((255, 0, 255)),
Color::LightCyan => Some((0, 255, 255)),
Color::White => Some((255, 255, 255)),
Color::Indexed(i) => Some(xterm256_to_rgb(i)),
Color::Reset => None,
}
}
fn quantize_color_256(color: Color) -> Color {
match color {
Color::Rgb(r, g, b) => Color::Indexed(rgb_to_xterm256(r, g, b)),
other => other,
}
}
fn quantize_color_16(color: Color) -> Color {
match color {
Color::Rgb(r, g, b) => nearest_ansi16_from_rgb(r, g, b),
Color::Indexed(i) => {
let (r, g, b) = xterm256_to_rgb(i);
nearest_ansi16_from_rgb(r, g, b)
}
other => other,
}
}
fn nearest_ansi16_from_rgb(r: u8, g: u8, b: u8) -> Color {
const ANSI16: &[(u8, u8, u8, Color); 16] = &[
(0, 0, 0, Color::Black), (205, 0, 0, Color::Red), (0, 205, 0, Color::Green), (205, 205, 0, Color::Yellow), (0, 0, 205, Color::Blue), (205, 0, 205, Color::Magenta), (0, 205, 205, Color::Cyan), (192, 192, 192, Color::Gray), (128, 128, 128, Color::DarkGray), (255, 0, 0, Color::LightRed), (0, 255, 0, Color::LightGreen), (255, 255, 0, Color::LightYellow), (92, 92, 255, Color::LightBlue), (255, 0, 255, Color::LightMagenta), (0, 255, 255, Color::LightCyan), (255, 255, 255, Color::White), ];
let mut best = 0usize;
let mut best_dist = u32::MAX;
for (i, &(rr, gg, bb, _)) in ANSI16.iter().enumerate() {
let dr = rr as i32 - r as i32;
let dg = gg as i32 - g as i32;
let db = bb as i32 - b as i32;
let dist = (dr * dr + dg * dg + db * db) as u32;
if dist < best_dist {
best_dist = dist;
best = i;
}
}
ANSI16[best].3
}
fn rgb_to_xterm256(r: u8, g: u8, b: u8) -> u8 {
let cube_index = rgb_to_xterm_cube_index(r, g, b);
let (cr, cg, cb) = xterm256_to_rgb(cube_index);
let cube_dist = color_dist_sq(r, g, b, cr, cg, cb);
let gray_index = rgb_to_xterm_gray_index(r, g, b);
let (gr, gg, gb) = xterm256_to_rgb(gray_index);
let gray_dist = color_dist_sq(r, g, b, gr, gg, gb);
if gray_dist < cube_dist {
gray_index
} else {
cube_index
}
}
fn rgb_to_xterm_cube_index(r: u8, g: u8, b: u8) -> u8 {
fn map_comp(c: u8) -> u8 {
if c < 48 {
0
} else if c < 114 {
1
} else {
((c - 35) / 40).min(5)
}
}
let ri = map_comp(r);
let gi = map_comp(g);
let bi = map_comp(b);
16 + 36 * ri + 6 * gi + bi
}
fn rgb_to_xterm_gray_index(r: u8, g: u8, b: u8) -> u8 {
let avg = (r as u16 + g as u16 + b as u16) / 3;
let idx = if avg <= 3 {
16
} else {
((avg.saturating_sub(8)) / 10) as u8
};
let idx = idx.min(23);
232 + idx
}
fn color_dist_sq(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 xterm_cube_comp(i: u8) -> u8 {
if i == 0 {
0
} else {
55 + 40 * i
}
}
pub fn xterm256_to_rgb(i: u8) -> (u8, u8, u8) {
match i {
0 => (0, 0, 0),
1 => (205, 0, 0),
2 => (0, 205, 0),
3 => (205, 205, 0),
4 => (0, 0, 205),
5 => (205, 0, 205),
6 => (0, 205, 205),
7 => (229, 229, 229),
8 => (127, 127, 127),
9 => (255, 0, 0),
10 => (0, 255, 0),
11 => (255, 255, 0),
12 => (92, 92, 255),
13 => (255, 0, 255),
14 => (0, 255, 255),
15 => (255, 255, 255),
16..=231 => {
let mut n = i - 16;
let r = n / 36;
n %= 36;
let g = n / 6;
n %= 6;
let b = n;
(xterm_cube_comp(r), xterm_cube_comp(g), xterm_cube_comp(b))
}
232..=255 => {
let v = 8 + 10 * (i - 232);
(v, v, v)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::utils::test_utils::TestEnvVarGuard;
#[test]
fn detects_truecolor_from_env() {
let mut env = TestEnvVarGuard::new();
env.remove_var("CHABEAU_COLOR");
env.set_var("COLORTERM", "truecolor");
assert_eq!(detect_color_depth(), ColorDepth::Truecolor);
}
#[test]
fn detects_256_from_term() {
let mut env = TestEnvVarGuard::new();
env.remove_var("CHABEAU_COLOR");
env.remove_var("COLORTERM");
env.set_var("TERM", "xterm-256color");
assert_eq!(detect_color_depth(), ColorDepth::X256);
}
#[test]
fn quantize_rgb_to_256_index() {
let idx = rgb_to_xterm256(255, 0, 0);
assert!(idx == 9 || (16..=231).contains(&idx));
}
#[test]
fn quantize_rgb_to_ansi16() {
let c = nearest_ansi16_from_rgb(250, 10, 10);
assert!(matches!(c, Color::Red | Color::LightRed));
}
}