835bd46d
feat(pty): spawn child shell via forkpty
a73x 2026-04-08 06:01
Adds Pty.spawn using forkpty(3): forks a child process, execs the given shell, sets the master fd non-blocking, and returns the fd + PID. Wires pty module into build.zig with libc + libutil linkage and a dedicated test step. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
diff --git a/build.zig b/build.zig index 10e595b..0f315c7 100644 --- a/build.zig +++ b/build.zig @@ -40,8 +40,31 @@ pub fn build(b: *std.Build) void { const run_step = b.step("run", "Run waystty"); run_step.dependOn(&run_cmd.step); // pty module — forkpty-based PTY spawn const pty_mod = b.createModule(.{ .root_source_file = b.path("src/pty.zig"), .target = target, .optimize = optimize, .link_libc = true, }); pty_mod.linkSystemLibrary("util", .{}); exe_mod.addImport("pty", pty_mod); const test_step = b.step("test", "Run unit tests"); // Test pty.zig const pty_test_mod = b.createModule(.{ .root_source_file = b.path("src/pty.zig"), .target = target, .optimize = optimize, .link_libc = true, }); pty_test_mod.linkSystemLibrary("util", .{}); const pty_tests = b.addTest(.{ .root_module = pty_test_mod, }); test_step.dependOn(&b.addRunArtifact(pty_tests).step); // Test main.zig (and transitively vt.zig via its import) const main_test_mod = b.createModule(.{ .root_source_file = b.path("src/main.zig"), diff --git a/src/pty.zig b/src/pty.zig new file mode 100644 index 0000000..01e6e9b --- /dev/null +++ b/src/pty.zig @@ -0,0 +1,71 @@ const std = @import("std"); const c = @cImport({ @cInclude("pty.h"); @cInclude("termios.h"); @cInclude("unistd.h"); @cInclude("sys/ioctl.h"); @cInclude("fcntl.h"); @cInclude("sys/wait.h"); @cInclude("stdlib.h"); }); pub const Pty = struct { master_fd: std.posix.fd_t, child_pid: std.posix.pid_t, pub const SpawnOptions = struct { cols: u16, rows: u16, shell: [:0]const u8, }; pub fn spawn(opts: SpawnOptions) !Pty { var master: c_int = undefined; var winsize = c.struct_winsize{ .ws_row = opts.rows, .ws_col = opts.cols, .ws_xpixel = 0, .ws_ypixel = 0, }; const pid = c.forkpty(&master, null, null, &winsize); if (pid < 0) return error.ForkptyFailed; if (pid == 0) { // Child process _ = c.setenv("TERM", "xterm-256color", 1); var argv = [_:null]?[*:0]const u8{ opts.shell.ptr, null }; std.posix.execveZ(opts.shell.ptr, &argv, std.c.environ) catch {}; std.process.exit(1); } // Parent: set master fd non-blocking const flags = try std.posix.fcntl(master, std.posix.F.GETFL, 0); const nonblock_bit: usize = @as(u32, @bitCast(std.posix.O{ .NONBLOCK = true })); _ = try std.posix.fcntl(master, std.posix.F.SETFL, flags | nonblock_bit); return .{ .master_fd = master, .child_pid = pid, }; } 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); } }; test "Pty.spawn launches /bin/sh and returns valid fd" { var pty = try Pty.spawn(.{ .cols = 80, .rows = 24, .shell = "/bin/sh", }); defer pty.deinit(); try std.testing.expect(pty.master_fd >= 0); try std.testing.expect(pty.child_pid > 0); }