diff --git a/src/main.rs b/src/main.rs index e6dbb09..94fde3f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ use clap::Parser; +use std::io::IsTerminal; use tracing_subscriber::EnvFilter; mod cli; @@ -63,6 +64,10 @@ fn main() { EnvFilter::try_from_default_env().unwrap_or_else(|_| "bugsee_cli=info".into()), ) .with_writer(std::io::stderr) + .with_ansi(ansi_enabled( + std::io::stderr().is_terminal(), + std::env::var_os("NO_COLOR").as_deref(), + )) .init(); let runtime = match tokio::runtime::Builder::new_multi_thread() @@ -87,3 +92,28 @@ fn main() { }); std::process::exit(code.as_i32()); } + +/// Colour only an interactive terminal: Xcode / Gradle / CI logs and the +/// detached `bugsee-cli.log` file would otherwise be full of escape codes. +/// Honours (any non-empty `NO_COLOR` disables colour). +fn ansi_enabled(stderr_is_terminal: bool, no_color: Option<&std::ffi::OsStr>) -> bool { + stderr_is_terminal && no_color.is_none_or(|v| v.is_empty()) +} + +#[cfg(test)] +mod ansi_tests { + use super::ansi_enabled; + use std::ffi::OsStr; + + #[test] + fn colour_only_on_a_terminal() { + assert!(ansi_enabled(true, None)); + assert!(!ansi_enabled(false, None)); + } + + #[test] + fn no_color_disables_colour_unless_empty() { + assert!(!ansi_enabled(true, Some(OsStr::new("1")))); + assert!(ansi_enabled(true, Some(OsStr::new("")))); + } +} diff --git a/tests/no_ansi_when_piped.rs b/tests/no_ansi_when_piped.rs new file mode 100644 index 0000000..3096169 --- /dev/null +++ b/tests/no_ansi_when_piped.rs @@ -0,0 +1,30 @@ +//! Log output must carry no ANSI escape codes unless stderr is a terminal. +//! +//! Xcode, Gradle and CI capture stderr into logs, and the detached daemon writes +//! it to `bugsee-cli.log`; escape codes there are noise. Under `assert_cmd` stderr +//! is a pipe, so any ESC byte means the `with_ansi(...)` wiring in `main` regressed. + +mod common; + +/// `sourcemaps inject` on an empty directory needs no network and logs one info line. +#[test] +fn piped_stderr_has_no_escape_codes() { + let dir = tempfile::tempdir().unwrap(); + let out = common::cli() + .env("RUST_LOG", "bugsee_cli=info") + .env_remove("NO_COLOR") + .args(["sourcemaps", "inject"]) + .arg(dir.path()) + .assert() + .success(); + let stderr = out.get_output().stderr.clone(); + let text = String::from_utf8_lossy(&stderr); + assert!( + text.contains("sourcemaps inject complete"), + "expected a log line on stderr, got: {text:?}" + ); + assert!( + !stderr.contains(&0x1b), + "stderr contains ANSI escapes: {text:?}" + ); +}