use std::sync::LazyLock;
use crate::cell::Color;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
pub enum ColorLevel {
None,
Basic,
Ansi256,
#[default]
TrueColor,
}
impl ColorLevel {
pub fn has_color(self) -> bool {
self >= Self::Basic
}
pub fn has_256(self) -> bool {
self >= Self::Ansi256
}
pub fn has_truecolor(self) -> bool {
self >= Self::TrueColor
}
}
static COLOR_LEVEL: LazyLock<ColorLevel> = LazyLock::new(detect_color_level_inner);
pub fn detect_color_level() -> ColorLevel {
*COLOR_LEVEL
}
fn detect_color_level_inner() -> ColorLevel {
if std::env::var_os("NO_COLOR").is_some() {
return ColorLevel::None;
}
let level = match supports_color::on(supports_color::Stream::Stdout) {
None => ColorLevel::TrueColor,
Some(level) => {
if level.has_16m {
ColorLevel::TrueColor
} else if level.has_256 {
ColorLevel::Ansi256
} else if level.has_basic {
ColorLevel::Basic
} else {
ColorLevel::None
}
}
};
if level < ColorLevel::TrueColor && terminal_supports_truecolor() {
return ColorLevel::TrueColor;
}
level
}
fn terminal_supports_truecolor() -> bool {
const TRUECOLOR_TERMINALS: &[&str] = &["WezTerm", "ghostty", "kitty", "rio", "tabby", "vscode"];
if std::env::var_os("ITERM_SESSION_ID").is_some() {
return true;
}
if std::env::var_os("GHOSTTY_RESOURCES_DIR").is_some() {
return true;
}
if let Some(term_program) = std::env::var_os("TERM_PROGRAM")
&& let Some(s) = term_program.to_str()
&& TRUECOLOR_TERMINALS.contains(&s)
{
return true;
}
false
}
pub fn rgb_to_ansi256(r: u8, g: u8, b: u8) -> u8 {
if r == g && g == b {
if r < 8 {
return 16;
}
if r > 248 {
return 231;
}
return ((r as u16 - 8) * 24 / 240) as u8 + 232;
}
let cube = |c: u8| -> u8 {
if c < 48 {
0
} else if c < 115 {
1
} else {
(c as u16 * 5 / 255) as u8
}
};
16 + 36 * cube(r) + 6 * cube(g) + cube(b)
}
pub fn ansi256_to_basic(idx: u8) -> u8 {
match idx {
0..=7 => idx, 8 => 0, 9..=15 => idx, 16 => 0, 17..=21 => 4, 22..=27 => 2, 28..=51 => 2, 52..=87 => 3, 88..=123 => 1, 124..=159 => 1, 160..=195 => 3, 196..=231 => 9, 232..=237 => 0, 238..=243 => 8, 244..=249 => 7, 250..=255 => 15, }
}
pub fn adapt_color(color: Color, level: ColorLevel) -> Color {
match (color, level) {
(_, ColorLevel::None) => Color::Reset,
(Color::Reset, _) => Color::Reset,
(c, ColorLevel::TrueColor) => c,
(Color::Rgb(r, g, b), ColorLevel::Ansi256) => Color::Indexed(rgb_to_ansi256(r, g, b)),
(Color::Rgb(r, g, b), ColorLevel::Basic) => {
Color::Indexed(ansi256_to_basic(rgb_to_ansi256(r, g, b)))
}
(c @ Color::Indexed(_), ColorLevel::Ansi256) => c,
(Color::Indexed(idx), ColorLevel::Basic) => {
if idx < 16 {
Color::Indexed(idx) } else {
Color::Indexed(ansi256_to_basic(idx))
}
}
(c, ColorLevel::Basic | ColorLevel::Ansi256) => c,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_color_level_ordering() {
assert!(ColorLevel::TrueColor > ColorLevel::Ansi256);
assert!(ColorLevel::Ansi256 > ColorLevel::Basic);
assert!(ColorLevel::Basic > ColorLevel::None);
}
#[test]
fn test_color_level_predicates() {
assert!(ColorLevel::TrueColor.has_truecolor());
assert!(ColorLevel::TrueColor.has_256());
assert!(ColorLevel::TrueColor.has_color());
assert!(!ColorLevel::Ansi256.has_truecolor());
assert!(ColorLevel::Ansi256.has_256());
assert!(ColorLevel::Ansi256.has_color());
assert!(!ColorLevel::Basic.has_truecolor());
assert!(!ColorLevel::Basic.has_256());
assert!(ColorLevel::Basic.has_color());
assert!(!ColorLevel::None.has_color());
}
#[test]
fn test_detect_caches_after_first_call() {
let first = detect_color_level();
let second = detect_color_level();
assert_eq!(first, second);
}
#[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_midgray() {
let idx = rgb_to_ansi256(128, 128, 128);
assert!((232..=255).contains(&idx));
}
#[test]
fn test_ansi256_to_basic_corners() {
assert_eq!(ansi256_to_basic(0), 0);
assert_eq!(ansi256_to_basic(196), 9);
assert_eq!(ansi256_to_basic(46), 2);
assert_eq!(ansi256_to_basic(21), 4);
assert_eq!(ansi256_to_basic(255), 15);
}
#[test]
fn test_terminal_supports_truecolor_iterm() {
unsafe {
std::env::set_var("ITERM_SESSION_ID", "test:value");
}
assert!(terminal_supports_truecolor());
unsafe {
std::env::remove_var("ITERM_SESSION_ID");
}
}
#[test]
fn test_terminal_does_not_support_truecolor_unknown() {
unsafe {
std::env::remove_var("ITERM_SESSION_ID");
std::env::remove_var("GHOSTTY_RESOURCES_DIR");
std::env::remove_var("TERM_PROGRAM");
}
assert!(!terminal_supports_truecolor());
}
#[test]
fn test_adapt_color_truecolor_passes_through_rgb() {
let c = Color::Rgb(123, 45, 67);
assert_eq!(adapt_color(c, ColorLevel::TrueColor), c);
}
#[test]
fn test_adapt_color_ansi256_downgrades_rgb() {
let c = Color::Rgb(255, 0, 0);
let adapted = adapt_color(c, ColorLevel::Ansi256);
assert_eq!(adapted, Color::Indexed(196));
}
#[test]
fn test_adapt_color_basic_downgrades_rgb_to_basic() {
let c = Color::Rgb(255, 0, 0);
let adapted = adapt_color(c, ColorLevel::Basic);
match adapted {
Color::Indexed(idx) => assert!(idx < 16, "expected basic color, got {idx}"),
other => panic!("expected Indexed, got {other:?}"),
}
}
#[test]
fn test_adapt_color_none_returns_reset() {
let c = Color::Rgb(123, 45, 67);
assert_eq!(adapt_color(c, ColorLevel::None), Color::Reset);
}
#[test]
fn test_adapt_color_named_stays_named() {
let c = Color::Red;
assert_eq!(adapt_color(c, ColorLevel::Basic), c);
assert_eq!(adapt_color(c, ColorLevel::Ansi256), c);
assert_eq!(adapt_color(c, ColorLevel::TrueColor), c);
}
#[test]
fn test_adapt_color_none_named_to_reset() {
assert_eq!(adapt_color(Color::Red, ColorLevel::None), Color::Reset);
}
#[test]
fn test_adapt_color_reset_always_reset() {
assert_eq!(adapt_color(Color::Reset, ColorLevel::None), Color::Reset);
assert_eq!(
adapt_color(Color::Reset, ColorLevel::TrueColor),
Color::Reset
);
}
#[test]
fn test_adapt_color_ansi256_indexed_stays_at_ansi256() {
let c = Color::Indexed(196);
assert_eq!(adapt_color(c, ColorLevel::Ansi256), c);
}
#[test]
fn test_adapt_color_ansi256_indexed_downgrades_at_basic() {
let c = Color::Indexed(196);
assert_eq!(adapt_color(c, ColorLevel::Basic), Color::Indexed(9));
}
}