diff --git a/jetsocat/src/main.rs b/jetsocat/src/main.rs index c57b8dd90..2efeff64b 100644 --- a/jetsocat/src/main.rs +++ b/jetsocat/src/main.rs @@ -31,11 +31,13 @@ use jetsocat::pipe::PipeMode; use jetsocat::proxy::{ProxyConfig, ProxyType, detect_proxy}; use jmux_proxy::JmuxConfig; use seahorse::{App, Command, Context, Flag, FlagType}; -use std::env; +use std::cmp::PartialEq; use std::error::Error; use std::future::Future; +use std::io::IsTerminal; use std::path::PathBuf; use std::time::Duration; +use std::{env, io}; use tokio::runtime; fn main() { @@ -462,7 +464,7 @@ fn apply_common_flags(cmd: Command) -> Command { .flag(Flag::new("watch-process", FlagType::Int).description("Watch given process and stop piping when it dies")) } -#[derive(Debug)] +#[derive(Debug, PartialEq)] enum Logging { Term, File { filepath: PathBuf, clean_old: bool }, @@ -559,41 +561,17 @@ impl CommonArgs { let coloring = match opt_string_flag(c, "color")?.as_deref() { Some("never") => Coloring::Never, Some("always") => Coloring::Always, - Some("auto") => Coloring::Auto, + Some("auto") | None => Coloring::Auto, Some(_) => anyhow::bail!("invalid value for 'color'; expect: `never`, `always` or `auto`"), - None => { - // Infer using the environment. - parse_env_for_coloring() - } }; - return Ok(Self { + Ok(Self { logging, coloring, proxy_cfg, pipe_timeout, watch_process, - }); - - fn parse_env_for_coloring() -> Coloring { - // https://no-color.org/ - if env::var("NO_COLOR").is_ok() { - return Coloring::Never; - } - - match env::var("FORCE_COLOR").as_deref() { - Ok("0" | "false" | "no" | "off") => return Coloring::Never, - Ok(_) => return Coloring::Always, - Err(_) => {} - } - - match env::var("TERM").as_deref() { - Ok("dumb") => return Coloring::Never, - _ => {} - } - - Coloring::Auto - } + }) } } @@ -976,6 +954,39 @@ struct LoggerGuard { _worker_guard: tracing_appender::non_blocking::WorkerGuard, } +fn is_ansi_supported(logging: &Logging, coloring: Coloring) -> bool { + match coloring { + Coloring::Never => false, + Coloring::Always => true, + Coloring::Auto => { + if env::var("NO_COLOR").is_ok() { + return false; + } + + match env::var("FORCE_COLOR").as_deref() { + Ok("0" | "false" | "no" | "off") => return false, + Ok(_) => return true, + Err(_) => {} + } + + if logging == &Logging::Term { + // Check whether stderr refers to a terminal. If it's redirected or piped, ANSI is disabled. + return if io::stderr().is_terminal() { + if let Ok("dumb") = env::var("TERM").as_deref() { + return false; + } + + true + } else { + false + }; + } + + false + } + } +} + fn setup_logger(logging: &Logging, coloring: Coloring) -> LoggerGuard { use std::fs::OpenOptions; use std::panic; @@ -984,26 +995,16 @@ fn setup_logger(logging: &Logging, coloring: Coloring) -> LoggerGuard { use tracing_subscriber::prelude::*; use tracing_subscriber::{EnvFilter, fmt}; + let ansi = is_ansi_supported(logging, coloring); + let (layer, guard) = match &logging { Logging::Term => { - let ansi = match coloring { - Coloring::Never => false, - Coloring::Always => true, - Coloring::Auto => true, - }; - - let (non_blocking_stdio, guard) = tracing_appender::non_blocking(std::io::stdout()); + let (non_blocking_stdio, guard) = tracing_appender::non_blocking(io::stderr()); let stdio_layer = fmt::layer().with_writer(non_blocking_stdio).with_ansi(ansi); (stdio_layer, guard) } Logging::File { filepath, clean_old: _ } => { - let ansi = match coloring { - Coloring::Never => false, - Coloring::Always => true, - Coloring::Auto => false, - }; - let file = OpenOptions::new() .create(true) .write(true) diff --git a/testsuite/tests/cli/jetsocat.rs b/testsuite/tests/cli/jetsocat.rs index ab8e0d1b2..b96c62828 100644 --- a/testsuite/tests/cli/jetsocat.rs +++ b/testsuite/tests/cli/jetsocat.rs @@ -37,15 +37,15 @@ fn all_subcommands() { } #[rstest] -#[case::default(&[], &[], true)] +#[case::default(&[], &[], false)] // is_terminal = false #[case::cli_always(&["--color=always"], &[], true)] #[case::cli_never(&["--color=never"], &[], false)] -#[case::cli_auto(&["--color=auto"], &[], true)] +#[case::cli_auto(&["--color=auto"], &[], false)] // is_terminal = false #[case::cli_always_and_env(&["--color=always"], &[("NO_COLOR", "")], true)] -#[case::cli_auto_and_env(&["--color=auto"], &[("NO_COLOR", "")], true)] +#[case::cli_auto_and_env(&["--color=auto"], &[("NO_COLOR", "")], false)] // is_terminal = false #[case::env_no_color(&[], &[("NO_COLOR", ""), ("FORCE_COLOR", "1")], false)] #[case::env_term_dumb(&[], &[("TERM", "dumb")], false)] -#[case::env_term_other(&[], &[("TERM", "other")], true)] +#[case::env_term_other(&[], &[("TERM", "other")], false)] // is_terminal = false #[case::env_force_color_0(&[], &[("FORCE_COLOR", "0")], false)] #[case::env_force_color_1(&[], &[("FORCE_COLOR", "1"), ("TERM", "dumb")], true)] fn log_term_coloring(#[case] args: &[&str], #[case] envs: &[(&str, &str)], #[case] expect_ansi: bool) { @@ -57,12 +57,12 @@ fn log_term_coloring(#[case] args: &[&str], #[case] envs: &[(&str, &str)], #[cas .assert() .success(); - let stdout = std::str::from_utf8(&output.get_output().stdout).unwrap(); + let stderr = std::str::from_utf8(&output.get_output().stderr).unwrap(); if expect_ansi { - assert!(stdout.contains("  INFO jetsocat"), "{stdout}"); + assert!(stderr.contains("  INFO jetsocat"), "{stderr}"); } else { - assert!(stdout.contains(" INFO jetsocat"), "{stdout}"); + assert!(stderr.contains(" INFO jetsocat"), "{stderr}"); } } @@ -72,7 +72,7 @@ fn log_term_coloring(#[case] args: &[&str], #[case] envs: &[(&str, &str)], #[cas #[case::cli_never(&["--color", "never"], &[], false)] #[case::cli_auto(&["--color", "auto"], &[], false)] #[case::cli_always_and_env(&["--color", "always"], &[("NO_COLOR", "1")], true)] -#[case::cli_auto_and_env(&["--color", "auto"], &[("FORCE_COLOR", "1")], false)] +#[case::cli_auto_and_env(&["--color", "auto"], &[("FORCE_COLOR", "1")], true)] #[case::env_no_color(&[], &[("NO_COLOR", "1"), ("FORCE_COLOR", "1")], false)] #[case::env_term_dumb(&[], &[("TERM", "dumb")], false)] #[case::env_term_other(&[], &[("TERM", "other")], false)] @@ -481,11 +481,9 @@ fn jetsocat_log_environment_variable() { .timeout(COMMAND_TIMEOUT) .assert(); - let stdout = std::str::from_utf8(&output.get_output().stdout).unwrap(); - assert!(stdout.contains("DEBUG")); - assert!(stdout.contains("hello")); - let stderr = std::str::from_utf8(&output.get_output().stderr).unwrap(); + assert!(stderr.contains("DEBUG")); + assert!(stderr.contains("hello")); assert!(!stderr.contains("bad")); assert!(!stderr.contains("invalid")); assert!(!stderr.contains("unknown")); @@ -874,7 +872,7 @@ async fn execute_mcp_request(request: &str) -> String { let mut jetsocat_process = jetsocat_tokio_cmd() .args(&["mcp-proxy", "stdio", &server_url, "--log-term", "--color=never"]) .stdin(std::process::Stdio::piped()) - .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()) .kill_on_drop(true) .spawn() .expect("start jetsocat mcp-proxy"); @@ -894,23 +892,23 @@ async fn execute_mcp_request(request: &str) -> String { jetsocat_process.start_kill().unwrap(); let output = jetsocat_process.wait_with_output().await.unwrap(); - String::from_utf8(output.stdout).unwrap() + String::from_utf8(output.stderr).unwrap() } #[tokio::test] async fn mcp_proxy_malformed_request_with_id() { - let stdout = execute_mcp_request("{\"jsonrpc\":\"2.0\",\"decoy\":\":\",\"id\":1\n").await; - assert!(stdout.contains("Malformed JSON-RPC request from client"), "{stdout}"); - assert!(stdout.contains("Unexpected EOF"), "{stdout}"); - assert!(stdout.contains("id=1"), "{stdout}"); + let stderr = execute_mcp_request("{\"jsonrpc\":\"2.0\",\"decoy\":\":\",\"id\":1\n").await; + assert!(stderr.contains("Malformed JSON-RPC request from client"), "{stderr}"); + assert!(stderr.contains("Unexpected EOF"), "{stderr}"); + assert!(stderr.contains("id=1"), "{stderr}"); } #[tokio::test] async fn mcp_proxy_malformed_request_no_id() { - let stdout = execute_mcp_request("{\"jsonrpc\":\"2.0\",}\n").await; - assert!(stdout.contains("Malformed JSON-RPC request from client"), "{stdout}"); - assert!(stdout.contains("Invalid character"), "{stdout}"); - assert!(!stdout.contains("id=1"), "{stdout}"); + let stderr = execute_mcp_request("{\"jsonrpc\":\"2.0\",}\n").await; + assert!(stderr.contains("Malformed JSON-RPC request from client"), "{stderr}"); + assert!(stderr.contains("Invalid character"), "{stderr}"); + assert!(!stderr.contains("id=1"), "{stderr}"); } #[tokio::test]