Skip to main content

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(env_name)
51                .and_then(|v| v.coerce_bool().ok())
52                .unwrap_or(false)
53        };
54
55        if env_value("force_color") {
56            return true;
57        }
58
59        if env_value("no_color") {
60            return false;
61        }
62
63        if let Some(cli_color) = engine_state.get_env_var("clicolor")
64            && let Ok(cli_color) = cli_color.coerce_bool()
65        {
66            return cli_color;
67        }
68
69        // If the TERM environment variable is set to "dumb", disable ANSI colors
70        if let Some(term) = engine_state.get_env_var("term")
71            && term.as_str().ok() == Some("dumb")
72        {
73            return false;
74        }
75
76        is_terminal
77    }
78}
79
80impl From<bool> for UseAnsiColoring {
81    fn from(value: bool) -> Self {
82        match value {
83            true => Self::True,
84            false => Self::False,
85        }
86    }
87}
88
89impl FromValue for UseAnsiColoring {
90    fn from_value(v: Value) -> Result<Self, ShellError> {
91        if let Ok(v) = v.as_bool() {
92            return Ok(v.into());
93        }
94
95        #[derive(FromValue)]
96        enum UseAnsiColoringString {
97            Auto = 0,
98            True = 1,
99            False = 2,
100        }
101
102        Ok(match UseAnsiColoringString::from_value(v)? {
103            UseAnsiColoringString::Auto => Self::Auto,
104            UseAnsiColoringString::True => Self::True,
105            UseAnsiColoringString::False => Self::False,
106        })
107    }
108}
109
110impl UpdateFromValue for UseAnsiColoring {
111    fn update<'a>(
112        &mut self,
113        value: &'a Value,
114        path: &mut ConfigPath<'a>,
115        errors: &mut ConfigErrors,
116    ) {
117        let Ok(value) = UseAnsiColoring::from_value(value.clone()) else {
118            errors.type_mismatch(path, UseAnsiColoring::expected_type(), value);
119            return;
120        };
121
122        *self = value;
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129    use nu_protocol::Config;
130
131    fn set_env(engine_state: &mut EngineState, name: &str, value: bool) {
132        engine_state.add_env_var(name.to_string(), Value::test_bool(value));
133    }
134
135    #[test]
136    fn test_use_ansi_coloring_true() {
137        let mut engine_state = EngineState::new();
138        engine_state.set_config(Config {
139            use_ansi_coloring: UseAnsiColoring::True,
140            ..Default::default()
141        });
142
143        // explicit `True` ignores environment variables
144        assert!(
145            engine_state
146                .get_config()
147                .use_ansi_coloring
148                .get(&engine_state)
149        );
150
151        set_env(&mut engine_state, "clicolor", false);
152        assert!(
153            engine_state
154                .get_config()
155                .use_ansi_coloring
156                .get(&engine_state)
157        );
158        set_env(&mut engine_state, "clicolor", true);
159        assert!(
160            engine_state
161                .get_config()
162                .use_ansi_coloring
163                .get(&engine_state)
164        );
165        set_env(&mut engine_state, "no_color", true);
166        assert!(
167            engine_state
168                .get_config()
169                .use_ansi_coloring
170                .get(&engine_state)
171        );
172        set_env(&mut engine_state, "force_color", true);
173        assert!(
174            engine_state
175                .get_config()
176                .use_ansi_coloring
177                .get(&engine_state)
178        );
179    }
180
181    #[test]
182    fn test_use_ansi_coloring_false() {
183        let mut engine_state = EngineState::new();
184        engine_state.set_config(Config {
185            use_ansi_coloring: UseAnsiColoring::False,
186            ..Default::default()
187        });
188
189        // explicit `False` ignores environment variables
190        assert!(
191            !engine_state
192                .get_config()
193                .use_ansi_coloring
194                .get(&engine_state)
195        );
196
197        set_env(&mut engine_state, "clicolor", false);
198        assert!(
199            !engine_state
200                .get_config()
201                .use_ansi_coloring
202                .get(&engine_state)
203        );
204        set_env(&mut engine_state, "clicolor", true);
205        assert!(
206            !engine_state
207                .get_config()
208                .use_ansi_coloring
209                .get(&engine_state)
210        );
211        set_env(&mut engine_state, "no_color", true);
212        assert!(
213            !engine_state
214                .get_config()
215                .use_ansi_coloring
216                .get(&engine_state)
217        );
218        set_env(&mut engine_state, "force_color", true);
219        assert!(
220            !engine_state
221                .get_config()
222                .use_ansi_coloring
223                .get(&engine_state)
224        );
225    }
226
227    #[test]
228    fn test_use_ansi_coloring_auto() {
229        let mut engine_state = EngineState::new();
230        engine_state.set_config(Config {
231            use_ansi_coloring: UseAnsiColoring::Auto,
232            ..Default::default()
233        });
234
235        // no environment variables, behavior depends on terminal state
236        let is_terminal = std::io::stdout().is_terminal();
237        assert_eq!(
238            engine_state
239                .get_config()
240                .use_ansi_coloring
241                .get(&engine_state),
242            is_terminal
243        );
244
245        // `clicolor` determines ANSI behavior if no higher-priority variables are set
246        set_env(&mut engine_state, "clicolor", true);
247        assert!(
248            engine_state
249                .get_config()
250                .use_ansi_coloring
251                .get(&engine_state)
252        );
253
254        set_env(&mut engine_state, "clicolor", false);
255        assert!(
256            !engine_state
257                .get_config()
258                .use_ansi_coloring
259                .get(&engine_state)
260        );
261
262        // `no_color` overrides `clicolor` and terminal state
263        set_env(&mut engine_state, "no_color", true);
264        assert!(
265            !engine_state
266                .get_config()
267                .use_ansi_coloring
268                .get(&engine_state)
269        );
270
271        // `force_color` overrides everything
272        set_env(&mut engine_state, "force_color", true);
273        assert!(
274            engine_state
275                .get_config()
276                .use_ansi_coloring
277                .get(&engine_state)
278        );
279    }
280
281    #[test]
282    fn test_use_ansi_coloring_auto_term_dumb() {
283        let mut engine_state = EngineState::new();
284        engine_state.set_config(Config {
285            use_ansi_coloring: UseAnsiColoring::Auto,
286            ..Default::default()
287        });
288
289        // `term` set to "dumb" disables ANSI colors
290        engine_state.add_env_var("term".to_string(), Value::test_string("dumb"));
291        assert!(
292            !engine_state
293                .get_config()
294                .use_ansi_coloring
295                .get(&engine_state)
296        );
297    }
298}