use std::env;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ColorEnvOverride {
NoColor,
ForceColor,
ForceColorTruecolor,
None,
}
pub fn detect_color_env() -> ColorEnvOverride {
if env::var("NO_COLOR").is_ok_and(|v| !v.is_empty()) {
return ColorEnvOverride::NoColor;
}
if let Ok(val) = env::var("FORCE_COLOR") {
match val.as_str() {
"" => {} "0" => return ColorEnvOverride::NoColor,
"1" | "2" => return ColorEnvOverride::ForceColor,
"3" => return ColorEnvOverride::ForceColorTruecolor,
_ => return ColorEnvOverride::ForceColor,
}
}
if let Ok(val) = env::var("CLICOLOR_FORCE") {
if val != "0" {
return ColorEnvOverride::ForceColor;
}
}
if let Ok(val) = env::var("CLICOLOR") {
if val == "0" {
return ColorEnvOverride::NoColor;
}
}
ColorEnvOverride::None
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TtyOverride {
ForceTty,
ForceNotTty,
None,
}
pub fn detect_tty_compatible() -> TtyOverride {
match env::var("TTY_COMPATIBLE").as_deref() {
Ok("1") => TtyOverride::ForceTty,
Ok("0") => TtyOverride::ForceNotTty,
_ => TtyOverride::None,
}
}
pub fn detect_tty_interactive() -> TtyOverride {
match env::var("TTY_INTERACTIVE").as_deref() {
Ok("1") => TtyOverride::ForceTty,
Ok("0") => TtyOverride::ForceNotTty,
_ => TtyOverride::None,
}
}
pub fn detect_reduce_motion() -> bool {
match env::var("REDUCE_MOTION") {
Ok(val) => val == "1" || val.eq_ignore_ascii_case("true"),
Err(_) => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::env;
use std::sync::Mutex;
static ENV_LOCK: Mutex<()> = Mutex::new(());
fn with_env<F: FnOnce() -> ColorEnvOverride>(
vars: &[(&str, Option<&str>)],
f: F,
) -> ColorEnvOverride {
let _guard = ENV_LOCK.lock().unwrap();
let all_keys = ["NO_COLOR", "FORCE_COLOR", "CLICOLOR_FORCE", "CLICOLOR"];
let saved: Vec<(&str, Option<String>)> =
all_keys.iter().map(|k| (*k, env::var(k).ok())).collect();
for key in &all_keys {
env::remove_var(key);
}
for &(key, val) in vars {
match val {
Some(v) => env::set_var(key, v),
None => env::remove_var(key),
}
}
let result = f();
for (key, val) in saved {
match val {
Some(v) => env::set_var(key, v),
None => env::remove_var(key),
}
}
result
}
#[test]
fn test_no_color_set_disables_color() {
let r = with_env(&[("NO_COLOR", Some("1"))], detect_color_env);
assert_eq!(r, ColorEnvOverride::NoColor);
}
#[test]
fn test_no_color_any_value() {
let r = with_env(&[("NO_COLOR", Some("1"))], detect_color_env);
assert_eq!(r, ColorEnvOverride::NoColor);
}
#[test]
fn test_force_color_3_truecolor() {
let r = with_env(&[("FORCE_COLOR", Some("3"))], detect_color_env);
assert_eq!(r, ColorEnvOverride::ForceColorTruecolor);
}
#[test]
fn test_force_color_0_disables() {
let r = with_env(&[("FORCE_COLOR", Some("0"))], detect_color_env);
assert_eq!(r, ColorEnvOverride::NoColor);
}
#[test]
fn test_force_color_1_forces() {
let r = with_env(&[("FORCE_COLOR", Some("1"))], detect_color_env);
assert_eq!(r, ColorEnvOverride::ForceColor);
}
#[test]
fn test_force_color_unknown_value_forces() {
let r = with_env(&[("FORCE_COLOR", Some("yes"))], detect_color_env);
assert_eq!(r, ColorEnvOverride::ForceColor);
}
#[test]
fn test_clicolor_force_1() {
let r = with_env(&[("CLICOLOR_FORCE", Some("1"))], detect_color_env);
assert_eq!(r, ColorEnvOverride::ForceColor);
}
#[test]
fn test_clicolor_force_0_does_not_force() {
let r = with_env(&[("CLICOLOR_FORCE", Some("0"))], detect_color_env);
assert_eq!(r, ColorEnvOverride::None);
}
#[test]
fn test_clicolor_0_disables() {
let r = with_env(&[("CLICOLOR", Some("0"))], detect_color_env);
assert_eq!(r, ColorEnvOverride::NoColor);
}
#[test]
fn test_clicolor_1_no_override() {
let r = with_env(&[("CLICOLOR", Some("1"))], detect_color_env);
assert_eq!(r, ColorEnvOverride::None);
}
#[test]
fn test_no_vars_set_returns_none() {
let r = with_env(&[], detect_color_env);
assert_eq!(r, ColorEnvOverride::None);
}
#[test]
fn test_no_color_wins_over_force_color() {
let r = with_env(
&[("NO_COLOR", Some("1")), ("FORCE_COLOR", Some("3"))],
detect_color_env,
);
assert_eq!(r, ColorEnvOverride::NoColor);
}
#[test]
fn test_force_color_wins_over_clicolor_force() {
let r = with_env(
&[("FORCE_COLOR", Some("0")), ("CLICOLOR_FORCE", Some("1"))],
detect_color_env,
);
assert_eq!(r, ColorEnvOverride::NoColor);
}
fn with_reduce_motion<F: FnOnce() -> bool>(val: Option<&str>, f: F) -> bool {
let _guard = ENV_LOCK.lock().unwrap();
let saved = env::var("REDUCE_MOTION").ok();
env::remove_var("REDUCE_MOTION");
if let Some(v) = val {
env::set_var("REDUCE_MOTION", v);
}
let result = f();
match saved {
Some(v) => env::set_var("REDUCE_MOTION", v),
None => env::remove_var("REDUCE_MOTION"),
}
result
}
#[test]
fn test_reduce_motion_unset() {
let r = with_reduce_motion(None, super::detect_reduce_motion);
assert!(!r, "should be false when REDUCE_MOTION is not set");
}
#[test]
fn test_reduce_motion_1() {
let r = with_reduce_motion(Some("1"), super::detect_reduce_motion);
assert!(r, "should be true when REDUCE_MOTION=1");
}
#[test]
fn test_reduce_motion_true_lowercase() {
let r = with_reduce_motion(Some("true"), super::detect_reduce_motion);
assert!(r, "should be true when REDUCE_MOTION=true");
}
#[test]
fn test_reduce_motion_true_uppercase() {
let r = with_reduce_motion(Some("TRUE"), super::detect_reduce_motion);
assert!(r, "should be true when REDUCE_MOTION=TRUE");
}
#[test]
fn test_reduce_motion_true_mixed_case() {
let r = with_reduce_motion(Some("True"), super::detect_reduce_motion);
assert!(r, "should be true when REDUCE_MOTION=True");
}
#[test]
fn test_reduce_motion_0() {
let r = with_reduce_motion(Some("0"), super::detect_reduce_motion);
assert!(!r, "should be false when REDUCE_MOTION=0");
}
#[test]
fn test_reduce_motion_empty() {
let r = with_reduce_motion(Some(""), super::detect_reduce_motion);
assert!(!r, "should be false when REDUCE_MOTION is empty");
}
#[test]
fn test_reduce_motion_arbitrary_value() {
let r = with_reduce_motion(Some("yes"), super::detect_reduce_motion);
assert!(!r, "should be false for arbitrary values like 'yes'");
}
#[test]
fn no_color_unset_means_color_enabled() {
let r = with_env(&[], detect_color_env);
assert_ne!(r, ColorEnvOverride::NoColor);
}
#[test]
fn no_color_empty_string_does_not_disable() {
let r = with_env(&[("NO_COLOR", Some(""))], detect_color_env);
assert_ne!(
r,
ColorEnvOverride::NoColor,
"NO_COLOR='' should not disable color"
);
}
#[test]
fn no_color_nonempty_disables() {
let r = with_env(&[("NO_COLOR", Some("yes"))], detect_color_env);
assert_eq!(r, ColorEnvOverride::NoColor);
}
#[test]
fn force_color_unset_no_force() {
let r = with_env(&[], detect_color_env);
assert_eq!(r, ColorEnvOverride::None);
}
#[test]
fn force_color_empty_string_does_not_force() {
let r = with_env(&[("FORCE_COLOR", Some(""))], detect_color_env);
assert_eq!(
r,
ColorEnvOverride::None,
"FORCE_COLOR='' should not force color"
);
}
#[test]
fn force_color_nonempty_forces() {
let r = with_env(&[("FORCE_COLOR", Some("1"))], detect_color_env);
assert_eq!(r, ColorEnvOverride::ForceColor);
}
fn with_tty_env<R, F: FnOnce() -> R>(vars: &[(&str, Option<&str>)], f: F) -> R {
let _guard = ENV_LOCK.lock().unwrap();
let all_keys = ["TTY_COMPATIBLE", "TTY_INTERACTIVE"];
let saved: Vec<(&str, Option<String>)> =
all_keys.iter().map(|k| (*k, env::var(k).ok())).collect();
for key in &all_keys {
env::remove_var(key);
}
for &(key, val) in vars {
match val {
Some(v) => env::set_var(key, v),
None => env::remove_var(key),
}
}
let result = f();
for (key, val) in saved {
match val {
Some(v) => env::set_var(key, v),
None => env::remove_var(key),
}
}
result
}
#[test]
fn tty_compatible_unset_yields_none() {
let r = with_tty_env(&[], detect_tty_compatible);
assert_eq!(r, TtyOverride::None);
}
#[test]
fn tty_compatible_one_forces_tty() {
let r = with_tty_env(&[("TTY_COMPATIBLE", Some("1"))], detect_tty_compatible);
assert_eq!(r, TtyOverride::ForceTty);
}
#[test]
fn tty_compatible_zero_forces_not_tty() {
let r = with_tty_env(&[("TTY_COMPATIBLE", Some("0"))], detect_tty_compatible);
assert_eq!(r, TtyOverride::ForceNotTty);
}
#[test]
fn tty_compatible_other_value_is_none() {
let r = with_tty_env(&[("TTY_COMPATIBLE", Some("yes"))], detect_tty_compatible);
assert_eq!(r, TtyOverride::None);
}
#[test]
fn tty_interactive_one_forces_interactive() {
let r = with_tty_env(&[("TTY_INTERACTIVE", Some("1"))], detect_tty_interactive);
assert_eq!(r, TtyOverride::ForceTty);
}
#[test]
fn tty_interactive_zero_forces_not_interactive() {
let r = with_tty_env(&[("TTY_INTERACTIVE", Some("0"))], detect_tty_interactive);
assert_eq!(r, TtyOverride::ForceNotTty);
}
#[test]
fn tty_interactive_independent_of_tty_compatible() {
let (tc, ti) = with_tty_env(
&[
("TTY_COMPATIBLE", Some("0")),
("TTY_INTERACTIVE", Some("1")),
],
|| (detect_tty_compatible(), detect_tty_interactive()),
);
assert_eq!(tc, TtyOverride::ForceNotTty);
assert_eq!(ti, TtyOverride::ForceTty);
}
}