Commit e586f62

Eric Bower  ·  2026-08-30 10:35:22 -0400 EDT
parent a8e9573
refactor: replace 1-pixel shift with sigwinch to process group
3 files changed,  +15, -29
+11, -24
......@@ -469,7 +469,7 @@ fn daemonLoop(daemon: *Daemon, gpa: std.mem.Allocator, io: std.Io, server_sock_f
469469 .Input => try daemon.handleInput(gpa, client, msg.payload),
470470 .Send => daemon.handleSend(gpa, msg.payload),
471471 .Output => try daemon.handleOutput(gpa, msg.payload, &term, &vt_stream),
472- .Init => try daemon.handleInit(gpa, io, client, pty_fd, &term, msg.payload),
472+ .Init => try daemon.handleInit(gpa, client, pty_fd, &term, msg.payload),
473473 .Switch => try daemon.handleSwitch(gpa, msg.payload),
474474 .Resize => try daemon.handleResize(gpa, client, pty_fd, &term, msg.payload),
475475 .Detach => {
......@@ -651,22 +651,6 @@ pub const Daemon = struct {
651651 try term.resize(gpa, .{ .cols = cols, .rows = rows });
652652 }
653653
654- /// If the PTY is already at `ws`, briefly shrink it by one row so the
655- /// program receives a SIGWINCH and redraws (setting an identical size
656- /// raises no signal). Rows rather than cols so nothing re-wraps. The pause
657- /// lets the program read the intermediate size before the caller restores
658- /// it; programs react in well under 1ms, 5ms is margin.
659- fn forceSigwinchIfUnchanged(io: std.Io, pty_fd: i32, ws: cross.c.struct_winsize) void {
660- var cur: cross.c.struct_winsize = undefined;
661- if (cross.c.ioctl(pty_fd, cross.c.TIOCGWINSZ, &cur) != 0) return;
662- if (cur.ws_row != ws.ws_row or cur.ws_col != ws.ws_col or
663- cur.ws_xpixel != ws.ws_xpixel or cur.ws_ypixel != ws.ws_ypixel) return;
664- if (cur.ws_row < 2) return;
665- cur.ws_row -= 1;
666- _ = cross.c.ioctl(pty_fd, cross.c.TIOCSWINSZ, &cur);
667- std.Io.sleep(io, std.Io.Duration.fromMilliseconds(5), .real) catch {};
668- }
669-
670654 /// True while a client that sent .Init (a real `zmx attach`) is connected.
671655 fn hasTerminalClient(self: *const Daemon) bool {
672656 for (self.clients.items) |c| {
......@@ -966,7 +950,6 @@ pub const Daemon = struct {
966950 pub fn handleInit(
967951 self: *Daemon,
968952 gpa: std.mem.Allocator,
969- io: std.Io,
970953 client: *Client,
971954 pty_fd: i32,
972955 term: *ghostty_vt.Terminal,
......@@ -1020,14 +1003,18 @@ pub const Daemon = struct {
10201003 // only resize if leader
10211004 if (is_leader) {
10221005 var ws = resize.winsize();
1023- // On re-attach the program should redraw, since what's on screen
1024- // is now our replay rather than what it last drew. A size change
1025- // makes it do that via SIGWINCH, but the kernel sends none when the
1026- // size is unchanged, so force one in that case.
1006+ _ = cross.c.ioctl(pty_fd, cross.c.TIOCSWINSZ, &ws);
1007+
1008+ // On re-attach, deliver SIGWINCH to the foreground process group so
1009+ // incremental renderers (Ink, Claude Code, etc.) know to repaint.
1010+ // If the size changed, TIOCSWINSZ above already sent SIGWINCH; if the size
1011+ // was unchanged, the kernel suppressed it, so signal the pgrp explicitly.
10271012 if (self.has_pty_output and self.has_had_client) {
1028- forceSigwinchIfUnchanged(io, pty_fd, ws);
1013+ var pgrp: lib_posix.pid_t = 0;
1014+ if (cross.c.ioctl(pty_fd, cross.c.TIOCGPGRP, &pgrp) == 0 and pgrp > 0) {
1015+ lib_posix.kill(-pgrp, .WINCH) catch {};
1016+ }
10291017 }
1030- _ = cross.c.ioctl(pty_fd, cross.c.TIOCSWINSZ, &ws);
10311018
10321019 // Mark that we've had a client init, so subsequent clients get terminal state
10331020 self.has_had_client = true;
+1, -1
......@@ -26,7 +26,7 @@ else switch (native_os) {
2626
2727 const E = system.E;
2828 const PATH_MAX = system.PATH_MAX;
29-const pid_t = system.pid_t;
29+pub const pid_t = system.pid_t;
3030 const lfs64_abi = native_os == .linux and builtin.link_libc and (builtin.abi.isGnu() or builtin.abi.isAndroid());
3131 const uid_t = system.uid_t;
3232 const mode_t = system.mode_t;
+3, -4
......@@ -48,20 +48,19 @@ count() { "$ZMX" history "$1" | grep -o "$2" | wc -l | tr -d ' '; }
4848 fake_attach test-da2 24 80 5 &
4949 wait_for_output test-da2 "DA1_TIMEOUT" 5
5050 kill %1 2>/dev/null || true
51- wait
5251 }
5352
54-@test "re-attach at the same size still delivers SIGWINCH" {
53+@test "re-attach at the same size delivers SIGWINCH to foreground process" {
5554 # The trap prints a word assembled at runtime so it can't be confused with
5655 # the echoed command line in history.
5756 "$ZMX" run test-winch -d bash -c 'trap "printf \"GOT_%s\n\" WINCH" WINCH; echo READY; while :; do sleep 0.1; done'
5857 wait_for_output test-winch "READY"
5958
60- fake_attach test-winch 24 80 # first attach: sets the size, no bounce
59+ fake_attach test-winch 24 80 # first attach: sets the initial size
6160 sleep 0.5
6261 before=$(count test-winch GOT_WINCH)
6362
64- fake_attach test-winch 24 80 # same size again
63+ fake_attach test-winch 24 80 # re-attach at the same size
6564 for _ in $(seq 30); do
6665 after=$(count test-winch GOT_WINCH)
6766 [ "$after" -gt "$before" ] && break