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 | |
| 469 | 469 | .Input => try daemon.handleInput(gpa, client, msg.payload), | |
| 470 | 470 | .Send => daemon.handleSend(gpa, msg.payload), | |
| 471 | 471 | .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), | |
| 473 | 473 | .Switch => try daemon.handleSwitch(gpa, msg.payload), | |
| 474 | 474 | .Resize => try daemon.handleResize(gpa, client, pty_fd, &term, msg.payload), | |
| 475 | 475 | .Detach => { |
| ... | ... | @@ -651,22 +651,6 @@ pub const Daemon = struct { | |
| 651 | 651 | try term.resize(gpa, .{ .cols = cols, .rows = rows }); | |
| 652 | 652 | } | |
| 653 | 653 | ||
| 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 | - | ||
| 670 | 654 | /// True while a client that sent .Init (a real `zmx attach`) is connected. | |
| 671 | 655 | fn hasTerminalClient(self: *const Daemon) bool { | |
| 672 | 656 | for (self.clients.items) |c| { |
| ... | ... | @@ -966,7 +950,6 @@ pub const Daemon = struct { | |
| 966 | 950 | pub fn handleInit( | |
| 967 | 951 | self: *Daemon, | |
| 968 | 952 | gpa: std.mem.Allocator, | |
| 969 | - | io: std.Io, | |
| 970 | 953 | client: *Client, | |
| 971 | 954 | pty_fd: i32, | |
| 972 | 955 | term: *ghostty_vt.Terminal, |
| ... | ... | @@ -1020,14 +1003,18 @@ pub const Daemon = struct { | |
| 1020 | 1003 | // only resize if leader | |
| 1021 | 1004 | if (is_leader) { | |
| 1022 | 1005 | 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. | |
| 1027 | 1012 | 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 | + | } | |
| 1029 | 1017 | } | |
| 1030 | - | _ = cross.c.ioctl(pty_fd, cross.c.TIOCSWINSZ, &ws); | |
| 1031 | 1018 | ||
| 1032 | 1019 | // Mark that we've had a client init, so subsequent clients get terminal state | |
| 1033 | 1020 | self.has_had_client = true; |
+1,
-1
| ... | ... | @@ -26,7 +26,7 @@ else switch (native_os) { | |
| 26 | 26 | ||
| 27 | 27 | const E = system.E; | |
| 28 | 28 | const PATH_MAX = system.PATH_MAX; | |
| 29 | - | const pid_t = system.pid_t; | |
| 29 | + | pub const pid_t = system.pid_t; | |
| 30 | 30 | const lfs64_abi = native_os == .linux and builtin.link_libc and (builtin.abi.isGnu() or builtin.abi.isAndroid()); | |
| 31 | 31 | const uid_t = system.uid_t; | |
| 32 | 32 | const mode_t = system.mode_t; |
+3,
-4
| ... | ... | @@ -48,20 +48,19 @@ count() { "$ZMX" history "$1" | grep -o "$2" | wc -l | tr -d ' '; } | |
| 48 | 48 | fake_attach test-da2 24 80 5 & | |
| 49 | 49 | wait_for_output test-da2 "DA1_TIMEOUT" 5 | |
| 50 | 50 | kill %1 2>/dev/null || true | |
| 51 | - | wait | |
| 52 | 51 | } | |
| 53 | 52 | ||
| 54 | - | @test "re-attach at the same size still delivers SIGWINCH" { | |
| 53 | + | @test "re-attach at the same size delivers SIGWINCH to foreground process" { | |
| 55 | 54 | # The trap prints a word assembled at runtime so it can't be confused with | |
| 56 | 55 | # the echoed command line in history. | |
| 57 | 56 | "$ZMX" run test-winch -d bash -c 'trap "printf \"GOT_%s\n\" WINCH" WINCH; echo READY; while :; do sleep 0.1; done' | |
| 58 | 57 | wait_for_output test-winch "READY" | |
| 59 | 58 | ||
| 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 | |
| 61 | 60 | sleep 0.5 | |
| 62 | 61 | before=$(count test-winch GOT_WINCH) | |
| 63 | 62 | ||
| 64 | - | fake_attach test-winch 24 80 # same size again | |
| 63 | + | fake_attach test-winch 24 80 # re-attach at the same size | |
| 65 | 64 | for _ in $(seq 30); do | |
| 66 | 65 | after=$(count test-winch GOT_WINCH) | |
| 67 | 66 | [ "$after" -gt "$before" ] && break |