Let terminal bindings win over non-remappable macOS menu built-ins#459
Merged
Conversation
A Ghostty binding chord that collided with a default macOS app/window management menu item fired both: pressing super+alt+h bound to goto_split:left moved the split focus and also triggered Hide Others, because the surface forwarded the chord to the whole main menu via performKeyEquivalent. Resolve the specific app-owned menu item for the chord and dispatch only that item, skipping the non-remappable system built-ins (Hide, Hide Others, Show All, Minimize, Zoom, Enter Full Screen, Bring All to Front). Dispatching the resolved item directly also avoids AppKit firing a built-in that happens to share a chord with an app-owned item. App-owned shortcuts (Quit, command palette, and friends) still forward and flash, and Ghostty-only bindings such as reload_config still reach the terminal. Closes #450
sbertix
added a commit
that referenced
this pull request
Jun 22, 2026
#459 dispatched a resolved menu item via `performActionForItem(at:)` after calling the parent menu's `update()`. For SwiftUI-backed command items (Close Terminal, Quit) `update()` makes SwiftUI rebuild the menu and replace the item, so `index(of:)` returned -1, the dispatch silently failed, and the chord fell through to the terminal. ⌘W and ⌘Q stopped closing and quitting. Forward to the menu only when the chord resolves to an app-owned item, so Ghostty-only shortcuts like ⌘⇧, are not eaten by AppKit's menu-matching quirks. For the common case dispatch through the native `NSMenu.performKeyEquivalent(with:)` path, which fires SwiftUI command items reliably. When the chord also collides with a non-remappable macOS built-in (Hide, Minimize, ...), fire the resolved item directly via `NSApp.sendAction` so the built-in can't fire too and the app action's own side effects still run: a remapped close_surface keeps its explicit-close bookkeeping instead of falling through to Ghostty, which would reattach a zmx surface rather than close it. A chord with no forwardable item (e.g. ⌥⌘H Hide Others vs a goto_split binding) still falls through to Ghostty so the terminal binding wins, keeping the #459 fix intact.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
⌘⌥Hbound to a Ghostty terminal action (e.g.keybind = super+alt+h=goto_split:left) fired both the terminal action and the macOS "Hide Others" menu item, which the user cannot disable or remap from Settings → Shortcuts.⌘⌥j/k/lworked cleanly because only⌘⌥hhas a default macOS menu item.The surface forwarded any chord matching a main-menu item to
NSApp.mainMenu.performKeyEquivalenton the whole menu, so the built-in fired alongside the terminal binding.Fix
NSMenuItemfor the chord (forwardableMenuItem) and dispatch only that item (performMenuItem→performActionForItem), rather than re-runningperformKeyEquivalenton the entire menu. This also prevents AppKit from firing a built-in that happens to share a chord with an app-owned item (it would otherwise pick the first match).performGhosttyBindingMenuKeyEquivalent.⌘⇧,reload_config still reach the terminal.Tests
Added unit coverage in
GhosttySurfaceViewTestsfor the matcher (built-in skipped flat and nested, app-owned kept, implicit-shift, exact-mask interplay, and that an app-owned item sharing a chord with a built-in is the one resolved) and for the dispatch helper (fires the resolved item, rejects detached/disabled items).Closes #450