6a5f175f
Refine selection span semantics
a73x 2026-04-09 17:47
diff --git a/src/main.zig b/src/main.zig index 9514ac6..0badb6e 100644 --- a/src/main.zig +++ b/src/main.zig @@ -213,16 +213,22 @@ 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()); _ = 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(); 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, ); // PTY output if (pollfds[1].revents & std.posix.POLL.IN != 0) { @@ -532,11 +538,23 @@ 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) i32 { fn computePollTimeoutMs(next_repeat_in_ms: ?i32, render_pending: bool, wayland_read_prepared: bool) i32 { if (!wayland_read_prepared) return 0; 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; } @@ -557,13 +575,11 @@ const SelectionSpan = struct { } fn isEmpty(self: SelectionSpan) bool { const span = self.normalized(); return span.start.row == span.end.row and span.start.col == span.end.col; _ = self; return false; } fn containsCell(self: SelectionSpan, col: u32, row: u32) bool { if (self.isEmpty()) return false; const span = self.normalized(); if (row < span.start.row or row > span.end.row) return false; if (span.start.row == span.end.row) { @@ -575,35 +591,26 @@ const SelectionSpan = struct { } }; fn selectionIntersectsVisibleGrid(span: SelectionSpan, cols: u16, rows: u16) bool { if (cols == 0 or rows == 0) return false; fn clampSelectionSpan(span: SelectionSpan, cols: u16, rows: u16) ?SelectionSpan { if (cols == 0 or rows == 0) return null; const normalized = span.normalized(); const max_col = @as(u32, cols) - 1; const max_row = @as(u32, rows) - 1; if (normalized.start.row > max_row) return null; if (normalized.start.row == normalized.end.row and normalized.start.col > max_col) return null; if (normalized.start.row > max_row) return false; if (normalized.start.row == normalized.end.row and normalized.start.col > max_col) return false; return true; } fn clampSelectionSpan(span: SelectionSpan, cols: u16, rows: u16) ?SelectionSpan { if (!selectionIntersectsVisibleGrid(span, cols, rows)) return null; const max_col = @as(u32, cols) - 1; const max_row = @as(u32, rows) - 1; const clamped = SelectionSpan{ .start = .{ .col = @min(span.start.col, max_col), .row = @min(span.start.row, max_row), .col = @min(normalized.start.col, max_col), .row = @min(normalized.start.row, max_row), }, .end = .{ .col = @min(span.end.col, max_col), .row = @min(span.end.row, max_row), .col = @min(normalized.end.col, max_col), .row = @min(normalized.end.row, max_row), }, }; if (clamped.isEmpty()) return null; return clamped.normalized(); } @@ -633,14 +640,28 @@ test "SelectionSpan.containsCell includes the normalized endpoints" { try std.testing.expect(!span.containsCell(4, 2)); } test "SelectionSpan.containsCell treats a degenerate span as empty" { test "SelectionSpan.containsCell includes a same-cell span" { const span = SelectionSpan{ .start = .{ .col = 5, .row = 5 }, .end = .{ .col = 5, .row = 5 }, }; try std.testing.expect(span.isEmpty()); try std.testing.expect(!span.containsCell(5, 5)); try std.testing.expect(span.containsCell(5, 5)); try std.testing.expect(!span.containsCell(4, 5)); try std.testing.expect(!span.containsCell(5, 4)); } test "clampSelectionSpan preserves a same-cell visible span" { const clamped = clampSelectionSpan(.{ .start = .{ .col = 5, .row = 5 }, .end = .{ .col = 5, .row = 5 }, }, 80, 24).?; try std.testing.expectEqual(@as(u32, 5), clamped.start.col); try std.testing.expectEqual(@as(u32, 5), clamped.start.row); try std.testing.expectEqual(@as(u32, 5), clamped.end.col); try std.testing.expectEqual(@as(u32, 5), clamped.end.row); try std.testing.expect(clamped.containsCell(5, 5)); } test "clampSelectionSpan clears offscreen spans and trims resized spans" { @@ -658,11 +679,19 @@ test "clampSelectionSpan clears offscreen spans and trims resized spans" { try std.testing.expectEqual(@as(u32, 22), clamped.start.row); try std.testing.expectEqual(@as(u32, 79), clamped.end.col); try std.testing.expectEqual(@as(u32, 23), clamped.end.row); } try std.testing.expect(clampSelectionSpan(.{ .start = .{ .col = 81, .row = 5 }, .end = .{ .col = 90, .row = 5 }, }, 80, 24) == null); test "clampSelectionSpan preserves a larger span that collapses to one visible cell" { const clamped = clampSelectionSpan(.{ .start = .{ .col = 0, .row = 0 }, .end = .{ .col = 120, .row = 80 }, }, 1, 1).?; try std.testing.expectEqual(@as(u32, 0), clamped.start.col); try std.testing.expectEqual(@as(u32, 0), clamped.start.row); try std.testing.expectEqual(@as(u32, 0), clamped.end.col); try std.testing.expectEqual(@as(u32, 0), clamped.end.row); try std.testing.expect(clamped.containsCell(0, 0)); } const ComparisonVariant = struct { @@ -1147,9 +1176,101 @@ 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)); try std.testing.expectEqual(@as(i32, 0), computePollTimeoutMs(5, true)); try std.testing.expectEqual(@as(i32, 17), computePollTimeoutMs(17, false)); 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" {