use crate::cli::ColourPolicy;
use std::env;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OutputMode {
Accessible,
Standard,
}
impl OutputMode {
#[must_use]
pub const fn is_accessible(self) -> bool {
matches!(self, Self::Accessible)
}
}
#[must_use]
#[expect(
clippy::disallowed_methods,
reason = "composition root: supplies the process environment to the read_env seam"
)]
pub fn resolve(explicit: Option<bool>, colour_policy: Option<ColourPolicy>) -> OutputMode {
resolve_with(explicit, colour_policy, |key| env::var(key).ok())
}
#[must_use]
pub fn resolve_with<F>(
explicit: Option<bool>,
colour_policy: Option<ColourPolicy>,
read_env: F,
) -> OutputMode
where
F: Fn(&str) -> Option<String>,
{
if let Some(forced) = explicit {
return if forced {
OutputMode::Accessible
} else {
OutputMode::Standard
};
}
if no_color_active_with(colour_policy, &read_env) {
return OutputMode::Accessible;
}
if read_env("TERM").as_deref() == Some("dumb") {
return OutputMode::Accessible;
}
OutputMode::Standard
}
pub(crate) fn no_color_active_with<F>(colour_policy: Option<ColourPolicy>, read_env: &F) -> bool
where
F: Fn(&str) -> Option<String>,
{
match colour_policy {
Some(ColourPolicy::Always) => false,
Some(ColourPolicy::Never) => true,
Some(ColourPolicy::Auto) | None => read_env("NO_COLOR").is_some(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use rstest::rstest;
fn fake_env<'a>(
no_color: Option<&'a str>,
term: Option<&'a str>,
) -> impl Fn(&str) -> Option<String> + 'a {
move |key| match key {
"NO_COLOR" => no_color.map(String::from),
"TERM" => term.map(String::from),
_ => None,
}
}
#[rstest]
#[case::explicit_true_forces_accessible(
Some(true),
None,
(None, None),
OutputMode::Accessible
)]
#[case::explicit_false_overrides_all(
Some(false),
None,
(Some("1"), Some("dumb")),
OutputMode::Standard
)]
#[case::no_color_triggers_accessible(
None,
None,
(Some("1"), None),
OutputMode::Accessible
)]
#[case::term_dumb_triggers_accessible(
None,
None,
(None, Some("dumb")),
OutputMode::Accessible
)]
#[case::normal_term_stays_standard(
None,
None,
(None, Some("xterm-256color")),
OutputMode::Standard
)]
#[case::no_env_defaults_to_standard(
None,
None,
(None, None),
OutputMode::Standard
)]
#[case::empty_no_color_triggers_accessible(
None,
None,
(Some(""), None),
OutputMode::Accessible
)]
#[case::no_color_takes_precedence_over_term(
None,
None,
(Some("1"), Some("xterm-256color")),
OutputMode::Accessible
)]
#[case::explicit_false_overrides_no_color(
Some(false),
None,
(Some("1"), None),
OutputMode::Standard
)]
#[case::explicit_true_without_env(
Some(true),
None,
(None, Some("xterm-256color")),
OutputMode::Accessible
)]
#[case::colour_policy_always_ignores_no_color(
None,
Some(ColourPolicy::Always),
(Some("1"), Some("xterm-256color")),
OutputMode::Standard
)]
#[case::colour_policy_never_forces_accessible(
None,
Some(ColourPolicy::Never),
(None, Some("xterm-256color")),
OutputMode::Accessible
)]
fn resolve_output_mode(
#[case] explicit: Option<bool>,
#[case] colour_policy: Option<ColourPolicy>,
#[case] env_values: (Option<&str>, Option<&str>),
#[case] expected: OutputMode,
) {
let (no_color, term) = env_values;
let env = fake_env(no_color, term);
assert_eq!(resolve_with(explicit, colour_policy, env), expected);
}
#[test]
fn is_accessible_returns_true_for_accessible() {
assert!(OutputMode::Accessible.is_accessible());
}
#[test]
fn is_accessible_returns_false_for_standard() {
assert!(!OutputMode::Standard.is_accessible());
}
}