use ratatui::style::Color as RColor;
use std::sync::OnceLock;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ColorLevel {
TrueColor,
Ansi256,
Ansi16,
NoColor,
}
static GLOBAL: OnceLock<ColorLevel> = OnceLock::new();
pub fn init_from_config(mode: &str) {
let level = parse_mode(mode);
let _ = GLOBAL.set(level);
}
pub fn current() -> ColorLevel {
GLOBAL.get().copied().unwrap_or(ColorLevel::TrueColor)
}
fn parse_mode(s: &str) -> ColorLevel {
match s.trim().to_lowercase().as_str() {
"truecolor" | "rgb" | "24bit" | "16m" => ColorLevel::TrueColor,
"ansi256" | "256" => ColorLevel::Ansi256,
"ansi16" | "16" | "basic" => ColorLevel::Ansi16,
"none" | "off" | "no" | "false" => ColorLevel::NoColor,
_ => detect(),
}
}
fn detect() -> ColorLevel {
use supports_color::Stream;
match supports_color::on_cached(Stream::Stderr) {
Some(level) if level.has_16m => ColorLevel::TrueColor,
Some(level) if level.has_256 => ColorLevel::Ansi256,
Some(level) if level.has_basic => ColorLevel::Ansi16,
Some(_) => ColorLevel::NoColor,
None => ColorLevel::NoColor,
}
}
pub fn degrade(color: RColor) -> RColor {
let level = current();
if let RColor::Reset = color {
return RColor::Reset;
}
if matches!(level, ColorLevel::NoColor) {
return RColor::Reset;
}
match (level, color) {
(ColorLevel::TrueColor, c) => c,
(ColorLevel::Ansi256, RColor::Rgb(r, g, b)) => {
RColor::Indexed(ansi_colours::ansi256_from_rgb((r, g, b)))
}
(ColorLevel::Ansi16, RColor::Rgb(r, g, b)) => nearest_ansi16(r, g, b),
(_, c) => c,
}
}
const ANSI16_PALETTE: &[(RColor, (u8, u8, u8))] = &[
(RColor::Black, (0, 0, 0)),
(RColor::Red, (170, 0, 0)),
(RColor::Green, (0, 170, 0)),
(RColor::Yellow, (170, 85, 0)),
(RColor::Blue, (0, 0, 170)),
(RColor::Magenta, (170, 0, 170)),
(RColor::Cyan, (0, 170, 170)),
(RColor::Gray, (170, 170, 170)),
(RColor::DarkGray, (85, 85, 85)),
(RColor::LightRed, (255, 85, 85)),
(RColor::LightGreen, (85, 255, 85)),
(RColor::LightYellow, (255, 255, 85)),
(RColor::LightBlue, (85, 85, 255)),
(RColor::LightMagenta, (255, 85, 255)),
(RColor::LightCyan, (85, 255, 255)),
(RColor::White, (255, 255, 255)),
];
fn nearest_ansi16(r: u8, g: u8, b: u8) -> RColor {
ANSI16_PALETTE
.iter()
.min_by_key(|(_, (cr, cg, cb))| {
let dr = (r as i32 - *cr as i32).pow(2) as u32;
let dg = (g as i32 - *cg as i32).pow(2) as u32;
let db = (b as i32 - *cb as i32).pow(2) as u32;
299 * dr + 587 * dg + 114 * db
})
.map(|(c, _)| *c)
.unwrap_or(RColor::Reset)
}
pub fn apply_fg(text: &str, color: RColor) -> colored::ColoredString {
use colored::Colorize;
let degraded = degrade(color);
match degraded {
RColor::Reset => text.normal(),
RColor::Rgb(r, g, b) => text.truecolor(r, g, b),
RColor::Indexed(i) => {
let (r, g, b) = ansi_colours::rgb_from_ansi256(i);
text.truecolor(r, g, b)
}
named => match ratatui_to_colored(named) {
Some(c) => text.color(c),
None => text.normal(),
},
}
}
fn ratatui_to_colored(c: RColor) -> Option<colored::Color> {
use colored::Color as CColor;
Some(match c {
RColor::Black => CColor::Black,
RColor::Red => CColor::Red,
RColor::Green => CColor::Green,
RColor::Yellow => CColor::Yellow,
RColor::Blue => CColor::Blue,
RColor::Magenta => CColor::Magenta,
RColor::Cyan => CColor::Cyan,
RColor::Gray => CColor::White,
RColor::DarkGray => CColor::BrightBlack,
RColor::LightRed => CColor::BrightRed,
RColor::LightGreen => CColor::BrightGreen,
RColor::LightYellow => CColor::BrightYellow,
RColor::LightBlue => CColor::BrightBlue,
RColor::LightMagenta => CColor::BrightMagenta,
RColor::LightCyan => CColor::BrightCyan,
RColor::White => CColor::BrightWhite,
_ => return None,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_mode_explicit() {
assert!(matches!(parse_mode("truecolor"), ColorLevel::TrueColor));
assert!(matches!(parse_mode("ansi16"), ColorLevel::Ansi16));
assert!(matches!(parse_mode("16"), ColorLevel::Ansi16));
assert!(matches!(parse_mode("256"), ColorLevel::Ansi256));
assert!(matches!(parse_mode("none"), ColorLevel::NoColor));
}
#[test]
fn nearest_ansi16_palette_self_map() {
for (color, (r, g, b)) in ANSI16_PALETTE {
assert_eq!(
nearest_ansi16(*r, *g, *b),
*color,
"palette entry {:?} should map to itself",
color
);
}
}
#[test]
fn nearest_ansi16_white_high() {
assert!(matches!(nearest_ansi16(250, 250, 250), RColor::White));
}
#[test]
fn nearest_ansi16_pure_black() {
assert!(matches!(nearest_ansi16(0, 0, 0), RColor::Black));
}
}