macOS: restore menu shortcuts for non terminal first responders

pull/12522/head
Lukas 2026-04-29 21:11:21 +02:00
parent bc50714ce9
commit 72f4983b8a
No known key found for this signature in database
GPG Key ID: 1944A0A77B561220
2 changed files with 56 additions and 3 deletions

View File

@ -1,4 +1,5 @@
import AppKit
import Combine
import SwiftUI
import UserNotifications
import OSLog
@ -154,6 +155,10 @@ class AppDelegate: NSObject,
private let appIconUpdater = AppIconUpdater()
@MainActor private lazy var menuShortcutManager = Ghostty.MenuShortcutManager()
/// A signal to trigger restoration for shortcuts of registered menus, like copy and paste
let restoreShortcutsRequest = PassthroughSubject<Void, Never>()
/// A throttle observer for the signal above
private var resetMenuObserver: Any?
override init() {
#if DEBUG
@ -209,6 +214,8 @@ class AppDelegate: NSObject,
toggleSecureInput(self)
}
saveRestorableMenuItems()
// Initial config loading
ghosttyConfigDidChange(config: ghostty.config)
@ -614,8 +621,11 @@ class AppDelegate: NSObject,
return event
}
@MainActor
@objc private func windowDidBecomeKey(_ notification: Notification) {
syncFloatOnTopMenu(notification.object as? NSWindow)
let window = notification.object as? NSWindow
syncFloatOnTopMenu(window)
restoreRegisteredMenusIfNeeded(for: window)
}
@objc private func quickTerminalDidChangeVisibility(_ notification: Notification) {
@ -1138,6 +1148,17 @@ extension AppDelegate {
]
.compactMap { $0 }
.forEach(menuShortcutManager.saveRestorableMenuItem(_:))
resetMenuObserver = restoreShortcutsRequest
.throttle(for: .seconds(0.5), scheduler: DispatchQueue.main, latest: false)
.sink { [weak self] in
guard let self else { return }
// We need to check the first responder again,
// because a request could be fired multiple time in a short time.
// It's hard for us to filter them out,
// but firstResponder will be updated correctly
restoreRegisteredMenusIfNeeded(for: nil)
}
}
/// Sync all of our menu item keyboard shortcuts with the Ghostty configuration.
@ -1216,6 +1237,34 @@ extension AppDelegate {
menuShortcutManager.syncMenuShortcut(config, action: action, menuItem: menuItem)
}
@MainActor private func restoreRegisteredMenusIfNeeded(for window: NSWindow?) {
guard let window = window ?? NSApp.keyWindow else {
return
}
guard
window is TerminalWindow || window is QuickTerminalWindow,
window.firstResponder is Ghostty.SurfaceView
else {
// Restore for:
// 1. About Window
// 2. Alert modal
// 3. ConfigurationErrors
// 4. InlineTitleEditor
// 5. SearchOverlay
// 6. CommandPalette
// 7. Help search
menuShortcutManager.restoreMenuShortcuts()
return
}
// If it's a terminal window with surface focused,
// then we re-sync the menu shortcuts
syncMenuShortcuts(ghostty.config)
// For cases like after closing About which is the last window,
// the restore shortcuts will stays there and most of them should be disabled or no-op.
// The next time a terminal window is open, the shortcuts will be updated
}
@MainActor func performGhosttyBindingMenuKeyEquivalent(with event: NSEvent) -> Bool {
menuShortcutManager.performGhosttyBindingMenuKeyEquivalent(with: event)
}

View File

@ -786,16 +786,20 @@ extension Ghostty {
override func becomeFirstResponder() -> Bool {
let result = super.becomeFirstResponder()
if result { focusDidChange(true) }
(NSApp.delegate as? AppDelegate)?
.restoreShortcutsRequest
.send()
return result
}
override func resignFirstResponder() -> Bool {
let result = super.resignFirstResponder()
// We sometimes call this manually (see SplitView) as a way to force us to
// yield our focus state.
if result { focusDidChange(false) }
(NSApp.delegate as? AppDelegate)?
.restoreShortcutsRequest
.send()
return result
}