Skip to main content

acme_proxy/cli/
style.rs

1//! Whether the admin CLI's human-readable output is coloured.
2//!
3//! The palette itself — what colour *means*, and the pad-first rule — is
4//! [`acme_proxy_core::palette`]; this module decides whether a run gets one that is on
5//! ([`resolve`]).
6//!
7//! **Precedence deliberately differs from `logging.ansi`.**
8//! [`acme_proxy_server::logging`] documents that neither its switch nor `NO_COLOR`
9//! can turn colour *on* against the other, which is right for a configuration
10//! file — an ambient setting should not override an ambient veto. A
11//! `--color always` is neither ambient nor a setting: it was typed by the
12//! person reading the output, one command ago, and it beats both the TTY test
13//! and `NO_COLOR`. That is what makes piping into `less -R` work.
14
15use acme_proxy_core::palette::Palette;
16use acme_proxy_core::palette::no_color_set;
17
18/// When to colour human-readable output — the `--color` flag's values.
19#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, clap::ValueEnum)]
20pub enum ColorChoice {
21    /// Colour when stdout is a terminal and `NO_COLOR` is unset.
22    #[default]
23    Auto,
24    /// Always colour, whatever the stream and whatever the environment says.
25    Always,
26    /// Never colour.
27    Never,
28}
29
30/// The palette a run of the CLI gets, from the flag, the stream and the
31/// environment.
32///
33/// Pure in all three, so the precedence documented at the top of this
34/// module is testable without a terminal or a process environment.
35#[must_use]
36pub fn resolve(choice: ColorChoice, is_terminal: bool, no_color: Option<&str>) -> Palette {
37    let on = match choice {
38        ColorChoice::Never => false,
39        ColorChoice::Always => true,
40        ColorChoice::Auto => is_terminal && !no_color_set(no_color),
41    };
42    Palette::new(on)
43}
44
45#[cfg(test)]
46mod tests {
47    use super::*;
48
49    /// The precedence table in full. The row that matters is `Always` beating
50    /// `NO_COLOR`: that is where this deliberately parts company with
51    /// `logging.ansi`, and without it there is no way to colour into a pager.
52    #[test]
53    fn resolve_covers_every_choice_against_the_stream_and_the_environment() {
54        for (choice, is_terminal, no_color, expected) in [
55            (ColorChoice::Auto, true, None, true),
56            (ColorChoice::Auto, false, None, false),
57            (ColorChoice::Auto, true, Some("1"), false),
58            (ColorChoice::Auto, true, Some("anything"), false),
59            // The convention counts only a non-empty value.
60            (ColorChoice::Auto, true, Some(""), true),
61            (ColorChoice::Always, false, None, true),
62            (ColorChoice::Always, false, Some("1"), true),
63            (ColorChoice::Always, true, Some("1"), true),
64            (ColorChoice::Never, true, None, false),
65            (ColorChoice::Never, true, Some(""), false),
66        ] {
67            assert_eq!(
68                resolve(choice, is_terminal, no_color).is_on(),
69                expected,
70                "{choice:?} tty={is_terminal} NO_COLOR={no_color:?}"
71            );
72        }
73    }
74
75    /// `Auto` is the default, so a bare `acme-proxy account list` behaves the
76    /// way every other tool does.
77    #[test]
78    fn auto_is_the_default_choice() {
79        assert_eq!(ColorChoice::default(), ColorChoice::Auto);
80    }
81}