a73x

94e7b0ed

feat(pty): add resize

a73x   2026-04-08 06:03

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

diff --git a/src/pty.zig b/src/pty.zig
index 2185a54..4afda9c 100644
--- a/src/pty.zig
+++ b/src/pty.zig
@@ -13,6 +13,7 @@ const c = @cImport({
pub const Pty = struct {
    master_fd: std.posix.fd_t,
    child_pid: std.posix.pid_t,
    child_reaped: bool = false,

    pub const SpawnOptions = struct {
        cols: u16,
@@ -60,10 +61,36 @@ pub const Pty = struct {
        return std.posix.write(self.master_fd, data);
    }

    pub fn resize(self: *Pty, cols: u16, rows: u16) !void {
        var ws = c.struct_winsize{
            .ws_row = rows,
            .ws_col = cols,
            .ws_xpixel = 0,
            .ws_ypixel = 0,
        };
        if (c.ioctl(self.master_fd, c.TIOCSWINSZ, &ws) < 0) {
            return error.IoctlFailed;
        }
    }

    pub fn isChildAlive(self: *Pty) bool {
        if (self.child_reaped) return false;
        var status: c_int = 0;
        const rc = c.waitpid(self.child_pid, &status, c.WNOHANG);
        if (rc == 0) return true;
        if (rc == self.child_pid) {
            self.child_reaped = true;
            return false;
        }
        return true;
    }

    pub fn deinit(self: *Pty) void {
        std.posix.close(self.master_fd);
        _ = std.c.kill(self.child_pid, std.c.SIG.TERM);
        _ = std.c.waitpid(self.child_pid, null, 0);
        if (!self.child_reaped) {
            _ = std.c.kill(self.child_pid, std.c.SIG.TERM);
            _ = std.c.waitpid(self.child_pid, null, 0);
        }
    }
};

@@ -112,3 +139,46 @@ test "Pty.spawn launches /bin/sh and returns valid fd" {
    try std.testing.expect(pty.master_fd >= 0);
    try std.testing.expect(pty.child_pid > 0);
}

test "Pty.resize sets winsize via ioctl" {
    var pty = try Pty.spawn(.{
        .cols = 80,
        .rows = 24,
        .shell = "/bin/sh",
    });
    defer pty.deinit();

    try pty.resize(120, 40);

    var ws: c.struct_winsize = undefined;
    const rc = c.ioctl(pty.master_fd, c.TIOCGWINSZ, &ws);
    try std.testing.expectEqual(@as(c_int, 0), rc);
    try std.testing.expectEqual(@as(c_ushort, 120), ws.ws_col);
    try std.testing.expectEqual(@as(c_ushort, 40), ws.ws_row);
}

test "Pty.isChildAlive returns true while shell runs, false after exit" {
    var pty = try Pty.spawn(.{
        .cols = 80,
        .rows = 24,
        .shell = "/bin/sh",
    });
    defer pty.deinit();

    try std.testing.expect(pty.isChildAlive());

    // Tell the shell to exit
    _ = try pty.write("exit\n");

    // Wait up to 1 second for it to actually exit
    const deadline = std.time.nanoTimestamp() + 1 * std.time.ns_per_s;
    var saw_dead = false;
    while (std.time.nanoTimestamp() < deadline) {
        if (!pty.isChildAlive()) {
            saw_dead = true;
            break;
        }
        std.Thread.sleep(20 * std.time.ns_per_ms);
    }
    try std.testing.expect(saw_dead);
}