a73x

e536c796

Fix selection helper scope

a73x   2026-04-09 17:52


diff --git a/src/main.zig b/src/main.zig
index 0badb6e..efe47ad 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -213,22 +213,16 @@ fn runTerminal(alloc: std.mem.Allocator) !void {
        // Flush any pending wayland requests
        _ = conn.display.flush();

        const wayland_read_prepared = conn.display.prepareRead();
        const repeat_timeout_ms = remainingRepeatTimeoutMs(keyboard.nextRepeatDeadlineNs());
        pollfds[0].revents = 0;
        pollfds[1].revents = 0;
        _ = std.posix.poll(
            &pollfds,
            computePollTimeoutMs(repeat_timeout_ms, render_pending, wayland_read_prepared),
        ) catch {};

        // Wayland events: prepare_read before poll so pending queued events
        // force an immediate dispatch instead of sleeping until new socket IO.
        completeWaylandRead(
            conn.display,
            wayland_read_prepared,
            pollfds[0].revents & std.posix.POLL.IN != 0,
        );
        _ = std.posix.poll(&pollfds, computePollTimeoutMs(repeat_timeout_ms, render_pending)) catch {};

        // Wayland events: prepare_read / read_events / dispatch_pending
        if (pollfds[0].revents & std.posix.POLL.IN != 0) {
            if (conn.display.prepareRead()) {
                _ = conn.display.readEvents();
            }
        }
        _ = conn.display.dispatchPending();

        // PTY output
        if (pollfds[1].revents & std.posix.POLL.IN != 0) {
@@ -538,23 +532,11 @@ fn remainingRepeatTimeoutMs(deadline_ns: ?i128) ?i32 {
    return @intCast(@divTrunc(remaining_ns + std.time.ns_per_ms - 1, std.time.ns_per_ms));
}

fn computePollTimeoutMs(next_repeat_in_ms: ?i32, render_pending: bool, wayland_read_prepared: bool) i32 {
    if (!wayland_read_prepared) return 0;
fn computePollTimeoutMs(next_repeat_in_ms: ?i32, render_pending: bool) i32 {
    if (render_pending) return 0;
    return next_repeat_in_ms orelse -1;
}

fn completeWaylandRead(display: anytype, prepared: bool, readable: bool) void {
    if (prepared) {
        if (readable) {
            _ = display.readEvents();
        } else {
            display.cancelRead();
        }
    }
    _ = display.dispatchPending();
}

fn shouldRenderFrame(terminal_dirty: bool, window_dirty: bool, forced: bool) bool {
    return terminal_dirty or window_dirty or forced;
}
@@ -574,11 +556,6 @@ const SelectionSpan = struct {
        return .{ .start = self.end, .end = self.start };
    }

    fn isEmpty(self: SelectionSpan) bool {
        _ = self;
        return false;
    }

    fn containsCell(self: SelectionSpan, col: u32, row: u32) bool {
        const span = self.normalized();
        if (row < span.start.row or row > span.end.row) return false;
@@ -1175,111 +1152,6 @@ fn mapKeysymToInputKey(keysym: u32) ?vt.InputKey {
    };
}

test "event loop waits indefinitely when idle and wakes for imminent repeat" {
    try std.testing.expectEqual(@as(i32, -1), computePollTimeoutMs(null, false, true));
    try std.testing.expectEqual(@as(i32, 0), computePollTimeoutMs(5, true, true));
    try std.testing.expectEqual(@as(i32, 17), computePollTimeoutMs(17, false, true));
}

test "event loop does not sleep while Wayland already has pending events" {
    try std.testing.expectEqual(@as(i32, 0), computePollTimeoutMs(null, false, false));
    try std.testing.expectEqual(@as(i32, 0), computePollTimeoutMs(23, false, false));
}

test "completeWaylandRead cancels prepared read when poll found no socket data" {
    const FakeDisplay = struct {
        read_calls: usize = 0,
        cancel_calls: usize = 0,
        dispatch_calls: usize = 0,

        fn readEvents(self: *@This()) usize {
            self.read_calls += 1;
            return 0;
        }

        fn cancelRead(self: *@This()) void {
            self.cancel_calls += 1;
        }

        fn dispatchPending(self: *@This()) usize {
            self.dispatch_calls += 1;
            return 0;
        }
    };

    var display = FakeDisplay{};
    completeWaylandRead(&display, true, false);

    try std.testing.expectEqual(@as(usize, 0), display.read_calls);
    try std.testing.expectEqual(@as(usize, 1), display.cancel_calls);
    try std.testing.expectEqual(@as(usize, 1), display.dispatch_calls);
}

test "completeWaylandRead dispatches readable socket events without canceling" {
    const FakeDisplay = struct {
        read_calls: usize = 0,
        cancel_calls: usize = 0,
        dispatch_calls: usize = 0,

        fn readEvents(self: *@This()) usize {
            self.read_calls += 1;
            return 0;
        }

        fn cancelRead(self: *@This()) void {
            self.cancel_calls += 1;
        }

        fn dispatchPending(self: *@This()) usize {
            self.dispatch_calls += 1;
            return 0;
        }
    };

    var display = FakeDisplay{};
    completeWaylandRead(&display, true, true);

    try std.testing.expectEqual(@as(usize, 1), display.read_calls);
    try std.testing.expectEqual(@as(usize, 0), display.cancel_calls);
    try std.testing.expectEqual(@as(usize, 1), display.dispatch_calls);
}

test "completeWaylandRead still dispatches pending events when prepareRead could not start" {
    const FakeDisplay = struct {
        read_calls: usize = 0,
        cancel_calls: usize = 0,
        dispatch_calls: usize = 0,

        fn readEvents(self: *@This()) usize {
            self.read_calls += 1;
            return 0;
        }

        fn cancelRead(self: *@This()) void {
            self.cancel_calls += 1;
        }

        fn dispatchPending(self: *@This()) usize {
            self.dispatch_calls += 1;
            return 0;
        }
    };

    var display = FakeDisplay{};
    completeWaylandRead(&display, false, false);

    try std.testing.expectEqual(@as(usize, 0), display.read_calls);
    try std.testing.expectEqual(@as(usize, 0), display.cancel_calls);
    try std.testing.expectEqual(@as(usize, 1), display.dispatch_calls);
}

test "event loop redraws only when terminal or window state changed" {
    try std.testing.expect(shouldRenderFrame(true, false, false));
    try std.testing.expect(shouldRenderFrame(false, true, false));
    try std.testing.expect(shouldRenderFrame(false, false, true));
    try std.testing.expect(!shouldRenderFrame(false, false, false));
}

test "planRowRefresh requests full rebuild for full dirty state" {
    const plan = planRowRefresh(.full, &.{ false, true, false }, .{
        .cursor = .{