From 5f3c8d45d8dc1c00e538b3fadbd772fb77ec23a7 Mon Sep 17 00:00:00 2001 From: Piepmatz Date: Thu, 26 Dec 2024 18:00:01 +0100 Subject: [PATCH] Add `auto` option for `config.use_ansi_coloring` (#14647) # Description In this PR I continued the idea of #11494, it added an `auto` option to the ansi coloring config option, I did this too but in a more simple approach. So I added a new enum `UseAnsiColoring` with the three values `True`, `False` and `Auto`. When that value is set to `auto`, the default value, it will use `std::io::stdout().is_terminal()` to decided whether to use ansi coloring. This allows to dynamically decide whether to print ansi color codes or not, [cargo does it the same way](https://github.com/rust-lang/cargo/blob/652623b779c88fe44afede28bf7f1c9c07812511/src/bin/cargo/main.rs#L72). `True` and `False` act as overrides to the `is_terminal` check. So with that PR it is possible to force ansi colors on the `table` command or automatically remove them from the miette errors if no terminal is used. # User-Facing Changes Terminal users shouldn't be affected by this change as the default value was `true` and `is_terminal` returns for terminals `true` (duh). Non-terminal users, that use `nu` in some embedded way or the engine implemented in some other way (like my jupyter kernel) will now have by default no ansi coloring and need to enable it manually if their environment allows it. # Tests + Formatting The test for fancy errors expected ansi codes, since tests aren't run "in terminal", the ansi codes got stripped away. I added a line that forced ansi colors above it. I'm not sure if that should be the case or if we should test against no ansi colors. - :green_circle: `toolkit fmt` - :green_circle: `toolkit clippy` - :green_circle: `toolkit test` - :green_circle: `toolkit test stdlib` # After Submitting This should resolve #11464 and partially #11847. This also closes #11494. --- .../src/completions/completion_common.rs | 16 +- crates/nu-cli/src/config_files.rs | 20 +- crates/nu-cli/src/repl.rs | 6 +- crates/nu-cli/src/util.rs | 5 +- crates/nu-command/src/help/help_aliases.rs | 2 +- crates/nu-command/src/help/help_modules.rs | 2 +- crates/nu-command/src/platform/ansi/ansi_.rs | 10 +- crates/nu-command/src/system/run_external.rs | 6 +- crates/nu-command/src/viewers/griddle.rs | 2 +- crates/nu-command/src/viewers/table.rs | 31 +-- crates/nu-engine/src/documentation.rs | 4 +- .../nu-protocol/src/config/ansi_coloring.rs | 248 ++++++++++++++++++ crates/nu-protocol/src/config/mod.rs | 6 +- crates/nu-protocol/src/config/output.rs | 3 +- crates/nu-protocol/src/errors/cli_error.rs | 2 +- crates/nu-protocol/src/value/mod.rs | 69 +++++ crates/nu-protocol/tests/test_config.rs | 3 +- .../nu-utils/src/default_files/doc_config.nu | 20 +- src/config_files.rs | 4 +- src/main.rs | 5 +- src/run.rs | 5 +- 21 files changed, 411 insertions(+), 58 deletions(-) create mode 100644 crates/nu-protocol/src/config/ansi_coloring.rs diff --git a/crates/nu-cli/src/completions/completion_common.rs b/crates/nu-cli/src/completions/completion_common.rs index 4b5ef857e1..4969cbd51d 100644 --- a/crates/nu-cli/src/completions/completion_common.rs +++ b/crates/nu-cli/src/completions/completion_common.rs @@ -196,14 +196,14 @@ pub fn complete_item( .map(|cwd| Path::new(cwd.as_ref()).to_path_buf()) .collect(); let ls_colors = (engine_state.config.completions.use_ls_colors - && engine_state.config.use_ansi_coloring) - .then(|| { - let ls_colors_env_str = match stack.get_env_var(engine_state, "LS_COLORS") { - Some(v) => env_to_string("LS_COLORS", v, engine_state, stack).ok(), - None => None, - }; - get_ls_colors(ls_colors_env_str) - }); + && engine_state.config.use_ansi_coloring.get(engine_state)) + .then(|| { + let ls_colors_env_str = match stack.get_env_var(engine_state, "LS_COLORS") { + Some(v) => env_to_string("LS_COLORS", v, engine_state, stack).ok(), + None => None, + }; + get_ls_colors(ls_colors_env_str) + }); let mut cwds = cwd_pathbufs.clone(); let mut prefix_len = 0; diff --git a/crates/nu-cli/src/config_files.rs b/crates/nu-cli/src/config_files.rs index 7627252acc..28332fe998 100644 --- a/crates/nu-cli/src/config_files.rs +++ b/crates/nu-cli/src/config_files.rs @@ -49,7 +49,10 @@ pub fn read_plugin_file(engine_state: &mut EngineState, plugin_file: Option bool { perf!( "migrate old plugin file", start_time, - engine_state.get_config().use_ansi_coloring + engine_state + .get_config() + .use_ansi_coloring + .get(&engine_state) ); true } diff --git a/crates/nu-cli/src/repl.rs b/crates/nu-cli/src/repl.rs index 06ecd1d49a..19f4aba22f 100644 --- a/crates/nu-cli/src/repl.rs +++ b/crates/nu-cli/src/repl.rs @@ -61,7 +61,7 @@ pub fn evaluate_repl( // from the Arc. This lets us avoid copying stack variables needlessly let mut unique_stack = stack.clone(); let config = engine_state.get_config(); - let use_color = config.use_ansi_coloring; + let use_color = config.use_ansi_coloring.get(engine_state); confirm_stdin_is_terminal()?; @@ -390,7 +390,7 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) { ))) .with_quick_completions(config.completions.quick) .with_partial_completions(config.completions.partial) - .with_ansi_colors(config.use_ansi_coloring) + .with_ansi_colors(config.use_ansi_coloring.get(engine_state)) .with_cwd(Some( engine_state .cwd(None) @@ -410,7 +410,7 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) { let style_computer = StyleComputer::from_config(engine_state, &stack_arc); start_time = std::time::Instant::now(); - line_editor = if config.use_ansi_coloring { + line_editor = if config.use_ansi_coloring.get(engine_state) { line_editor.with_hinter(Box::new({ // As of Nov 2022, "hints" color_config closures only get `null` passed in. let style = style_computer.compute("hints", &Value::nothing(Span::unknown())); diff --git a/crates/nu-cli/src/util.rs b/crates/nu-cli/src/util.rs index 50db15a68e..6b0bade8c6 100644 --- a/crates/nu-cli/src/util.rs +++ b/crates/nu-cli/src/util.rs @@ -265,7 +265,10 @@ pub fn eval_source( perf!( &format!("eval_source {}", &fname), start_time, - engine_state.get_config().use_ansi_coloring + engine_state + .get_config() + .use_ansi_coloring + .get(engine_state) ); exit_code diff --git a/crates/nu-command/src/help/help_aliases.rs b/crates/nu-command/src/help/help_aliases.rs index 0d6d4b4f15..0e5241e6a9 100644 --- a/crates/nu-command/src/help/help_aliases.rs +++ b/crates/nu-command/src/help/help_aliases.rs @@ -144,7 +144,7 @@ pub fn help_aliases( long_desc.push_str(&format!("{G}Expansion{RESET}:\n {alias_expansion}")); let config = stack.get_config(engine_state); - if !config.use_ansi_coloring { + if !config.use_ansi_coloring.get(engine_state) { long_desc = nu_utils::strip_ansi_string_likely(long_desc); } diff --git a/crates/nu-command/src/help/help_modules.rs b/crates/nu-command/src/help/help_modules.rs index 579bd3f768..84fff36dfb 100644 --- a/crates/nu-command/src/help/help_modules.rs +++ b/crates/nu-command/src/help/help_modules.rs @@ -231,7 +231,7 @@ pub fn help_modules( } let config = stack.get_config(engine_state); - if !config.use_ansi_coloring { + if !config.use_ansi_coloring.get(engine_state) { long_desc = nu_utils::strip_ansi_string_likely(long_desc); } diff --git a/crates/nu-command/src/platform/ansi/ansi_.rs b/crates/nu-command/src/platform/ansi/ansi_.rs index fe66cb9e15..4e40f7df3f 100644 --- a/crates/nu-command/src/platform/ansi/ansi_.rs +++ b/crates/nu-command/src/platform/ansi/ansi_.rs @@ -654,7 +654,10 @@ Operating system commands: let list: bool = call.has_flag(engine_state, stack, "list")?; let escape: bool = call.has_flag(engine_state, stack, "escape")?; let osc: bool = call.has_flag(engine_state, stack, "osc")?; - let use_ansi_coloring = stack.get_config(engine_state).use_ansi_coloring; + let use_ansi_coloring = stack + .get_config(engine_state) + .use_ansi_coloring + .get(engine_state); if list { return Ok(generate_ansi_code_list( @@ -691,7 +694,10 @@ Operating system commands: let list: bool = call.has_flag_const(working_set, "list")?; let escape: bool = call.has_flag_const(working_set, "escape")?; let osc: bool = call.has_flag_const(working_set, "osc")?; - let use_ansi_coloring = working_set.get_config().use_ansi_coloring; + let use_ansi_coloring = working_set + .get_config() + .use_ansi_coloring + .get(working_set.permanent()); if list { return Ok(generate_ansi_code_list( diff --git a/crates/nu-command/src/system/run_external.rs b/crates/nu-command/src/system/run_external.rs index e3d74c055b..304b0fff8d 100644 --- a/crates/nu-command/src/system/run_external.rs +++ b/crates/nu-command/src/system/run_external.rs @@ -1,7 +1,9 @@ use nu_cmd_base::hook::eval_hook; use nu_engine::{command_prelude::*, env_to_strings}; use nu_path::{dots::expand_ndots, expand_tilde, AbsolutePath}; -use nu_protocol::{did_you_mean, process::ChildProcess, ByteStream, NuGlob, OutDest, Signals}; +use nu_protocol::{ + did_you_mean, process::ChildProcess, ByteStream, NuGlob, OutDest, Signals, UseAnsiColoring, +}; use nu_system::ForegroundChild; use nu_utils::IgnoreCaseExt; use pathdiff::diff_paths; @@ -417,7 +419,7 @@ fn write_pipeline_data( stack.start_collect_value(); // Turn off color as we pass data through - Arc::make_mut(&mut engine_state.config).use_ansi_coloring = false; + Arc::make_mut(&mut engine_state.config).use_ansi_coloring = UseAnsiColoring::False; // Invoke the `table` command. let output = diff --git a/crates/nu-command/src/viewers/griddle.rs b/crates/nu-command/src/viewers/griddle.rs index 23b8259ddd..336026fa38 100644 --- a/crates/nu-command/src/viewers/griddle.rs +++ b/crates/nu-command/src/viewers/griddle.rs @@ -72,7 +72,7 @@ prints out the list properly."# None => None, }; - let use_color: bool = color_param && config.use_ansi_coloring; + let use_color: bool = color_param && config.use_ansi_coloring.get(engine_state); let cwd = engine_state.cwd(Some(stack))?; match input { diff --git a/crates/nu-command/src/viewers/table.rs b/crates/nu-command/src/viewers/table.rs index 4f74e4b0a8..3c699f52df 100644 --- a/crates/nu-command/src/viewers/table.rs +++ b/crates/nu-command/src/viewers/table.rs @@ -15,12 +15,7 @@ use nu_table::{ StringResult, TableOpts, TableOutput, }; use nu_utils::{get_ls_colors, terminal_size}; -use std::{ - collections::VecDeque, - io::{IsTerminal, Read}, - path::PathBuf, - str::FromStr, -}; +use std::{collections::VecDeque, io::Read, path::PathBuf, str::FromStr}; use url::Url; use web_time::Instant; @@ -231,6 +226,7 @@ struct TableConfig { term_width: usize, theme: TableMode, abbreviation: Option, + use_ansi_coloring: bool, } impl TableConfig { @@ -240,6 +236,7 @@ impl TableConfig { theme: TableMode, abbreviation: Option, index: Option, + use_ansi_coloring: bool, ) -> Self { Self { index, @@ -247,6 +244,7 @@ impl TableConfig { term_width, abbreviation, theme, + use_ansi_coloring, } } } @@ -280,12 +278,15 @@ fn parse_table_config( let term_width = get_width_param(width_param); + let use_ansi_coloring = state.get_config().use_ansi_coloring.get(state); + Ok(TableConfig::new( table_view, term_width, theme, abbrivation, index, + use_ansi_coloring, )) } @@ -563,7 +564,7 @@ fn handle_record( let result = build_table_kv(record, cfg.table_view, opts, span)?; let result = match result { - Some(output) => maybe_strip_color(output, &config), + Some(output) => maybe_strip_color(output, cfg.use_ansi_coloring), None => report_unsuccessful_output(input.engine_state.signals(), cfg.term_width), }; @@ -947,14 +948,9 @@ impl Iterator for PagingTableCreator { self.row_offset += batch_size; - let config = { - let state = &self.engine_state; - let stack = &self.stack; - stack.get_config(state) - }; convert_table_to_output( table, - &config, + &self.cfg, self.engine_state.signals(), self.cfg.term_width, ) @@ -1116,9 +1112,8 @@ enum TableView { }, } -fn maybe_strip_color(output: String, config: &Config) -> String { - // the terminal is for when people do ls from vim, there should be no coloring there - if !config.use_ansi_coloring || !std::io::stdout().is_terminal() { +fn maybe_strip_color(output: String, use_ansi_coloring: bool) -> String { + if !use_ansi_coloring { // Draw the table without ansi colors nu_utils::strip_ansi_string_likely(output) } else { @@ -1154,13 +1149,13 @@ fn create_empty_placeholder( fn convert_table_to_output( table: Result, ShellError>, - config: &Config, + cfg: &TableConfig, signals: &Signals, term_width: usize, ) -> Option, ShellError>> { match table { Ok(Some(table)) => { - let table = maybe_strip_color(table, config); + let table = maybe_strip_color(table, cfg.use_ansi_coloring); let mut bytes = table.as_bytes().to_vec(); bytes.push(b'\n'); // nu-table tables don't come with a newline on the end diff --git a/crates/nu-engine/src/documentation.rs b/crates/nu-engine/src/documentation.rs index 81add7af4d..4598e1b325 100644 --- a/crates/nu-engine/src/documentation.rs +++ b/crates/nu-engine/src/documentation.rs @@ -258,7 +258,7 @@ fn get_documentation( long_desc.push_str(" "); long_desc.push_str(example.description); - if !nu_config.use_ansi_coloring { + if !nu_config.use_ansi_coloring.get(engine_state) { let _ = write!(long_desc, "\n > {}\n", example.example); } else { let code_string = nu_highlight_string(example.example, engine_state, stack); @@ -329,7 +329,7 @@ fn get_documentation( long_desc.push('\n'); - if !nu_config.use_ansi_coloring { + if !nu_config.use_ansi_coloring.get(engine_state) { nu_utils::strip_ansi_string_likely(long_desc) } else { long_desc diff --git a/crates/nu-protocol/src/config/ansi_coloring.rs b/crates/nu-protocol/src/config/ansi_coloring.rs new file mode 100644 index 0000000000..408eee52cc --- /dev/null +++ b/crates/nu-protocol/src/config/ansi_coloring.rs @@ -0,0 +1,248 @@ +use super::{ConfigErrors, ConfigPath, IntoValue, ShellError, UpdateFromValue, Value}; +use crate::{self as nu_protocol, engine::EngineState, FromValue}; +use serde::{Deserialize, Serialize}; +use std::io::IsTerminal; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, IntoValue, Serialize, Deserialize)] +pub enum UseAnsiColoring { + #[default] + Auto, + True, + False, +} + +impl UseAnsiColoring { + /// Determines whether ANSI colors should be used. + /// + /// This method evaluates the `UseAnsiColoring` setting and considers environment variables + /// (`FORCE_COLOR`, `NO_COLOR`, and `CLICOLOR`) when the value is set to `Auto`. + /// The configuration value (`UseAnsiColoring`) takes precedence over environment variables, as + /// it is more direct and internally may be modified to override ANSI coloring behavior. + /// + /// Most users should have the default value `Auto` which allows the environment variables to + /// control ANSI coloring. + /// However, when explicitly set to `True` or `False`, the environment variables are ignored. + /// + /// Behavior based on `UseAnsiColoring`: + /// - `True`: Forces ANSI colors to be enabled, ignoring terminal support and environment variables. + /// - `False`: Disables ANSI colors completely. + /// - `Auto`: Determines whether ANSI colors should be used based on environment variables and terminal support. + /// + /// When set to `Auto`, the following environment variables are checked in order: + /// 1. `FORCE_COLOR`: If set, ANSI colors are always enabled, overriding all other settings. + /// 2. `NO_COLOR`: If set, ANSI colors are disabled, overriding `CLICOLOR` and terminal checks. + /// 3. `CLICOLOR`: If set, its value determines whether ANSI colors are enabled (`1` for enabled, `0` for disabled). + /// + /// If none of these variables are set, ANSI coloring is enabled only if the standard output is + /// a terminal. + /// + /// By prioritizing the `UseAnsiColoring` value, we ensure predictable behavior and prevent + /// conflicts with internal overrides that depend on this configuration. + pub fn get(self, engine_state: &EngineState) -> bool { + let is_terminal = match self { + Self::Auto => std::io::stdout().is_terminal(), + Self::True => return true, + Self::False => return false, + }; + + let env_value = |env_name| { + engine_state + .get_env_var_insensitive(env_name) + .and_then(Value::as_env_bool) + .unwrap_or(false) + }; + + if env_value("force_color") { + return true; + } + + if env_value("no_color") { + return false; + } + + if let Some(cli_color) = engine_state.get_env_var_insensitive("clicolor") { + if let Some(cli_color) = cli_color.as_env_bool() { + return cli_color; + } + } + + is_terminal + } +} + +impl From for UseAnsiColoring { + fn from(value: bool) -> Self { + match value { + true => Self::True, + false => Self::False, + } + } +} + +impl FromValue for UseAnsiColoring { + fn from_value(v: Value) -> Result { + if let Ok(v) = v.as_bool() { + return Ok(v.into()); + } + + #[derive(FromValue)] + enum UseAnsiColoringString { + Auto = 0, + True = 1, + False = 2, + } + + Ok(match UseAnsiColoringString::from_value(v)? { + UseAnsiColoringString::Auto => Self::Auto, + UseAnsiColoringString::True => Self::True, + UseAnsiColoringString::False => Self::False, + }) + } +} + +impl UpdateFromValue for UseAnsiColoring { + fn update<'a>( + &mut self, + value: &'a Value, + path: &mut ConfigPath<'a>, + errors: &mut ConfigErrors, + ) { + let Ok(value) = UseAnsiColoring::from_value(value.clone()) else { + errors.type_mismatch(path, UseAnsiColoring::expected_type(), value); + return; + }; + + *self = value; + } +} + +#[cfg(test)] +mod tests { + use super::*; + use nu_protocol::Config; + + fn set_env(engine_state: &mut EngineState, name: &str, value: bool) { + engine_state.add_env_var(name.to_string(), Value::test_bool(value)); + } + + #[test] + fn test_use_ansi_coloring_true() { + let mut engine_state = EngineState::new(); + engine_state.config = Config { + use_ansi_coloring: UseAnsiColoring::True, + ..Default::default() + } + .into(); + + // explicit `True` ignores environment variables + assert!(engine_state + .get_config() + .use_ansi_coloring + .get(&engine_state)); + + set_env(&mut engine_state, "clicolor", false); + assert!(engine_state + .get_config() + .use_ansi_coloring + .get(&engine_state)); + set_env(&mut engine_state, "clicolor", true); + assert!(engine_state + .get_config() + .use_ansi_coloring + .get(&engine_state)); + set_env(&mut engine_state, "no_color", true); + assert!(engine_state + .get_config() + .use_ansi_coloring + .get(&engine_state)); + set_env(&mut engine_state, "force_color", true); + assert!(engine_state + .get_config() + .use_ansi_coloring + .get(&engine_state)); + } + + #[test] + fn test_use_ansi_coloring_false() { + let mut engine_state = EngineState::new(); + engine_state.config = Config { + use_ansi_coloring: UseAnsiColoring::False, + ..Default::default() + } + .into(); + + // explicit `False` ignores environment variables + assert!(!engine_state + .get_config() + .use_ansi_coloring + .get(&engine_state)); + + set_env(&mut engine_state, "clicolor", false); + assert!(!engine_state + .get_config() + .use_ansi_coloring + .get(&engine_state)); + set_env(&mut engine_state, "clicolor", true); + assert!(!engine_state + .get_config() + .use_ansi_coloring + .get(&engine_state)); + set_env(&mut engine_state, "no_color", true); + assert!(!engine_state + .get_config() + .use_ansi_coloring + .get(&engine_state)); + set_env(&mut engine_state, "force_color", true); + assert!(!engine_state + .get_config() + .use_ansi_coloring + .get(&engine_state)); + } + + #[test] + fn test_use_ansi_coloring_auto() { + let mut engine_state = EngineState::new(); + engine_state.config = Config { + use_ansi_coloring: UseAnsiColoring::Auto, + ..Default::default() + } + .into(); + + // no environment variables, behavior depends on terminal state + let is_terminal = std::io::stdout().is_terminal(); + assert_eq!( + engine_state + .get_config() + .use_ansi_coloring + .get(&engine_state), + is_terminal + ); + + // `clicolor` determines ANSI behavior if no higher-priority variables are set + set_env(&mut engine_state, "clicolor", true); + assert!(engine_state + .get_config() + .use_ansi_coloring + .get(&engine_state)); + + set_env(&mut engine_state, "clicolor", false); + assert!(!engine_state + .get_config() + .use_ansi_coloring + .get(&engine_state)); + + // `no_color` overrides `clicolor` and terminal state + set_env(&mut engine_state, "no_color", true); + assert!(!engine_state + .get_config() + .use_ansi_coloring + .get(&engine_state)); + + // `force_color` overrides everything + set_env(&mut engine_state, "force_color", true); + assert!(engine_state + .get_config() + .use_ansi_coloring + .get(&engine_state)); + } +} diff --git a/crates/nu-protocol/src/config/mod.rs b/crates/nu-protocol/src/config/mod.rs index d2ee40a2a0..6ce7335e9c 100644 --- a/crates/nu-protocol/src/config/mod.rs +++ b/crates/nu-protocol/src/config/mod.rs @@ -6,6 +6,7 @@ use helper::*; use prelude::*; use std::collections::HashMap; +pub use ansi_coloring::UseAnsiColoring; pub use completions::{ CompletionAlgorithm, CompletionConfig, CompletionSort, ExternalCompleterConfig, }; @@ -23,6 +24,7 @@ pub use rm::RmConfig; pub use shell_integration::ShellIntegrationConfig; pub use table::{FooterMode, TableConfig, TableIndexMode, TableMode, TrimStrategy}; +mod ansi_coloring; mod completions; mod datetime_format; mod display_errors; @@ -49,7 +51,7 @@ pub struct Config { pub footer_mode: FooterMode, pub float_precision: i64, pub recursion_limit: i64, - pub use_ansi_coloring: bool, + pub use_ansi_coloring: UseAnsiColoring, pub completions: CompletionConfig, pub edit_mode: EditBindings, pub history: HistoryConfig, @@ -106,7 +108,7 @@ impl Default for Config { footer_mode: FooterMode::RowCount(25), float_precision: 2, buffer_editor: Value::nothing(Span::unknown()), - use_ansi_coloring: true, + use_ansi_coloring: UseAnsiColoring::default(), bracketed_paste: true, edit_mode: EditBindings::default(), diff --git a/crates/nu-protocol/src/config/output.rs b/crates/nu-protocol/src/config/output.rs index 8db3467d6e..082856bdce 100644 --- a/crates/nu-protocol/src/config/output.rs +++ b/crates/nu-protocol/src/config/output.rs @@ -1,5 +1,6 @@ use super::{config_update_string_enum, prelude::*}; -use crate as nu_protocol; + +use crate::{self as nu_protocol}; #[derive(Clone, Copy, Debug, IntoValue, PartialEq, Eq, Serialize, Deserialize)] pub enum ErrorStyle { diff --git a/crates/nu-protocol/src/errors/cli_error.rs b/crates/nu-protocol/src/errors/cli_error.rs index 5680ab172c..ecf35e247d 100644 --- a/crates/nu-protocol/src/errors/cli_error.rs +++ b/crates/nu-protocol/src/errors/cli_error.rs @@ -70,7 +70,7 @@ impl std::fmt::Debug for CliError<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let config = self.1.get_config(); - let ansi_support = config.use_ansi_coloring; + let ansi_support = config.use_ansi_coloring.get(self.1.permanent()); let error_style = &config.error_style; diff --git a/crates/nu-protocol/src/value/mod.rs b/crates/nu-protocol/src/value/mod.rs index c12dd3d71d..ac663f4c49 100644 --- a/crates/nu-protocol/src/value/mod.rs +++ b/crates/nu-protocol/src/value/mod.rs @@ -655,6 +655,36 @@ impl Value { } } + /// Interprets this `Value` as a boolean based on typical conventions for environment values. + /// + /// The following rules are used: + /// - Values representing `false`: + /// - Empty strings + /// - The number `0` (as an integer, float or string) + /// - `Nothing` + /// - Explicit boolean `false` + /// - Values representing `true`: + /// - Non-zero numbers (integer or float) + /// - Non-empty strings + /// - Explicit boolean `true` + /// + /// For all other, more complex variants of [`Value`], the function cannot determine a + /// boolean representation and returns `None`. + pub fn as_env_bool(&self) -> Option { + match self { + Value::Bool { val: false, .. } + | Value::Int { val: 0, .. } + | Value::Float { val: 0.0, .. } + | Value::Nothing { .. } => Some(false), + Value::String { val, .. } => match val.as_str() { + "" | "0" => Some(false), + _ => Some(true), + }, + Value::Bool { .. } | Value::Int { .. } | Value::Float { .. } => Some(true), + _ => None, + } + } + /// Returns a reference to the inner [`CustomValue`] trait object or an error if this `Value` is not a custom value pub fn as_custom_value(&self) -> Result<&dyn CustomValue, ShellError> { if let Value::Custom { val, .. } = self { @@ -3874,4 +3904,43 @@ mod tests { assert_eq!("-0316-02-11T06:13:20+00:00", formatted); } } + + #[test] + fn test_env_as_bool() { + // explicit false values + assert_eq!(Value::test_bool(false).as_env_bool(), Some(false)); + assert_eq!(Value::test_int(0).as_env_bool(), Some(false)); + assert_eq!(Value::test_float(0.0).as_env_bool(), Some(false)); + assert_eq!(Value::test_string("").as_env_bool(), Some(false)); + assert_eq!(Value::test_string("0").as_env_bool(), Some(false)); + assert_eq!(Value::test_nothing().as_env_bool(), Some(false)); + + // explicit true values + assert_eq!(Value::test_bool(true).as_env_bool(), Some(true)); + assert_eq!(Value::test_int(1).as_env_bool(), Some(true)); + assert_eq!(Value::test_float(1.0).as_env_bool(), Some(true)); + assert_eq!(Value::test_string("1").as_env_bool(), Some(true)); + + // implicit true values + assert_eq!(Value::test_int(42).as_env_bool(), Some(true)); + assert_eq!(Value::test_float(0.5).as_env_bool(), Some(true)); + assert_eq!(Value::test_string("not zero").as_env_bool(), Some(true)); + + // complex values returning None + assert_eq!(Value::test_record(Record::default()).as_env_bool(), None); + assert_eq!( + Value::test_list(vec![Value::test_int(1)]).as_env_bool(), + None + ); + assert_eq!( + Value::test_date( + chrono::DateTime::parse_from_rfc3339("2024-01-01T12:00:00+00:00").unwrap(), + ) + .as_env_bool(), + None + ); + assert_eq!(Value::test_glob("*.rs").as_env_bool(), None); + assert_eq!(Value::test_binary(vec![1, 2, 3]).as_env_bool(), None); + assert_eq!(Value::test_duration(3600).as_env_bool(), None); + } } diff --git a/crates/nu-protocol/tests/test_config.rs b/crates/nu-protocol/tests/test_config.rs index eb57903685..81a0a7dede 100644 --- a/crates/nu-protocol/tests/test_config.rs +++ b/crates/nu-protocol/tests/test_config.rs @@ -53,6 +53,7 @@ fn filesize_format_auto_metric_false() { #[test] fn fancy_default_errors() { let code = nu_repl_code(&[ + "$env.config.use_ansi_coloring = true", r#"def force_error [x] { error make { msg: "oh no!" @@ -69,7 +70,7 @@ fn fancy_default_errors() { assert_eq!( actual.err, - "Error: \n \u{1b}[31m×\u{1b}[0m oh no!\n ╭─[\u{1b}[36;1;4mline1:1:13\u{1b}[0m]\n \u{1b}[2m1\u{1b}[0m │ force_error \"My error\"\n · \u{1b}[35;1m ─────┬────\u{1b}[0m\n · \u{1b}[35;1m╰── \u{1b}[35;1mhere's the error\u{1b}[0m\u{1b}[0m\n ╰────\n\n" + "Error: \n \u{1b}[31m×\u{1b}[0m oh no!\n ╭─[\u{1b}[36;1;4mline2:1:13\u{1b}[0m]\n \u{1b}[2m1\u{1b}[0m │ force_error \"My error\"\n · \u{1b}[35;1m ─────┬────\u{1b}[0m\n · \u{1b}[35;1m╰── \u{1b}[35;1mhere's the error\u{1b}[0m\u{1b}[0m\n ╰────\n\n" ); } diff --git a/crates/nu-utils/src/default_files/doc_config.nu b/crates/nu-utils/src/default_files/doc_config.nu index db83e11110..08de7724f2 100644 --- a/crates/nu-utils/src/default_files/doc_config.nu +++ b/crates/nu-utils/src/default_files/doc_config.nu @@ -244,12 +244,20 @@ $env.config.shell_integration.reset_application_mode = true # Nushell. $env.config.bracketed_paste = true -# use_ansi_coloring (bool): -# true/false to enable/disable the use of ANSI colors in Nushell internal commands. -# When disabled, output from Nushell built-in commands will display only in the default -# foreground color. -# Note: Does not apply to the `ansi` command. -$env.config.use_ansi_coloring = true +# use_ansi_coloring ("auto" or bool): +# The default value `"auto"` dynamically determines if ANSI coloring is used. +# It evaluates the following environment variables in decreasingly priority: +# `FORCE_COLOR`, `NO_COLOR`, and `CLICOLOR`. +# - If `FORCE_COLOR` is set, coloring is always enabled. +# - If `NO_COLOR` is set, coloring is disabled. +# - If `CLICOLOR` is set, its value (0 or 1) decides whether coloring is used. +# If none of these are set, it checks whether the standard output is a terminal +# and enables coloring if it is. +# A value of `true` or `false` overrides this behavior, explicitly enabling or +# disabling ANSI coloring in Nushell's internal commands. +# When disabled, built-in commands will only use the default foreground color. +# Note: This setting does not affect the `ansi` command. +$env.config.use_ansi_coloring = "auto" # ---------------------- # Error Display Settings diff --git a/src/config_files.rs b/src/config_files.rs index 5c43764580..6a612ea4a7 100644 --- a/src/config_files.rs +++ b/src/config_files.rs @@ -39,7 +39,7 @@ pub(crate) fn read_config_file( let start_time = std::time::Instant::now(); let config = engine_state.get_config(); - let use_color = config.use_ansi_coloring; + let use_color = config.use_ansi_coloring.get(engine_state); // Translate environment variables from Strings to Values if let Err(e) = convert_env_values(engine_state, stack) { report_shell_error(engine_state, &e); @@ -53,7 +53,7 @@ pub(crate) fn read_config_file( } else { let start_time = std::time::Instant::now(); let config = engine_state.get_config(); - let use_color = config.use_ansi_coloring; + let use_color = config.use_ansi_coloring.get(engine_state); if let Err(e) = convert_env_values(engine_state, stack) { report_shell_error(engine_state, &e); } diff --git a/src/main.rs b/src/main.rs index 4ebc215531..bfae9c2f31 100644 --- a/src/main.rs +++ b/src/main.rs @@ -213,7 +213,10 @@ fn main() -> Result<()> { engine_state.history_enabled = parsed_nu_cli_args.no_history.is_none(); - let use_color = engine_state.get_config().use_ansi_coloring; + let use_color = engine_state + .get_config() + .use_ansi_coloring + .get(&engine_state); // Set up logger if let Some(level) = parsed_nu_cli_args diff --git a/src/run.rs b/src/run.rs index 89bad3866f..640cc9bc35 100644 --- a/src/run.rs +++ b/src/run.rs @@ -197,7 +197,10 @@ pub(crate) fn run_repl( } // Reload use_color from config in case it's different from the default value - let use_color = engine_state.get_config().use_ansi_coloring; + let use_color = engine_state + .get_config() + .use_ansi_coloring + .get(engine_state); perf!("setup_config", start_time, use_color); let start_time = std::time::Instant::now();