mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-18 05:20:29 +00:00
macOS: Selection for Find feature
Adds the `selection_for_search` action, with Cmd+E keybind by default. This action inputs the currently selected text into the search field without changing focus, matching standard macOS behavior.
This commit is contained in:
@@ -810,6 +810,11 @@ typedef struct {
|
||||
ssize_t selected;
|
||||
} ghostty_action_search_selected_s;
|
||||
|
||||
// apprt.action.SelectionForSearch
|
||||
typedef struct {
|
||||
const char* text;
|
||||
} ghostty_action_selection_for_search_s;
|
||||
|
||||
// terminal.Scrollbar
|
||||
typedef struct {
|
||||
uint64_t total;
|
||||
@@ -878,11 +883,12 @@ typedef enum {
|
||||
GHOSTTY_ACTION_SHOW_ON_SCREEN_KEYBOARD,
|
||||
GHOSTTY_ACTION_COMMAND_FINISHED,
|
||||
GHOSTTY_ACTION_START_SEARCH,
|
||||
GHOSTTY_ACTION_SELECTION_FOR_SEARCH,
|
||||
GHOSTTY_ACTION_END_SEARCH,
|
||||
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;
|
||||
@@ -919,6 +925,7 @@ typedef union {
|
||||
ghostty_action_progress_report_s progress_report;
|
||||
ghostty_action_command_finished_s command_finished;
|
||||
ghostty_action_start_search_s start_search;
|
||||
ghostty_action_selection_for_search_s selection_for_search;
|
||||
ghostty_action_search_total_s search_total;
|
||||
ghostty_action_search_selected_s search_selected;
|
||||
ghostty_action_readonly_e readonly;
|
||||
|
||||
@@ -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: "selection_for_search", menuItem: self.menuSelectionForFind)
|
||||
syncMenuShortcut(config, action: "search:next", menuItem: self.menuFindNext)
|
||||
syncMenuShortcut(config, action: "search:previous", menuItem: self.menuFindPrevious)
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="24412" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="24506" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<dependencies>
|
||||
<deployment identifier="macosx"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="24412"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="24506"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<customObject id="-2" userLabel="File's Owner" customClass="NSApplication">
|
||||
@@ -58,6 +58,7 @@
|
||||
<outlet property="menuSelectSplitBelow" destination="QDz-d9-CBr" id="FsH-Dq-jij"/>
|
||||
<outlet property="menuSelectSplitLeft" destination="cTK-oy-KuV" id="Jpr-5q-dqz"/>
|
||||
<outlet property="menuSelectSplitRight" destination="upj-mc-L7X" id="nLY-o1-lky"/>
|
||||
<outlet property="menuSelectionForSearch" destination="TDN-42-Bu7" id="M04-1K-vze"/>
|
||||
<outlet property="menuServices" destination="aQe-vS-j8Q" id="uWQ-Wo-T1L"/>
|
||||
<outlet property="menuSplitDown" destination="UDZ-4y-6xL" id="ptr-mj-Azh"/>
|
||||
<outlet property="menuSplitLeft" destination="Ppv-GP-lQU" id="Xd5-Cd-Jut"/>
|
||||
@@ -262,6 +263,12 @@
|
||||
<action selector="find:" target="-1" id="PeY-3u-IxC"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Selection for Find" id="TDN-42-Bu7">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="selectionForFind:" target="-1" id="rhL-7g-XQQ"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Find Next" id="XqU-X8-q32">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -128,6 +128,18 @@ extension Ghostty.Action {
|
||||
}
|
||||
}
|
||||
|
||||
struct SelectionForSearch {
|
||||
let text: String?
|
||||
|
||||
init(c: ghostty_action_selection_for_search_s) {
|
||||
if let contentCString = c.text {
|
||||
self.text = String(cString: contentCString)
|
||||
} else {
|
||||
self.text = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum PromptTitle {
|
||||
case surface
|
||||
case tab
|
||||
|
||||
@@ -621,6 +621,9 @@ extension Ghostty {
|
||||
case GHOSTTY_ACTION_START_SEARCH:
|
||||
startSearch(app, target: target, v: action.action.start_search)
|
||||
|
||||
case GHOSTTY_ACTION_SELECTION_FOR_SEARCH:
|
||||
selectionForSearch(app, target: target, v: action.action.selection_for_search)
|
||||
|
||||
case GHOSTTY_ACTION_END_SEARCH:
|
||||
endSearch(app, target: target)
|
||||
|
||||
@@ -1881,6 +1884,38 @@ extension Ghostty {
|
||||
}
|
||||
}
|
||||
|
||||
private static func selectionForSearch(
|
||||
_ app: ghostty_app_t,
|
||||
target: ghostty_target_s,
|
||||
v: ghostty_action_selection_for_search_s
|
||||
) {
|
||||
switch (target.tag) {
|
||||
case GHOSTTY_TARGET_APP:
|
||||
Ghostty.logger.warning("selection_for_search does nothing with an app target")
|
||||
return
|
||||
|
||||
case GHOSTTY_TARGET_SURFACE:
|
||||
guard let surface = target.target.surface else { return }
|
||||
guard let surfaceView = self.surfaceView(from: surface) else { return }
|
||||
|
||||
let selectionForSearch = Ghostty.Action.SelectionForSearch(c: v)
|
||||
DispatchQueue.main.async {
|
||||
if surfaceView.searchState != nil, let text = selectionForSearch.text {
|
||||
NotificationCenter.default.post(
|
||||
name: .ghosttySelectionForSearch,
|
||||
object: surfaceView,
|
||||
userInfo: [
|
||||
"text": text
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
|
||||
private static func endSearch(
|
||||
_ app: ghostty_app_t,
|
||||
target: ghostty_target_s) {
|
||||
|
||||
@@ -406,6 +406,7 @@ extension Notification.Name {
|
||||
|
||||
/// Focus the search field
|
||||
static let ghosttySearchFocus = Notification.Name("com.mitchellh.ghostty.searchFocus")
|
||||
static let ghosttySelectionForSearch = Notification.Name("com.mitchellh.ghostty.selectionForSearch")
|
||||
}
|
||||
|
||||
// NOTE: I am moving all of these to Notification.Name extensions over time. This
|
||||
|
||||
@@ -475,7 +475,16 @@ 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
|
||||
}
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: .ghosttySelectionForSearch)) { notification in
|
||||
guard notification.object as? SurfaceView === surfaceView else { return }
|
||||
if let userInfo = notification.userInfo, let text = userInfo["text"] as? String {
|
||||
searchState.needle = text
|
||||
// We do not focus the textfield after the action to match macOS behavior
|
||||
}
|
||||
}
|
||||
.background(
|
||||
GeometryReader { barGeo in
|
||||
|
||||
@@ -1519,6 +1519,14 @@ extension Ghostty {
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func selectionForFind(_ sender: Any?) {
|
||||
guard let surface = self.surface else { return }
|
||||
let action = "selection_for_search"
|
||||
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"
|
||||
|
||||
@@ -5163,6 +5163,15 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
|
||||
);
|
||||
},
|
||||
|
||||
.selection_for_search => {
|
||||
const selection = try self.selectionString(self.alloc) orelse return false;
|
||||
return try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.selection_for_search,
|
||||
.{ .text = 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
|
||||
|
||||
@@ -316,6 +316,9 @@ pub const Action = union(Key) {
|
||||
/// Start the search overlay with an optional initial needle.
|
||||
start_search: StartSearch,
|
||||
|
||||
/// Input the selected text into the search field.
|
||||
selection_for_search: SelectionForSearch,
|
||||
|
||||
/// End the search overlay, clearing the search state and hiding it.
|
||||
end_search,
|
||||
|
||||
@@ -389,6 +392,7 @@ pub const Action = union(Key) {
|
||||
show_on_screen_keyboard,
|
||||
command_finished,
|
||||
start_search,
|
||||
selection_for_search,
|
||||
end_search,
|
||||
search_total,
|
||||
search_selected,
|
||||
@@ -914,3 +918,18 @@ pub const SearchSelected = struct {
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const SelectionForSearch = struct {
|
||||
text: [:0]const u8,
|
||||
|
||||
// Sync with: ghostty_action_selection_for_search_s
|
||||
pub const C = extern struct {
|
||||
text: [*:0]const u8,
|
||||
};
|
||||
|
||||
pub fn cval(self: SelectionForSearch) C {
|
||||
return .{
|
||||
.text = self.text.ptr,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -6585,6 +6585,12 @@ pub const Keybinds = struct {
|
||||
.start_search,
|
||||
.{ .performable = true },
|
||||
);
|
||||
try self.set.putFlags(
|
||||
alloc,
|
||||
.{ .key = .{ .unicode = 'e' }, .mods = .{ .super = true } },
|
||||
.selection_for_search,
|
||||
.{ .performable = true },
|
||||
);
|
||||
try self.set.putFlags(
|
||||
alloc,
|
||||
.{ .key = .{ .unicode = 'f' }, .mods = .{ .super = true, .shift = true } },
|
||||
|
||||
@@ -368,6 +368,9 @@ pub const Action = union(enum) {
|
||||
/// If a previous search is active, it is replaced.
|
||||
search: []const u8,
|
||||
|
||||
/// Input the selected text into the search field.
|
||||
selection_for_search,
|
||||
|
||||
/// Navigate the search results. If there is no active search, this
|
||||
/// is not performed.
|
||||
navigate_search: NavigateSearch,
|
||||
@@ -1284,6 +1287,7 @@ pub const Action = union(enum) {
|
||||
.cursor_key,
|
||||
.search,
|
||||
.navigate_search,
|
||||
.selection_for_search,
|
||||
.start_search,
|
||||
.end_search,
|
||||
.reset,
|
||||
|
||||
@@ -189,6 +189,12 @@ fn actionCommands(action: Action.Key) []const Command {
|
||||
.description = "Start a search if one isn't already active.",
|
||||
}},
|
||||
|
||||
.selection_for_search => comptime &.{.{
|
||||
.action = .selection_for_search,
|
||||
.title = "Selection for Search",
|
||||
.description = "Input the selected text into the search field.",
|
||||
}},
|
||||
|
||||
.end_search => comptime &.{.{
|
||||
.action = .end_search,
|
||||
.title = "End Search",
|
||||
|
||||
Reference in New Issue
Block a user