51f41728
main: wire blink state machine into the render loop
a73x 2026-04-19 07:10
Deadline folded into computePollTimeoutMs alongside key-repeat. tickBlinkPhase runs right after poll() so a just-flipped tick sets render_pending before the idle-bail. reconfigureBlink runs post-snapshot and detects cursor/focus identity changes against previous_cursor + previous_has_focus. Draw rule now gated on shouldDrawCursor; DECSCUSR shapes dispatch into renderer.cursorInstance. As a side effect the 500ms tick gives the main loop a heartbeat whenever blink is enabled, which is enough to recover from a suspend wedge in the common interactive case (bare shell). See docs/superpowers/specs/2026-04-18-cursor-blink-design.md for the limit on that benefit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
diff --git a/src/main.zig b/src/main.zig index f40f522..5c1e818 100644 --- a/src/main.zig +++ b/src/main.zig @@ -338,13 +338,22 @@ fn runTerminal(alloc: std.mem.Allocator) !void { var resize_pending = false; var scale_pending = false; var consecutive_vk_timeouts: u32 = 0; var blink_state: BlinkState = .{}; var previous_has_focus: bool = keyboard.has_focus; while (!window.should_close and p.isChildAlive()) { const repeat_timeout_ms = remainingRepeatTimeoutMs(keyboard.nextRepeatDeadlineNs()); // Blink deadline wired up in Task 4; null for now. const timeout = computePollTimeoutMs(repeat_timeout_ms, null, render_pending); const blink_timeout_ms = remainingRepeatTimeoutMs(blink_state.next_deadline_ns); const timeout = computePollTimeoutMs(repeat_timeout_ms, blink_timeout_ms, render_pending); try frame_loop.waitForWork(&pollfds_extra, timeout); // Phase-flip first so a flipped-this-tick deadline marks render_pending // before the `if (!render_pending) continue;` bail below. const tick = tickBlinkPhase(blink_state, std.time.nanoTimestamp()); blink_state = tick.state; var cursor_rebuild_from_blink = tick.flipped; if (tick.flipped) render_pending = true; // PTY output if (pollfds_extra[0].revents & std.posix.POLL.IN != 0) { while (true) { @@ -495,6 +504,33 @@ fn runTerminal(alloc: std.mem.Allocator) !void { try term.snapshot(); frame_timing.snapshot_us = usFromTimer(§ion_timer); // Recompute blink arm/disarm from current state. Edge-driven updates // are deliberately avoided — `want_blink` is a pure function of the // state we see right now, so no transition can be "missed". const current_cursor = term.render_state.cursor; const viewport_present = current_cursor.viewport != null; const want_blink = current_cursor.visible and viewport_present and current_cursor.blinking and keyboard.has_focus; const cursor_identity_changed = !std.meta.eql(current_cursor.viewport, previous_cursor.viewport) or current_cursor.visual_style != previous_cursor.visual_style or current_cursor.visible != previous_cursor.visible or current_cursor.blinking != previous_cursor.blinking; const focus_regained = keyboard.has_focus and !previous_has_focus; const reconf = reconfigureBlink( blink_state, want_blink, cursor_identity_changed, focus_regained, std.time.nanoTimestamp(), ); blink_state = reconf.state; if (reconf.cursor_rebuild) cursor_rebuild_from_blink = true; section_timer = std.time.Timer.start() catch unreachable; const default_bg = term.backgroundColor(); const bg_uv = atlas.cursorUV(); @@ -517,6 +553,7 @@ fn runTerminal(alloc: std.mem.Allocator) !void { }, }, ); const cursor_rebuild_effective = refresh_plan.cursor_rebuild or cursor_rebuild_from_blink; var rows_rebuilt: usize = 0; var row_idx: usize = 0; @@ -551,11 +588,19 @@ fn runTerminal(alloc: std.mem.Allocator) !void { } var cursor_rebuilt = false; if (refresh_plan.cursor_rebuild) { if (cursor_rebuild_effective) { var cursor_instances_buf: [1]renderer.Instance = undefined; var cursor_instances: []const renderer.Instance = &.{}; if (term.render_state.cursor.viewport) |cursor| { const shape: renderer.CursorShape = switch (term.render_state.cursor.visual_style) { const draw_cursor = shouldDrawCursor( current_cursor.visible, current_cursor.viewport != null, keyboard.has_focus, current_cursor.blinking, blink_state.blink_on, ); if (draw_cursor) { const cursor = current_cursor.viewport.?; // guaranteed by shouldDrawCursor const shape: renderer.CursorShape = switch (current_cursor.visual_style) { .block, .block_hollow => .block, .underline => .underline, .bar => .bar, @@ -715,6 +760,7 @@ fn runTerminal(alloc: std.mem.Allocator) !void { frame_timing.present_us = submit_timing.present_us; frame_ring.push(frame_timing); previous_has_focus = keyboard.has_focus; clearConsumedDirtyFlags(&term.render_state.dirty, dirty_rows, refresh_plan); if (!bench_unthrottled) frame_loop.commitRender() catch {};