a73x

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();
}