From 6a41c9f42ff011bc68a18e24e8e5294469388c5f Mon Sep 17 00:00:00 2001 From: Achieve Date: Fri, 27 Mar 2026 06:12:32 +0800 Subject: [PATCH] fix(quit): enforce Warn Before Quit when Cmd+Q arrives via app switcher (#2186) applicationShouldTerminate was returning .terminateNow unconditionally, bypassing QuitWarningSettings. Now it shows the same confirmation alert used by handleQuitShortcutWarning, with an isQuitWarningConfirmed flag to prevent a double dialog when the Cmd+Q shortcut path already confirmed. Fixes #2139 Co-authored-by: Claude Sonnet 4.6 --- Sources/AppDelegate.swift | 47 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) 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