macOS: fix Find Next/Previous button in the menu bar is not working as expected (#12070)

I don’t know why the search-related commands were added as performable
keybinds in 240d5e0fc5, but **I asked
Claude to add some tests for that**

> This won't fix cmd+g/G not working when the search bar is focused.
This commit is contained in:
Mitchell Hashimoto
2026-04-02 12:56:39 -07:00
committed by GitHub
5 changed files with 81 additions and 20 deletions

View File

@@ -1171,8 +1171,8 @@ extension AppDelegate {
syncMenuShortcut(config, action: "start_search", menuItem: self.menuFind)
syncMenuShortcut(config, action: "search_selection", menuItem: self.menuSelectionForFind)
syncMenuShortcut(config, action: "scroll_to_selection", menuItem: self.menuScrollToSelection)
syncMenuShortcut(config, action: "search:next", menuItem: self.menuFindNext)
syncMenuShortcut(config, action: "search:previous", menuItem: self.menuFindPrevious)
syncMenuShortcut(config, action: "navigate_search:next", menuItem: self.menuFindNext)
syncMenuShortcut(config, action: "navigate_search:previous", menuItem: self.menuFindPrevious)
syncMenuShortcut(config, action: "toggle_split_zoom", menuItem: self.menuZoomSplit)
syncMenuShortcut(config, action: "goto_split:previous", menuItem: self.menuPreviousSplit)

View File

@@ -446,18 +446,16 @@ extension Ghostty {
}
#endif
.backport.onKeyPress(.return) { modifiers in
guard let surface = surfaceView.surface else { return .ignored }
let action = modifiers.contains(.shift)
? "navigate_search:previous"
: "navigate_search:next"
ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))
if modifiers.contains(.shift) {
_ = surfaceView.navigateSearchToPrevious()
} else {
_ = surfaceView.navigateSearchToNext()
}
return .handled
}
Button(action: {
guard let surface = surfaceView.surface else { return }
let action = "navigate_search:next"
ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))
_ = surfaceView.navigateSearchToNext()
}, label: {
Image(systemName: "chevron.up")
})
@@ -1277,4 +1275,28 @@ extension Ghostty.SurfaceView {
self.needle = startSearch.needle ?? ""
}
}
func navigateSearchToNext() -> Bool {
guard let surface = self.surface else { return false }
let action = "navigate_search:next"
if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) {
#if canImport(AppKit)
AppDelegate.logger.warning("action failed action=\(action)")
#endif
return false
}
return true
}
func navigateSearchToPrevious() -> Bool {
guard let surface = self.surface else { return false }
let action = "navigate_search:previous"
if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) {
#if canImport(AppKit)
AppDelegate.logger.warning("action failed action=\(action)")
#endif
return false
}
return true
}
}

View File

@@ -1604,19 +1604,11 @@ extension Ghostty {
}
@IBAction func findNext(_ sender: Any?) {
guard let surface = self.surface else { return }
let action = "search:next"
if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) {
AppDelegate.logger.warning("action failed action=\(action)")
}
_ = self.navigateSearchToNext()
}
@IBAction func findPrevious(_ sender: Any?) {
guard let surface = self.surface else { return }
let action = "search:previous"
if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) {
AppDelegate.logger.warning("action failed action=\(action)")
}
_ = navigateSearchToPrevious()
}
@IBAction func findHide(_ sender: Any?) {

View File

@@ -221,6 +221,8 @@ struct ConfigTests {
#expect(config.focusFollowsMouse == true)
}
// MARK: - Keybind
@Test
func uppercasedLetterShouldBeNormalized() async throws {
let config = try TemporaryConfig("""
@@ -235,4 +237,13 @@ struct ConfigTests {
let shortcut2 = try #require(config2.keyboardShortcut(for: "goto_split:left"))
#expect(shortcut2 == .init("ä", modifiers: [.command]))
}
@Test
func emptyConfigShouldBeHaveDefaultShortcut() async throws {
let config = try TemporaryConfig("")
let newWindow = try #require(config.keyboardShortcut(for: "new_window"))
#expect(newWindow == .init("n", modifiers: [.command]))
let gotoToNextSplit = try #require(config.keyboardShortcut(for: "goto_split:next"))
#expect(gotoToNextSplit == .init("]", modifiers: [.command]))
}
}

View File

@@ -1,3 +1,4 @@
const builtin = @import("builtin");
const std = @import("std");
const inputpkg = @import("../input.zig");
const state = &@import("../global.zig").state;
@@ -242,3 +243,38 @@ test "ghostty_config_get: struct cval conversion" {
try testing.expectEqual(@as(u8, 34), out.g);
try testing.expectEqual(@as(u8, 56), out.b);
}
test "ghostty_config_trigger: default keybind" {
const testing = std.testing;
var cfg = try Config.default(testing.allocator);
defer cfg.deinit();
// Default commands should be fetchable through config_trigger_
{
const trigger = try config_trigger_(&cfg, "open_config");
try testing.expectEqual(.unicode, trigger.tag);
try testing.expectEqual(@as(u32, ','), trigger.key.unicode);
}
{
const trigger = try config_trigger_(&cfg, "reload_config");
try testing.expectEqual(.unicode, trigger.tag);
try testing.expectEqual(@as(u32, ','), trigger.key.unicode);
}
// Performable bindings are not tracked in the reverse map,
// so config_trigger_ should return a default (empty) trigger.
if (comptime builtin.target.os.tag.isDarwin()) {
const next = try config_trigger_(&cfg, "navigate_search:next");
try testing.expectEqual(.physical, next.tag);
try testing.expectEqual(.unidentified, next.key.physical);
const prev = try config_trigger_(&cfg, "navigate_search:previous");
try testing.expectEqual(.physical, prev.tag);
try testing.expectEqual(.unidentified, prev.key.physical);
}
{
const trigger = try config_trigger_(&cfg, "adjust_selection:left");
try testing.expectEqual(.physical, trigger.tag);
try testing.expectEqual(.unidentified, trigger.key.physical);
}
}