a73x

b1c9ad85

main: thread optional ScenarioState through runTerminal

a73x   2026-04-19 13:26

Signature change only — tick_ctx parameter is unused this commit
beyond the loopShouldRun guard. Loop guard relaxed via loopShouldRun
helper so scenario mode can exit on state.isDone().

Existing entry points pass null — no behavior change for the
normal non-scenario path.

Stub src/scenario_runtime.zig added so the import resolves;
Task 3 replaces it with real TickIO callbacks.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

diff --git a/build.zig b/build.zig
index 479c8c1..467df22 100644
--- a/build.zig
+++ b/build.zig
@@ -391,6 +391,17 @@ pub fn build(b: *std.Build) void {
    scenario_mod.addImport("png", png_mod);
    scenario_mod.addImport("imgdiff", imgdiff_lib_mod);

    // scenario_runtime stub module (Task 3 populates)
    const scenario_runtime_mod = b.createModule(.{
        .root_source_file = b.path("src/scenario_runtime.zig"),
        .target = target,
        .optimize = optimize,
    });
    scenario_runtime_mod.addImport("scenario", scenario_mod);

    exe_mod.addImport("scenario", scenario_mod);
    exe_mod.addImport("scenario_runtime", scenario_runtime_mod);

    const scenario_test_mod = b.createModule(.{
        .root_source_file = b.path("src/scenario.zig"),
        .target = target,
diff --git a/src/main.zig b/src/main.zig
index 687c775..d69c6f0 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -1,6 +1,8 @@
const std = @import("std");
const vt = @import("vt");
const pty = @import("pty");
const scenario = @import("scenario");
const scenario_runtime = @import("scenario_runtime");
const wayland_client = @import("wayland-client");
const frame_loop_mod = @import("frame_loop");
const renderer = @import("renderer");
@@ -126,6 +128,16 @@ fn writeBenchJson(alloc: std.mem.Allocator, stats: FrameTimingStats, workload: ?
    try out.writeAll(json_bytes);
}

fn loopShouldRun(p: *pty.Pty, tick_ctx: ?*scenario_runtime.RunContext) bool {
    if (tick_ctx) |rc| {
        // Scenario mode — check state inside RunContext. Task 3 populates this field.
        if (rc.state) |s| {
            if (s.isDone()) return false;
        }
    }
    return p.isChildAlive();
}

pub fn main() !void {
    var gpa: std.heap.DebugAllocator(.{}) = .init;
    defer _ = gpa.deinit();
@@ -167,10 +179,10 @@ pub fn main() !void {
        return capture.run(alloc, args[1..]);
    }

    return runTerminal(alloc);
    return runTerminal(alloc, null);
}

fn runTerminal(alloc: std.mem.Allocator) !void {
fn runTerminal(alloc: std.mem.Allocator, tick_ctx: ?*scenario_runtime.RunContext) !void {
    // === font first, to know cell size ===
    var font_lookup = try font.lookupConfiguredFont(alloc);
    defer font_lookup.deinit(alloc);
@@ -341,7 +353,7 @@ fn runTerminal(alloc: std.mem.Allocator) !void {
    var blink_state: BlinkState = .{};
    var previous_has_focus: bool = keyboard.has_focus;

    while (!window.should_close and p.isChildAlive()) {
    while (!window.should_close and loopShouldRun(&p, tick_ctx)) {
        const repeat_timeout_ms = remainingRepeatTimeoutMs(keyboard.nextRepeatDeadlineNs());
        const blink_timeout_ms = remainingRepeatTimeoutMs(blink_state.next_deadline_ns);
        const timeout = computePollTimeoutMs(repeat_timeout_ms, blink_timeout_ms, render_pending);
@@ -3527,5 +3539,5 @@ fn runHiddenFreezeRegression(alloc: std.mem.Allocator) !void {
    var buf: [1]u8 = undefined;
    _ = try stdin_file.read(&buf);

    return runTerminal(alloc);
    return runTerminal(alloc, null);
}
diff --git a/src/scenario_runtime.zig b/src/scenario_runtime.zig
new file mode 100644
index 0000000..18de389
--- /dev/null
+++ b/src/scenario_runtime.zig
@@ -0,0 +1,5 @@
// src/scenario_runtime.zig (STUB — Task 3 replaces)
const scenario = @import("scenario");
pub const RunContext = struct {
    state: ?*scenario.ScenarioState = null,
};