a73x

94e7b0ed

feat(pty): add resize

a73x   2026-04-08 06:03

Commit message
feat(pty): add resize

Implements Pty.resize(cols, rows) via ioctl(TIOCSWINSZ).
Includes test verifying the new window size is readable back via TIOCGWINSZ.

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 }