use std::sync::OnceLock;
use tracing::{debug, info, instrument};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ColorCapability {
Monochrome,
Ansi16,
Ansi256,
TrueColor,
}
impl ColorCapability {
#[inline]
#[must_use]
pub const fn supports_color(&self) -> bool {
!matches!(self, Self::Monochrome)
}
#[inline]
#[must_use]
pub const fn supports_truecolor(&self) -> bool {
matches!(self, Self::TrueColor)
}
#[inline]
#[must_use]
pub fn detect() -> Self {
detect_color_capability()
}
}
impl Default for ColorCapability {
fn default() -> Self {
Self::Ansi256
}
}
impl std::fmt::Display for ColorCapability {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Monochrome => write!(f, "Monochrome (no color)"),
Self::Ansi16 => write!(f, "ANSI 16 colors"),
Self::Ansi256 => write!(f, "ANSI 256 colors"),
Self::TrueColor => write!(f, "True Color (24-bit RGB)"),
}
}
}
static DETECTED_CAPABILITY: OnceLock<ColorCapability> = OnceLock::new();
#[instrument(level = "debug")]
pub fn detect_color_capability() -> ColorCapability {
*DETECTED_CAPABILITY.get_or_init(|| {
let capability = detect_from_environment();
info!(capability = ?capability, "Terminal color capability detected");
capability
})
}
fn detect_from_environment() -> ColorCapability {
use std::env;
if let Ok(colorterm) = env::var("COLORTERM") {
debug!(colorterm = %colorterm, "Checking COLORTERM environment variable");
let colorterm_lower = colorterm.to_lowercase();
if colorterm_lower.contains("truecolor") || colorterm_lower.contains("24bit") {
debug!("COLORTERM indicates TrueColor support");
return ColorCapability::TrueColor;
}
if colorterm_lower.contains("256") {
debug!("COLORTERM indicates 256-color support");
return ColorCapability::Ansi256;
}
}
if let Ok(term) = env::var("TERM") {
debug!(term = %term, "Checking TERM environment variable");
let term_lower = term.to_lowercase();
if term_lower.contains("256color") {
debug!("TERM indicates 256-color support");
return ColorCapability::Ansi256;
}
if term_lower.contains("color") {
debug!("TERM indicates basic color support");
return ColorCapability::Ansi16;
}
if term_lower.contains("xterm")
|| term_lower.contains("screen")
|| term_lower.contains("tmux")
|| term_lower.contains("vt100")
|| term_lower.contains("linux")
|| term_lower.contains("ansi")
{
debug!("TERM implies at least basic color support");
return ColorCapability::Ansi16;
}
}
debug!("Using default fallback: Ansi256");
ColorCapability::Ansi256
}
#[must_use]
pub fn detect_with_env(colorterm: Option<&str>, term: Option<&str>) -> ColorCapability {
if let Some(colorterm_val) = colorterm {
let colorterm_lower = colorterm_val.to_lowercase();
if colorterm_lower.contains("truecolor") || colorterm_lower.contains("24bit") {
return ColorCapability::TrueColor;
}
if colorterm_lower.contains("256") {
return ColorCapability::Ansi256;
}
}
if let Some(term_val) = term {
let term_lower = term_val.to_lowercase();
if term_lower.contains("256color") {
return ColorCapability::Ansi256;
}
if term_lower.contains("color") {
return ColorCapability::Ansi16;
}
if term_lower.contains("xterm")
|| term_lower.contains("screen")
|| term_lower.contains("tmux")
|| term_lower.contains("vt100")
|| term_lower.contains("linux")
|| term_lower.contains("ansi")
{
return ColorCapability::Ansi16;
}
}
ColorCapability::Ansi256
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_color_capability_debug() {
let cap = ColorCapability::TrueColor;
let debug_str = format!("{:?}", cap);
assert!(debug_str.contains("TrueColor"));
}
#[test]
fn test_color_capability_clone() {
let cap = ColorCapability::Ansi256;
#[allow(clippy::clone_on_copy)]
let cloned = cap.clone();
assert_eq!(cap, cloned);
}
#[test]
fn test_color_capability_copy() {
let cap = ColorCapability::Ansi16;
let copied = cap; assert_eq!(cap, copied); }
#[test]
fn test_color_capability_eq() {
assert_eq!(ColorCapability::Monochrome, ColorCapability::Monochrome);
assert_eq!(ColorCapability::Ansi16, ColorCapability::Ansi16);
assert_eq!(ColorCapability::Ansi256, ColorCapability::Ansi256);
assert_eq!(ColorCapability::TrueColor, ColorCapability::TrueColor);
assert_ne!(ColorCapability::Monochrome, ColorCapability::TrueColor);
}
#[test]
fn test_color_capability_hash() {
use std::collections::HashSet;
let mut set = HashSet::new();
set.insert(ColorCapability::TrueColor);
set.insert(ColorCapability::Ansi256);
assert!(set.contains(&ColorCapability::TrueColor));
assert!(set.contains(&ColorCapability::Ansi256));
assert!(!set.contains(&ColorCapability::Monochrome));
}
#[test]
fn test_supports_color_monochrome_returns_false() {
assert!(!ColorCapability::Monochrome.supports_color());
}
#[test]
fn test_supports_color_ansi16_returns_true() {
assert!(ColorCapability::Ansi16.supports_color());
}
#[test]
fn test_supports_color_ansi256_returns_true() {
assert!(ColorCapability::Ansi256.supports_color());
}
#[test]
fn test_supports_color_truecolor_returns_true() {
assert!(ColorCapability::TrueColor.supports_color());
}
#[test]
fn test_supports_truecolor_monochrome_returns_false() {
assert!(!ColorCapability::Monochrome.supports_truecolor());
}
#[test]
fn test_supports_truecolor_ansi16_returns_false() {
assert!(!ColorCapability::Ansi16.supports_truecolor());
}
#[test]
fn test_supports_truecolor_ansi256_returns_false() {
assert!(!ColorCapability::Ansi256.supports_truecolor());
}
#[test]
fn test_supports_truecolor_only_for_truecolor() {
assert!(ColorCapability::TrueColor.supports_truecolor());
}
#[test]
fn test_detect_alias_returns_same_as_function() {
let via_detect = ColorCapability::detect();
let via_function = detect_color_capability();
assert_eq!(via_detect, via_function);
}
#[test]
fn test_detect_colorterm_truecolor() {
let result = detect_with_env(Some("truecolor"), None);
assert_eq!(result, ColorCapability::TrueColor);
}
#[test]
fn test_detect_colorterm_truecolor_uppercase() {
let result = detect_with_env(Some("TRUECOLOR"), None);
assert_eq!(result, ColorCapability::TrueColor);
}
#[test]
fn test_detect_colorterm_24bit() {
let result = detect_with_env(Some("24bit"), None);
assert_eq!(result, ColorCapability::TrueColor);
}
#[test]
fn test_detect_colorterm_24bit_uppercase() {
let result = detect_with_env(Some("24BIT"), None);
assert_eq!(result, ColorCapability::TrueColor);
}
#[test]
fn test_detect_term_256color() {
let result = detect_with_env(None, Some("xterm-256color"));
assert_eq!(result, ColorCapability::Ansi256);
}
#[test]
fn test_detect_term_256color_uppercase() {
let result = detect_with_env(None, Some("XTERM-256COLOR"));
assert_eq!(result, ColorCapability::Ansi256);
}
#[test]
fn test_detect_term_color() {
let result = detect_with_env(None, Some("xterm-color"));
assert_eq!(result, ColorCapability::Ansi16);
}
#[test]
fn test_detect_term_plain_xterm() {
let result = detect_with_env(None, Some("xterm"));
assert_eq!(result, ColorCapability::Ansi16);
}
#[test]
fn test_detect_term_screen() {
let result = detect_with_env(None, Some("screen"));
assert_eq!(result, ColorCapability::Ansi16);
}
#[test]
fn test_detect_term_tmux() {
let result = detect_with_env(None, Some("tmux-256color"));
assert_eq!(result, ColorCapability::Ansi256);
}
#[test]
fn test_detect_term_linux() {
let result = detect_with_env(None, Some("linux"));
assert_eq!(result, ColorCapability::Ansi16);
}
#[test]
fn test_fallback_when_no_env_vars() {
let result = detect_with_env(None, None);
assert_eq!(result, ColorCapability::Ansi256);
}
#[test]
fn test_colorterm_takes_precedence_over_term() {
let result = detect_with_env(Some("truecolor"), Some("xterm-color"));
assert_eq!(result, ColorCapability::TrueColor);
}
#[test]
fn test_colorterm_256_detected() {
let result = detect_with_env(Some("256"), None);
assert_eq!(result, ColorCapability::Ansi256);
}
#[test]
fn test_detect_returns_same_value_on_repeated_calls() {
let first = detect_color_capability();
let second = detect_color_capability();
let third = detect_color_capability();
assert_eq!(first, second);
assert_eq!(second, third);
}
#[test]
fn test_detect_is_deterministic() {
for _ in 0..100 {
let result = detect_color_capability();
assert_eq!(result, detect_color_capability());
}
}
#[test]
fn test_no_panic_with_empty_colorterm() {
let result = detect_with_env(Some(""), None);
assert_eq!(result, ColorCapability::Ansi256); }
#[test]
fn test_no_panic_with_empty_term() {
let result = detect_with_env(None, Some(""));
assert_eq!(result, ColorCapability::Ansi256); }
#[test]
fn test_no_panic_with_unusual_values() {
let result = detect_with_env(Some("some-random-value"), Some("unknown-term"));
assert_eq!(result, ColorCapability::Ansi256); }
#[test]
fn test_graceful_handling_of_unicode() {
let result = detect_with_env(Some("truecolor-"), Some("xterm-256color-"));
assert_eq!(result, ColorCapability::TrueColor);
}
#[test]
fn test_display_monochrome() {
let s = format!("{}", ColorCapability::Monochrome);
assert_eq!(s, "Monochrome (no color)");
}
#[test]
fn test_display_ansi16() {
let s = format!("{}", ColorCapability::Ansi16);
assert_eq!(s, "ANSI 16 colors");
}
#[test]
fn test_display_ansi256() {
let s = format!("{}", ColorCapability::Ansi256);
assert_eq!(s, "ANSI 256 colors");
}
#[test]
fn test_display_truecolor() {
let s = format!("{}", ColorCapability::TrueColor);
assert_eq!(s, "True Color (24-bit RGB)");
}
#[test]
fn test_default() {
assert_eq!(ColorCapability::default(), ColorCapability::Ansi256);
}
}