Commit 92f2425

Eric Bower  ·  2026-08-11 11:49:22 -0400 EDT
parent 347fbf4
fix(pty): preserve terminal width and prevent readline truncation in run tasks

- Fetch terminal size in daemonize() before stdio redirection to /dev/null
- Send .Resize before .Run in zmx run to match client terminal size
- Default TERM to xterm-256color when unset/dumb to prevent readline horizontal scroll artifacts
4 files changed,  +41, -3
+13, -3
 1@@ -63,6 +63,14 @@ fn exec(sesh_name: []const u8, cmd: Cmd) !noreturn {
 2     );
 3     _ = cross.c.putenv(session_env.ptr);
 4 
 5+    if (cross.c.getenv("TERM")) |term_env| {
 6+        if (std.mem.eql(u8, std.mem.span(term_env), "dumb")) {
 7+            _ = cross.c.putenv(@constCast("TERM=xterm-256color"));
 8+        }
 9+    } else {
10+        _ = cross.c.putenv(@constCast("TERM=xterm-256color"));
11+    }
12+
13     const err = lib_posix.execvpeZ(cmd.file, cmd.argv_ptr, std.c.environ);
14     std.log.err("execvpe failed: cmd={s} err={s}", .{ cmd.file, @errorName(err) });
15     lib_posix.exit(1);
16@@ -78,8 +86,7 @@ pub const PtyInfo = struct {
17 ///
18 /// This is the second fork in the double-fork technique explained in the
19 /// daemonize() comment.
20-pub fn spawnPty(sesh_name: []const u8, cmd: Cmd) !PtyInfo {
21-    const size = ipc.getTerminalSize(lib_posix.STDOUT_FILENO);
22+pub fn spawnPty(sesh_name: []const u8, cmd: Cmd, size: ipc.Resize) !PtyInfo {
23     var ws: cross.c.struct_winsize = .{
24         .ws_row = size.rows,
25         .ws_col = size.cols,
26@@ -179,6 +186,9 @@ pub fn daemonize(sesh_name: []const u8, cmd: Cmd, keep_fds_open: []i32) !PtyInfo
27     // becomes the session leader and detaches process from its controlling terminal
28     _ = try lib_posix.setsid();
29 
30+    // Fetch terminal size before redirecting stdio FDs to /dev/null.
31+    const term_size = ipc.getTerminalSize(lib_posix.STDOUT_FILENO);
32+
33     // Redirect stdin/stdout/stderr to /dev/null. The daemon
34     // communicates via its unix socket, not stdio. Without
35     // this, any pipe on FDs 0-2 (e.g. from bats' `run`
36@@ -225,5 +235,5 @@ pub fn daemonize(sesh_name: []const u8, cmd: Cmd, keep_fds_open: []i32) !PtyInfo
37         }
38     }
39 
40-    return spawnPty(sesh_name, cmd);
41+    return spawnPty(sesh_name, cmd, term_size);
42 }
+13, -0
 1@@ -52,6 +52,19 @@ pub fn getTerminalSize(fd: i32) Resize {
 2     if (cross.c.ioctl(fd, cross.c.TIOCGWINSZ, &ws) == 0 and ws.ws_row > 0 and ws.ws_col > 0) {
 3         return .{ .rows = ws.ws_row, .cols = ws.ws_col, .xpixel = ws.ws_xpixel, .ypixel = ws.ws_ypixel };
 4     }
 5+    inline for (.{ lib_posix.STDOUT_FILENO, lib_posix.STDIN_FILENO, lib_posix.STDERR_FILENO }) |fallback_fd| {
 6+        if (fallback_fd != fd) {
 7+            if (cross.c.ioctl(fallback_fd, cross.c.TIOCGWINSZ, &ws) == 0 and ws.ws_row > 0 and ws.ws_col > 0) {
 8+                return .{ .rows = ws.ws_row, .cols = ws.ws_col, .xpixel = ws.ws_xpixel, .ypixel = ws.ws_ypixel };
 9+            }
10+        }
11+    }
12+    if (lib_posix.open("/dev/tty", .{ .ACCMODE = .RDWR }, 0)) |tty_fd| {
13+        defer lib_posix.close(tty_fd);
14+        if (cross.c.ioctl(tty_fd, cross.c.TIOCGWINSZ, &ws) == 0 and ws.ws_row > 0 and ws.ws_col > 0) {
15+            return .{ .rows = ws.ws_row, .cols = ws.ws_col, .xpixel = ws.ws_xpixel, .ypixel = ws.ws_ypixel };
16+        }
17+    } else |_| {}
18     return .{ .rows = 24, .cols = 120 };
19 }
20 
+3, -0
 1@@ -1599,6 +1599,9 @@ fn run(gpa: std.mem.Allocator, io: std.Io, daemon: *Daemon, detached: bool, comm
 2     };
 3     defer lib_posix.close(client_sock);
 4 
 5+    const term_size = ipc.getTerminalSize(lib_posix.STDOUT_FILENO);
 6+    ipc.send(client_sock, .Resize, std.mem.asBytes(&term_size)) catch {};
 7+
 8     var fds = try std.ArrayList(i32).initCapacity(gpa, 1);
 9     defer fds.deinit(gpa);
10     try fds.append(gpa, client_sock);
+12, -0
 1@@ -295,3 +295,15 @@ load test_helper
 2   run "$ZMX" print
 3   [ "$status" -ne 0 ]
 4 }
 5+
 6+@test "run: long command line does not truncate history when creating session" {
 7+  local longcmd="echo 1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"
 8+  run "$ZMX" run test-long-cmd -d "$longcmd"
 9+  [ "$status" -eq 0 ]
10+  wait_for_session test-long-cmd
11+  wait_for_output test-long-cmd 12345678901234567890
12+  run "$ZMX" history test-long-cmd
13+  [[ "$output" != *"<1234567890"* ]]
14+  [[ "$output" == *"12345678901234567890"* ]]
15+}
16+