diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index 1b1e1c823..287277a11 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -5064,6 +5064,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent } private func confirmCloseMainWindow(_ window: NSWindow) -> Bool { + guard CloseWindowSettings.isEnabled() else { return true } + #if DEBUG if let debugCloseMainWindowConfirmationHandler { return debugCloseMainWindowConfirmationHandler(window) diff --git a/Sources/TabManager.swift b/Sources/TabManager.swift index adb789ba0..8061ff215 100644 --- a/Sources/TabManager.swift +++ b/Sources/TabManager.swift @@ -2827,14 +2827,16 @@ class TabManager: ObservableObject { func closeOtherTabsInFocusedPaneWithConfirmation() { guard let plan = closeOtherTabsInFocusedPanePlan() else { return } - let count = plan.panelIds.count - let titleLines = plan.titles.map { "• \($0)" }.joined(separator: "\n") - let message = "This is about to close \(count) tab\(count == 1 ? "" : "s") in this pane:\n\(titleLines)" - guard confirmClose( - title: "Close other tabs?", - message: message, - acceptCmdD: false - ) else { return } + if BatchCloseSettings.isEnabled() { + let count = plan.panelIds.count + let titleLines = plan.titles.map { "• \($0)" }.joined(separator: "\n") + let message = "This is about to close \(count) tab\(count == 1 ? "" : "s") in this pane:\n\(titleLines)" + guard confirmClose( + title: "Close other tabs?", + message: message, + acceptCmdD: false + ) else { return } + } for panelId in plan.panelIds { _ = plan.workspace.closePanel(panelId, force: true) @@ -2861,7 +2863,7 @@ class TabManager: ObservableObject { @discardableResult func closeWorkspaceWithConfirmation(_ workspace: Workspace) -> Bool { - if workspace.isPinned { + if workspace.isPinned && ClosePinnedWorkspaceSettings.isEnabled() { guard confirmClose( title: String(localized: "dialog.closePinnedWorkspace.title", defaultValue: "Close pinned workspace?"), message: String( @@ -2899,15 +2901,25 @@ class TabManager: ObservableObject { } let plan = closeWorkspacesPlan(for: workspaces) - guard confirmClose( - title: plan.title, - message: plan.message, - acceptCmdD: plan.acceptCmdD - ) else { return } + let batchConfirmed: Bool + if BatchCloseSettings.isEnabled() { + guard confirmClose( + title: plan.title, + message: plan.message, + acceptCmdD: plan.acceptCmdD + ) else { return } + batchConfirmed = true + } else { + batchConfirmed = false + } for workspace in plan.workspaces { guard tabs.contains(where: { $0.id == workspace.id }) else { continue } - closeWorkspaceIfRunningProcess(workspace, requiresConfirmation: false) + if batchConfirmed { + closeWorkspaceIfRunningProcess(workspace, requiresConfirmation: false) + } else { + _ = closeWorkspaceWithConfirmation(workspace) + } } } diff --git a/Sources/Workspace.swift b/Sources/Workspace.swift index e94abe72c..83b5d5d17 100644 --- a/Sources/Workspace.swift +++ b/Sources/Workspace.swift @@ -6735,7 +6735,7 @@ final class Workspace: Identifiable, ObservableObject { case .promptIdle: return false case .commandRunning: - return true + return CloseRunningProcessSettings.isEnabled() case .unknown: return fallbackNeedsConfirmClose } diff --git a/Sources/cmuxApp.swift b/Sources/cmuxApp.swift index d896224bb..2a655fdbe 100644 --- a/Sources/cmuxApp.swift +++ b/Sources/cmuxApp.swift @@ -3768,6 +3768,70 @@ enum QuitWarningSettings { } } +enum CloseRunningProcessSettings { + static let enabledKey = "confirmCloseRunningProcess" + static let defaultEnabled = true + + static func isEnabled(defaults: UserDefaults = .standard) -> Bool { + if defaults.object(forKey: enabledKey) == nil { + return defaultEnabled + } + return defaults.bool(forKey: enabledKey) + } + + static func setEnabled(_ isEnabled: Bool, defaults: UserDefaults = .standard) { + defaults.set(isEnabled, forKey: enabledKey) + } +} + +enum ClosePinnedWorkspaceSettings { + static let enabledKey = "confirmClosePinnedWorkspace" + static let defaultEnabled = true + + static func isEnabled(defaults: UserDefaults = .standard) -> Bool { + if defaults.object(forKey: enabledKey) == nil { + return defaultEnabled + } + return defaults.bool(forKey: enabledKey) + } + + static func setEnabled(_ isEnabled: Bool, defaults: UserDefaults = .standard) { + defaults.set(isEnabled, forKey: enabledKey) + } +} + +enum CloseWindowSettings { + static let enabledKey = "confirmCloseWindow" + static let defaultEnabled = true + + static func isEnabled(defaults: UserDefaults = .standard) -> Bool { + if defaults.object(forKey: enabledKey) == nil { + return defaultEnabled + } + return defaults.bool(forKey: enabledKey) + } + + static func setEnabled(_ isEnabled: Bool, defaults: UserDefaults = .standard) { + defaults.set(isEnabled, forKey: enabledKey) + } +} + +enum BatchCloseSettings { + static let enabledKey = "confirmBatchClose" + static let defaultEnabled = true + + static func isEnabled(defaults: UserDefaults = .standard) -> Bool { + if defaults.object(forKey: enabledKey) == nil { + return defaultEnabled + } + return defaults.bool(forKey: enabledKey) + } + + static func setEnabled(_ isEnabled: Bool, defaults: UserDefaults = .standard) { + defaults.set(isEnabled, forKey: enabledKey) + } +} + enum CommandPaletteRenameSelectionSettings { static let selectAllOnFocusKey = "commandPalette.renameSelectAllOnFocus" static let defaultSelectAllOnFocus = true @@ -3932,6 +3996,10 @@ struct SettingsView: View { @AppStorage(NotificationPaneFlashSettings.enabledKey) private var notificationPaneFlashEnabled = NotificationPaneFlashSettings.defaultEnabled @AppStorage(MenuBarExtraSettings.showInMenuBarKey) private var showMenuBarExtra = MenuBarExtraSettings.defaultShowInMenuBar @AppStorage(QuitWarningSettings.warnBeforeQuitKey) private var warnBeforeQuitShortcut = QuitWarningSettings.defaultWarnBeforeQuit + @AppStorage(CloseRunningProcessSettings.enabledKey) private var confirmCloseRunningProcess = CloseRunningProcessSettings.defaultEnabled + @AppStorage(ClosePinnedWorkspaceSettings.enabledKey) private var confirmClosePinnedWorkspace = ClosePinnedWorkspaceSettings.defaultEnabled + @AppStorage(CloseWindowSettings.enabledKey) private var confirmCloseWindow = CloseWindowSettings.defaultEnabled + @AppStorage(BatchCloseSettings.enabledKey) private var confirmBatchClose = BatchCloseSettings.defaultEnabled @AppStorage(CommandPaletteRenameSelectionSettings.selectAllOnFocusKey) private var commandPaletteRenameSelectAllOnFocus = CommandPaletteRenameSelectionSettings.defaultSelectAllOnFocus @AppStorage(CommandPaletteSwitcherSearchSettings.searchAllSurfacesKey) @@ -4784,6 +4852,58 @@ struct SettingsView: View { SettingsCardDivider() + SettingsCardRow( + String(localized: "settings.app.confirmCloseRunningProcess", defaultValue: "Confirm Close Running Process"), + subtitle: confirmCloseRunningProcess + ? String(localized: "settings.app.confirmCloseRunningProcess.subtitleOn", defaultValue: "Show a confirmation before closing a workspace with a running command.") + : String(localized: "settings.app.confirmCloseRunningProcess.subtitleOff", defaultValue: "Close workspaces without confirmation, even with running commands.") + ) { + Toggle("", isOn: $confirmCloseRunningProcess) + .labelsHidden() + .controlSize(.small) + } + + SettingsCardDivider() + + SettingsCardRow( + String(localized: "settings.app.confirmClosePinnedWorkspace", defaultValue: "Confirm Close Pinned Workspace"), + subtitle: confirmClosePinnedWorkspace + ? String(localized: "settings.app.confirmClosePinnedWorkspace.subtitleOn", defaultValue: "Show a confirmation before closing a pinned workspace.") + : String(localized: "settings.app.confirmClosePinnedWorkspace.subtitleOff", defaultValue: "Close pinned workspaces without extra confirmation.") + ) { + Toggle("", isOn: $confirmClosePinnedWorkspace) + .labelsHidden() + .controlSize(.small) + } + + SettingsCardDivider() + + SettingsCardRow( + String(localized: "settings.app.confirmCloseWindow", defaultValue: "Confirm Close Window"), + subtitle: confirmCloseWindow + ? String(localized: "settings.app.confirmCloseWindow.subtitleOn", defaultValue: "Show a confirmation before closing a window with workspaces.") + : String(localized: "settings.app.confirmCloseWindow.subtitleOff", defaultValue: "Close windows without confirmation.") + ) { + Toggle("", isOn: $confirmCloseWindow) + .labelsHidden() + .controlSize(.small) + } + + SettingsCardDivider() + + SettingsCardRow( + String(localized: "settings.app.confirmBatchClose", defaultValue: "Confirm Batch Close"), + subtitle: confirmBatchClose + ? String(localized: "settings.app.confirmBatchClose.subtitleOn", defaultValue: "Show a confirmation before closing multiple tabs or workspaces at once.") + : String(localized: "settings.app.confirmBatchClose.subtitleOff", defaultValue: "Close multiple tabs or workspaces without confirmation.") + ) { + Toggle("", isOn: $confirmBatchClose) + .labelsHidden() + .controlSize(.small) + } + + SettingsCardDivider() + SettingsCardRow( String(localized: "settings.app.renameSelectsName", defaultValue: "Rename Selects Existing Name"), subtitle: commandPaletteRenameSelectAllOnFocus @@ -5910,6 +6030,10 @@ struct SettingsView: View { notificationPaneFlashEnabled = NotificationPaneFlashSettings.defaultEnabled showMenuBarExtra = MenuBarExtraSettings.defaultShowInMenuBar warnBeforeQuitShortcut = QuitWarningSettings.defaultWarnBeforeQuit + confirmCloseRunningProcess = CloseRunningProcessSettings.defaultEnabled + confirmClosePinnedWorkspace = ClosePinnedWorkspaceSettings.defaultEnabled + confirmCloseWindow = CloseWindowSettings.defaultEnabled + confirmBatchClose = BatchCloseSettings.defaultEnabled commandPaletteRenameSelectAllOnFocus = CommandPaletteRenameSelectionSettings.defaultSelectAllOnFocus commandPaletteSearchAllSurfaces = CommandPaletteSwitcherSearchSettings.defaultSearchAllSurfaces ShortcutHintDebugSettings.resetVisibilityDefaults()