Add Cmd+Q quit warning with suppression toggle (#295)
This commit is contained in:
parent
8f2a52fbf2
commit
4c733d4e8e
3 changed files with 92 additions and 0 deletions
|
|
@ -1647,6 +1647,32 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
|
|||
return StoredShortcut(key: key, command: command, shift: shift, option: option, control: control)
|
||||
}
|
||||
|
||||
private func handleQuitShortcutWarning() -> Bool {
|
||||
if !QuitWarningSettings.isEnabled() {
|
||||
NSApp.terminate(nil)
|
||||
return true
|
||||
}
|
||||
|
||||
let alert = NSAlert()
|
||||
alert.alertStyle = .warning
|
||||
alert.messageText = "Quit cmux?"
|
||||
alert.informativeText = "This will close all windows and workspaces."
|
||||
alert.addButton(withTitle: "Quit")
|
||||
alert.addButton(withTitle: "Cancel")
|
||||
alert.showsSuppressionButton = true
|
||||
alert.suppressionButton?.title = "Don't warn again for Cmd+Q"
|
||||
|
||||
let response = alert.runModal()
|
||||
if alert.suppressionButton?.state == .on {
|
||||
QuitWarningSettings.setEnabled(false)
|
||||
}
|
||||
|
||||
if response == .alertFirstButtonReturn {
|
||||
NSApp.terminate(nil)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private func handleCustomShortcut(event: NSEvent) -> Bool {
|
||||
// `charactersIgnoringModifiers` can be nil for some synthetic NSEvents and certain special keys.
|
||||
// Most shortcuts below use keyCode fallbacks, so treat nil as "" rather than bailing out.
|
||||
|
|
@ -1698,6 +1724,11 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
|
|||
return false
|
||||
}
|
||||
|
||||
let normalizedFlags = flags.subtracting([.numericPad, .function])
|
||||
if normalizedFlags == [.command], chars == "q" {
|
||||
return handleQuitShortcutWarning()
|
||||
}
|
||||
|
||||
// When the terminal has active IME composition (e.g. Korean, Japanese, Chinese
|
||||
// input), don't intercept key events — let them flow through to the input method.
|
||||
if let ghosttyView = NSApp.keyWindow?.firstResponder as? GhosttyNSView,
|
||||
|
|
|
|||
|
|
@ -2416,6 +2416,22 @@ enum AppearanceSettings {
|
|||
}
|
||||
}
|
||||
|
||||
enum QuitWarningSettings {
|
||||
static let warnBeforeQuitKey = "warnBeforeQuitShortcut"
|
||||
static let defaultWarnBeforeQuit = true
|
||||
|
||||
static func isEnabled(defaults: UserDefaults = .standard) -> Bool {
|
||||
if defaults.object(forKey: warnBeforeQuitKey) == nil {
|
||||
return defaultWarnBeforeQuit
|
||||
}
|
||||
return defaults.bool(forKey: warnBeforeQuitKey)
|
||||
}
|
||||
|
||||
static func setEnabled(_ isEnabled: Bool, defaults: UserDefaults = .standard) {
|
||||
defaults.set(isEnabled, forKey: warnBeforeQuitKey)
|
||||
}
|
||||
}
|
||||
|
||||
enum ClaudeCodeIntegrationSettings {
|
||||
static let hooksEnabledKey = "claudeCodeHooksEnabled"
|
||||
static let defaultHooksEnabled = true
|
||||
|
|
@ -2446,6 +2462,7 @@ struct SettingsView: View {
|
|||
@AppStorage(BrowserLinkOpenSettings.browserHostWhitelistKey) private var browserHostWhitelist = BrowserLinkOpenSettings.defaultBrowserHostWhitelist
|
||||
@AppStorage(BrowserInsecureHTTPSettings.allowlistKey) private var browserInsecureHTTPAllowlist = BrowserInsecureHTTPSettings.defaultAllowlistText
|
||||
@AppStorage(NotificationBadgeSettings.dockBadgeEnabledKey) private var notificationDockBadgeEnabled = NotificationBadgeSettings.defaultDockBadgeEnabled
|
||||
@AppStorage(QuitWarningSettings.warnBeforeQuitKey) private var warnBeforeQuitShortcut = QuitWarningSettings.defaultWarnBeforeQuit
|
||||
@AppStorage(WorkspacePlacementSettings.placementKey) private var newWorkspacePlacement = WorkspacePlacementSettings.defaultPlacement.rawValue
|
||||
@AppStorage(WorkspaceAutoReorderSettings.key) private var workspaceAutoReorder = WorkspaceAutoReorderSettings.defaultValue
|
||||
@AppStorage(SidebarBranchLayoutSettings.key) private var sidebarBranchVerticalLayout = SidebarBranchLayoutSettings.defaultVerticalLayout
|
||||
|
|
@ -2542,6 +2559,19 @@ struct SettingsView: View {
|
|||
|
||||
SettingsCardDivider()
|
||||
|
||||
SettingsCardRow(
|
||||
"Warn Before Quit",
|
||||
subtitle: warnBeforeQuitShortcut
|
||||
? "Show a confirmation before quitting with Cmd+Q."
|
||||
: "Cmd+Q quits immediately without confirmation."
|
||||
) {
|
||||
Toggle("", isOn: $warnBeforeQuitShortcut)
|
||||
.labelsHidden()
|
||||
.controlSize(.small)
|
||||
}
|
||||
|
||||
SettingsCardDivider()
|
||||
|
||||
SettingsCardRow(
|
||||
"Sidebar Branch Layout",
|
||||
subtitle: sidebarBranchVerticalLayout
|
||||
|
|
@ -2947,6 +2977,7 @@ struct SettingsView: View {
|
|||
browserInsecureHTTPAllowlist = BrowserInsecureHTTPSettings.defaultAllowlistText
|
||||
browserInsecureHTTPAllowlistDraft = BrowserInsecureHTTPSettings.defaultAllowlistText
|
||||
notificationDockBadgeEnabled = NotificationBadgeSettings.defaultDockBadgeEnabled
|
||||
warnBeforeQuitShortcut = QuitWarningSettings.defaultWarnBeforeQuit
|
||||
newWorkspacePlacement = WorkspacePlacementSettings.defaultPlacement.rawValue
|
||||
workspaceAutoReorder = WorkspaceAutoReorderSettings.defaultValue
|
||||
sidebarBranchVerticalLayout = SidebarBranchLayoutSettings.defaultVerticalLayout
|
||||
|
|
|
|||
|
|
@ -886,6 +886,36 @@ final class AppearanceSettingsTests: XCTestCase {
|
|||
}
|
||||
}
|
||||
|
||||
final class QuitWarningSettingsTests: XCTestCase {
|
||||
func testDefaultWarnBeforeQuitIsEnabledWhenUnset() {
|
||||
let suiteName = "QuitWarningSettingsTests.Default.\(UUID().uuidString)"
|
||||
guard let defaults = UserDefaults(suiteName: suiteName) else {
|
||||
XCTFail("Failed to create isolated UserDefaults suite")
|
||||
return
|
||||
}
|
||||
defer { defaults.removePersistentDomain(forName: suiteName) }
|
||||
|
||||
defaults.removeObject(forKey: QuitWarningSettings.warnBeforeQuitKey)
|
||||
|
||||
XCTAssertTrue(QuitWarningSettings.isEnabled(defaults: defaults))
|
||||
}
|
||||
|
||||
func testStoredPreferenceOverridesDefault() {
|
||||
let suiteName = "QuitWarningSettingsTests.Stored.\(UUID().uuidString)"
|
||||
guard let defaults = UserDefaults(suiteName: suiteName) else {
|
||||
XCTFail("Failed to create isolated UserDefaults suite")
|
||||
return
|
||||
}
|
||||
defer { defaults.removePersistentDomain(forName: suiteName) }
|
||||
|
||||
defaults.set(false, forKey: QuitWarningSettings.warnBeforeQuitKey)
|
||||
XCTAssertFalse(QuitWarningSettings.isEnabled(defaults: defaults))
|
||||
|
||||
defaults.set(true, forKey: QuitWarningSettings.warnBeforeQuitKey)
|
||||
XCTAssertTrue(QuitWarningSettings.isEnabled(defaults: defaults))
|
||||
}
|
||||
}
|
||||
|
||||
final class UpdateChannelSettingsTests: XCTestCase {
|
||||
func testResolvedFeedFallsBackWhenInfoFeedMissing() {
|
||||
let resolved = UpdateFeedResolver.resolvedFeedURLString(infoFeedURL: nil)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue