use crate::output::{Env, OutputMode};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Stream {
Stdout,
Stderr,
}
pub(crate) fn decide_color(
stream: Stream,
mode: OutputMode,
env: &dyn Env,
is_terminal: bool,
) -> bool {
if mode == OutputMode::Json && stream == Stream::Stdout {
return false;
}
if env.var("NO_COLOR").is_some_and(|v| !v.is_empty()) {
return false;
}
if matches!(env.var("TERM").as_deref(), Some("dumb")) {
return false;
}
if env.var("FORCE_COLOR").is_some_and(|v| !v.is_empty()) {
return true;
}
is_terminal
}
#[cfg(test)]
mod tests {
use super::*;
use rstest::rstest;
use std::collections::HashMap;
#[derive(Debug, Default)]
struct FakeEnv {
vars: HashMap<String, String>,
}
impl FakeEnv {
fn new() -> Self {
Self::default()
}
fn with(mut self, name: &str, value: &str) -> Self {
self.vars.insert(name.to_owned(), value.to_owned());
self
}
}
impl Env for FakeEnv {
fn var(&self, name: &str) -> Option<String> {
self.vars.get(name).cloned()
}
fn stdout_is_terminal(&self) -> bool {
false
}
}
#[rstest]
#[case(true, FakeEnv::new())]
#[case(false, FakeEnv::new())]
#[case(true, FakeEnv::new().with("FORCE_COLOR", "1"))]
#[case(false, FakeEnv::new().with("FORCE_COLOR", "1"))]
fn json_stdout_never_emits_color(#[case] is_terminal: bool, #[case] env: FakeEnv) {
assert!(!decide_color(
Stream::Stdout,
OutputMode::Json,
&env,
is_terminal
));
}
#[test]
fn json_stderr_follows_normal_rules() {
let env = FakeEnv::new();
assert!(decide_color(Stream::Stderr, OutputMode::Json, &env, true));
}
#[rstest]
#[case(Stream::Stdout, OutputMode::Human)]
#[case(Stream::Stderr, OutputMode::Human)]
#[case(Stream::Stdout, OutputMode::Json)]
#[case(Stream::Stderr, OutputMode::Json)]
fn no_color_disables_everywhere(#[case] stream: Stream, #[case] mode: OutputMode) {
let env = FakeEnv::new()
.with("NO_COLOR", "1")
.with("FORCE_COLOR", "1");
assert!(!decide_color(stream, mode, &env, true));
}
#[test]
fn no_color_empty_value_does_not_disable() {
let env = FakeEnv::new().with("NO_COLOR", "");
assert!(decide_color(Stream::Stderr, OutputMode::Human, &env, true));
}
#[test]
fn term_dumb_disables() {
let env = FakeEnv::new().with("TERM", "dumb");
assert!(!decide_color(Stream::Stderr, OutputMode::Human, &env, true));
}
#[rstest]
#[case(Stream::Stdout, OutputMode::Human, false)]
#[case(Stream::Stderr, OutputMode::Human, false)]
#[case(Stream::Stderr, OutputMode::Json, false)]
fn force_color_enables_when_not_blocked(
#[case] stream: Stream,
#[case] mode: OutputMode,
#[case] is_terminal: bool,
) {
let env = FakeEnv::new().with("FORCE_COLOR", "1");
assert!(decide_color(stream, mode, &env, is_terminal));
}
#[rstest]
#[case(Stream::Stdout, OutputMode::Human, true, true)]
#[case(Stream::Stdout, OutputMode::Human, false, false)]
#[case(Stream::Stderr, OutputMode::Human, true, true)]
#[case(Stream::Stderr, OutputMode::Human, false, false)]
#[case(Stream::Stderr, OutputMode::Json, true, true)]
#[case(Stream::Stderr, OutputMode::Json, false, false)]
fn isatty_decides_in_default_case(
#[case] stream: Stream,
#[case] mode: OutputMode,
#[case] is_terminal: bool,
#[case] expected: bool,
) {
let env = FakeEnv::new();
assert_eq!(decide_color(stream, mode, &env, is_terminal), expected);
}
}