use std::io::IsTerminal;
use std::sync::atomic::{AtomicU8, Ordering};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ColorChoice {
Auto,
Always,
Never,
}
const AUTO: u8 = 0;
const ALWAYS: u8 = 1;
const NEVER: u8 = 2;
static CHOICE: AtomicU8 = AtomicU8::new(AUTO);
#[cfg(test)]
pub(crate) static TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
pub fn set_color_choice(choice: ColorChoice) {
let encoded = match choice {
ColorChoice::Auto => AUTO,
ColorChoice::Always => ALWAYS,
ColorChoice::Never => NEVER,
};
CHOICE.store(encoded, Ordering::Relaxed);
}
#[must_use]
pub fn color_choice() -> ColorChoice {
match CHOICE.load(Ordering::Relaxed) {
ALWAYS => ColorChoice::Always,
NEVER => ColorChoice::Never,
_ => ColorChoice::Auto,
}
}
fn resolve(choice: ColorChoice, no_color: bool, is_terminal: bool) -> bool {
match choice {
ColorChoice::Always => true,
ColorChoice::Never => false,
ColorChoice::Auto => !no_color && is_terminal,
}
}
pub(crate) fn color_enabled() -> bool {
let no_color = std::env::var_os("NO_COLOR").is_some_and(|value| !value.is_empty());
resolve(color_choice(), no_color, std::io::stderr().is_terminal())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{OrFail, TestResult};
use test_better_matchers::{check, eq, is_false, is_true};
#[test]
fn resolve_handles_every_choice_and_no_color() -> TestResult {
check!(resolve(ColorChoice::Always, true, false))
.satisfies(is_true())
.or_fail()?;
check!(resolve(ColorChoice::Never, false, true))
.satisfies(is_false())
.or_fail()?;
check!(resolve(ColorChoice::Auto, false, true))
.satisfies(is_true())
.or_fail()?;
check!(resolve(ColorChoice::Auto, true, true))
.satisfies(is_false())
.or_fail()?;
check!(resolve(ColorChoice::Auto, false, false))
.satisfies(is_false())
.or_fail()?;
Ok(())
}
#[test]
fn choice_round_trips_through_the_global_slot() -> TestResult {
let _guard = TEST_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let original = color_choice();
set_color_choice(ColorChoice::Always);
let after_always = color_choice();
set_color_choice(ColorChoice::Never);
let after_never = color_choice();
set_color_choice(ColorChoice::Auto);
let after_auto = color_choice();
set_color_choice(original);
check!(after_always)
.satisfies(eq(ColorChoice::Always))
.or_fail()?;
check!(after_never)
.satisfies(eq(ColorChoice::Never))
.or_fail()?;
check!(after_auto)
.satisfies(eq(ColorChoice::Auto))
.or_fail()?;
Ok(())
}
}