terminal: search thread refresh timer to reconcile state

pull/9602/head
Mitchell Hashimoto 2025-11-15 13:41:35 -08:00
parent acab8c90a2
commit f0af63db15
No known key found for this signature in database
GPG Key ID: 523D5DC389D273BC
1 changed files with 86 additions and 0 deletions

View File

@ -28,6 +28,12 @@ const ViewportSearch = @import("viewport.zig").ViewportSearch;
const log = std.log.scoped(.search_thread); const log = std.log.scoped(.search_thread);
/// The interval at which we refresh the terminal state to check if
/// there are any changes that require us to re-search. This should be
/// balanced to be fast enough to be responsive but not so fast that
/// we hold the terminal lock too often.
const REFRESH_INTERVAL = 24; // 40 FPS
/// Allocator used for some state /// Allocator used for some state
alloc: std.mem.Allocator, alloc: std.mem.Allocator,
@ -47,6 +53,13 @@ wakeup_c: xev.Completion = .{},
stop: xev.Async, stop: xev.Async,
stop_c: xev.Completion = .{}, stop_c: xev.Completion = .{},
/// The timer used for refreshing the terminal state to determine if
/// we have a stale active area, viewport, screen change, etc. This is
/// CPU intensive so we stop doing this under certain conditions.
refresh: xev.Timer,
refresh_c: xev.Completion = .{},
refresh_active: bool = false,
/// Search state. Starts as null and is populated when a search is /// Search state. Starts as null and is populated when a search is
/// started (a needle is given). /// started (a needle is given).
search: ?Search = null, search: ?Search = null,
@ -74,12 +87,17 @@ pub fn init(alloc: Allocator, opts: Options) !Thread {
var stop_h = try xev.Async.init(); var stop_h = try xev.Async.init();
errdefer stop_h.deinit(); errdefer stop_h.deinit();
// Refresh timer, see comments.
var refresh_h = try xev.Timer.init();
errdefer refresh_h.deinit();
return .{ return .{
.alloc = alloc, .alloc = alloc,
.mailbox = mailbox, .mailbox = mailbox,
.loop = loop, .loop = loop,
.wakeup = wakeup_h, .wakeup = wakeup_h,
.stop = stop_h, .stop = stop_h,
.refresh = refresh_h,
.opts = opts, .opts = opts,
}; };
} }
@ -87,6 +105,7 @@ pub fn init(alloc: Allocator, opts: Options) !Thread {
/// Clean up the thread. This is only safe to call once the thread /// Clean up the thread. This is only safe to call once the thread
/// completes executing; the caller must join prior to this. /// completes executing; the caller must join prior to this.
pub fn deinit(self: *Thread) void { pub fn deinit(self: *Thread) void {
self.refresh.deinit();
self.wakeup.deinit(); self.wakeup.deinit();
self.stop.deinit(); self.stop.deinit();
self.loop.deinit(); self.loop.deinit();
@ -130,6 +149,9 @@ fn threadMain_(self: *Thread) !void {
// Send an initial wakeup so we drain our mailbox immediately. // Send an initial wakeup so we drain our mailbox immediately.
try self.wakeup.notify(); try self.wakeup.notify();
// Start the refresh timer
self.startRefreshTimer();
// Run // Run
log.debug("starting search thread", .{}); log.debug("starting search thread", .{});
defer log.debug("starting search thread shutdown", .{}); defer log.debug("starting search thread shutdown", .{});
@ -192,6 +214,13 @@ fn threadMain_(self: *Thread) !void {
// Ticking can complete our search. // Ticking can complete our search.
if (s.isComplete()) { if (s.isComplete()) {
if (self.opts.event_cb) |cb| { if (self.opts.event_cb) |cb| {
// Send all pending notifications before we send complete.
s.notify(
self.alloc,
cb,
self.opts.event_userdata,
);
cb( cb(
.complete, .complete,
self.opts.event_userdata, self.opts.event_userdata,
@ -240,6 +269,30 @@ fn changeNeedle(self: *Thread, needle: []const u8) !void {
self.search.?.feed(self.alloc, self.opts.terminal); self.search.?.feed(self.alloc, self.opts.terminal);
} }
fn startRefreshTimer(self: *Thread) void {
// Set our active state so it knows we're running. We set this before
// even checking the active state in case we have a pending shutdown.
self.refresh_active = true;
// If our timer is already active, then we don't have to do anything.
if (self.refresh_c.state() == .active) return;
// Start the timer which loops
self.refresh.run(
&self.loop,
&self.refresh_c,
REFRESH_INTERVAL,
Thread,
self,
refreshCallback,
);
}
fn stopRefreshTimer(self: *Thread) void {
// This will stop the refresh on the next iteration.
self.refresh_active = false;
}
fn wakeupCallback( fn wakeupCallback(
self_: ?*Thread, self_: ?*Thread,
_: *xev.Loop, _: *xev.Loop,
@ -272,6 +325,39 @@ fn stopCallback(
return .disarm; return .disarm;
} }
fn refreshCallback(
self_: ?*Thread,
_: *xev.Loop,
_: *xev.Completion,
r: xev.Timer.RunError!void,
) xev.CallbackAction {
_ = r catch unreachable;
const self: *Thread = self_ orelse {
// This shouldn't happen so we log it.
log.warn("refresh callback fired without data set", .{});
return .disarm;
};
// Run our feed if we have a search active.
if (self.search) |*s| {
self.opts.mutex.lock();
defer self.opts.mutex.unlock();
s.feed(self.alloc, self.opts.terminal);
}
// Only continue if we're still active
if (self.refresh_active) self.refresh.run(
&self.loop,
&self.refresh_c,
REFRESH_INTERVAL,
Thread,
self,
refreshCallback,
);
return .disarm;
}
pub const Options = struct { pub const Options = struct {
/// Mutex that must be held while reading/writing the terminal. /// Mutex that must be held while reading/writing the terminal.
mutex: *Mutex, mutex: *Mutex,