aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-04-22 20:58:34 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-04-22 20:58:34 +0300
commite9a3e3784c81938255e92e73479b71a170edaa9e (patch)
tree61601df8ee15a47d652eb7df7cb73174ba28690f /src
parent4b482f37a1e233b9aca6bf99b12e8b4da3e5c05e (diff)
fix: set a stable terminal identity for panes
Diffstat (limited to 'src')
-rw-r--r--src/pane.rs65
1 files changed, 65 insertions, 0 deletions
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);