use crate::config::{ColorDepthMode, ColorMode};
#[derive(Debug, Clone, Copy, Eq, Ord, PartialEq, PartialOrd)]
pub enum ColorLevel {
NoColor,
Ansi16,
Ansi256,
TrueColor,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub struct TerminalCapabilities {
pub is_terminal: bool,
pub color_level: ColorLevel,
}
pub(crate) trait EnvProvider {
fn get(&self, key: &str) -> Option<String>;
fn is_set(&self, key: &str) -> bool {
self.get(key).is_some()
}
}
pub(crate) struct ProcessEnv;
impl EnvProvider for ProcessEnv {
fn get(&self, key: &str) -> Option<String> {
std::env::var(key).ok()
}
fn is_set(&self, key: &str) -> bool {
std::env::var_os(key).is_some()
}
}
pub(crate) fn terminal_capabilities(
is_terminal: bool,
color_mode: ColorMode,
depth_mode: ColorDepthMode,
) -> TerminalCapabilities {
TerminalCapabilities {
is_terminal,
color_level: detect_color_level(is_terminal, color_mode, depth_mode, &ProcessEnv),
}
}
pub(crate) fn color_level_for_capabilities(
capabilities: TerminalCapabilities,
color_mode: ColorMode,
depth_mode: ColorDepthMode,
) -> ColorLevel {
if let Some(level) = hard_disable_level(color_mode, depth_mode, &ProcessEnv) {
return level;
}
capabilities.color_level
}
pub(crate) fn detect_color_level(
is_terminal: bool,
color_mode: ColorMode,
depth_mode: ColorDepthMode,
env: &impl EnvProvider,
) -> ColorLevel {
if let Some(level) = hard_disable_level(color_mode, depth_mode, env) {
return level;
}
if let Some(forced) = force_color_level(env) {
return forced;
}
let clicolor_forced = clicolor_force_enabled(env);
if clicolor_disabled(env) && !clicolor_forced {
return ColorLevel::NoColor;
}
if color_mode == ColorMode::Auto && !is_terminal && !clicolor_forced {
return ColorLevel::NoColor;
}
let mut level = match detect_env_color_level(env) {
Some(detected) if clicolor_forced => detected.max(ColorLevel::Ansi16),
Some(detected) => detected,
None if clicolor_forced => ColorLevel::Ansi16,
None => ColorLevel::TrueColor,
};
if let Some(explicit) = explicit_depth_level(depth_mode) {
level = explicit;
}
level
}
fn hard_disable_level(
color_mode: ColorMode,
depth_mode: ColorDepthMode,
env: &impl EnvProvider,
) -> Option<ColorLevel> {
if env.is_set("NO_COLOR")
|| color_mode == ColorMode::Never
|| depth_mode == ColorDepthMode::NoColor
{
Some(ColorLevel::NoColor)
} else {
None
}
}
fn force_color_level(env: &impl EnvProvider) -> Option<ColorLevel> {
let value = env.get("FORCE_COLOR")?;
let normalized = normalize_env_value(&value);
match normalized.as_str() {
"" => None,
"0" | "no_color" | "none" | "never" | "false" | "off" => Some(ColorLevel::NoColor),
"1" | "ansi" | "ansi16" | "basic" | "true" | "yes" | "on" => Some(ColorLevel::Ansi16),
"2" | "ansi256" | "256" | "8bit" | "8-bit" => Some(ColorLevel::Ansi256),
"3" | "truecolor" | "true_color" | "24bit" | "24-bit" | "16m" => {
Some(ColorLevel::TrueColor)
}
_ => Some(ColorLevel::Ansi16),
}
}
fn explicit_depth_level(depth_mode: ColorDepthMode) -> Option<ColorLevel> {
match depth_mode {
ColorDepthMode::Auto | ColorDepthMode::NoColor => None,
ColorDepthMode::Ansi16 => Some(ColorLevel::Ansi16),
ColorDepthMode::Ansi256 => Some(ColorLevel::Ansi256),
ColorDepthMode::TrueColor => Some(ColorLevel::TrueColor),
}
}
fn clicolor_force_enabled(env: &impl EnvProvider) -> bool {
env.get("CLICOLOR_FORCE").is_some_and(|value| {
let normalized = normalize_env_value(&value);
!normalized.is_empty() && normalized != "0"
})
}
fn clicolor_disabled(env: &impl EnvProvider) -> bool {
env.get("CLICOLOR")
.is_some_and(|value| normalize_env_value(&value) == "0")
}
fn detect_env_color_level(env: &impl EnvProvider) -> Option<ColorLevel> {
if env
.get("TERM")
.is_some_and(|term| term.eq_ignore_ascii_case("dumb"))
{
return Some(ColorLevel::NoColor);
}
if env
.get("COLORTERM")
.is_some_and(|value| matches!(normalize_env_value(&value).as_str(), "truecolor" | "24bit"))
{
return Some(ColorLevel::TrueColor);
}
if env.is_set("WT_SESSION") {
return Some(ColorLevel::TrueColor);
}
if env
.get("TERM")
.is_some_and(|term| term.to_ascii_lowercase().contains("256color"))
{
return Some(ColorLevel::Ansi256);
}
if env
.get("ConEmuANSI")
.is_some_and(|value| value.eq_ignore_ascii_case("ON"))
|| env.is_set("ANSICON")
|| env.is_set("CI")
{
return Some(ColorLevel::Ansi16);
}
if env.get("TERM").is_some_and(|term| !term.is_empty()) {
return Some(ColorLevel::Ansi16);
}
None
}
fn normalize_env_value(value: &str) -> String {
value.trim().to_ascii_lowercase()
}
#[cfg(test)]
pub(crate) mod tests {
use std::collections::HashMap;
use super::*;
#[derive(Default)]
pub(crate) struct TestEnv {
values: HashMap<String, String>,
}
impl TestEnv {
pub(crate) fn with(mut self, key: &str, value: &str) -> Self {
self.values.insert(key.to_string(), value.to_string());
self
}
}
impl EnvProvider for TestEnv {
fn get(&self, key: &str) -> Option<String> {
self.values.get(key).cloned()
}
}
}