5824bccb
feat(main): headless mode — pty + vt proof of life
a73x 2026-04-08 06:16
Commit message
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 | } |