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}