From 177881c6a3b8f43bebd3f391c91a66f5b7e2fa25 Mon Sep 17 00:00:00 2001 From: MsfPablo Date: Thu, 13 Aug 2026 15:44:49 +0200 Subject: [PATCH] Honor NO_COLOR without suppressing text attributes Per , the NO_COLOR convention is intended to disable *color* output, not text attributes such as bold, italic, or underline. The current behavior of the StyledObject fmt implementation drops the attribute escapes whenever colors are disabled (which NO_COLOR triggers), contradicting the spec. Likewise, an empty value like `NO_COLOR=""` should not disable color, but `env::var("NO_COLOR").is_ok()` returns true for any value, including the empty string. This commit: * moves the attribute escape emission in impl_fmt! outside the colors_enabled() gate so attrs survive when colors are suppressed; * changes `env::var("NO_COLOR").is_ok()` in the unix and windows backends to a non-empty check that matches the spec; * adds a regression test exercising both behaviors. Fixes #291 --- src/unix_term.rs | 2 +- src/utils.rs | 53 +++++++++++++++++++++++++++++++++++++++++ src/windows_term/mod.rs | 2 +- 3 files changed, 55 insertions(+), 2 deletions(-) diff --git a/src/unix_term.rs b/src/unix_term.rs index 815d976a..154af2c7 100644 --- a/src/unix_term.rs +++ b/src/unix_term.rs @@ -27,7 +27,7 @@ pub(crate) fn is_a_color_terminal(out: &Term) -> bool { return false; } - if env::var("NO_COLOR").is_ok() { + if env::var_os("NO_COLOR").is_some_and(|v| !v.is_empty()) { return false; } diff --git a/src/utils.rs b/src/utils.rs index 3a48628e..e4537e4f 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -823,6 +823,16 @@ macro_rules! impl_fmt { write!(f, "{}", self.style.attrs)?; reset = true; } + } else { + // NO_COLOR and similar controls disable color, but per + // text attributes (bold, italic, + // underline, etc.) are not "color" and should remain + // visible. Emit them outside the colors_enabled gate so + // they survive when colors are suppressed. + if !self.style.attrs.is_empty() { + write!(f, "{}", self.style.attrs)?; + reset = true; + } } fmt::$name::fmt(&self.val, f)?; if reset { @@ -1227,3 +1237,46 @@ fn test_style_from_non_ascii_bg() { // silently ignores non-ascii assert_eq!(parsed_style, Style::default()); } + +#[test] +fn test_attrs_survive_colors_disabled() { + // Regression test for : + // per , `NO_COLOR` suppresses *color*, not text + // attributes. When color output is otherwise disabled, attributes such as + // bold/italic/underline should still be emitted (with a trailing reset). + let prev = colors_enabled(); + set_colors_enabled(false); + + let bold = style("foo").bold().to_string(); + assert!( + bold.contains("\x1b[1m"), + "bold escape sequence missing when colors are disabled: {bold:?}" + ); + assert!( + bold.ends_with("\x1b[0m"), + "reset escape sequence missing after bold: {bold:?}" + ); + + let italic = style("bar").italic().to_string(); + assert!( + italic.contains("\x1b[3m"), + "italic escape sequence missing when colors are disabled: {italic:?}" + ); + assert!( + italic.ends_with("\x1b[0m"), + "reset escape sequence missing after italic: {italic:?}" + ); + + // Foreground color is *still* suppressed when colors are disabled. + let colored = style("baz").red().to_string(); + assert!( + !colored.contains("\x1b[31m"), + "foreground color escape sequence should be suppressed when colors are disabled: {colored:?}" + ); + assert!( + !colored.contains("\x1b[0m"), + "reset escape sequence should not be emitted when nothing is styled: {colored:?}" + ); + + set_colors_enabled(prev); +} diff --git a/src/windows_term/mod.rs b/src/windows_term/mod.rs index 8d8a7803..a967c094 100644 --- a/src/windows_term/mod.rs +++ b/src/windows_term/mod.rs @@ -63,7 +63,7 @@ pub(crate) fn is_a_color_terminal(out: &Term) -> bool { if !is_a_terminal(out) { return false; } - if env::var("NO_COLOR").is_ok() { + if env::var_os("NO_COLOR").is_some_and(|v| !v.is_empty()) { return false; } if msys_tty_on(out) {