nu_protocol/config/
ansi_coloring.rs

1use super::{ConfigErrors, ConfigPath, IntoValue, ShellError, UpdateFromValue, Value};
2use crate::{self as nu_protocol, engine::EngineState, FromValue};
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!(engine_state
140            .get_config()
141            .use_ansi_coloring
142            .get(&engine_state));
143
144        set_env(&mut engine_state, "clicolor", false);
145        assert!(engine_state
146            .get_config()
147            .use_ansi_coloring
148            .get(&engine_state));
149        set_env(&mut engine_state, "clicolor", true);
150        assert!(engine_state
151            .get_config()
152            .use_ansi_coloring
153            .get(&engine_state));
154        set_env(&mut engine_state, "no_color", true);
155        assert!(engine_state
156            .get_config()
157            .use_ansi_coloring
158            .get(&engine_state));
159        set_env(&mut engine_state, "force_color", true);
160        assert!(engine_state
161            .get_config()
162            .use_ansi_coloring
163            .get(&engine_state));
164    }
165
166    #[test]
167    fn test_use_ansi_coloring_false() {
168        let mut engine_state = EngineState::new();
169        engine_state.config = Config {
170            use_ansi_coloring: UseAnsiColoring::False,
171            ..Default::default()
172        }
173        .into();
174
175        // explicit `False` ignores environment variables
176        assert!(!engine_state
177            .get_config()
178            .use_ansi_coloring
179            .get(&engine_state));
180
181        set_env(&mut engine_state, "clicolor", false);
182        assert!(!engine_state
183            .get_config()
184            .use_ansi_coloring
185            .get(&engine_state));
186        set_env(&mut engine_state, "clicolor", true);
187        assert!(!engine_state
188            .get_config()
189            .use_ansi_coloring
190            .get(&engine_state));
191        set_env(&mut engine_state, "no_color", true);
192        assert!(!engine_state
193            .get_config()
194            .use_ansi_coloring
195            .get(&engine_state));
196        set_env(&mut engine_state, "force_color", true);
197        assert!(!engine_state
198            .get_config()
199            .use_ansi_coloring
200            .get(&engine_state));
201    }
202
203    #[test]
204    fn test_use_ansi_coloring_auto() {
205        let mut engine_state = EngineState::new();
206        engine_state.config = Config {
207            use_ansi_coloring: UseAnsiColoring::Auto,
208            ..Default::default()
209        }
210        .into();
211
212        // no environment variables, behavior depends on terminal state
213        let is_terminal = std::io::stdout().is_terminal();
214        assert_eq!(
215            engine_state
216                .get_config()
217                .use_ansi_coloring
218                .get(&engine_state),
219            is_terminal
220        );
221
222        // `clicolor` determines ANSI behavior if no higher-priority variables are set
223        set_env(&mut engine_state, "clicolor", true);
224        assert!(engine_state
225            .get_config()
226            .use_ansi_coloring
227            .get(&engine_state));
228
229        set_env(&mut engine_state, "clicolor", false);
230        assert!(!engine_state
231            .get_config()
232            .use_ansi_coloring
233            .get(&engine_state));
234
235        // `no_color` overrides `clicolor` and terminal state
236        set_env(&mut engine_state, "no_color", true);
237        assert!(!engine_state
238            .get_config()
239            .use_ansi_coloring
240            .get(&engine_state));
241
242        // `force_color` overrides everything
243        set_env(&mut engine_state, "force_color", true);
244        assert!(engine_state
245            .get_config()
246            .use_ansi_coloring
247            .get(&engine_state));
248    }
249}