Skip to content

Let terminal bindings win over non-remappable macOS menu built-ins#459

Merged
sbertix merged 2 commits into
mainfrom
sbertix/gh-450
Jun 22, 2026
Merged

Let terminal bindings win over non-remappable macOS menu built-ins#459
sbertix merged 2 commits into
mainfrom
sbertix/gh-450

Conversation

@sbertix

@sbertix sbertix commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

Summary

⌘⌥H bound 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/l worked cleanly because only ⌘⌥h has a default macOS menu item.

The surface forwarded any chord matching a main-menu item to NSApp.mainMenu.performKeyEquivalent on the whole menu, so the built-in fired alongside the terminal binding.

Fix

  • Resolve the specific app-owned NSMenuItem for the chord (forwardableMenuItem) and dispatch only that item (performMenuItemperformActionForItem), rather than re-running performKeyEquivalent on 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).
  • Skip the non-remappable macOS app/window built-ins when matching: Hide, Hide Others, Show All, Minimize, Zoom, Enter Full Screen, Bring All to Front. A colliding terminal binding wins over them. This mirrors upstream Ghostty's performGhosttyBindingMenuKeyEquivalent.
  • App-owned shortcuts (Quit, command palette, etc.) still forward and flash; Ghostty-only bindings such as ⌘⇧, reload_config still reach the terminal.

Tests

Added unit coverage in GhosttySurfaceViewTests for 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

@sbertix sbertix enabled auto-merge (squash) June 22, 2026 20:25
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
@tuist

tuist Bot commented Jun 22, 2026

Copy link
Copy Markdown

🛠️ Tuist Run Report 🛠️

Builds 🔨

Scheme Status Duration Commit
supacode 2m 24s 1b92f47ef

@sbertix sbertix merged commit dccf58f into main Jun 22, 2026
1 check passed
@sbertix sbertix deleted the sbertix/gh-450 branch June 22, 2026 20:46
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

⌘⌥H ("Hide Others") fires even when the chord is bound to a terminal action

1 participant