From 72f4983b8abc9a6f576f1cfda68f028e94996b5e Mon Sep 17 00:00:00 2001 From: Lukas <134181853+bo2themax@users.noreply.github.com> Date: Wed, 29 Apr 2026 21:11:21 +0200 Subject: [PATCH] macOS: restore menu shortcuts for non terminal first responders --- macos/Sources/App/macOS/AppDelegate.swift | 51 ++++++++++++++++++- .../Surface View/SurfaceView_AppKit.swift | 8 ++- 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index 038eda1c5..4bdbd247f 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -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() + /// 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) } diff --git a/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift index f631c2c05..adb7334cc 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift @@ -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 }