Skip to main content

wt/output/
color.rs

1//! Color output decision (spec §11 color precedence) and ANSI painting.
2
3/// ANSI SGR codes used by `wt`'s human output.
4pub mod ansi {
5    /// Reset all attributes.
6    pub const RESET: &str = "\x1b[0m";
7    /// Red.
8    pub const RED: &str = "\x1b[31m";
9    /// Green.
10    pub const GREEN: &str = "\x1b[32m";
11    /// Yellow.
12    pub const YELLOW: &str = "\x1b[33m";
13    /// Cyan.
14    pub const CYAN: &str = "\x1b[36m";
15    /// Magenta.
16    pub const MAGENTA: &str = "\x1b[35m";
17    /// Dim.
18    pub const DIM: &str = "\x1b[2m";
19}
20
21/// Wraps `text` in the SGR `code` when `enabled` (and `text` is not blank),
22/// otherwise returns it unchanged. ANSI codes are zero display-width, so this is
23/// safe to apply to already-laid-out cells.
24pub fn paint(text: &str, code: &str, enabled: bool) -> String {
25    if enabled && !text.trim().is_empty() {
26        format!("{code}{text}{}", ansi::RESET)
27    } else {
28        text.to_string()
29    }
30}
31
32/// How to colorize output, as selected by `--color` or the `ui.color` config.
33#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
34pub enum ColorChoice {
35    /// Color when the relevant stream is a TTY.
36    Auto,
37    /// Always colorize.
38    Always,
39    /// Never colorize.
40    Never,
41}
42
43impl ColorChoice {
44    /// Parses a `--color`/`ui.color` value (`auto`, `always`, `never`).
45    pub fn parse(value: &str) -> Option<ColorChoice> {
46        match value {
47            "auto" => Some(ColorChoice::Auto),
48            "always" => Some(ColorChoice::Always),
49            "never" => Some(ColorChoice::Never),
50            _ => None,
51        }
52    }
53}
54
55/// Resolves whether to emit ANSI color, following the spec §11 precedence
56/// (first match wins):
57/// 1. an explicit `--color always|never`;
58/// 2. `NO_COLOR` set and non-empty → never;
59/// 3. `ui.color` set to `always`/`never`;
60/// 4. otherwise auto — color when stdout is a TTY.
61///
62/// An explicit `--color auto` (or no flag) and a `ui.color = "auto"` both fall
63/// through to the next rule rather than forcing a decision.
64pub fn resolve_color(
65    flag: Option<ColorChoice>,
66    no_color: bool,
67    config: Option<ColorChoice>,
68    stdout_is_tty: bool,
69) -> bool {
70    match flag {
71        Some(ColorChoice::Always) => return true,
72        Some(ColorChoice::Never) => return false,
73        Some(ColorChoice::Auto) | None => {}
74    }
75    if no_color {
76        return false;
77    }
78    match config {
79        Some(ColorChoice::Always) => return true,
80        Some(ColorChoice::Never) => return false,
81        Some(ColorChoice::Auto) | None => {}
82    }
83    stdout_is_tty
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89
90    #[test]
91    fn parse_known_and_unknown() {
92        assert_eq!(ColorChoice::parse("auto"), Some(ColorChoice::Auto));
93        assert_eq!(ColorChoice::parse("always"), Some(ColorChoice::Always));
94        assert_eq!(ColorChoice::parse("never"), Some(ColorChoice::Never));
95        assert_eq!(ColorChoice::parse("bogus"), None);
96    }
97
98    #[test]
99    fn flag_always_wins_over_no_color() {
100        assert!(resolve_color(
101            Some(ColorChoice::Always),
102            true,
103            Some(ColorChoice::Never),
104            false
105        ));
106    }
107
108    #[test]
109    fn flag_never_wins() {
110        assert!(!resolve_color(
111            Some(ColorChoice::Never),
112            false,
113            Some(ColorChoice::Always),
114            true
115        ));
116    }
117
118    #[test]
119    fn no_color_env_beats_config_and_auto() {
120        assert!(!resolve_color(None, true, Some(ColorChoice::Always), true));
121        assert!(!resolve_color(Some(ColorChoice::Auto), true, None, true));
122    }
123
124    #[test]
125    fn config_used_when_no_flag_or_no_color() {
126        assert!(resolve_color(None, false, Some(ColorChoice::Always), false));
127        assert!(!resolve_color(None, false, Some(ColorChoice::Never), true));
128    }
129
130    #[test]
131    fn auto_falls_back_to_tty() {
132        assert!(resolve_color(None, false, None, true));
133        assert!(!resolve_color(None, false, None, false));
134        assert!(resolve_color(
135            Some(ColorChoice::Auto),
136            false,
137            Some(ColorChoice::Auto),
138            true
139        ));
140    }
141}