terminal: handle backwards selection timestamps

SelectionGesture passed caller-supplied repeat timestamps directly to
Instant.since. A C API client or non-monotonic timer could provide an
earlier timestamp after a later one, causing a runtime safety panic
while converting negative elapsed seconds to u64.

Compare instants before calculating elapsed time and treat backwards
timestamps as failed repeats. The next press becomes a new single-click
anchor, matching other invalid repeat inputs.
This commit is contained in:
Mitchell Hashimoto
2026-07-09 20:48:58 -07:00
parent 0cb004734e
commit 8f1c2fe959

View File

@@ -228,7 +228,8 @@ pub fn validatedLeftClickPin(
}
pub const Press = struct {
/// The time when the press event occurred. Use a monotonic timer.
/// The time when the press event occurred. Prefer a monotonic timer;
/// backwards timestamps reset the repeat sequence.
/// This can be null if you're on a system that doesn't support
/// time for some reason. In that case, we only support single clicks.
time: ?Time,
@@ -630,7 +631,8 @@ fn pressRepeat(
const time = p.time orelse return error.PressRequiresReset;
{
const prev_time = self.left_click_time orelse return error.PressRequiresReset;
const since = timeSince(time, prev_time);
const since = timeSince(time, prev_time) orelse
return error.PressRequiresReset;
if (since > p.repeat_interval) return error.PressRequiresReset;
}
@@ -664,8 +666,13 @@ fn pressRepeat(
self.left_click_behavior = p.behaviors[self.left_click_count - 1];
}
fn timeSince(time: Time, prev_time: Time) u64 {
if (comptime freestanding_wasm) return time -| prev_time;
fn timeSince(time: Time, prev_time: Time) ?u64 {
if (comptime freestanding_wasm) {
if (time < prev_time) return null;
return time - prev_time;
}
if (time.order(prev_time) == .lt) return null;
return time.since(prev_time);
}
@@ -935,6 +942,16 @@ fn testPress(t: *Terminal, x: u16, y: u32, time: ?std.time.Instant) Press {
};
}
fn testInstant(ns: u64) std.time.Instant {
return switch (builtin.os.tag) {
.windows, .uefi, .wasi => .{ .timestamp = ns },
else => .{ .timestamp = .{
.sec = @intCast(ns / std.time.ns_per_s),
.nsec = @intCast(ns % std.time.ns_per_s),
} },
};
}
fn testDrag(t: *Terminal, x: u16, y: u32, xpos: f64, ypos: f64) Drag {
return .{
.pin = t.screens.active.pages.pin(.{ .active = .{
@@ -1965,6 +1982,22 @@ test "SelectionGesture expired repeat resets click count" {
try testing.expectEqual(@as(u3, 1), gesture.left_click_count);
}
test "SelectionGesture backwards repeat time resets click count" {
var t = try Terminal.init(testing.allocator, .{ .cols = 5, .rows = 5 });
defer t.deinit(testing.allocator);
var gesture: SelectionGesture = .init;
defer gesture.deinit(&t);
const earlier = testInstant(std.time.ns_per_s);
const later = testInstant(2 * std.time.ns_per_s);
_ = try gesture.press(&t, testPress(&t, 1, 1, later));
_ = try gesture.press(&t, testPress(&t, 1, 1, earlier));
try testing.expectEqual(@as(u3, 1), gesture.left_click_count);
try testing.expectEqual(.eq, gesture.left_click_time.?.order(earlier));
}
test "SelectionGesture screen switch resets click count" {
var t = try Terminal.init(testing.allocator, .{ .cols = 5, .rows = 5 });
defer t.deinit(testing.allocator);