Protect pinned workspaces from close actions
This commit is contained in:
parent
d219a41dd0
commit
b10cddcb9b
5 changed files with 144 additions and 29 deletions
|
|
@ -62649,6 +62649,23 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"sidebar.pinnedWorkspaceProtected.tooltip": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Pinned workspace protected from closing. Unpin to close."
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "ピン留めされたワークスペースは閉じられません。閉じるにはピンを外してください。"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"sidebar.folderIcon.dragHint": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
|
|
@ -74882,6 +74899,23 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"workspace.closeProtected.message": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Pinned workspaces can't be closed while pinned. Unpin the workspace first."
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "ピン留めされたワークスペースは閉じられません。先にピンを外してください。"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"workspace.placement.afterCurrent": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
|
|
|
|||
|
|
@ -1664,6 +1664,7 @@ struct ContentView: View {
|
|||
static let workspaceHasCustomName = "workspace.hasCustomName"
|
||||
static let workspaceMinimalModeEnabled = "workspace.minimalModeEnabled"
|
||||
static let workspaceShouldPin = "workspace.shouldPin"
|
||||
static let workspaceCanClose = "workspace.canClose"
|
||||
static let workspaceHasPullRequests = "workspace.hasPullRequests"
|
||||
static let workspaceHasSplits = "workspace.hasSplits"
|
||||
static let workspaceHasPeers = "workspace.hasPeers"
|
||||
|
|
@ -4949,6 +4950,7 @@ struct ContentView: View {
|
|||
snapshot.setString(CommandPaletteContextKeys.workspaceName, workspaceDisplayName(workspace))
|
||||
snapshot.setBool(CommandPaletteContextKeys.workspaceHasCustomName, workspace.customTitle != nil)
|
||||
snapshot.setBool(CommandPaletteContextKeys.workspaceShouldPin, !workspace.isPinned)
|
||||
snapshot.setBool(CommandPaletteContextKeys.workspaceCanClose, tabManager.canCloseWorkspace(workspace))
|
||||
snapshot.setBool(
|
||||
CommandPaletteContextKeys.workspaceHasPullRequests,
|
||||
!workspace.sidebarPullRequestsInDisplayOrder().isEmpty
|
||||
|
|
@ -5111,7 +5113,8 @@ struct ContentView: View {
|
|||
title: constant(String(localized: "command.closeWorkspace.title", defaultValue: "Close Workspace")),
|
||||
subtitle: constant(String(localized: "command.closeWorkspace.subtitle", defaultValue: "Workspace")),
|
||||
shortcutHint: "⌘⇧W",
|
||||
keywords: ["close", "workspace"]
|
||||
keywords: ["close", "workspace"],
|
||||
enablement: { $0.bool(CommandPaletteContextKeys.workspaceCanClose) }
|
||||
)
|
||||
)
|
||||
contributions.append(
|
||||
|
|
@ -10881,6 +10884,10 @@ private struct TabItemView: View, Equatable {
|
|||
|
||||
var body: some View {
|
||||
let closeWorkspaceTooltip = String(localized: "sidebar.closeWorkspace.tooltip", defaultValue: "Close Workspace")
|
||||
let protectedWorkspaceTooltip = String(
|
||||
localized: "sidebar.pinnedWorkspaceProtected.tooltip",
|
||||
defaultValue: "Pinned workspace protected from closing. Unpin to close."
|
||||
)
|
||||
let accessibilityHintText = String(localized: "sidebar.workspace.accessibilityHint", defaultValue: "Activate to focus this workspace. Drag to reorder, or use Move Up and Move Down actions.")
|
||||
let moveUpActionText = String(localized: "sidebar.workspace.moveUpAction", defaultValue: "Move Up")
|
||||
let moveDownActionText = String(localized: "sidebar.workspace.moveDownAction", defaultValue: "Move Down")
|
||||
|
|
@ -10942,6 +10949,7 @@ private struct TabItemView: View, Equatable {
|
|||
Image(systemName: "pin.fill")
|
||||
.font(.system(size: 9, weight: .semibold))
|
||||
.foregroundColor(activeSecondaryColor(0.8))
|
||||
.safeHelp(protectedWorkspaceTooltip)
|
||||
}
|
||||
|
||||
Text(tab.title)
|
||||
|
|
@ -10954,21 +10962,30 @@ private struct TabItemView: View, Equatable {
|
|||
Spacer(minLength: 0)
|
||||
|
||||
ZStack(alignment: .trailing) {
|
||||
Button(action: {
|
||||
#if DEBUG
|
||||
dlog("sidebar.close workspace=\(tab.id.uuidString.prefix(5)) method=button")
|
||||
#endif
|
||||
tabManager.closeWorkspaceWithConfirmation(tab)
|
||||
}) {
|
||||
Image(systemName: "xmark")
|
||||
if tab.isPinned {
|
||||
Image(systemName: "lock.fill")
|
||||
.font(.system(size: 9, weight: .medium))
|
||||
.foregroundColor(activeSecondaryColor(0.7))
|
||||
.foregroundColor(activeSecondaryColor(0.65))
|
||||
.safeHelp(protectedWorkspaceTooltip)
|
||||
.frame(width: SidebarTrailingAccessoryWidthPolicy.closeButtonWidth, height: 16, alignment: .center)
|
||||
.opacity(showCloseButton && !showsWorkspaceShortcutHint ? 1 : 0)
|
||||
} else {
|
||||
Button(action: {
|
||||
#if DEBUG
|
||||
dlog("sidebar.close workspace=\(tab.id.uuidString.prefix(5)) method=button")
|
||||
#endif
|
||||
tabManager.closeWorkspaceWithConfirmation(tab)
|
||||
}) {
|
||||
Image(systemName: "xmark")
|
||||
.font(.system(size: 9, weight: .medium))
|
||||
.foregroundColor(activeSecondaryColor(0.7))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.safeHelp(KeyboardShortcutSettings.Action.closeWorkspace.tooltip(closeWorkspaceTooltip))
|
||||
.frame(width: SidebarTrailingAccessoryWidthPolicy.closeButtonWidth, height: 16, alignment: .center)
|
||||
.opacity(showCloseButton && !showsWorkspaceShortcutHint ? 1 : 0)
|
||||
.allowsHitTesting(showCloseButton && !showsWorkspaceShortcutHint)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.safeHelp(KeyboardShortcutSettings.Action.closeWorkspace.tooltip(closeWorkspaceTooltip))
|
||||
.frame(width: SidebarTrailingAccessoryWidthPolicy.closeButtonWidth, height: 16, alignment: .center)
|
||||
.opacity(showCloseButton && !showsWorkspaceShortcutHint ? 1 : 0)
|
||||
.allowsHitTesting(showCloseButton && !showsWorkspaceShortcutHint)
|
||||
|
||||
if showsWorkspaceShortcutHint, let workspaceShortcutLabel {
|
||||
Text(workspaceShortcutLabel)
|
||||
|
|
@ -11289,6 +11306,10 @@ private struct TabItemView: View, Equatable {
|
|||
multi: String(localized: "contextMenu.closeWorkspaces", defaultValue: "Close Workspaces"),
|
||||
single: String(localized: "contextMenu.closeWorkspace", defaultValue: "Close Workspace"),
|
||||
isMulti: isMulti)
|
||||
let hasClosableTargets = targetIds.contains { workspaceId in
|
||||
guard let workspace = tabManager.tabs.first(where: { $0.id == workspaceId }) else { return false }
|
||||
return tabManager.canCloseWorkspace(workspace)
|
||||
}
|
||||
let markReadLabel = contextMenuLabel(
|
||||
multi: String(localized: "contextMenu.markWorkspacesRead", defaultValue: "Mark Workspaces as Read"),
|
||||
single: String(localized: "contextMenu.markWorkspaceRead", defaultValue: "Mark Workspace as Read"),
|
||||
|
|
@ -11427,15 +11448,15 @@ private struct TabItemView: View, Equatable {
|
|||
|
||||
if let key = closeWorkspaceShortcut.keyEquivalent {
|
||||
Button(closeLabel) {
|
||||
closeTabs(targetIds, allowPinned: true)
|
||||
closeTabs(targetIds, allowPinned: false)
|
||||
}
|
||||
.keyboardShortcut(key, modifiers: closeWorkspaceShortcut.eventModifiers)
|
||||
.disabled(targetIds.isEmpty)
|
||||
.disabled(!hasClosableTargets)
|
||||
} else {
|
||||
Button(closeLabel) {
|
||||
closeTabs(targetIds, allowPinned: true)
|
||||
closeTabs(targetIds, allowPinned: false)
|
||||
}
|
||||
.disabled(targetIds.isEmpty)
|
||||
.disabled(!hasClosableTargets)
|
||||
}
|
||||
|
||||
Button(String(localized: "contextMenu.closeOtherWorkspaces", defaultValue: "Close Other Workspaces")) {
|
||||
|
|
|
|||
|
|
@ -2201,7 +2201,7 @@ class TabManager: ObservableObject {
|
|||
#endif
|
||||
let sidebarSelectionIds = orderedSidebarSelectedWorkspaceIds()
|
||||
if sidebarSelectionIds.count > 1 {
|
||||
closeWorkspacesWithConfirmation(sidebarSelectionIds, allowPinned: true)
|
||||
closeWorkspacesWithConfirmation(sidebarSelectionIds, allowPinned: false)
|
||||
return
|
||||
}
|
||||
guard let selectedId = selectedTabId,
|
||||
|
|
@ -2209,13 +2209,24 @@ class TabManager: ObservableObject {
|
|||
closeWorkspaceWithConfirmation(workspace)
|
||||
}
|
||||
|
||||
func closeWorkspaceWithConfirmation(_ workspace: Workspace) {
|
||||
closeWorkspaceIfRunningProcess(workspace)
|
||||
func canCloseWorkspace(_ workspace: Workspace, allowPinned: Bool = false) -> Bool {
|
||||
allowPinned || !workspace.isPinned
|
||||
}
|
||||
|
||||
func closeWorkspaceWithConfirmation(tabId: UUID) {
|
||||
guard let workspace = tabs.first(where: { $0.id == tabId }) else { return }
|
||||
closeWorkspaceWithConfirmation(workspace)
|
||||
@discardableResult
|
||||
func closeWorkspaceWithConfirmation(_ workspace: Workspace) -> Bool {
|
||||
guard canCloseWorkspace(workspace) else {
|
||||
signalProtectedWorkspaceCloseAttempt(workspace)
|
||||
return false
|
||||
}
|
||||
closeWorkspaceIfRunningProcess(workspace)
|
||||
return true
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func closeWorkspaceWithConfirmation(tabId: UUID) -> Bool {
|
||||
guard let workspace = tabs.first(where: { $0.id == tabId }) else { return false }
|
||||
return closeWorkspaceWithConfirmation(workspace)
|
||||
}
|
||||
|
||||
func setSidebarSelectedWorkspaceIds(_ workspaceIds: Set<UUID>) {
|
||||
|
|
@ -2225,7 +2236,15 @@ class TabManager: ObservableObject {
|
|||
|
||||
func closeWorkspacesWithConfirmation(_ workspaceIds: [UUID], allowPinned: Bool) {
|
||||
let workspaces = orderedClosableWorkspaces(workspaceIds, allowPinned: allowPinned)
|
||||
guard !workspaces.isEmpty else { return }
|
||||
guard !workspaces.isEmpty else {
|
||||
if !allowPinned,
|
||||
workspaceIds.contains(where: { id in
|
||||
tabs.first(where: { $0.id == id })?.isPinned == true
|
||||
}) {
|
||||
signalProtectedWorkspaceCloseAttempt()
|
||||
}
|
||||
return
|
||||
}
|
||||
guard workspaces.count > 1 else {
|
||||
closeWorkspaceWithConfirmation(workspaces[0])
|
||||
return
|
||||
|
|
@ -2354,6 +2373,17 @@ class TabManager: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
private func signalProtectedWorkspaceCloseAttempt(_ workspace: Workspace? = nil) {
|
||||
#if DEBUG
|
||||
if let workspace {
|
||||
dlog("workspace.close.skip workspace=\(workspace.id.uuidString.prefix(5)) reason=pinned")
|
||||
} else {
|
||||
dlog("workspace.close.skip reason=pinned")
|
||||
}
|
||||
#endif
|
||||
NSSound.beep()
|
||||
}
|
||||
|
||||
private func closeWorkspacesPlan(for workspaces: [Workspace]) -> CloseWorkspacesPlan {
|
||||
let willCloseWindow = workspaces.count == tabs.count
|
||||
let title = willCloseWindow
|
||||
|
|
|
|||
|
|
@ -3436,14 +3436,29 @@ class TerminalController {
|
|||
}
|
||||
|
||||
var found = false
|
||||
var protected = false
|
||||
v2MainSync {
|
||||
if let ws = tabManager.tabs.first(where: { $0.id == wsId }) {
|
||||
guard tabManager.canCloseWorkspace(ws) else {
|
||||
protected = true
|
||||
found = true
|
||||
return
|
||||
}
|
||||
tabManager.closeWorkspace(ws)
|
||||
found = true
|
||||
}
|
||||
}
|
||||
|
||||
let windowId = v2ResolveWindowId(tabManager: tabManager)
|
||||
if protected {
|
||||
return .err(code: "protected", message: workspaceCloseProtectedMessage(), data: [
|
||||
"window_id": v2OrNull(windowId?.uuidString),
|
||||
"window_ref": v2Ref(kind: .window, uuid: windowId),
|
||||
"workspace_id": wsId.uuidString,
|
||||
"workspace_ref": v2Ref(kind: .workspace, uuid: wsId),
|
||||
"pinned": true
|
||||
])
|
||||
}
|
||||
return found
|
||||
? .ok([
|
||||
"window_id": v2OrNull(windowId?.uuidString),
|
||||
|
|
@ -3456,6 +3471,14 @@ class TerminalController {
|
|||
"workspace_ref": v2Ref(kind: .workspace, uuid: wsId)
|
||||
])
|
||||
}
|
||||
|
||||
private func workspaceCloseProtectedMessage() -> String {
|
||||
String(
|
||||
localized: "workspace.closeProtected.message",
|
||||
defaultValue: "Pinned workspaces can't be closed while pinned. Unpin the workspace first."
|
||||
)
|
||||
}
|
||||
|
||||
private func v2WorkspaceMoveToWindow(params: [String: Any]) -> V2CallResult {
|
||||
guard let wsId = v2UUID(params, "workspace_id") else {
|
||||
return .err(code: "invalid_params", message: "Missing or invalid workspace_id", data: nil)
|
||||
|
|
@ -12958,14 +12981,18 @@ class TerminalController {
|
|||
guard let tabManager = tabManager else { return "ERROR: TabManager not available" }
|
||||
guard let uuid = UUID(uuidString: tabId) else { return "ERROR: Invalid tab ID" }
|
||||
|
||||
var success = false
|
||||
var result = "ERROR: Tab not found"
|
||||
DispatchQueue.main.sync {
|
||||
if let tab = tabManager.tabs.first(where: { $0.id == uuid }) {
|
||||
guard tabManager.canCloseWorkspace(tab) else {
|
||||
result = "ERROR: \(workspaceCloseProtectedMessage())"
|
||||
return
|
||||
}
|
||||
tabManager.closeTab(tab)
|
||||
success = true
|
||||
result = "OK"
|
||||
}
|
||||
}
|
||||
return success ? "OK" : "ERROR: Tab not found"
|
||||
return result
|
||||
}
|
||||
|
||||
private func selectWorkspace(_ arg: String) -> String {
|
||||
|
|
|
|||
|
|
@ -644,6 +644,9 @@ struct cmuxApp: App {
|
|||
splitCommandButton(title: String(localized: "menu.file.closeWorkspace", defaultValue: "Close Workspace"), shortcut: closeWorkspaceMenuShortcut) {
|
||||
closeTabOrWindow()
|
||||
}
|
||||
.disabled(
|
||||
activeTabManager.selectedWorkspace.map { !activeTabManager.canCloseWorkspace($0) } ?? true
|
||||
)
|
||||
|
||||
Menu(String(localized: "commandPalette.switcher.workspaceLabel", defaultValue: "Workspace")) {
|
||||
workspaceCommandMenuContent(manager: activeTabManager)
|
||||
|
|
@ -1171,7 +1174,7 @@ struct cmuxApp: App {
|
|||
Button(String(localized: "menu.file.closeWorkspace", defaultValue: "Close Workspace")) {
|
||||
manager.closeCurrentWorkspaceWithConfirmation()
|
||||
}
|
||||
.disabled(workspace == nil)
|
||||
.disabled(workspace.map { !manager.canCloseWorkspace($0) } ?? true)
|
||||
|
||||
Button(String(localized: "contextMenu.closeOtherWorkspaces", defaultValue: "Close Other Workspaces")) {
|
||||
closeOtherSelectedWorkspacePeers(in: manager)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue