nu_protocol/config/
ansi_coloring.rs

1use super::{ConfigErrors, ConfigPath, IntoValue, ShellError, UpdateFromValue, Value};
2use crate::{self as nu_protocol, FromValue, engine::EngineState};
3use serde::{Deserialize, Serialize};
4use std::io::IsTerminal;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, IntoValue, Serialize, Deserialize)]
7pub enum UseAnsiColoring {
8    #[default]
9    Auto,
10    True,
11    False,
12}
13
14impl UseAnsiColoring {
15    /// Determines whether ANSI colors should be used.
16    ///
17    /// This method evaluates the `UseAnsiColoring` setting and considers environment variables
18    /// (`FORCE_COLOR`, `NO_COLOR`, and `CLICOLOR`) when the value is set to `Auto`.
19    /// The configuration value (`UseAnsiColoring`) takes precedence over environment variables, as
20    /// it is more direct and internally may be modified to override ANSI coloring behavior.
21    ///
22    /// Most users should have the default value `Auto` which allows the environment variables to
23    /// control ANSI coloring.
24    /// However, when explicitly set to `True` or `False`, the environment variables are ignored.
25    ///
26    /// Behavior based on `UseAnsiColoring`:
27    /// - `True`: Forces ANSI colors to be enabled, ignoring terminal support and environment variables.
28    /// - `False`: Disables ANSI colors completely.
29    /// - `Auto`: Determines whether ANSI colors should be used based on environment variables and terminal support.
30    ///
31    /// When set to `Auto`, the following environment variables are checked in order:
32    /// 1. `FORCE_COLOR`: If set, ANSI colors are always enabled, overriding all other settings.
33    /// 2. `NO_COLOR`: If set, ANSI colors are disabled, overriding `CLICOLOR` and terminal checks.
34    /// 3. `CLICOLOR`: If set, its value determines whether ANSI colors are enabled (`1` for enabled, `0` for disabled).
35    ///
36    /// If none of these variables are set, ANSI coloring is enabled only if the standard output is
37    /// a terminal.
38    ///
39    /// By prioritizing the `UseAnsiColoring` value, we ensure predictable behavior and prevent
40    /// conflicts with internal overrides that depend on this configuration.
41    pub fn get(self, engine_state: &EngineState) -> bool {
42        let is_terminal = match self {
43            Self::Auto => std::io::stdout().is_terminal(),
44            Self::True => return true,
45            Self::False => return false,
46        };
47
48        let env_value = |env_name| {
49            engine_state
50                .get_env_var_insensitive(env_name)
51                .map(|(_, v)| v)
52                .and_then(|v| v.coerce_bool().ok())
53                .unwrap_or(false)
54        };
55
56        if env_value("force_color") {
57            return true;
58        }
59
60        if env_value("no_color") {
61            return false;
62        }
63
64        if let Some((_, cli_color)) = engine_state.get_env_var_insensitive("clicolor") {
65            if let Ok(cli_color) = cli_color.coerce_bool() {
66                return cli_color;
67            }
68        }
69
70        is_terminal
71    }
72}
73
74impl From<bool> for UseAnsiColoring {
75    fn from(value: bool) -> Self {
76        match value {
77            true => Self::True,
78            false => Self::False,
79        }
80    }
81}
82
83impl FromValue for UseAnsiColoring {
84    fn from_value(v: Value) -> Result<Self, ShellError> {
85        if let Ok(v) = v.as_bool() {
86            return Ok(v.into());
87        }
88
89        #[derive(FromValue)]
90        enum UseAnsiColoringString {
91            Auto = 0,
92            True = 1,
93            False = 2,
94        }
95
96        Ok(match UseAnsiColoringString::from_value(v)? {
97            UseAnsiColoringString::Auto => Self::Auto,
98            UseAnsiColoringString::True => Self::True,
99            UseAnsiColoringString::False => Self::False,
100        })
101    }
102}
103
104impl UpdateFromValue for UseAnsiColoring {
105    fn update<'a>(
106        &mut self,
107        value: &'a Value,
108        path: &mut ConfigPath<'a>,
109        errors: &mut ConfigErrors,
110    ) {
111        let Ok(value) = UseAnsiColoring::from_value(value.clone()) else {
112            errors.type_mismatch(path, UseAnsiColoring::expected_type(), value);
113            return;
114        };
115
116        *self = value;
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123    use nu_protocol::Config;
124
125    fn set_env(engine_state: &mut EngineState, name: &str, value: bool) {
126        engine_state.add_env_var(name.to_string(), Value::test_bool(value));
127    }
128
129    #[test]
130    fn test_use_ansi_coloring_true() {
131        let mut engine_state = EngineState::new();
132        engine_state.config = Config {
133            use_ansi_coloring: UseAnsiColoring::True,
134            ..Default::default()
135        }
136        .into();
137
138        // explicit `True` ignores environment variables
139        assert!(
140            engine_state
141                .get_config()
142                .use_ansi_coloring
143                .get(&engine_state)
144        );
145
146        set_env(&mut engine_state, "clicolor", false);
147        assert!(
148            engine_state
149                .get_config()
150                .use_ansi_coloring
151                .get(&engine_state)
152        );
153        set_env(&mut engine_state, "clicolor", true);
154        assert!(
155            engine_state
156                .get_config()
157                .use_ansi_coloring
158                .get(&engine_state)
159        );
160        set_env(&mut engine_state, "no_color", true);
161        assert!(
162            engine_state
163                .get_config()
164                .use_ansi_coloring
165                .get(&engine_state)
166        );
167        set_env(&mut engine_state, "force_color", true);
168        assert!(
169            engine_state
170                .get_config()
171                .use_ansi_coloring
172                .get(&engine_state)
173        );
174    }
175
176    #[test]
177    fn test_use_ansi_coloring_false() {
178        let mut engine_state = EngineState::new();
179        engine_state.config = Config {
180            use_ansi_coloring: UseAnsiColoring::False,
181            ..Default::default()
182        }
183        .into();
184
185        // explicit `False` ignores environment variables
186        assert!(
187            !engine_state
188                .get_config()
189                .use_ansi_coloring
190                .get(&engine_state)
191        );
192
193        set_env(&mut engine_state, "clicolor", false);
194        assert!(
195            !engine_state
196                .get_config()
197                .use_ansi_coloring
198                .get(&engine_state)
199        );
200        set_env(&mut engine_state, "clicolor", true);
201        assert!(
202            !engine_state
203                .get_config()
204                .use_ansi_coloring
205                .get(&engine_state)
206        );
207        set_env(&mut engine_state, "no_color", true);
208        assert!(
209            !engine_state
210                .get_config()
211                .use_ansi_coloring
212                .get(&engine_state)
213        );
214        set_env(&mut engine_state, "force_color", true);
215        assert!(
216            !engine_state
217                .get_config()
218                .use_ansi_coloring
219                .get(&engine_state)
220        );
221    }
222
223    #[test]
224    fn test_use_ansi_coloring_auto() {
225        let mut engine_state = EngineState::new();
226        engine_state.config = Config {
227            use_ansi_coloring: UseAnsiColoring::Auto,
228            ..Default::default()
229        }
230        .into();
231
232        // no environment variables, behavior depends on terminal state
233        let is_terminal = std::io::stdout().is_terminal();
234        assert_eq!(
235            engine_state
236                .get_config()
237                .use_ansi_coloring
238                .get(&engine_state),
239            is_terminal
240        );
241
242        // `clicolor` determines ANSI behavior if no higher-priority variables are set
243        set_env(&mut engine_state, "clicolor", true);
244        assert!(
245            engine_state
246                .get_config()
247                .use_ansi_coloring
248                .get(&engine_state)
249        );
250
251        set_env(&mut engine_state, "clicolor", false);
252        assert!(
253            !engine_state
254                .get_config()
255                .use_ansi_coloring
256                .get(&engine_state)
257        );
258
259        // `no_color` overrides `clicolor` and terminal state
260        set_env(&mut engine_state, "no_color", true);
261        assert!(
262            !engine_state
263                .get_config()
264                .use_ansi_coloring
265                .get(&engine_state)
266        );
267
268        // `force_color` overrides everything
269        set_env(&mut engine_state, "force_color", true);
270        assert!(
271            engine_state
272                .get_config()
273                .use_ansi_coloring
274                .get(&engine_state)
275        );
276    }
277}