a73x

5824bccb

feat(main): headless mode — pty + vt proof of life

a73x   2026-04-08 06:16

Commit message
feat(main): headless mode — pty + vt proof of life

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>

src/main.zig
Old New
@@ -1,7 +1,98 @@
1 const std = @import("std"); 1 const std = @import("std");
2 const vt = @import("vt"); 2 const vt = @import("vt");
3 const pty = @import("pty");
3 4
4 pub fn main() !void { 5 pub fn main() !void {
5 std.debug.print("waystty\n", .{}); 6 var gpa: std.heap.DebugAllocator(.{}) = .init;
6 _ = vt; 7 defer _ = gpa.deinit();
8 const alloc = gpa.allocator();
9
10 const args = try std.process.argsAlloc(alloc);
11 defer std.process.argsFree(alloc, args);
12
13 if (args.len >= 2 and std.mem.eql(u8, args[1], "--headless")) {
14 return runHeadless(alloc);
15 }
16
17 std.debug.print("waystty (run with --headless for CLI dump mode)\n", .{});
18 }
19
20 fn runHeadless(alloc: std.mem.Allocator) !void {
21 const shell: [:0]const u8 = "/bin/sh";
22
23 var p = try pty.Pty.spawn(.{
24 .cols = 80,
25 .rows = 24,
26 .shell = shell,
27 });
28 defer p.deinit();
29
30 var term = try vt.Terminal.init(alloc, .{
31 .cols = 80,
32 .rows = 24,
33 });
34 defer term.deinit();
35
36 // Give shell a moment to start up
37 std.Thread.sleep(100 * std.time.ns_per_ms);
38
39 // Write a command that prints something then exits
40 _ = try p.write("echo hello; exit\n");
41
42 // Drain output for up to 2 seconds, or until child exits
43 var buf: [4096]u8 = undefined;
44 const deadline = std.time.nanoTimestamp() + 2 * std.time.ns_per_s;
45 while (std.time.nanoTimestamp() < deadline) {
46 const n = p.read(&buf) catch |err| switch (err) {
47 error.WouldBlock => {
48 if (!p.isChildAlive()) break;
49 std.Thread.sleep(10 * std.time.ns_per_ms);
50 continue;
51 },
52 // EIO typically means the slave side of the PTY was closed (child exited)
53 error.InputOutput => break,
54 else => return err,
55 };
56 if (n == 0) break;
57 term.write(buf[0..n]);
58 }
59
60 // Drain any remaining data after child exits
61 drain: while (true) {
62 const n = p.read(&buf) catch break :drain;
63 if (n == 0) break;
64 term.write(buf[0..n]);
65 }
66
67 try term.snapshot();
68
69 // Dump the grid to stdout
70 var out_buf: [65536]u8 = undefined;
71 var fw = std.fs.File.stdout().writer(&out_buf);
72 const w = &fw.interface;
73
74 const rows = term.render_state.row_data.items(.cells);
75 for (rows) |*row_cells| {
76 // Each row is a MultiArrayList(RenderState.Cell)
77 // RenderState.Cell has a .raw field of type page.Cell
78 // page.Cell has a .codepoint() method returning u21
79 const raw_cells = row_cells.items(.raw);
80 for (raw_cells) |cell| {
81 const cp = cell.codepoint();
82 if (cp == 0) {
83 // Empty cell — print a space
84 try w.writeByte(' ');
85 } else {
86 var utf8_buf: [4]u8 = undefined;
87 const utf8_len = std.unicode.utf8Encode(cp, &utf8_buf) catch {
88 try w.writeByte('?');
89 continue;
90 };
91 try w.writeAll(utf8_buf[0..utf8_len]);
92 }
93 }
94 try w.writeByte('\n');
95 }
96
97 try w.flush();
7 } 98 }