`search` binding action starts a search thread on surface

pull/9687/head
Mitchell Hashimoto 2025-11-15 20:02:35 -08:00
parent 6623c20c2d
commit e49f4a6dbc
No known key found for this signature in database
GPG Key ID: 523D5DC389D273BC
4 changed files with 84 additions and 0 deletions

View File

@ -155,6 +155,9 @@ selection_scroll_active: bool = false,
/// the wall clock time that has elapsed between timestamps.
command_timer: ?std.time.Instant = null,
/// Search state
search: ?Search = null,
/// The effect of an input event. This can be used by callers to take
/// the appropriate action after an input event. For example, key
/// input can be forwarded to the OS for further processing if it
@ -174,6 +177,26 @@ pub const InputEffect = enum {
closed,
};
/// The search state for the surface.
const Search = struct {
state: terminal.search.Thread,
thread: std.Thread,
pub fn deinit(self: *Search) void {
// Notify the thread to stop
self.state.stop.notify() catch |err| log.err(
"error notifying search thread to stop, may stall err={}",
.{err},
);
// Wait for the OS thread to quit
self.thread.join();
// Now it is safe to deinit the state
self.state.deinit();
}
};
/// Mouse state for the surface.
const Mouse = struct {
/// The last tracked mouse button state by button.
@ -728,6 +751,9 @@ pub fn init(
}
pub fn deinit(self: *Surface) void {
// Stop search thread
if (self.search) |*s| s.deinit();
// Stop rendering thread
{
self.renderer_thread.stop.notify() catch |err|
@ -1301,6 +1327,12 @@ fn reportColorScheme(self: *Surface, force: bool) void {
self.io.queueMessage(.{ .write_stable = output }, .unlocked);
}
fn searchCallback(event: terminal.search.Thread.Event, ud: ?*anyopaque) void {
const self: *Surface = @ptrCast(@alignCast(ud.?));
_ = self;
_ = event;
}
/// Call this when modifiers change. This is safe to call even if modifiers
/// match the previous state.
///
@ -4770,6 +4802,49 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
self.renderer_state.terminal.fullReset();
},
.search => |text| search: {
const s: *Search = if (self.search) |*s| s else init: {
// If we're stopping the search and we had no prior search,
// then there is nothing to do.
if (text.len == 0) break :search;
// We need to assign directly to self.search because we need
// a stable pointer back to the thread state.
self.search = .{
.state = try .init(self.alloc, .{
.mutex = self.renderer_state.mutex,
.terminal = self.renderer_state.terminal,
.event_cb = &searchCallback,
.event_userdata = self,
}),
.thread = undefined,
};
const s: *Search = &self.search.?;
errdefer s.state.deinit();
s.thread = try .spawn(
.{},
terminal.search.Thread.threadMain,
.{&s.state},
);
s.thread.setName("search") catch {};
break :init s;
};
// Zero-length text means stop searching.
if (text.len == 0) {
s.deinit();
self.search = null;
break :search;
}
_ = s.state.mailbox.push(
.{ .change_needle = text },
.forever,
);
},
.copy_to_clipboard => |format| {
// We can read from the renderer state without holding
// the lock because only we will write to this field.

View File

@ -332,6 +332,10 @@ pub const Action = union(enum) {
/// to 14.5 points.
set_font_size: f32,
/// Start a search for the given text. If the text is empty, then
/// the search is canceled. If a previous search is active, it is replaced.
search: []const u8,
/// Clear the screen and all scrollback.
clear_screen,
@ -1152,6 +1156,7 @@ pub const Action = union(enum) {
.esc,
.text,
.cursor_key,
.search,
.reset,
.copy_to_clipboard,
.copy_url_to_clipboard,

View File

@ -604,6 +604,7 @@ fn actionCommands(action: Action.Key) []const Command {
.csi,
.esc,
.cursor_key,
.search,
.set_font_size,
.scroll_to_row,
.scroll_page_fractional,

View File

@ -591,6 +591,7 @@ const Search = struct {
// Check our total match data
const total = screen_search.matchesLen();
if (total != self.last_total) {
log.debug("notifying total matches={}", .{total});
self.last_total = total;
cb(.{ .total_matches = total }, ud);
}
@ -626,11 +627,13 @@ const Search = struct {
};
}
log.debug("notifying viewport matches len={}", .{results.items.len});
cb(.{ .viewport_matches = results.items }, ud);
}
// Send our complete notification if we just completed.
if (!self.last_complete and self.isComplete()) {
log.debug("notifying search complete", .{});
self.last_complete = true;
cb(.complete, ud);
}