From e4adaebe6235a6547314e4171e1a34229cde157e Mon Sep 17 00:00:00 2001 From: Miguel Lopez Date: Mon, 13 Jul 2026 21:30:16 -0400 Subject: [PATCH] Restore ANSI colors for agent-launched terminals --- native/unterm/src/pty.rs | 69 +++++++++++++++++++++++++++++++++------ native/unterm/src/term.rs | 34 +++++++++++++++++++ 2 files changed, 93 insertions(+), 10 deletions(-) diff --git a/native/unterm/src/pty.rs b/native/unterm/src/pty.rs index 1079d9e..e1ca27f 100644 --- a/native/unterm/src/pty.rs +++ b/native/unterm/src/pty.rs @@ -7,6 +7,27 @@ use portable_pty::{native_pty_system, Child, CommandBuilder, MasterPty, PtySize}; use std::io::{Read, Write}; +fn configure_terminal_environment(cmd: &mut CommandBuilder) { + // The host process can be launched from a non-interactive agent or build + // environment that disables color globally. Unterm is a color-capable PTY, + // so do not leak that host-only preference into its interactive shell. Keep + // an explicit NO_COLOR inherited from a real terminal, where it can represent + // the user's preference rather than the host agent's output policy. + let host_is_noninteractive = cmd.get_env("TERM") == Some(std::ffi::OsStr::new("dumb")); + if host_is_noninteractive { + cmd.env_remove("NO_COLOR"); + } + cmd.env("TERM", "xterm-256color"); + cmd.env("COLORTERM", "truecolor"); + + // macOS ships /etc/zshrc_Apple_Terminal (and a bash equivalent) that reports + // the shell's working directory via OSC 7 — but only when TERM_PROGRAM marks + // an Apple terminal. Set it so the shell emits OSC 7 on every prompt; the + // reader captures it for cwd-on-resume (no sysinfo, no rc injection). + #[cfg(target_os = "macos")] + cmd.env("TERM_PROGRAM", "Apple_Terminal"); +} + pub struct Pty { pub master: Box, pub child: Box, @@ -48,16 +69,7 @@ pub fn spawn( if !cwd.is_empty() && std::path::Path::new(cwd).is_dir() { cmd.cwd(cwd); } - // Advertise a capable terminal so programs emit colors/cursor sequences. - cmd.env("TERM", "xterm-256color"); - cmd.env("COLORTERM", "truecolor"); - - // macOS ships /etc/zshrc_Apple_Terminal (and a bash equivalent) that reports - // the shell's working directory via OSC 7 — but only when TERM_PROGRAM marks - // an Apple terminal. Set it so the shell emits OSC 7 on every prompt; the - // reader captures it for cwd-on-resume (no sysinfo, no rc injection). - #[cfg(target_os = "macos")] - cmd.env("TERM_PROGRAM", "Apple_Terminal"); + configure_terminal_environment(&mut cmd); // Ensure a UTF-8 locale so the shell's line editor handles multibyte input // (e.g. Japanese) instead of garbling it. GUI hosts like Unity often launch @@ -118,3 +130,40 @@ impl Drop for Pty { let _ = self.child.wait(); } } + +#[cfg(test)] +mod tests { + use super::*; + use std::ffi::OsStr; + + #[test] + fn terminal_environment_enables_color_when_host_disables_it() { + // Arrange + let mut cmd = CommandBuilder::new("dummy"); + cmd.env("TERM", "dumb"); + cmd.env("COLORTERM", ""); + cmd.env("NO_COLOR", "1"); + + // Act + configure_terminal_environment(&mut cmd); + + // Assert + assert_eq!(cmd.get_env("TERM"), Some(OsStr::new("xterm-256color"))); + assert_eq!(cmd.get_env("COLORTERM"), Some(OsStr::new("truecolor"))); + assert_eq!(cmd.get_env("NO_COLOR"), None); + } + + #[test] + fn terminal_environment_preserves_explicit_no_color_from_interactive_host() { + // Arrange + let mut cmd = CommandBuilder::new("dummy"); + cmd.env("TERM", "xterm-256color"); + cmd.env("NO_COLOR", "1"); + + // Act + configure_terminal_environment(&mut cmd); + + // Assert + assert_eq!(cmd.get_env("NO_COLOR"), Some(OsStr::new("1"))); + } +} diff --git a/native/unterm/src/term.rs b/native/unterm/src/term.rs index 654c6bf..1e5f1ab 100644 --- a/native/unterm/src/term.rs +++ b/native/unterm/src/term.rs @@ -729,3 +729,37 @@ fn spawn_reader( shared.dirty.store(true, Ordering::Relaxed); }) } + +#[cfg(test)] +mod tests { + use super::*; + + #[derive(Clone, Copy)] + struct IgnoreEvents; + + impl EventListener for IgnoreEvents { + fn send_event(&self, _event: Event) {} + } + + #[test] + fn ansi_red_sgr_sets_red_until_reset() { + // Arrange + let mut term = Term::new( + Config::default(), + &TermSize { cols: 8, rows: 2 }, + IgnoreEvents, + ); + let mut parser: Processor = Processor::new(); + + // Act + parser.advance(&mut term, b"\x1b[31mR\x1b[0mN"); + + // Assert + let grid = term.grid(); + assert_eq!(grid[Line(0)][Column(0)].fg, Color::Named(NamedColor::Red)); + assert_eq!( + grid[Line(0)][Column(1)].fg, + Color::Named(NamedColor::Foreground) + ); + } +}