diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index 9560ae56..938c54f7 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -2212,6 +2212,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent private var lastTypingActivityAt: TimeInterval = 0 private var didHandleExplicitOpenIntentAtStartup = false private var isTerminatingApp = false + // Set to true when the user has already confirmed quit via the warning dialog, + // so applicationShouldTerminate does not show a second alert. + private var isQuitWarningConfirmed = false private var didInstallLifecycleSnapshotObservers = false private var didDisableSuddenTermination = false private var commandPaletteVisibilityByWindowId: [UUID: Bool] = [:] @@ -2701,7 +2704,46 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply { isTerminatingApp = true _ = saveSessionSnapshot(includeScrollback: true, removeWhenEmpty: false) - return .terminateNow + + // If the user already confirmed via the Cmd+Q shortcut warning dialog + // (handleQuitShortcutWarning), skip the check to avoid a second alert. + if isQuitWarningConfirmed { + return .terminateNow + } + + // Respect the "Warn Before Quit" setting even when Cmd+Q arrives via + // the Cmd+Tab app switcher, bypassing handleCustomShortcut. + guard QuitWarningSettings.isEnabled() else { + return .terminateNow + } + + // Show the same confirmation dialog used by the Cmd+Q shortcut path, + // then reply asynchronously so we can return .terminateLater now. + DispatchQueue.main.async { + let alert = NSAlert() + alert.alertStyle = .warning + alert.messageText = String(localized: "dialog.quitCmux.title", defaultValue: "Quit cmux?") + alert.informativeText = String(localized: "dialog.quitCmux.message", defaultValue: "This will close all windows and workspaces.") + alert.addButton(withTitle: String(localized: "dialog.quitCmux.quit", defaultValue: "Quit")) + alert.addButton(withTitle: String(localized: "common.cancel", defaultValue: "Cancel")) + alert.showsSuppressionButton = true + alert.suppressionButton?.title = String(localized: "dialog.dontWarnCmdQ", defaultValue: "Don't warn again for Cmd+Q") + + let response = alert.runModal() + if alert.suppressionButton?.state == .on { + QuitWarningSettings.setEnabled(false) + } + + let shouldQuit = response == .alertFirstButtonReturn + if shouldQuit { + self.isQuitWarningConfirmed = true + } else { + // Reset so that the next quit attempt can show the dialog again. + self.isTerminatingApp = false + } + NSApp.reply(toApplicationShouldTerminate: shouldQuit) + } + return .terminateLater } func applicationWillTerminate(_ notification: Notification) { @@ -8983,6 +9025,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent } if response == .alertFirstButtonReturn { + // Mark as confirmed so applicationShouldTerminate does not show a + // second alert when NSApp.terminate re-enters the delegate callback. + isQuitWarningConfirmed = true NSApp.terminate(nil) } return true