94e7b0ed
feat(pty): add resize
a73x 2026-04-08 06:03
Commit message
src/pty.zig
| Old | New | ||
|---|---|---|---|
| @@ -13,6 +13,7 @@ const c = @cImport({ | |||
| 13 | pub const Pty = struct { | 13 | pub const Pty = struct { |
| 14 | master_fd: std.posix.fd_t, | 14 | master_fd: std.posix.fd_t, |
| 15 | child_pid: std.posix.pid_t, | 15 | child_pid: std.posix.pid_t, |
| 16 | child_reaped: bool = false, | ||
| 16 | 17 | ||
| 17 | pub const SpawnOptions = struct { | 18 | pub const SpawnOptions = struct { |
| 18 | cols: u16, | 19 | cols: u16, |
| @@ -60,10 +61,36 @@ pub const Pty = struct { | |||
| 60 | return std.posix.write(self.master_fd, data); | 61 | return std.posix.write(self.master_fd, data); |
| 61 | } | 62 | } |
| 62 | 63 | ||
| 64 | pub fn resize(self: *Pty, cols: u16, rows: u16) !void { | ||
| 65 | var ws = c.struct_winsize{ | ||
| 66 | .ws_row = rows, | ||
| 67 | .ws_col = cols, | ||
| 68 | .ws_xpixel = 0, | ||
| 69 | .ws_ypixel = 0, | ||
| 70 | }; | ||
| 71 | if (c.ioctl(self.master_fd, c.TIOCSWINSZ, &ws) < 0) { | ||
| 72 | return error.IoctlFailed; | ||
| 73 | } | ||
| 74 | } | ||
| 75 | |||
| 76 | pub fn isChildAlive(self: *Pty) bool { | ||
| 77 | if (self.child_reaped) return false; | ||
| 78 | var status: c_int = 0; | ||
| 79 | const rc = c.waitpid(self.child_pid, &status, c.WNOHANG); | ||
| 80 | if (rc == 0) return true; | ||
| 81 | if (rc == self.child_pid) { | ||
| 82 | self.child_reaped = true; | ||
| 83 | return false; | ||
| 84 | } | ||
| 85 | return true; | ||
| 86 | } | ||
| 87 | |||
| 63 | pub fn deinit(self: *Pty) void { | 88 | pub fn deinit(self: *Pty) void { |
| 64 | std.posix.close(self.master_fd); | 89 | std.posix.close(self.master_fd); |
| 65 | _ = std.c.kill(self.child_pid, std.c.SIG.TERM); | 90 | if (!self.child_reaped) { |
| 66 | _ = std.c.waitpid(self.child_pid, null, 0); | 91 | _ = std.c.kill(self.child_pid, std.c.SIG.TERM); |
| 92 | _ = std.c.waitpid(self.child_pid, null, 0); | ||
| 93 | } | ||
| 67 | } | 94 | } |
| 68 | }; | 95 | }; |
| 69 | 96 | ||
| @@ -112,3 +139,46 @@ test "Pty.spawn launches /bin/sh and returns valid fd" { | |||
| 112 | try std.testing.expect(pty.master_fd >= 0); | 139 | try std.testing.expect(pty.master_fd >= 0); |
| 113 | try std.testing.expect(pty.child_pid > 0); | 140 | try std.testing.expect(pty.child_pid > 0); |
| 114 | } | 141 | } |
| 142 | |||
| 143 | test "Pty.resize sets winsize via ioctl" { | ||
| 144 | var pty = try Pty.spawn(.{ | ||
| 145 | .cols = 80, | ||
| 146 | .rows = 24, | ||
| 147 | .shell = "/bin/sh", | ||
| 148 | }); | ||
| 149 | defer pty.deinit(); | ||
| 150 | |||
| 151 | try pty.resize(120, 40); | ||
| 152 | |||
| 153 | var ws: c.struct_winsize = undefined; | ||
| 154 | const rc = c.ioctl(pty.master_fd, c.TIOCGWINSZ, &ws); | ||
| 155 | try std.testing.expectEqual(@as(c_int, 0), rc); | ||
| 156 | try std.testing.expectEqual(@as(c_ushort, 120), ws.ws_col); | ||
| 157 | try std.testing.expectEqual(@as(c_ushort, 40), ws.ws_row); | ||
| 158 | } | ||
| 159 | |||
| 160 | test "Pty.isChildAlive returns true while shell runs, false after exit" { | ||
| 161 | var pty = try Pty.spawn(.{ | ||
| 162 | .cols = 80, | ||
| 163 | .rows = 24, | ||
| 164 | .shell = "/bin/sh", | ||
| 165 | }); | ||
| 166 | defer pty.deinit(); | ||
| 167 | |||
| 168 | try std.testing.expect(pty.isChildAlive()); | ||
| 169 | |||
| 170 | // Tell the shell to exit | ||
| 171 | _ = try pty.write("exit\n"); | ||
| 172 | |||
| 173 | // Wait up to 1 second for it to actually exit | ||
| 174 | const deadline = std.time.nanoTimestamp() + 1 * std.time.ns_per_s; | ||
| 175 | var saw_dead = false; | ||
| 176 | while (std.time.nanoTimestamp() < deadline) { | ||
| 177 | if (!pty.isChildAlive()) { | ||
| 178 | saw_dead = true; | ||
| 179 | break; | ||
| 180 | } | ||
| 181 | std.Thread.sleep(20 * std.time.ns_per_ms); | ||
| 182 | } | ||
| 183 | try std.testing.expect(saw_dead); | ||
| 184 | } | ||