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.config = Config {
139            use_ansi_coloring: UseAnsiColoring::True,
140            ..Default::default()
141        }
142        .into();
143
144        // explicit `True` ignores environment variables
145        assert!(
146            engine_state
147                .get_config()
148                .use_ansi_coloring
149                .get(&engine_state)
150        );
151
152        set_env(&mut engine_state, "clicolor", false);
153        assert!(
154            engine_state
155                .get_config()
156                .use_ansi_coloring
157                .get(&engine_state)
158        );
159        set_env(&mut engine_state, "clicolor", true);
160        assert!(
161            engine_state
162                .get_config()
163                .use_ansi_coloring
164                .get(&engine_state)
165        );
166        set_env(&mut engine_state, "no_color", true);
167        assert!(
168            engine_state
169                .get_config()
170                .use_ansi_coloring
171                .get(&engine_state)
172        );
173        set_env(&mut engine_state, "force_color", true);
174        assert!(
175            engine_state
176                .get_config()
177                .use_ansi_coloring
178                .get(&engine_state)
179        );
180    }
181
182    #[test]
183    fn test_use_ansi_coloring_false() {
184        let mut engine_state = EngineState::new();
185        engine_state.config = Config {
186            use_ansi_coloring: UseAnsiColoring::False,
187            ..Default::default()
188        }
189        .into();
190
191        // explicit `False` ignores environment variables
192        assert!(
193            !engine_state
194                .get_config()
195                .use_ansi_coloring
196                .get(&engine_state)
197        );
198
199        set_env(&mut engine_state, "clicolor", false);
200        assert!(
201            !engine_state
202                .get_config()
203                .use_ansi_coloring
204                .get(&engine_state)
205        );
206        set_env(&mut engine_state, "clicolor", true);
207        assert!(
208            !engine_state
209                .get_config()
210                .use_ansi_coloring
211                .get(&engine_state)
212        );
213        set_env(&mut engine_state, "no_color", true);
214        assert!(
215            !engine_state
216                .get_config()
217                .use_ansi_coloring
218                .get(&engine_state)
219        );
220        set_env(&mut engine_state, "force_color", true);
221        assert!(
222            !engine_state
223                .get_config()
224                .use_ansi_coloring
225                .get(&engine_state)
226        );
227    }
228
229    #[test]
230    fn test_use_ansi_coloring_auto() {
231        let mut engine_state = EngineState::new();
232        engine_state.config = Config {
233            use_ansi_coloring: UseAnsiColoring::Auto,
234            ..Default::default()
235        }
236        .into();
237
238        // no environment variables, behavior depends on terminal state
239        let is_terminal = std::io::stdout().is_terminal();
240        assert_eq!(
241            engine_state
242                .get_config()
243                .use_ansi_coloring
244                .get(&engine_state),
245            is_terminal
246        );
247
248        // `clicolor` determines ANSI behavior if no higher-priority variables are set
249        set_env(&mut engine_state, "clicolor", true);
250        assert!(
251            engine_state
252                .get_config()
253                .use_ansi_coloring
254                .get(&engine_state)
255        );
256
257        set_env(&mut engine_state, "clicolor", false);
258        assert!(
259            !engine_state
260                .get_config()
261                .use_ansi_coloring
262                .get(&engine_state)
263        );
264
265        // `no_color` overrides `clicolor` and terminal state
266        set_env(&mut engine_state, "no_color", true);
267        assert!(
268            !engine_state
269                .get_config()
270                .use_ansi_coloring
271                .get(&engine_state)
272        );
273
274        // `force_color` overrides everything
275        set_env(&mut engine_state, "force_color", true);
276        assert!(
277            engine_state
278                .get_config()
279                .use_ansi_coloring
280                .get(&engine_state)
281        );
282    }
283
284    #[test]
285    fn test_use_ansi_coloring_auto_term_dumb() {
286        let mut engine_state = EngineState::new();
287        engine_state.config = Config {
288            use_ansi_coloring: UseAnsiColoring::Auto,
289            ..Default::default()
290        }
291        .into();
292
293        // `term` set to "dumb" disables ANSI colors
294        engine_state.add_env_var("term".to_string(), Value::test_string("dumb"));
295        assert!(
296            !engine_state
297                .get_config()
298                .use_ansi_coloring
299                .get(&engine_state)
300        );
301    }
302}