5824bccb
feat(main): headless mode — pty + vt proof of life
a73x 2026-04-08 06:16
Implements --headless flag: spawns /bin/sh via Pty, writes "echo hello; exit\n", drains output into a vt.Terminal, snapshots the render state, and dumps the 80x24 grid as UTF-8 text to stdout. The word "hello" is visible in the output, proving the PTY + Terminal facade work end-to-end. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
diff --git a/src/main.zig b/src/main.zig index 233696c..3fc8430 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,7 +1,98 @@ const std = @import("std"); const vt = @import("vt"); const pty = @import("pty"); pub fn main() !void { std.debug.print("waystty\n", .{}); _ = vt; var gpa: std.heap.DebugAllocator(.{}) = .init; defer _ = gpa.deinit(); const alloc = gpa.allocator(); const args = try std.process.argsAlloc(alloc); defer std.process.argsFree(alloc, args); if (args.len >= 2 and std.mem.eql(u8, args[1], "--headless")) { return runHeadless(alloc); } std.debug.print("waystty (run with --headless for CLI dump mode)\n", .{}); } fn runHeadless(alloc: std.mem.Allocator) !void { const shell: [:0]const u8 = "/bin/sh"; var p = try pty.Pty.spawn(.{ .cols = 80, .rows = 24, .shell = shell, }); defer p.deinit(); var term = try vt.Terminal.init(alloc, .{ .cols = 80, .rows = 24, }); defer term.deinit(); // Give shell a moment to start up std.Thread.sleep(100 * std.time.ns_per_ms); // Write a command that prints something then exits _ = try p.write("echo hello; exit\n"); // Drain output for up to 2 seconds, or until child exits var buf: [4096]u8 = undefined; const deadline = std.time.nanoTimestamp() + 2 * std.time.ns_per_s; while (std.time.nanoTimestamp() < deadline) { const n = p.read(&buf) catch |err| switch (err) { error.WouldBlock => { if (!p.isChildAlive()) break; std.Thread.sleep(10 * std.time.ns_per_ms); continue; }, // EIO typically means the slave side of the PTY was closed (child exited) error.InputOutput => break, else => return err, }; if (n == 0) break; term.write(buf[0..n]); } // Drain any remaining data after child exits drain: while (true) { const n = p.read(&buf) catch break :drain; if (n == 0) break; term.write(buf[0..n]); } try term.snapshot(); // Dump the grid to stdout var out_buf: [65536]u8 = undefined; var fw = std.fs.File.stdout().writer(&out_buf); const w = &fw.interface; const rows = term.render_state.row_data.items(.cells); for (rows) |*row_cells| { // Each row is a MultiArrayList(RenderState.Cell) // RenderState.Cell has a .raw field of type page.Cell // page.Cell has a .codepoint() method returning u21 const raw_cells = row_cells.items(.raw); for (raw_cells) |cell| { const cp = cell.codepoint(); if (cp == 0) { // Empty cell — print a space try w.writeByte(' '); } else { var utf8_buf: [4]u8 = undefined; const utf8_len = std.unicode.utf8Encode(cp, &utf8_buf) catch { try w.writeByte('?'); continue; }; try w.writeAll(utf8_buf[0..utf8_len]); } } try w.writeByte('\n'); } try w.flush(); }