diff --git a/include/ghostty.h b/include/ghostty.h index 0ad15cf69..5fc3a7433 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -882,7 +882,7 @@ typedef enum { GHOSTTY_ACTION_SEARCH_TOTAL, GHOSTTY_ACTION_SEARCH_SELECTED, GHOSTTY_ACTION_READONLY, - } ghostty_action_tag_e; +} ghostty_action_tag_e; typedef union { ghostty_action_split_direction_e new_split; diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index 57bfba828..c0886607c 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -46,6 +46,7 @@ class AppDelegate: NSObject, @IBOutlet private var menuSelectAll: NSMenuItem? @IBOutlet private var menuFindParent: NSMenuItem? @IBOutlet private var menuFind: NSMenuItem? + @IBOutlet private var menuSelectionForFind: NSMenuItem? @IBOutlet private var menuFindNext: NSMenuItem? @IBOutlet private var menuFindPrevious: NSMenuItem? @IBOutlet private var menuHideFindBar: NSMenuItem? @@ -615,6 +616,7 @@ class AppDelegate: NSObject, syncMenuShortcut(config, action: "paste_from_selection", menuItem: self.menuPasteSelection) syncMenuShortcut(config, action: "select_all", menuItem: self.menuSelectAll) syncMenuShortcut(config, action: "start_search", menuItem: self.menuFind) + syncMenuShortcut(config, action: "search_selection", menuItem: self.menuSelectionForFind) syncMenuShortcut(config, action: "search:next", menuItem: self.menuFindNext) syncMenuShortcut(config, action: "search:previous", menuItem: self.menuFindPrevious) diff --git a/macos/Sources/App/macOS/MainMenu.xib b/macos/Sources/App/macOS/MainMenu.xib index a321061dd..aa9aca952 100644 --- a/macos/Sources/App/macOS/MainMenu.xib +++ b/macos/Sources/App/macOS/MainMenu.xib @@ -1,8 +1,8 @@ - + - + @@ -58,6 +58,7 @@ + @@ -281,6 +282,13 @@ + + + + + + + diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index fb86ce8f7..a4e0da7ee 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -1383,7 +1383,11 @@ class BaseTerminalController: NSWindowController, @IBAction func find(_ sender: Any) { focusedSurface?.find(sender) } - + + @IBAction func selectionForFind(_ sender: Any) { + focusedSurface?.selectionForFind(sender) + } + @IBAction func findNext(_ sender: Any) { focusedSurface?.findNext(sender) } diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index 4e9166168..959f78197 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -1869,11 +1869,15 @@ extension Ghostty { let startSearch = Ghostty.Action.StartSearch(c: v) DispatchQueue.main.async { - if surfaceView.searchState != nil { - NotificationCenter.default.post(name: .ghosttySearchFocus, object: surfaceView) + if let searchState = surfaceView.searchState { + if let needle = startSearch.needle, !needle.isEmpty { + searchState.needle = needle + } } else { surfaceView.searchState = Ghostty.SurfaceView.SearchState(from: startSearch) } + + NotificationCenter.default.post(name: .ghosttySearchFocus, object: surfaceView) } default: diff --git a/macos/Sources/Ghostty/Surface View/SurfaceView.swift b/macos/Sources/Ghostty/Surface View/SurfaceView.swift index c224d373e..5609f36b7 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceView.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceView.swift @@ -475,7 +475,9 @@ extension Ghostty { } .onReceive(NotificationCenter.default.publisher(for: .ghosttySearchFocus)) { notification in guard notification.object as? SurfaceView === surfaceView else { return } - isSearchFieldFocused = true + DispatchQueue.main.async { + isSearchFieldFocused = true + } } .background( GeometryReader { barGeo in diff --git a/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift index 7f33df45a..a5ba62571 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift @@ -1519,6 +1519,14 @@ extension Ghostty { } } + @IBAction func selectionForFind(_ sender: Any?) { + guard let surface = self.surface else { return } + let action = "search_selection" + if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) { + AppDelegate.logger.warning("action failed action=\(action)") + } + } + @IBAction func findNext(_ sender: Any?) { guard let surface = self.surface else { return } let action = "search:next" diff --git a/src/Surface.zig b/src/Surface.zig index 43ee440c2..1f3e4da8b 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -5163,6 +5163,15 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool ); }, + .search_selection => { + const selection = try self.selectionString(self.alloc) orelse return false; + return try self.rt_app.performAction( + .{ .surface = self }, + .start_search, + .{ .needle = selection }, + ); + }, + .end_search => { // We only return that this was performed if we actually // stopped a search, but we also send the apprt end_search so diff --git a/src/apprt/action.zig b/src/apprt/action.zig index 25fc6f08a..78f4bef54 100644 --- a/src/apprt/action.zig +++ b/src/apprt/action.zig @@ -313,7 +313,9 @@ pub const Action = union(Key) { /// A command has finished, command_finished: CommandFinished, - /// Start the search overlay with an optional initial needle. + /// Start the search overlay with an optional initial needle. If the + /// search is already active and the needle is non-empty, update the + /// current search needle and focus the search input. start_search: StartSearch, /// End the search overlay, clearing the search state and hiding it. diff --git a/src/config/Config.zig b/src/config/Config.zig index 88f3d5375..ef6132912 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -6585,6 +6585,12 @@ pub const Keybinds = struct { .start_search, .{ .performable = true }, ); + try self.set.putFlags( + alloc, + .{ .key = .{ .unicode = 'e' }, .mods = .{ .super = true } }, + .search_selection, + .{ .performable = true }, + ); try self.set.putFlags( alloc, .{ .key = .{ .unicode = 'f' }, .mods = .{ .super = true, .shift = true } }, diff --git a/src/input/Binding.zig b/src/input/Binding.zig index d5b24c61b..08f5fdf7c 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -368,6 +368,11 @@ pub const Action = union(enum) { /// If a previous search is active, it is replaced. search: []const u8, + /// Start a search for the current text selection. If there is no + /// selection, this does nothing. If a search is already active, this + /// changes the search terms. + search_selection, + /// Navigate the search results. If there is no active search, this /// is not performed. navigate_search: NavigateSearch, @@ -1284,6 +1289,7 @@ pub const Action = union(enum) { .cursor_key, .search, .navigate_search, + .search_selection, .start_search, .end_search, .reset, diff --git a/src/input/command.zig b/src/input/command.zig index f089112db..d6d2b0247 100644 --- a/src/input/command.zig +++ b/src/input/command.zig @@ -189,6 +189,12 @@ fn actionCommands(action: Action.Key) []const Command { .description = "Start a search if one isn't already active.", }}, + .search_selection => comptime &.{.{ + .action = .search_selection, + .title = "Search Selection", + .description = "Start a search for the current text selection.", + }}, + .end_search => comptime &.{.{ .action = .end_search, .title = "End Search",