use std::io::IsTerminal;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ColorChoice {
#[default]
Auto,
Always,
Never,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Stream {
Stdout,
Stderr,
}
pub trait ColorEnv {
fn var(&self, key: &str) -> Option<String>;
fn is_terminal(&self, stream: Stream) -> bool;
}
pub struct SystemColorEnv;
impl ColorEnv for SystemColorEnv {
fn var(&self, key: &str) -> Option<String> {
std::env::var(key).ok().filter(|v| !v.is_empty())
}
fn is_terminal(&self, stream: Stream) -> bool {
match stream {
Stream::Stdout => std::io::stdout().is_terminal(),
Stream::Stderr => std::io::stderr().is_terminal(),
}
}
}
#[must_use]
pub fn should_color(
choice: ColorChoice,
json_mode: bool,
stream: Stream,
env: &dyn ColorEnv,
) -> bool {
if json_mode {
return false;
}
match choice {
ColorChoice::Never => false,
ColorChoice::Always => true,
ColorChoice::Auto => {
if env.var("NO_COLOR").is_some() {
return false;
}
if env.var("CLICOLOR_FORCE").is_some() {
return true;
}
env.is_terminal(stream)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
#[derive(Default)]
struct FakeEnv {
vars: HashMap<String, String>,
stdout_tty: bool,
stderr_tty: bool,
}
impl ColorEnv for FakeEnv {
fn var(&self, key: &str) -> Option<String> {
self.vars.get(key).cloned().filter(|v| !v.is_empty())
}
fn is_terminal(&self, stream: Stream) -> bool {
match stream {
Stream::Stdout => self.stdout_tty,
Stream::Stderr => self.stderr_tty,
}
}
}
#[test]
fn json_mode_is_always_off() {
let env = FakeEnv {
stderr_tty: true,
..FakeEnv::default()
};
assert!(!should_color(
ColorChoice::Always,
true,
Stream::Stderr,
&env
));
}
#[test]
fn never_is_off_always_is_on() {
let env = FakeEnv::default();
assert!(!should_color(
ColorChoice::Never,
false,
Stream::Stderr,
&env
));
assert!(should_color(
ColorChoice::Always,
false,
Stream::Stderr,
&env
));
}
#[test]
fn no_color_env_forces_off_in_auto() {
let env = FakeEnv {
vars: HashMap::from([("NO_COLOR".to_string(), "1".to_string())]),
stderr_tty: true,
..FakeEnv::default()
};
assert!(!should_color(
ColorChoice::Auto,
false,
Stream::Stderr,
&env
));
}
#[test]
fn clicolor_force_turns_on_without_tty() {
let env = FakeEnv {
vars: HashMap::from([("CLICOLOR_FORCE".to_string(), "1".to_string())]),
..FakeEnv::default()
};
assert!(should_color(ColorChoice::Auto, false, Stream::Stderr, &env));
}
#[test]
fn no_color_beats_clicolor_force() {
let env = FakeEnv {
vars: HashMap::from([
("NO_COLOR".to_string(), "1".to_string()),
("CLICOLOR_FORCE".to_string(), "1".to_string()),
]),
..FakeEnv::default()
};
assert!(!should_color(
ColorChoice::Auto,
false,
Stream::Stderr,
&env
));
}
#[test]
fn auto_follows_tty_when_env_silent() {
let on = FakeEnv {
stderr_tty: true,
..FakeEnv::default()
};
let off = FakeEnv::default();
assert!(should_color(ColorChoice::Auto, false, Stream::Stderr, &on));
assert!(!should_color(
ColorChoice::Auto,
false,
Stream::Stderr,
&off
));
}
}