diff --git a/src/Surface.zig b/src/Surface.zig index 63af42680..6189aae8e 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -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. diff --git a/src/input/Binding.zig b/src/input/Binding.zig index c9f3a7343..1b681e725 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -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, diff --git a/src/input/command.zig b/src/input/command.zig index b6f75080d..11f65cea3 100644 --- a/src/input/command.zig +++ b/src/input/command.zig @@ -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, diff --git a/src/terminal/search/Thread.zig b/src/terminal/search/Thread.zig index fdd5f81bc..a35d658b3 100644 --- a/src/terminal/search/Thread.zig +++ b/src/terminal/search/Thread.zig @@ -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); }