From e9a3e3784c81938255e92e73479b71a170edaa9e Mon Sep 17 00:00:00 2001 From: Ogulcan Celik Date: Wed, 22 Apr 2026 20:58:34 +0300 Subject: [PATCH] fix: set a stable terminal identity for panes --- src/pane.rs | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/src/pane.rs b/src/pane.rs index eb37d0c1..8de53f59 100644 --- a/src/pane.rs +++ b/src/pane.rs @@ -30,6 +30,17 @@ pub use self::{ }; const RELEASE_REACQUIRE_SUPPRESSION: std::time::Duration = std::time::Duration::from_secs(1); +const PANE_TERM: &str = "xterm-256color"; +const PANE_COLORTERM: &str = "truecolor"; + +fn apply_pane_terminal_env(cmd: &mut CommandBuilder) { + // Each pane is rendered by herdr's own terminal layer, not the outer terminal + // that launched the app. Advertising the inherited TERM leaks the host terminal + // identity into shells and across SSH, which breaks redraw and cursor movement + // when the remote side lacks matching terminfo entries. + cmd.env("TERM", PANE_TERM); + cmd.env("COLORTERM", PANE_COLORTERM); +} #[derive(Debug, Clone, Copy)] struct PendingAgentRelease { @@ -246,6 +257,7 @@ impl PaneRuntime { let mut cmd = CommandBuilder::new(&shell); cmd.cwd(cwd); cmd.env(crate::HERDR_ENV_VAR, crate::HERDR_ENV_VALUE); + apply_pane_terminal_env(&mut cmd); crate::integration::apply_pane_env(&mut cmd, pane_id); Self::spawn_command_builder( pane_id, @@ -279,6 +291,7 @@ impl PaneRuntime { cmd.arg(command); cmd.cwd(cwd); cmd.env(crate::HERDR_ENV_VAR, crate::HERDR_ENV_VALUE); + apply_pane_terminal_env(&mut cmd); crate::integration::apply_pane_env(&mut cmd, pane_id); for (key, value) in extra_env { cmd.env(key, value); @@ -859,6 +872,58 @@ impl PaneRuntime { mod tests { use super::*; + fn capture_shell_output(command: &str, extra_env: &[(&str, &str)]) -> String { + let pair = native_pty_system() + .openpty(PtySize { + rows: 24, + cols: 80, + pixel_width: 0, + pixel_height: 0, + }) + .unwrap(); + let output_path = std::env::temp_dir().join(format!( + "herdr-pane-term-test-{}-{}.txt", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos() + )); + let mut cmd = CommandBuilder::new("/bin/sh"); + cmd.arg("-c"); + cmd.arg(format!("{command} > '{}'", output_path.display())); + cmd.cwd(std::env::current_dir().unwrap()); + cmd.env("TERM", "xterm-ghostty"); + cmd.env("COLORTERM", "falsecolor"); + apply_pane_terminal_env(&mut cmd); + for (key, value) in extra_env { + cmd.env(key, value); + } + + let mut child = pair.slave.spawn_command(cmd).unwrap(); + let status = child.wait().unwrap(); + assert!(status.success(), "shell command failed: {status:?}"); + + let output = std::fs::read_to_string(&output_path).unwrap(); + let _ = std::fs::remove_file(output_path); + output + } + + #[test] + fn pane_terminal_identity_overrides_outer_terminal_env() { + let output = capture_shell_output("printf '%s\\n%s\\n' \"$TERM\" \"$COLORTERM\"", &[]); + assert_eq!(output, "xterm-256color\ntruecolor\n"); + } + + #[test] + fn pane_terminal_identity_allows_explicit_override() { + let output = capture_shell_output( + "printf '%s\\n%s\\n' \"$TERM\" \"$COLORTERM\"", + &[("TERM", "vt100"), ("COLORTERM", "24bit")], + ); + assert_eq!(output, "vt100\n24bit\n"); + } + #[tokio::test] async fn focus_events_are_forwarded_when_enabled() { let (tx, mut rx) = mpsc::channel(4);