diff --git a/Packages/macOS/CmuxCanvas/Sources/CmuxCanvas/CanvasPlacer.swift b/Packages/macOS/CmuxCanvas/Sources/CmuxCanvas/CanvasPlacer.swift index 8c2133c963f4..907724946b94 100644 --- a/Packages/macOS/CmuxCanvas/Sources/CmuxCanvas/CanvasPlacer.swift +++ b/Packages/macOS/CmuxCanvas/Sources/CmuxCanvas/CanvasPlacer.swift @@ -3,9 +3,9 @@ import Foundation /// Chooses where a new pane goes on the canvas. /// /// Placement never moves existing panes: the placer tries the canonical-gap -/// position to the right of the anchor, then below, left, above, then scans -/// outward, and finally falls back to the area right of all existing content -/// (which is free by construction). +/// position in the requested direction when provided, then the remaining +/// cardinal neighbors, then scans outward, and finally falls back to the area +/// right of all existing content (which is free by construction). public struct CanvasPlacer: Sendable { /// The metrics supplying the canonical gap. public let metrics: CanvasMetrics @@ -28,12 +28,15 @@ public struct CanvasPlacer: Sendable { /// - anchor: The focused pane's frame, when one exists. New panes appear /// near it at the canonical gap. /// - existing: Frames of every pane already on the canvas. + /// - preferredDirection: First cardinal direction to try, when a shortcut + /// implies one. /// - Returns: A frame at least ``CanvasMetrics/gap`` away from every /// existing pane. public func frameForNewPane( size: CanvasSize, near anchor: CanvasRect?, - avoiding existing: [CanvasRect] + avoiding existing: [CanvasRect], + preferredDirection: CanvasDirection? = nil ) -> CanvasRect { guard !existing.isEmpty else { let origin = anchor?.origin ?? .zero @@ -43,13 +46,22 @@ public struct CanvasPlacer: Sendable { return frameRightOfContent(size: size, existing: existing) } - let neighbors: [CanvasRect] = [ - CanvasRect(x: anchor.maxX + metrics.gap, y: anchor.minY, width: size.width, height: size.height), - CanvasRect(x: anchor.minX, y: anchor.maxY + metrics.gap, width: size.width, height: size.height), - CanvasRect(x: anchor.minX - metrics.gap - size.width, y: anchor.minY, width: size.width, height: size.height), - CanvasRect(x: anchor.minX, y: anchor.minY - metrics.gap - size.height, width: size.width, height: size.height), + let neighbors: [(direction: CanvasDirection, frame: CanvasRect)] = [ + (.right, CanvasRect(x: anchor.maxX + metrics.gap, y: anchor.minY, width: size.width, height: size.height)), + (.down, CanvasRect(x: anchor.minX, y: anchor.maxY + metrics.gap, width: size.width, height: size.height)), + (.left, CanvasRect(x: anchor.minX - metrics.gap - size.width, y: anchor.minY, width: size.width, height: size.height)), + (.up, CanvasRect(x: anchor.minX, y: anchor.minY - metrics.gap - size.height, width: size.width, height: size.height)), ] - for candidate in neighbors where isFree(candidate, avoiding: existing) { + let orderedNeighbors: [CanvasRect] + if let preferredDirection, + let preferred = neighbors.first(where: { $0.direction == preferredDirection }) { + orderedNeighbors = [preferred.frame] + neighbors + .filter { $0.direction != preferredDirection } + .map(\.frame) + } else { + orderedNeighbors = neighbors.map(\.frame) + } + for candidate in orderedNeighbors where isFree(candidate, avoiding: existing) { return candidate } diff --git a/Packages/macOS/CmuxCanvasUI/Sources/CmuxCanvasUI/CanvasModel.swift b/Packages/macOS/CmuxCanvasUI/Sources/CmuxCanvasUI/CanvasModel.swift index ae292da04857..f4d1b0e7aef4 100644 --- a/Packages/macOS/CmuxCanvasUI/Sources/CmuxCanvasUI/CanvasModel.swift +++ b/Packages/macOS/CmuxCanvasUI/Sources/CmuxCanvasUI/CanvasModel.swift @@ -46,7 +46,12 @@ public final class CanvasModel { /// panels that no longer exist leave the canvas. Returns the IDs of /// panes that were newly added, so the caller can reveal them. @discardableResult - public func syncPanes(panelIds: [UUID], focusedPanelId: UUID?) -> [UUID] { + public func syncPanes( + panelIds: [UUID], + focusedPanelId: UUID?, + preferredDirection: CanvasDirection? = nil, + preferredNewPaneSize: CanvasSize? = nil + ) -> [UUID] { var changed = false let idSet = Set(panelIds.map(CanvasPanelID.init(rawValue:))) for panelId in layout.allPanelIds where !idSet.contains(panelId) { @@ -62,10 +67,12 @@ public final class CanvasModel { .flatMap { layout.pane(containing: CanvasPanelID(rawValue: $0)) } .flatMap { layout.frame(of: $0) } ?? layout.panes.last?.frame + let size = preferredNewPaneSize ?? anchor?.size ?? Self.defaultPaneSize let frame = placer.frameForNewPane( - size: Self.defaultPaneSize, + size: size, near: anchor, - avoiding: occupiedFrames + avoiding: occupiedFrames, + preferredDirection: preferredDirection ) let pane = CanvasPane(id: CanvasPaneID(rawValue: panelId), frame: frame) layout.add(pane) diff --git a/Packages/macOS/CmuxSettings/README.md b/Packages/macOS/CmuxSettings/README.md index a5ec7d60835a..1fbc0e6fcd77 100644 --- a/Packages/macOS/CmuxSettings/README.md +++ b/Packages/macOS/CmuxSettings/README.md @@ -188,11 +188,11 @@ needed. Build a context by hand and assert evaluation: ```swift var context = ShortcutContext() -context.setBool(ShortcutContextKnownKey.commandPaletteVisible.rawValue, true) +context.setBool(ShortcutContextKnownKey.workspaceCanvasLayout.rawValue, true) context.setString(ShortcutContextKnownKey.sidebarMode.rawValue, "find") context.setInt(ShortcutContextKnownKey.paneCount.rawValue, 2) -let clause = ShortcutWhenClause.parse("commandPaletteVisible && paneCount > 1") +let clause = ShortcutWhenClause.parse("workspaceCanvasLayout && paneCount > 1") #expect(clause?.evaluate(context) == true) ``` diff --git a/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/ShortcutAction+Defaults.swift b/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/ShortcutAction+Defaults.swift index 03907a350ba9..ff5c076bd78f 100644 --- a/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/ShortcutAction+Defaults.swift +++ b/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/ShortcutAction+Defaults.swift @@ -81,7 +81,7 @@ extension ShortcutAction { case .canvasOverview: return ShortcutStroke(key: "o", command: true, control: true) case .canvasZoomIn: return ShortcutStroke(key: "=", command: true, option: true) case .canvasZoomOut: return ShortcutStroke(key: "-", command: true, option: true) - case .canvasZoomReset: return ShortcutStroke(key: "0", command: true, option: true) + case .canvasZoomReset: return ShortcutStroke(key: "0", command: true) case .canvasTidy: return ShortcutStroke(key: "t", command: true, control: true) case .canvasAlignLeft, .canvasAlignRight, .canvasAlignTop, .canvasAlignBottom, .canvasEqualizeWidths, .canvasEqualizeHeights, diff --git a/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/ShortcutAction.swift b/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/ShortcutAction.swift index 67387ce40e5f..5370bff4b8b7 100644 --- a/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/ShortcutAction.swift +++ b/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/ShortcutAction.swift @@ -278,6 +278,17 @@ extension ShortcutAction { return .atom(.browserFocus) case .markdownZoomIn, .markdownZoomOut, .markdownZoomReset: return .atom(.markdownFocus) + case .canvasZoomReset: + return .and( + .key(ShortcutContextKnownKey.workspaceCanvasLayout.rawValue), + .and(.not(.atom(.browserFocus)), .not(.atom(.markdownFocus))) + ) + case .canvasRevealFocusedPane, .canvasOverview, + .canvasZoomIn, .canvasZoomOut, .canvasTidy, + .canvasAlignLeft, .canvasAlignRight, .canvasAlignTop, .canvasAlignBottom, + .canvasEqualizeWidths, .canvasEqualizeHeights, + .canvasDistributeHorizontally, .canvasDistributeVertically: + return .key(ShortcutContextKnownKey.workspaceCanvasLayout.rawValue) default: return .always } diff --git a/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/ShortcutContextKnownKey.swift b/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/ShortcutContextKnownKey.swift index 80198e6a79d5..06f945ad60cc 100644 --- a/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/ShortcutContextKnownKey.swift +++ b/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/ShortcutContextKnownKey.swift @@ -29,6 +29,8 @@ public enum ShortcutContextKnownKey: String, CaseIterable, Sendable { case commandPaletteVisible /// The focused terminal's find overlay is open. case terminalFindVisible + /// The focused workspace is using the freeform canvas layout. + case workspaceCanvasLayout /// The right sidebar's active mode (`files`, `find`, `sessions`, `feed`, `dock`). case sidebarMode /// The number of panes in the focused workspace. @@ -40,7 +42,7 @@ public enum ShortcutContextKnownKey: String, CaseIterable, Sendable { public var valueType: ShortcutContextValueType { switch self { case .sidebarFocus, .browserFocus, .markdownFocus, .terminalFocus, - .commandPaletteVisible, .terminalFindVisible: + .commandPaletteVisible, .terminalFindVisible, .workspaceCanvasLayout: return .bool case .sidebarMode: return .string diff --git a/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/KeyboardShortcutsSection.swift b/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/KeyboardShortcutsSection.swift index 5a1f739a45e4..3c88182dae0c 100644 --- a/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/KeyboardShortcutsSection.swift +++ b/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/KeyboardShortcutsSection.swift @@ -358,9 +358,7 @@ public struct KeyboardShortcutsSection: View { for other in ShortcutAction.allCases where other != action { // Two bindings on the same keystroke only collide when some focus // state activates both effective `when` clauses AND router priority - // cannot decide the overlap. Context-disjoint clauses (e.g. - // `!sidebarFocus` workspace digits vs the sidebar's own digits) - // coexist, and a pre-routed action (sidebar modes) wins its context + // cannot decide the overlap. Context-disjoint clauses coexist. // outright so the factory Select Surface ⌃1…9 coexists with the // sidebar's ⌃1…5 — matching the app target's authoritative check. guard ShortcutWhenClause.bindingsCollide( diff --git a/Sources/AppDelegate+CanvasShortcutRouting.swift b/Sources/AppDelegate+CanvasShortcutRouting.swift new file mode 100644 index 000000000000..9e10effbe743 --- /dev/null +++ b/Sources/AppDelegate+CanvasShortcutRouting.swift @@ -0,0 +1,83 @@ +import CmuxCanvas +import CmuxPanes +import CmuxSettings + +extension AppDelegate { + @discardableResult + func performBrowserSplitShortcut(direction: SplitDirection) -> Bool { + guard BrowserAvailabilitySettings.isEnabled() else { +#if DEBUG + cmuxDebugLog("split.browser.shortcut blocked reason=browser_disabled") +#endif + return false + } + + _ = synchronizeActiveMainWindowContext(preferredWindow: shortcutRoutingActiveWindow) + + if let workspace = tabManager?.selectedWorkspace, workspace.layoutMode == .canvas { + guard let panelId = workspace.openNewCanvasPane( + type: .browser, + focus: true, + direction: direction.canvasDirection + ) else { + return false + } + _ = focusBrowserAddressBar(panelId: panelId) + return true + } + +#if DEBUG + let directionLabel: String + switch direction { + case .left: directionLabel = "left" + case .right: directionLabel = "right" + case .up: directionLabel = "up" + case .down: directionLabel = "down" + } + let selectedTabBefore = tabManager?.selectedTabId?.uuidString.prefix(5) ?? "nil" + let focusedPanelBefore = tabManager?.selectedWorkspace?.focusedPanelId?.uuidString.prefix(5) ?? "nil" + cmuxDebugLog( + "split.browser.shortcut pre dir=\(directionLabel) " + + "tab=\(selectedTabBefore) focusedPanel=\(focusedPanelBefore)" + ) +#endif + + guard let panelId = tabManager?.createBrowserSplit(direction: direction) else { +#if DEBUG + cmuxDebugLog("split.browser.shortcut failed dir=\(directionLabel)") +#endif + return false + } + +#if DEBUG + let selectedTabAfter = tabManager?.selectedTabId?.uuidString.prefix(5) ?? "nil" + let focusedPanelAfter = tabManager?.selectedWorkspace?.focusedPanelId?.uuidString.prefix(5) ?? "nil" + cmuxDebugLog( + "split.browser.shortcut post dir=\(directionLabel) " + + "created=\(panelId.uuidString.prefix(5)) tab=\(selectedTabAfter) focusedPanel=\(focusedPanelAfter)" + ) +#endif + + _ = focusBrowserAddressBar(panelId: panelId) + return true + } + + func performToggleSplitZoomShortcut(tabManager routedManager: TabManager?) { + if let workspace = routedManager?.selectedWorkspace, workspace.layoutMode == .canvas { + _ = CanvasActionExecutor(workspace: workspace).perform(.toggleOverview) + } else { + _ = routedManager?.toggleFocusedSplitZoom() + } + } +} + +extension SplitDirection { + var canvasDirection: CanvasDirection { + switch self { + case .left: return .left + case .right: return .right + case .up: return .up + case .down: return .down + } + } +} diff --git a/Sources/AppDelegate+EqualizeSplitsShortcut.swift b/Sources/AppDelegate+EqualizeSplitsShortcut.swift index aa013d6d6b59..fb4c59494eed 100644 --- a/Sources/AppDelegate+EqualizeSplitsShortcut.swift +++ b/Sources/AppDelegate+EqualizeSplitsShortcut.swift @@ -9,6 +9,17 @@ extension AppDelegate { #if DEBUG cmuxDebugLog("shortcut.action name=equalizeSplits workspaceId=\(workspace.id)") #endif + if workspace.layoutMode == .canvas { + let executor = CanvasActionExecutor(workspace: workspace) + let didEqualizeWidths = executor.perform(.alignment(.equalizeWidths)) + let didEqualizeHeights = executor.perform(.alignment(.equalizeHeights)) +#if DEBUG + if !didEqualizeWidths && !didEqualizeHeights { + cmuxDebugLog("shortcut.action name=equalizeSplits result=noCanvasChange workspaceId=\(workspace.id)") + } +#endif + return + } if shouldSuppressSplitShortcutForTransientTerminalFocusState(tabManager: tabManager) { return } diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index 8e72230b6fb1..1d07c72cc84b 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -13028,7 +13028,13 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent return false } - if let mode = rightSidebarModeShortcut(for: event), + let canvasSurfaceDigitShortcutIsActive = + shortcutEventFocusContext(event).shortcutContext.bool(ShortcutContextKnownKey.workspaceCanvasLayout.rawValue) && + shortcutWhenClauseAllows(action: .selectSurfaceByNumber, event: event) && + numberedConfiguredShortcutDigit(event: event, action: .selectSurfaceByNumber) != nil + + if !canvasSurfaceDigitShortcutIsActive, + let mode = rightSidebarModeShortcut(for: event), let rightSidebarWindow = mainWindowForShortcutEvent(event) ?? event.window ?? shortcutRoutingActiveWindow, shouldRouteRightSidebarModeShortcut(in: rightSidebarWindow) { _ = focusRightSidebarInActiveMainWindow( @@ -13529,7 +13535,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent return true } - // Numeric shortcuts for surfaces within the focused pane (9 = last) + // Numeric shortcuts for surfaces: focused pane in split layout, + // workspace Canvas order in Canvas layout (9 = last). if shortcutWhenClauseAllows(action: .selectSurfaceByNumber, event: event), let digit = numberedConfiguredShortcutDigit(event: event, action: .selectSurfaceByNumber) { if digit == 9 { @@ -13547,7 +13554,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent arrowGlyph: "←", arrowKeyCode: 123 ) || (ghosttyGotoSplitLeftShortcut.map { matchDirectionalShortcut(event: event, shortcut: $0, arrowGlyph: "←", arrowKeyCode: 123) } ?? false) { - cmuxRememberFindSelectionBeforePanelFocusMove(tabManager: tabManager, window: shortcutRoutingKeyWindow); tabManager?.movePaneFocus(direction: .left) + let routedTabs = preferredMainWindowContextForShortcutRouting(event: event)?.tabManager ?? tabManager + cmuxRememberFindSelectionBeforePanelFocusMove(tabManager: routedTabs, window: shortcutRoutingKeyWindow) + routedTabs?.movePaneFocus(direction: .left) #if DEBUG recordGotoSplitMoveIfNeeded(direction: .left) #endif @@ -13559,7 +13568,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent arrowGlyph: "→", arrowKeyCode: 124 ) || (ghosttyGotoSplitRightShortcut.map { matchDirectionalShortcut(event: event, shortcut: $0, arrowGlyph: "→", arrowKeyCode: 124) } ?? false) { - cmuxRememberFindSelectionBeforePanelFocusMove(tabManager: tabManager, window: shortcutRoutingKeyWindow); tabManager?.movePaneFocus(direction: .right) + let routedTabs = preferredMainWindowContextForShortcutRouting(event: event)?.tabManager ?? tabManager + cmuxRememberFindSelectionBeforePanelFocusMove(tabManager: routedTabs, window: shortcutRoutingKeyWindow) + routedTabs?.movePaneFocus(direction: .right) #if DEBUG recordGotoSplitMoveIfNeeded(direction: .right) #endif @@ -13571,7 +13582,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent arrowGlyph: "↑", arrowKeyCode: 126 ) || (ghosttyGotoSplitUpShortcut.map { matchDirectionalShortcut(event: event, shortcut: $0, arrowGlyph: "↑", arrowKeyCode: 126) } ?? false) { - cmuxRememberFindSelectionBeforePanelFocusMove(tabManager: tabManager, window: shortcutRoutingKeyWindow); tabManager?.movePaneFocus(direction: .up) + let routedTabs = preferredMainWindowContextForShortcutRouting(event: event)?.tabManager ?? tabManager + cmuxRememberFindSelectionBeforePanelFocusMove(tabManager: routedTabs, window: shortcutRoutingKeyWindow) + routedTabs?.movePaneFocus(direction: .up) #if DEBUG recordGotoSplitMoveIfNeeded(direction: .up) #endif @@ -13583,7 +13596,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent arrowGlyph: "↓", arrowKeyCode: 125 ) || (ghosttyGotoSplitDownShortcut.map { matchDirectionalShortcut(event: event, shortcut: $0, arrowGlyph: "↓", arrowKeyCode: 125) } ?? false) { - cmuxRememberFindSelectionBeforePanelFocusMove(tabManager: tabManager, window: shortcutRoutingKeyWindow); tabManager?.movePaneFocus(direction: .down) + let routedTabs = preferredMainWindowContextForShortcutRouting(event: event)?.tabManager ?? tabManager + cmuxRememberFindSelectionBeforePanelFocusMove(tabManager: routedTabs, window: shortcutRoutingKeyWindow) + routedTabs?.movePaneFocus(direction: .down) #if DEBUG recordGotoSplitMoveIfNeeded(direction: .down) #endif @@ -13592,9 +13607,11 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent if matchConfiguredShortcut(event: event, action: .toggleSplitZoom) { let routedManager = preferredMainWindowContextForShortcutRouting(event: event)?.tabManager ?? tabManager - _ = routedManager?.toggleFocusedSplitZoom() + performToggleSplitZoomShortcut(tabManager: routedManager) #if DEBUG - recordGotoSplitZoomIfNeeded(tabManager: routedManager) + if routedManager?.selectedWorkspace?.layoutMode != .canvas { + recordGotoSplitZoomIfNeeded(tabManager: routedManager) + } #endif return true } @@ -13969,7 +13986,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent #endif @discardableResult - private func focusBrowserAddressBar(panelId: UUID) -> Bool { + func focusBrowserAddressBar(panelId: UUID) -> Bool { guard let tabManager, let workspace = tabManager.selectedWorkspace, let panel = workspace.browserPanel(for: panelId) else { @@ -14503,12 +14520,28 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent prepareFocusedBrowserDevToolsForSplit(directionLabel: directionLabel) let didCreateSplit: Bool = { if let terminalContext { + if let workspace = terminalContext.tabManager.tabs.first(where: { $0.id == terminalContext.workspaceId }), + workspace.layoutMode == .canvas { + return workspace.openNewCanvasPane( + type: .terminal, + focus: true, + direction: direction.canvasDirection + ) != nil + } return terminalContext.tabManager.createSplit( tabId: terminalContext.workspaceId, surfaceId: terminalContext.panelId, direction: direction ) != nil } + if let workspace = tabManager?.selectedWorkspace, + workspace.layoutMode == .canvas { + return workspace.openNewCanvasPane( + type: .terminal, + focus: true, + direction: direction.canvasDirection + ) != nil + } return tabManager?.createSplit(direction: direction) != nil }() #if DEBUG @@ -14540,53 +14573,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent return didCreateSplit } - @discardableResult - func performBrowserSplitShortcut(direction: SplitDirection) -> Bool { - guard BrowserAvailabilitySettings.isEnabled() else { -#if DEBUG - cmuxDebugLog("split.browser.shortcut blocked reason=browser_disabled") -#endif - return false - } - - _ = synchronizeActiveMainWindowContext(preferredWindow: shortcutRoutingActiveWindow) - - #if DEBUG - let directionLabel: String - switch direction { - case .left: directionLabel = "left" - case .right: directionLabel = "right" - case .up: directionLabel = "up" - case .down: directionLabel = "down" - } - let selectedTabBefore = tabManager?.selectedTabId?.uuidString.prefix(5) ?? "nil" - let focusedPanelBefore = tabManager?.selectedWorkspace?.focusedPanelId?.uuidString.prefix(5) ?? "nil" - cmuxDebugLog( - "split.browser.shortcut pre dir=\(directionLabel) " + - "tab=\(selectedTabBefore) focusedPanel=\(focusedPanelBefore)" - ) - #endif - - guard let panelId = tabManager?.createBrowserSplit(direction: direction) else { - #if DEBUG - cmuxDebugLog("split.browser.shortcut failed dir=\(directionLabel)") - #endif - return false - } - - #if DEBUG - let selectedTabAfter = tabManager?.selectedTabId?.uuidString.prefix(5) ?? "nil" - let focusedPanelAfter = tabManager?.selectedWorkspace?.focusedPanelId?.uuidString.prefix(5) ?? "nil" - cmuxDebugLog( - "split.browser.shortcut post dir=\(directionLabel) " + - "created=\(panelId.uuidString.prefix(5)) tab=\(selectedTabAfter) focusedPanel=\(focusedPanelAfter)" - ) - #endif - - _ = focusBrowserAddressBar(panelId: panelId) - return true - } - /// Allow AppKit-backed browser surfaces (WKWebView) to route non-menu shortcuts /// through the same app-level shortcut handler used by the local key monitor. @discardableResult diff --git a/Sources/Canvas/Workspace+CanvasLayout.swift b/Sources/Canvas/Workspace+CanvasLayout.swift index 95d7d769b5bb..9c7c71a9a26e 100644 --- a/Sources/Canvas/Workspace+CanvasLayout.swift +++ b/Sources/Canvas/Workspace+CanvasLayout.swift @@ -173,23 +173,47 @@ extension NavigationDirection { } extension Workspace { - /// Cycles the focused canvas pane's tabs by `offset` (wrapping). Returns - /// `false` when the focused pane has fewer than two tabs, so the caller - /// can fall back to bonsplit cycling semantics. + /// Cycles canvas surfaces by `offset` (wrapping). In canvas mode, surface + /// shortcuts address the whole floating workspace surface order, because + /// separate panes do not share one focused Bonsplit tab strip. func selectAdjacentCanvasTab(offset: Int) -> Bool { - guard let focusedPanelId, - let paneID = canvasModel.paneID(containing: focusedPanelId), - let tabs = canvasModel.layout.panelIds(in: paneID), - tabs.count > 1, - let selected = canvasModel.layout.selectedPanelId(in: paneID), - let index = tabs.firstIndex(of: selected) else { + let surfaceIds = selectableCanvasSurfaceIds() + guard surfaceIds.count > 1, + let focusedPanelId, + let index = surfaceIds.firstIndex(of: focusedPanelId) else { return false } - let next = tabs[(index + offset + tabs.count) % tabs.count] - focusPanel(next.rawValue) + let next = surfaceIds[(index + offset + surfaceIds.count) % surfaceIds.count] + focusPanel(next) canvasModel.viewport?.modelDidChangeExternally(animated: false) + canvasModel.viewport?.revealPane(next, animated: true) return true } + + /// Selects a canvas surface by zero-based workspace surface order. + func selectCanvasTab(at index: Int) -> Bool { + let surfaceIds = selectableCanvasSurfaceIds() + guard surfaceIds.indices.contains(index) else { return false } + let selected = surfaceIds[index] + focusPanel(selected) + canvasModel.viewport?.modelDidChangeExternally(animated: false) + canvasModel.viewport?.revealPane(selected, animated: true) + return true + } + + /// Selects the last canvas surface in workspace surface order. + func selectLastCanvasTab() -> Bool { + guard let selected = selectableCanvasSurfaceIds().last else { return false } + focusPanel(selected) + canvasModel.viewport?.modelDidChangeExternally(animated: false) + canvasModel.viewport?.revealPane(selected, animated: true) + return true + } + + private func selectableCanvasSurfaceIds() -> [UUID] { + let canvasPanelIds = Set(canvasModel.layout.allPanelIds.map(\.rawValue)) + return orderedPanelIds.filter { canvasPanelIds.contains($0) && panels[$0] != nil } + } } /// The kind of surface `openNewCanvasPane` should create. @@ -205,9 +229,17 @@ extension Workspace { /// when creation fails (e.g. no focused bonsplit pane, or the browser is /// disabled). Must be called in canvas mode. @discardableResult - func openNewCanvasPane(type: CanvasNewPaneType, focus: Bool = true) -> UUID? { + func openNewCanvasPane( + type: CanvasNewPaneType, + focus: Bool = true, + direction: CanvasDirection? = nil + ) -> UUID? { guard layoutMode == .canvas else { return nil } guard let focusedPaneId = bonsplitController.focusedPaneId else { return nil } + let anchorPanelId = focusedPanelId + let preferredSize: CanvasSize? = anchorPanelId + .flatMap { canvasModel.frame(of: $0) } + .map { CanvasSize(width: Double($0.width), height: Double($0.height)) } let newPanelId: UUID switch type { case .terminal: @@ -223,7 +255,12 @@ extension Workspace { } // Give the new surface its own canvas pane (the placer positions it // near the focused pane) rather than joining it as a tab. - canvasModel.syncPanes(panelIds: orderedPanelIds, focusedPanelId: newPanelId) + canvasModel.syncPanes( + panelIds: orderedPanelIds, + focusedPanelId: anchorPanelId, + preferredDirection: direction, + preferredNewPaneSize: preferredSize + ) focusPanel(newPanelId) canvasModel.viewport?.modelDidChangeExternally(animated: false) canvasModel.viewport?.revealPane(newPanelId, animated: true) diff --git a/Sources/ContentView.swift b/Sources/ContentView.swift index 7ebdee83c93b..d18f384bef73 100644 --- a/Sources/ContentView.swift +++ b/Sources/ContentView.swift @@ -6080,11 +6080,7 @@ struct ContentView: View { if let action = Self.commandPaletteShortcutAction(forCommandID: contribution.commandId) { let shortcut = KeyboardShortcutSettings.shortcut(for: action) guard !shortcut.isUnbound else { return nil } - guard action.shortcutContext.isAvailable( - focusedBrowserPanel: context.bool(CommandPaletteContextKeys.panelIsBrowser), - focusedMarkdownPanel: context.bool(CommandPaletteContextKeys.panelIsMarkdown), - rightSidebarFocused: false - ) else { + guard action.shortcutContext.isAvailable(commandPaletteContext: context) else { return nil } return shortcut.displayString diff --git a/Sources/KeyboardShortcutActionContext.swift b/Sources/KeyboardShortcutActionContext.swift new file mode 100644 index 000000000000..f61df4b22088 --- /dev/null +++ b/Sources/KeyboardShortcutActionContext.swift @@ -0,0 +1,123 @@ +import CmuxCommandPalette +import CmuxSettings + +extension KeyboardShortcutSettings.Action { + enum ShortcutContext: Equatable { + case application + case nonBrowserPanel + case browserPanel + case markdownPanel + case rightSidebarFocus + case canvasLayout + case canvasLayoutOutsideFocusedContent + + var isAlwaysAvailable: Bool { self == .application } + + func isAvailable( + focusedBrowserPanel: Bool, + focusedMarkdownPanel: Bool, + rightSidebarFocused: Bool, + workspaceCanvasLayout: Bool = false + ) -> Bool { + switch self { + case .application: return true + case .nonBrowserPanel: return !focusedBrowserPanel && !rightSidebarFocused + case .browserPanel: return focusedBrowserPanel + case .markdownPanel: return focusedMarkdownPanel + case .rightSidebarFocus: return rightSidebarFocused + case .canvasLayout: return workspaceCanvasLayout + case .canvasLayoutOutsideFocusedContent: return workspaceCanvasLayout && !focusedBrowserPanel && !focusedMarkdownPanel + } + } + + func isAvailable(_ context: ShortcutEventFocusContext) -> Bool { + isAvailable( + focusedBrowserPanel: context.browserPanel != nil, + focusedMarkdownPanel: context.markdownPanel != nil, + rightSidebarFocused: context.rightSidebarFocused, + workspaceCanvasLayout: context.shortcutContext.bool(ShortcutContextKnownKey.workspaceCanvasLayout.rawValue) + ) + } + + func isAvailable(commandPaletteContext context: CommandPaletteContextSnapshot) -> Bool { + isAvailable( + focusedBrowserPanel: context.bool(CommandPaletteContextKeys.panelIsBrowser), + focusedMarkdownPanel: context.bool(CommandPaletteContextKeys.panelIsMarkdown), + rightSidebarFocused: false, + workspaceCanvasLayout: context.bool(CommandPaletteContextKeys.workspaceCanvasLayout) + ) + } + + var defaultWhenClause: ShortcutWhenClause { + switch self { + case .application: return .always + case .nonBrowserPanel: return .and(.not(.atom(.browserFocus)), .not(.atom(.sidebarFocus))) + case .browserPanel: return .atom(.browserFocus) + case .markdownPanel: return .atom(.markdownFocus) + case .rightSidebarFocus: return .atom(.sidebarFocus) + case .canvasLayout: return .key(ShortcutContextKnownKey.workspaceCanvasLayout.rawValue) + case .canvasLayoutOutsideFocusedContent: + return .and( + .key(ShortcutContextKnownKey.workspaceCanvasLayout.rawValue), + .and(.not(.atom(.browserFocus)), .not(.atom(.markdownFocus))) + ) + } + } + + func overlaps(_ other: ShortcutContext) -> Bool { + if self == .application || other == .application || self == other { + return true + } + if (self == .markdownPanel && other == .nonBrowserPanel) + || (self == .nonBrowserPanel && other == .markdownPanel) { + return true + } + if self == .canvasLayout || other == .canvasLayout { + return true + } + if self == .canvasLayoutOutsideFocusedContent || other == .canvasLayoutOutsideFocusedContent { + return self != .browserPanel && other != .browserPanel && self != .markdownPanel && other != .markdownPanel + } + return false + } + } + + var hasPriorityShortcutRouting: Bool { + switch self { + case .switchRightSidebarToFiles, .switchRightSidebarToFind, + .switchRightSidebarToSessions, .switchRightSidebarToFeed, .switchRightSidebarToDock: + return true + default: + return false + } + } + + var shortcutContext: ShortcutContext { + switch self { + case .diffViewerScrollDown, .diffViewerScrollUp, .diffViewerScrollToBottom, + .diffViewerScrollToTop, .diffViewerOpenFileSearch: + return .browserPanel + case .switchRightSidebarToFiles, .switchRightSidebarToFind, .switchRightSidebarToSessions, + .switchRightSidebarToFeed, .switchRightSidebarToDock, .fileExplorerOpenSelection, + .fileExplorerOpenSelectionFinderAlias: + return .rightSidebarFocus + case .renameTab, .renameWorkspace, .sendCtrlFToTerminal, .clearScreenKeepScrollback: + return .nonBrowserPanel + case .browserBack, .browserForward, .browserReload, .browserHardReload, + .toggleBrowserDeveloperTools, .showBrowserJavaScriptConsole, .browserZoomIn, + .browserZoomOut, .browserZoomReset, .toggleBrowserFocusMode: + return .browserPanel + case .markdownZoomIn, .markdownZoomOut, .markdownZoomReset: + return .markdownPanel + case .canvasZoomReset: + return .canvasLayoutOutsideFocusedContent + case .canvasRevealFocusedPane, .canvasOverview, .canvasZoomIn, .canvasZoomOut, + .canvasTidy, .canvasAlignLeft, .canvasAlignRight, + .canvasAlignTop, .canvasAlignBottom, .canvasEqualizeWidths, + .canvasEqualizeHeights, .canvasDistributeHorizontally, .canvasDistributeVertically: + return .canvasLayout + default: + return .application + } + } +} diff --git a/Sources/KeyboardShortcutContext.swift b/Sources/KeyboardShortcutContext.swift index c56515557fb8..98231d8af1cb 100644 --- a/Sources/KeyboardShortcutContext.swift +++ b/Sources/KeyboardShortcutContext.swift @@ -6,10 +6,7 @@ struct ShortcutEventFocusContext { let browserPanel: BrowserPanel? let markdownPanel: MarkdownPanel? let rightSidebarFocused: Bool - /// The full context snapshot a ``ShortcutWhenClause`` evaluates against: the - /// focus atoms plus the non-focus keys (`commandPaletteVisible`, `sidebarMode`, - /// `terminalFindVisible`, `paneCount`, `workspaceCount`) read from the shortcut - /// window's state. + /// The full context snapshot a ``ShortcutWhenClause`` evaluates against. let shortcutContext: ShortcutContext /// Projects the runtime focus snapshot onto the atoms a @@ -28,120 +25,6 @@ struct ShortcutEventFocusContextCache { let context: ShortcutEventFocusContext } -extension KeyboardShortcutSettings.Action { - enum ShortcutContext: Equatable { - case application - case nonBrowserPanel - case browserPanel - case markdownPanel - case rightSidebarFocus - - var isAlwaysAvailable: Bool { - self == .application - } - - func isAvailable(focusedBrowserPanel: Bool, focusedMarkdownPanel: Bool, rightSidebarFocused: Bool) -> Bool { - switch self { - case .application: - return true - case .nonBrowserPanel: - return !focusedBrowserPanel && !rightSidebarFocused - case .browserPanel: - return focusedBrowserPanel - case .markdownPanel: - return focusedMarkdownPanel - case .rightSidebarFocus: - return rightSidebarFocused - } - } - - func isAvailable(_ context: ShortcutEventFocusContext) -> Bool { - isAvailable( - focusedBrowserPanel: context.browserPanel != nil, - focusedMarkdownPanel: context.markdownPanel != nil, - rightSidebarFocused: context.rightSidebarFocused - ) - } - - /// The built-in context expressed as a ``ShortcutWhenClause``, used as the - /// default when an action has no `shortcuts.when` override in cmux.json. - var defaultWhenClause: ShortcutWhenClause { - switch self { - case .application: - return .always - case .nonBrowserPanel: - return .and(.not(.atom(.browserFocus)), .not(.atom(.sidebarFocus))) - case .browserPanel: - return .atom(.browserFocus) - case .markdownPanel: - return .atom(.markdownFocus) - case .rightSidebarFocus: - return .atom(.sidebarFocus) - } - } - - func overlaps(_ other: ShortcutContext) -> Bool { - if self == .application || other == .application { - return true - } - if self == other { - return true - } - // A focused markdown viewer also satisfies `.nonBrowserPanel`, so the - // two contexts can be active at the same time. Treat them as - // overlapping so shortcut conflict detection rejects a chord bound to - // both a markdown-zoom action and a non-browser action. - if (self == .markdownPanel && other == .nonBrowserPanel) || - (self == .nonBrowserPanel && other == .markdownPanel) { - return true - } - return false - } - } - - /// Whether `handleCustomShortcut` consumes this action before general - /// configured-shortcut matching whenever its context holds (the - /// `rightSidebarModeShortcut` pre-route). Priority-resolved pairs — the - /// sidebar's `⌃1…5` over the Select Surface `⌃1…9` family — coexist in - /// conflict detection because the winner owns the overlapping context and - /// the other binding keeps every other state. Mirrors - /// `ShortcutAction.hasPriorityShortcutRouting` in CmuxSettings; the drift - /// test asserts the two stay aligned. - var hasPriorityShortcutRouting: Bool { - switch self { - case .switchRightSidebarToFiles, .switchRightSidebarToFind, - .switchRightSidebarToSessions, .switchRightSidebarToFeed, .switchRightSidebarToDock: - return true - default: - return false - } - } - - var shortcutContext: ShortcutContext { - switch self { - case .diffViewerScrollDown, - .diffViewerScrollUp, - .diffViewerScrollToBottom, - .diffViewerScrollToTop, - .diffViewerOpenFileSearch: - return .browserPanel - case .switchRightSidebarToFiles, .switchRightSidebarToFind, .switchRightSidebarToSessions, .switchRightSidebarToFeed, .switchRightSidebarToDock: - return .rightSidebarFocus - case .fileExplorerOpenSelection, .fileExplorerOpenSelectionFinderAlias: - return .rightSidebarFocus - case .renameTab, .renameWorkspace, .sendCtrlFToTerminal, .clearScreenKeepScrollback: - return .nonBrowserPanel - case .browserBack, .browserForward, .browserReload, .browserHardReload, .toggleBrowserDeveloperTools, .showBrowserJavaScriptConsole, - .browserZoomIn, .browserZoomOut, .browserZoomReset, .toggleBrowserFocusMode: - return .browserPanel - case .markdownZoomIn, .markdownZoomOut, .markdownZoomReset: - return .markdownPanel - default: - return .application - } - } -} - extension Notification.Name { static let debugBrowserReloadShortcutInvoked = Notification.Name("cmux.debugBrowserReloadShortcutInvoked") static let debugBrowserHardReloadShortcutInvoked = Notification.Name("cmux.debugBrowserHardReloadShortcutInvoked") @@ -210,6 +93,7 @@ extension AppDelegate { context.setInt(ShortcutContextKnownKey.workspaceCount.rawValue, tabManager.tabs.count) if let workspace = tabManager.selectedWorkspace { context.setInt(ShortcutContextKnownKey.paneCount.rawValue, workspace.panels.count) + context.setBool(ShortcutContextKnownKey.workspaceCanvasLayout.rawValue, workspace.layoutMode == .canvas) context.setBool( ShortcutContextKnownKey.terminalFindVisible.rawValue, workspace.focusedTerminalPanel?.searchState != nil diff --git a/Sources/KeyboardShortcutSettings.swift b/Sources/KeyboardShortcutSettings.swift index 7ed04a543b6e..5f38e54fdd2b 100644 --- a/Sources/KeyboardShortcutSettings.swift +++ b/Sources/KeyboardShortcutSettings.swift @@ -447,7 +447,7 @@ enum KeyboardShortcutSettings { case .canvasZoomOut: return StoredShortcut(key: "-", command: true, shift: false, option: true, control: false) case .canvasZoomReset: - return StoredShortcut(key: "0", command: true, shift: false, option: true, control: false) + return StoredShortcut(key: "0", command: true, shift: false, option: false, control: false) case .canvasTidy: return StoredShortcut(key: "t", command: true, shift: false, option: false, control: true) case .canvasAlignLeft, @@ -638,11 +638,9 @@ enum KeyboardShortcutSettings { // Two bindings on the same keystroke only collide when some focus // state activates both AND router priority cannot decide the overlap. // A `shortcuts.when` override (or the built-in context default) can - // make them non-overlapping — e.g. ⌃1 selecting a workspace only when - // the sidebar is NOT focused coexists with the sidebar's ⌃1 (issue - // #5189) — and a pre-routed action (sidebar modes) wins its context - // outright, so the factory Select Surface ⌃1…9 coexists with the - // sidebar's ⌃1…5 by priority. + // make them non-overlapping (issue #5189), and a pre-routed action + // wins its context outright, so factory Select Surface ⌃1…9 coexists + // with the sidebar's ⌃1…5 by priority. guard ShortcutWhenClause.bindingsCollide( KeyboardShortcutSettings.effectiveWhenClause(for: self), lhsHasPriority: hasPriorityShortcutRouting, diff --git a/Sources/Workspace+SurfaceNavigation.swift b/Sources/Workspace+SurfaceNavigation.swift new file mode 100644 index 000000000000..26f41565d0ba --- /dev/null +++ b/Sources/Workspace+SurfaceNavigation.swift @@ -0,0 +1,65 @@ +extension Workspace { + /// Select the next surface in the currently focused split pane, or in + /// workspace Canvas order when Canvas layout is active. + func selectNextSurface() { + if layoutMode == .canvas { + _ = selectAdjacentCanvasTab(offset: 1) + return + } + bonsplitController.selectNextTab() + + if let paneId = bonsplitController.focusedPaneId, + let tabId = bonsplitController.selectedTab(inPane: paneId)?.id { + applyTabSelection(tabId: tabId, inPane: paneId) + } + } + + /// Select the previous surface in the currently focused split pane, or in + /// workspace Canvas order when Canvas layout is active. + func selectPreviousSurface() { + if layoutMode == .canvas { + _ = selectAdjacentCanvasTab(offset: -1) + return + } + bonsplitController.selectPreviousTab() + + if let paneId = bonsplitController.focusedPaneId, + let tabId = bonsplitController.selectedTab(inPane: paneId)?.id { + applyTabSelection(tabId: tabId, inPane: paneId) + } + } + + /// Select a surface by index in the currently focused split pane, or in + /// workspace Canvas order when Canvas layout is active. + func selectSurface(at index: Int) { + if layoutMode == .canvas { + _ = selectCanvasTab(at: index) + return + } + guard let focusedPaneId = bonsplitController.focusedPaneId else { return } + let tabs = bonsplitController.tabs(inPane: focusedPaneId) + guard tabs.indices.contains(index) else { return } + bonsplitController.selectTab(tabs[index].id) + + if let tabId = bonsplitController.selectedTab(inPane: focusedPaneId)?.id { + applyTabSelection(tabId: tabId, inPane: focusedPaneId) + } + } + + /// Select the last surface in the currently focused split pane, or in + /// workspace Canvas order when Canvas layout is active. + func selectLastSurface() { + if layoutMode == .canvas { + _ = selectLastCanvasTab() + return + } + guard let focusedPaneId = bonsplitController.focusedPaneId else { return } + let tabs = bonsplitController.tabs(inPane: focusedPaneId) + guard let last = tabs.last else { return } + bonsplitController.selectTab(last.id) + + if let tabId = bonsplitController.selectedTab(inPane: focusedPaneId)?.id { + applyTabSelection(tabId: tabId, inPane: focusedPaneId) + } + } +} diff --git a/Sources/Workspace.swift b/Sources/Workspace.swift index 589390b92eae..276d89165747 100644 --- a/Sources/Workspace.swift +++ b/Sources/Workspace.swift @@ -9520,55 +9520,6 @@ final class Workspace: Identifiable, ObservableObject { } } - - // MARK: - Surface Navigation - - /// Select the next surface in the currently focused pane - func selectNextSurface() { - if layoutMode == .canvas, selectAdjacentCanvasTab(offset: 1) { return } - bonsplitController.selectNextTab() - - if let paneId = bonsplitController.focusedPaneId, - let tabId = bonsplitController.selectedTab(inPane: paneId)?.id { - applyTabSelection(tabId: tabId, inPane: paneId) - } - } - - /// Select the previous surface in the currently focused pane - func selectPreviousSurface() { - if layoutMode == .canvas, selectAdjacentCanvasTab(offset: -1) { return } - bonsplitController.selectPreviousTab() - - if let paneId = bonsplitController.focusedPaneId, - let tabId = bonsplitController.selectedTab(inPane: paneId)?.id { - applyTabSelection(tabId: tabId, inPane: paneId) - } - } - - /// Select a surface by index in the currently focused pane - func selectSurface(at index: Int) { - guard let focusedPaneId = bonsplitController.focusedPaneId else { return } - let tabs = bonsplitController.tabs(inPane: focusedPaneId) - guard index >= 0 && index < tabs.count else { return } - bonsplitController.selectTab(tabs[index].id) - - if let tabId = bonsplitController.selectedTab(inPane: focusedPaneId)?.id { - applyTabSelection(tabId: tabId, inPane: focusedPaneId) - } - } - - /// Select the last surface in the currently focused pane - func selectLastSurface() { - guard let focusedPaneId = bonsplitController.focusedPaneId else { return } - let tabs = bonsplitController.tabs(inPane: focusedPaneId) - guard let last = tabs.last else { return } - bonsplitController.selectTab(last.id) - - if let tabId = bonsplitController.selectedTab(inPane: focusedPaneId)?.id { - applyTabSelection(tabId: tabId, inPane: focusedPaneId) - } - } - /// Create a new terminal surface in the currently focused pane @discardableResult func newTerminalSurfaceInFocusedPane(focus: Bool? = nil, initialInput: String? = nil) -> TerminalPanel? { @@ -11347,7 +11298,7 @@ extension Workspace: BonsplitDelegate { /// Apply the side-effects of selecting a tab (unfocus others, focus this panel, update state). /// bonsplit doesn't always emit didSelectTab for programmatic selection paths (e.g. createTab). - private func applyTabSelection( + func applyTabSelection( tabId: TabID, inPane pane: PaneID, reassertAppKitFocus: Bool = true, diff --git a/cmux.xcodeproj/project.pbxproj b/cmux.xcodeproj/project.pbxproj index 11efbf72d772..002cb8404462 100644 --- a/cmux.xcodeproj/project.pbxproj +++ b/cmux.xcodeproj/project.pbxproj @@ -54,6 +54,7 @@ A9F200000000000000000014 /* AgentSessionWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9F100000000000000000014 /* AgentSessionWebView.swift */; }; F4350A110000000000000001 /* AppBundleIconPersistencePolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4350A130000000000000001 /* AppBundleIconPersistencePolicy.swift */; }; F4350A120000000000000001 /* AppBundleIconPersistencePolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4350A130000000000000001 /* AppBundleIconPersistencePolicy.swift */; }; + C4A570010000000000000001 /* AppDelegate+CanvasShortcutRouting.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A570010000000000000002 /* AppDelegate+CanvasShortcutRouting.swift */; }; C4160A030000000000000001 /* AppDelegate+ClosedItemHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4160A030000000000000002 /* AppDelegate+ClosedItemHistory.swift */; }; C3677004000000000000001 /* AppDelegate+CmuxSSHURL.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3677004000000000000002 /* AppDelegate+CmuxSSHURL.swift */; }; C65930010000000000000003 /* AppDelegate+CrashSessionSnapshotRemoval.swift in Sources */ = {isa = PBXBuildFile; fileRef = C65930010000000000000004 /* AppDelegate+CrashSessionSnapshotRemoval.swift */; }; @@ -156,6 +157,7 @@ CA52E0030000000000000000 /* CanvasInlineBrowserHostingEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA52F0030000000000000000 /* CanvasInlineBrowserHostingEnvironment.swift */; }; CA52B0060000000000000000 /* CanvasLayoutSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA52C0060000000000000000 /* CanvasLayoutSettings.swift */; }; CA52B0070000000000000000 /* CanvasPaneContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA52C0070000000000000000 /* CanvasPaneContent.swift */; }; + C4A570030000000000000001 /* CanvasShortcutContextTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A570030000000000000002 /* CanvasShortcutContextTests.swift */; }; F3000000A1B2C3D4E5F60718 /* CJKIMEInputTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3000001A1B2C3D4E5F60718 /* CJKIMEInputTests.swift */; }; D3571002A1B2C3D4E5F60718 /* CJKIMEMarkedSelectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3571003A1B2C3D4E5F60718 /* CJKIMEMarkedSelectionTests.swift */; }; C13519000000000000000007 /* ClaudeConfigDirectoryPathTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C13519000000000000000008 /* ClaudeConfigDirectoryPathTests.swift */; }; @@ -506,6 +508,7 @@ C0DEF0B10000000000000003 /* JSONCParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DEF0B10000000000000002 /* JSONCParser.swift */; }; B9000014A1B2C3D4E5F60719 /* JumpToUnreadUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9000013A1B2C3D4E5F60719 /* JumpToUnreadUITests.swift */; }; A50012F5 /* KeyboardLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50012F4 /* KeyboardLayout.swift */; }; + C4A570020000000000000001 /* KeyboardShortcutActionContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A570020000000000000002 /* KeyboardShortcutActionContext.swift */; }; C34670030000000000000001 /* KeyboardShortcutContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = C34670030000000000000002 /* KeyboardShortcutContext.swift */; }; C34670020000000000000001 /* KeyboardShortcutContextTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C34670020000000000000002 /* KeyboardShortcutContextTests.swift */; }; 6042A0026042A0026042A002 /* KeyboardShortcutModifierHoldHintsSettingsFileTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6042B0026042B0026042B002 /* KeyboardShortcutModifierHoldHintsSettingsFileTests.swift */; }; @@ -1007,6 +1010,7 @@ D7AB00000000000000000015 /* Workspace+DetachedSurfaceTransfer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7AB00000000000000000016 /* Workspace+DetachedSurfaceTransfer.swift */; }; E3309A05 /* Workspace+EqualizeSplitsSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3309A06 /* Workspace+EqualizeSplitsSupport.swift */; }; C3744E010000000000000001 /* Workspace+PanelLifecycle.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3744E020000000000000001 /* Workspace+PanelLifecycle.swift */; }; + CA52B0170000000000000000 /* Workspace+SurfaceNavigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA52C0170000000000000000 /* Workspace+SurfaceNavigation.swift */; }; E3B7A400000000000000020A /* Workspace+WorkspaceSurfaceTreeReading.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3B7A400000000000000020B /* Workspace+WorkspaceSurfaceTreeReading.swift */; }; A5001406 /* Workspace.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001416 /* Workspace.swift */; }; 7D77A0EBBE3D8E05CDC16229 /* WorkspaceActionDispatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5CC15CEAA80B779ADEE78AA /* WorkspaceActionDispatcher.swift */; }; @@ -1183,6 +1187,7 @@ A9E01000000000000000000D /* AgentSessionWebTheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/AgentSessionWebTheme.swift; sourceTree = ""; }; A9F100000000000000000014 /* AgentSessionWebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/AgentSessionWebView.swift; sourceTree = ""; }; F4350A130000000000000001 /* AppBundleIconPersistencePolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/AppBundleIconPersistencePolicy.swift; sourceTree = ""; }; + C4A570010000000000000002 /* AppDelegate+CanvasShortcutRouting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+CanvasShortcutRouting.swift"; sourceTree = ""; }; C4160A030000000000000002 /* AppDelegate+ClosedItemHistory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+ClosedItemHistory.swift"; sourceTree = ""; }; C3677004000000000000002 /* AppDelegate+CmuxSSHURL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+CmuxSSHURL.swift"; sourceTree = ""; }; C65930010000000000000004 /* AppDelegate+CrashSessionSnapshotRemoval.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+CrashSessionSnapshotRemoval.swift"; sourceTree = ""; }; @@ -1285,6 +1290,7 @@ CA52F0030000000000000000 /* CanvasInlineBrowserHostingEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CanvasInlineBrowserHostingEnvironment.swift"; sourceTree = ""; }; CA52C0060000000000000000 /* CanvasLayoutSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CanvasLayoutSettings.swift"; sourceTree = ""; }; CA52C0070000000000000000 /* CanvasPaneContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CanvasPaneContent.swift"; sourceTree = ""; }; + C4A570030000000000000002 /* CanvasShortcutContextTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CanvasShortcutContextTests.swift; sourceTree = ""; }; F3000001A1B2C3D4E5F60718 /* CJKIMEInputTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CJKIMEInputTests.swift; sourceTree = ""; }; D3571003A1B2C3D4E5F60718 /* CJKIMEMarkedSelectionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CJKIMEMarkedSelectionTests.swift; sourceTree = ""; }; C13519000000000000000008 /* ClaudeConfigDirectoryPathTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClaudeConfigDirectoryPathTests.swift; sourceTree = ""; }; @@ -1583,6 +1589,7 @@ C0DEF0B10000000000000002 /* JSONCParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONCParser.swift; sourceTree = ""; }; B9000013A1B2C3D4E5F60719 /* JumpToUnreadUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JumpToUnreadUITests.swift; sourceTree = ""; }; A50012F4 /* KeyboardLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardLayout.swift; sourceTree = ""; }; + C4A570020000000000000002 /* KeyboardShortcutActionContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardShortcutActionContext.swift; sourceTree = ""; }; C34670030000000000000002 /* KeyboardShortcutContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardShortcutContext.swift; sourceTree = ""; }; C34670020000000000000002 /* KeyboardShortcutContextTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardShortcutContextTests.swift; sourceTree = ""; }; 6042B0026042B0026042B002 /* KeyboardShortcutModifierHoldHintsSettingsFileTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardShortcutModifierHoldHintsSettingsFileTests.swift; sourceTree = ""; }; @@ -2070,6 +2077,7 @@ D7AB00000000000000000016 /* Workspace+DetachedSurfaceTransfer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Workspace+DetachedSurfaceTransfer.swift"; sourceTree = ""; }; E3309A06 /* Workspace+EqualizeSplitsSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Workspace+EqualizeSplitsSupport.swift"; sourceTree = ""; }; C3744E020000000000000001 /* Workspace+PanelLifecycle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Workspace+PanelLifecycle.swift"; sourceTree = ""; }; + CA52C0170000000000000000 /* Workspace+SurfaceNavigation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Workspace+SurfaceNavigation.swift"; sourceTree = ""; }; E3B7A400000000000000020B /* Workspace+WorkspaceSurfaceTreeReading.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Workspace+WorkspaceSurfaceTreeReading.swift"; sourceTree = ""; }; A5001416 /* Workspace.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Workspace.swift; sourceTree = ""; }; C5CC15CEAA80B779ADEE78AA /* WorkspaceActionDispatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceActionDispatcher.swift; sourceTree = ""; }; @@ -2529,6 +2537,7 @@ C0DE7B290000000000000002 /* TextBoxProcessTerminationStatus.swift */, C0DE7B100000000000000002 /* TextBoxInput.swift */, C1713003C1713003C1713003 /* KeyboardShortcutSettingsLookup.swift */, + C4A570020000000000000002 /* KeyboardShortcutActionContext.swift */, C34670030000000000000002 /* KeyboardShortcutContext.swift */, A50012F6 /* KeyboardShortcutRecorder.swift */, A50012F9 /* KeyboardShortcutSettingsControls.swift */, @@ -2558,6 +2567,7 @@ E3B7A400000000000000000A /* TabManager+NotificationDismissalHosting.swift */, E3B7A400000000000000000C /* TabManager+FocusHistoryHosting.swift */, E3B7A400000000000000020B /* Workspace+WorkspaceSurfaceTreeReading.swift */, + CA52C0170000000000000000 /* Workspace+SurfaceNavigation.swift */, A5001511 /* UITestRecorder.swift */, A5001520 /* PostHogAnalytics.swift */, A5001416 /* Workspace.swift */, @@ -2684,6 +2694,7 @@ F4350A130000000000000001 /* AppBundleIconPersistencePolicy.swift */, D1320AA0D1320AA0D1320AA4 /* AppIconDockTilePlugin.swift */, A5001090 /* AppDelegate.swift */, + C4A570010000000000000002 /* AppDelegate+CanvasShortcutRouting.swift */, A64610010000000000000002 /* QuitConfirmationAlertPresenter.swift */, 3865B0063865B0063865B006 /* AppDelegate+GlobalSearch.swift */, C3873002C3873002C3873002 /* GhosttyCrashBreadcrumb.swift */, @@ -3164,6 +3175,7 @@ F016B5C09357B3226FA2E014 /* SidebarWorkspaceSnapshotRefreshPolicyTests.swift */, C9A57514C9A57514C9A57514 /* SidebarScrollViewConfiguratorTests.swift */, C9A57506C9A57506C9A57506 /* SidebarWorkspaceScrollLayoutTests.swift */, + C4A570030000000000000002 /* CanvasShortcutContextTests.swift */, C0DEF0B30000000000000002 /* KeyboardShortcutSettingsFileStoreMigrationTests.swift */, E3337002E3337002E3337002 /* KeyboardShortcutSettingsFileStoreStartupTests.swift */, C34670020000000000000002 /* KeyboardShortcutContextTests.swift */, @@ -3778,6 +3790,7 @@ A9E02000000000000000000D /* AgentSessionWebTheme.swift in Sources */, A9F200000000000000000014 /* AgentSessionWebView.swift in Sources */, F4350A110000000000000001 /* AppBundleIconPersistencePolicy.swift in Sources */, + C4A570010000000000000001 /* AppDelegate+CanvasShortcutRouting.swift in Sources */, C4160A030000000000000001 /* AppDelegate+ClosedItemHistory.swift in Sources */, C3677004000000000000001 /* AppDelegate+CmuxSSHURL.swift in Sources */, C65930010000000000000003 /* AppDelegate+CrashSessionSnapshotRemoval.swift in Sources */, @@ -3992,6 +4005,7 @@ 4D8B37E0E11A29997632765F /* InternalTabDragConfiguration.swift in Sources */, C0DEF0B10000000000000001 /* JSONCParser.swift in Sources */, A50012F5 /* KeyboardLayout.swift in Sources */, + C4A570020000000000000001 /* KeyboardShortcutActionContext.swift in Sources */, C34670030000000000000001 /* KeyboardShortcutContext.swift in Sources */, A50012F7 /* KeyboardShortcutRecorder.swift in Sources */, A50012F3 /* KeyboardShortcutSettings.swift in Sources */, @@ -4318,6 +4332,7 @@ D7AB00000000000000000015 /* Workspace+DetachedSurfaceTransfer.swift in Sources */, E3309A05 /* Workspace+EqualizeSplitsSupport.swift in Sources */, C3744E010000000000000001 /* Workspace+PanelLifecycle.swift in Sources */, + CA52B0170000000000000000 /* Workspace+SurfaceNavigation.swift in Sources */, E3B7A400000000000000020A /* Workspace+WorkspaceSurfaceTreeReading.swift in Sources */, A5001406 /* Workspace.swift in Sources */, 7D77A0EBBE3D8E05CDC16229 /* WorkspaceActionDispatcher.swift in Sources */, @@ -4502,6 +4517,7 @@ C0DE49870000000000000001 /* BrowserWebContentProcessTests.swift in Sources */, C0DE62600000000000000001 /* BrowserWindowPortalRegistryNotificationTests.swift in Sources */, C0DE35530000000000000101 /* BundledCLILinkageTests.swift in Sources */, + C4A570030000000000000001 /* CanvasShortcutContextTests.swift in Sources */, F3000000A1B2C3D4E5F60718 /* CJKIMEInputTests.swift in Sources */, D3571002A1B2C3D4E5F60718 /* CJKIMEMarkedSelectionTests.swift in Sources */, C13519000000000000000007 /* ClaudeConfigDirectoryPathTests.swift in Sources */, diff --git a/cmuxTests/AppDelegateSurfaceShortcutRoutingTests.swift b/cmuxTests/AppDelegateSurfaceShortcutRoutingTests.swift index 5b4cc60b8f55..834e30ac14d5 100644 --- a/cmuxTests/AppDelegateSurfaceShortcutRoutingTests.swift +++ b/cmuxTests/AppDelegateSurfaceShortcutRoutingTests.swift @@ -1,4 +1,5 @@ import AppKit +import CmuxCanvasUI import Testing #if canImport(cmux_DEV) @@ -11,6 +12,23 @@ private final class ShortcutUnrelatedResponderView: NSView { override var acceptsFirstResponder: Bool { true } } +@MainActor +private final class CanvasViewportSpy: CanvasViewportControlling { + var revealedPanelIds: [UUID] = [] + var overviewToggleCount = 0 + var modelDidChangeCount = 0 + var resetZoomCount = 0 + var currentMagnification: CGFloat = 1 + var currentCenterInCanvas: CGPoint = .zero + + func revealPane(_ panelId: UUID, animated: Bool) { revealedPanelIds.append(panelId) } + func toggleOverview() { overviewToggleCount += 1 } + func zoom(by factor: CGFloat) {} + func resetZoom() { resetZoomCount += 1 } + func setViewport(center: CGPoint, magnification: CGFloat?) {} + func modelDidChangeExternally(animated: Bool) { modelDidChangeCount += 1 } +} + @MainActor @Suite(.serialized) struct AppDelegateSurfaceShortcutRoutingTests { @@ -159,11 +177,237 @@ struct AppDelegateSurfaceShortcutRoutingTests { } } - private func makeKeyDownEvent(key: String, keyCode: UInt16, windowNumber: Int) -> NSEvent? { + @Test func cmdShiftReturnInCanvasModeDoesNotToggleBonsplitSplitZoom() throws { + try withTemporaryShortcut(action: .toggleSplitZoom) { + let appDelegate = try #require(AppDelegate.shared) + + let windowId = appDelegate.createMainWindow() + defer { closeWindow(withId: windowId) } + + let window = try #require(mainWindow(for: windowId)) + let manager = try #require(appDelegate.tabManagerFor(windowId: windowId)) + let workspace = try #require(manager.selectedWorkspace) + let leftPanelId = try #require(workspace.focusedPanelId) + _ = try #require(workspace.newTerminalSplit(from: leftPanelId, orientation: .horizontal)) + let event = try #require(makeKeyDownEvent( + key: "\r", + modifiers: [.command, .shift], + keyCode: 36, + windowNumber: window.windowNumber + )) + + workspace.setLayoutMode(.canvas) + let viewport = CanvasViewportSpy() + workspace.canvasModel.viewport = viewport + #expect(workspace.layoutMode == .canvas) + #expect(!workspace.bonsplitController.isSplitZoomed) + #expect(KeyboardShortcutSettings.shortcut(for: .toggleSplitZoom).matches(event: event)) + +#if DEBUG + #expect(appDelegate.debugHandleCustomShortcut(event: event)) +#else + Issue.record("debugHandleCustomShortcut is only available in DEBUG") +#endif + #expect( + !workspace.bonsplitController.isSplitZoomed, + "In canvas mode, the split-zoom shortcut should drive canvas overview instead of Bonsplit zoom" + ) + #expect(viewport.overviewToggleCount == 1) + } + } + + @Test func cmdDInCanvasCreatesFloatingCanvasPaneWithoutBonsplitSplit() throws { + try withTemporaryShortcut(action: .splitRight) { + let appDelegate = try #require(AppDelegate.shared) + + let windowId = appDelegate.createMainWindow() + defer { closeWindow(withId: windowId) } + + let window = try #require(mainWindow(for: windowId)) + let manager = try #require(appDelegate.tabManagerFor(windowId: windowId)) + let workspace = try #require(manager.selectedWorkspace) + let focusedPanelId = try #require(workspace.focusedPanelId) + let event = try #require(makeKeyDownEvent( + key: "d", + modifiers: [.command], + keyCode: 2, + windowNumber: window.windowNumber + )) + + window.makeKeyAndOrderFront(nil) + workspace.setLayoutMode(.canvas) + let originalBonsplitPaneCount = workspace.bonsplitController.allPaneIds.count + let originalPanelIds = Set(workspace.panels.keys) + let originalFrame = try #require(workspace.canvasModel.frame(of: focusedPanelId)) + +#if DEBUG + #expect(appDelegate.debugHandleCustomShortcut(event: event)) +#else + Issue.record("debugHandleCustomShortcut is only available in DEBUG") +#endif + + let newPanelIds = Set(workspace.panels.keys).subtracting(originalPanelIds) + #expect(newPanelIds.count == 1) + #expect( + workspace.bonsplitController.allPaneIds.count == originalBonsplitPaneCount, + "Canvas split shortcuts should create visible canvas panes without splitting the hidden Bonsplit tree" + ) + #expect(workspace.canvasModel.persistablePanes.count == 2) + + let newPanelId = try #require(newPanelIds.first) + let newFrame = try #require(workspace.canvasModel.frame(of: newPanelId)) + #expect(newFrame.minX >= originalFrame.maxX) + #expect(newFrame.width == originalFrame.width) + #expect(newFrame.height == originalFrame.height) + #expect(workspace.focusedPanelId == newPanelId) + } + } + + @Test func cmdShiftDInCanvasPlacesFloatingCanvasPaneBelowFocusedPane() throws { + try withTemporaryShortcut(action: .splitDown) { + let appDelegate = try #require(AppDelegate.shared) + + let windowId = appDelegate.createMainWindow() + defer { closeWindow(withId: windowId) } + + let window = try #require(mainWindow(for: windowId)) + let manager = try #require(appDelegate.tabManagerFor(windowId: windowId)) + let workspace = try #require(manager.selectedWorkspace) + let focusedPanelId = try #require(workspace.focusedPanelId) + let event = try #require(makeKeyDownEvent( + key: "d", + modifiers: [.command, .shift], + keyCode: 2, + windowNumber: window.windowNumber + )) + + window.makeKeyAndOrderFront(nil) + workspace.setLayoutMode(.canvas) + let originalPanelIds = Set(workspace.panels.keys) + let originalFrame = try #require(workspace.canvasModel.frame(of: focusedPanelId)) + +#if DEBUG + #expect(appDelegate.debugHandleCustomShortcut(event: event)) +#else + Issue.record("debugHandleCustomShortcut is only available in DEBUG") +#endif + + let newPanelIds = Set(workspace.panels.keys).subtracting(originalPanelIds) + #expect(newPanelIds.count == 1) + let newPanelId = try #require(newPanelIds.first) + let newFrame = try #require(workspace.canvasModel.frame(of: newPanelId)) + #expect(newFrame.minY >= originalFrame.maxY) + } + } + + @Test func numberedSurfaceShortcutSelectsCanvasPaneTab() throws { + try withTemporaryShortcut(action: .selectSurfaceByNumber) { + let appDelegate = try #require(AppDelegate.shared) + + let windowId = appDelegate.createMainWindow() + defer { closeWindow(withId: windowId) } + + let window = try #require(mainWindow(for: windowId)) + let manager = try #require(appDelegate.tabManagerFor(windowId: windowId)) + let workspace = try #require(manager.selectedWorkspace) + let firstPanelId = try #require(workspace.focusedPanelId) + let event = try #require(makeKeyDownEvent( + key: "2", + keyCode: 19, + windowNumber: window.windowNumber + )) + + window.makeKeyAndOrderFront(nil) + workspace.setLayoutMode(.canvas) + let secondPanelId = try #require(workspace.openNewCanvasPane(type: .terminal, focus: true)) + let viewport = CanvasViewportSpy() + workspace.canvasModel.viewport = viewport + workspace.focusPanel(firstPanelId) + #expect(workspace.focusedPanelId == firstPanelId) + +#if DEBUG + #expect(appDelegate.debugHandleCustomShortcut(event: event)) +#else + Issue.record("debugHandleCustomShortcut is only available in DEBUG") +#endif + + #expect(workspace.focusedPanelId == secondPanelId) + #expect(viewport.revealedPanelIds.last == secondPanelId) + } + } + + @Test func canvasSurfaceSelectionKeepsNinthAndLastSeparate() throws { + let appDelegate = try #require(AppDelegate.shared) + let windowId = appDelegate.createMainWindow() + defer { closeWindow(withId: windowId) } + + let manager = try #require(appDelegate.tabManagerFor(windowId: windowId)) + let workspace = try #require(manager.selectedWorkspace) + var panelIds = [try #require(workspace.focusedPanelId)] + + workspace.setLayoutMode(.canvas) + for _ in 1..<10 { + panelIds.append(try #require(workspace.openNewCanvasPane(type: .terminal, focus: true))) + } + #expect(panelIds.count == 10) + + workspace.focusPanel(panelIds[0]) + workspace.selectSurface(at: 8) + #expect(workspace.focusedPanelId == panelIds[8]) + + workspace.selectLastSurface() + #expect(workspace.focusedPanelId == panelIds[9]) + } + + @Test func equalizeSplitsShortcutInCanvasEqualizesCanvasPaneSizesOnly() throws { + try withTemporaryShortcut(action: .equalizeSplits) { + let appDelegate = try #require(AppDelegate.shared) + + let windowId = appDelegate.createMainWindow() + defer { closeWindow(withId: windowId) } + + let window = try #require(mainWindow(for: windowId)) + let manager = try #require(appDelegate.tabManagerFor(windowId: windowId)) + let workspace = try #require(manager.selectedWorkspace) + let firstPanelId = try #require(workspace.focusedPanelId) + let event = try #require(makeKeyDownEvent( + key: "=", + modifiers: [.command, .control], + keyCode: 24, + windowNumber: window.windowNumber + )) + + window.makeKeyAndOrderFront(nil) + workspace.setLayoutMode(.canvas) + let secondPanelId = try #require(workspace.openNewCanvasPane(type: .terminal, focus: true)) + let originalBonsplitPaneCount = workspace.bonsplitController.allPaneIds.count + workspace.canvasModel.setFrame(CGRect(x: 0, y: 0, width: 640, height: 420), for: firstPanelId) + workspace.canvasModel.setFrame(CGRect(x: 720, y: 0, width: 320, height: 260), for: secondPanelId) + +#if DEBUG + #expect(appDelegate.debugHandleCustomShortcut(event: event)) +#else + Issue.record("debugHandleCustomShortcut is only available in DEBUG") +#endif + + let firstFrame = try #require(workspace.canvasModel.frame(of: firstPanelId)) + let secondFrame = try #require(workspace.canvasModel.frame(of: secondPanelId)) + #expect(firstFrame.width == secondFrame.width) + #expect(firstFrame.height == secondFrame.height) + #expect(workspace.bonsplitController.allPaneIds.count == originalBonsplitPaneCount) + } + } + + private func makeKeyDownEvent( + key: String, + modifiers: NSEvent.ModifierFlags = [.control], + keyCode: UInt16, + windowNumber: Int + ) -> NSEvent? { NSEvent.keyEvent( with: .keyDown, location: .zero, - modifierFlags: [.control], + modifierFlags: modifiers, timestamp: ProcessInfo.processInfo.systemUptime, windowNumber: windowNumber, context: nil, diff --git a/cmuxTests/CanvasShortcutContextTests.swift b/cmuxTests/CanvasShortcutContextTests.swift new file mode 100644 index 000000000000..26079c9508c1 --- /dev/null +++ b/cmuxTests/CanvasShortcutContextTests.swift @@ -0,0 +1,292 @@ +import AppKit +import CmuxCanvasUI +import CmuxSettings +import Testing + +#if canImport(cmux_DEV) +@testable import cmux_DEV +#elseif canImport(cmux) +@testable import cmux +#endif + +@MainActor +private final class CanvasRoutingViewportSpy: CanvasViewportControlling { + var revealedPanelIds: [UUID] = [] + var resetZoomCount = 0 + var currentMagnification: CGFloat = 1 + var currentCenterInCanvas: CGPoint = .zero + + func revealPane(_ panelId: UUID, animated: Bool) { revealedPanelIds.append(panelId) } + func resetZoom() { resetZoomCount += 1 } + func toggleOverview() {} + func zoom(by factor: CGFloat) {} + func setViewport(center: CGPoint, magnification: CGFloat?) {} + func modelDidChangeExternally(animated: Bool) {} +} + +@Suite("Canvas shortcut context") +struct CanvasShortcutContextTests { + @Test + func canvasOnlyShortcutDefaultWhenClausesRequireCanvasLayout() { + var splitContext = ShortcutFocusState(browser: false, markdown: false, sidebar: false).context + splitContext.setBool(ShortcutContextKnownKey.workspaceCanvasLayout.rawValue, false) + + var canvasContext = ShortcutFocusState(browser: false, markdown: false, sidebar: false).context + canvasContext.setBool(ShortcutContextKnownKey.workspaceCanvasLayout.rawValue, true) + + #expect( + KeyboardShortcutSettings.effectiveWhenClause(for: .toggleCanvasLayout).evaluate(splitContext), + "The layout toggle must stay available outside canvas mode" + ) + + for action in KeyboardShortcutSettings.Action.canvasActions where action != .toggleCanvasLayout { + let clause = KeyboardShortcutSettings.effectiveWhenClause(for: action) + #expect( + !clause.evaluate(splitContext), + "\(action.rawValue) must not claim its shortcut while the workspace uses split layout" + ) + #expect( + clause.evaluate(canvasContext), + "\(action.rawValue) must be available when the workspace uses canvas layout" + ) + } + } + + @Test + func canvasLayoutContextOverlapsNormalTerminalFocusShortcuts() { + let canvas = KeyboardShortcutSettings.Action.canvasOverview.shortcutContext + let nonBrowser = KeyboardShortcutSettings.Action.renameTab.shortcutContext + let browser = KeyboardShortcutSettings.Action.browserReload.shortcutContext + let markdown = KeyboardShortcutSettings.Action.markdownZoomIn.shortcutContext + let sidebar = KeyboardShortcutSettings.Action.fileExplorerOpenSelection.shortcutContext + + #expect(canvas == .canvasLayout) + #expect(nonBrowser == .nonBrowserPanel) + #expect(canvas.isAvailable( + focusedBrowserPanel: false, + focusedMarkdownPanel: false, + rightSidebarFocused: false, + workspaceCanvasLayout: true + )) + #expect(nonBrowser.isAvailable( + focusedBrowserPanel: false, + focusedMarkdownPanel: false, + rightSidebarFocused: false, + workspaceCanvasLayout: true + )) + #expect(canvas.overlaps(nonBrowser)) + #expect(nonBrowser.overlaps(canvas)) + #expect(canvas.overlaps(browser)) + #expect(browser.overlaps(canvas)) + #expect(canvas.overlaps(markdown)) + #expect(markdown.overlaps(canvas)) + #expect(canvas.overlaps(sidebar)) + #expect(sidebar.overlaps(canvas)) + } + + @Test + func canvasActualSizeSharesCommandZeroWithBrowserAndMarkdownActualSize() { + let canvasActualSize = KeyboardShortcutSettings.Action.canvasZoomReset.defaultShortcut + let browserActualSize = KeyboardShortcutSettings.Action.browserZoomReset.defaultShortcut + let markdownActualSize = KeyboardShortcutSettings.Action.markdownZoomReset.defaultShortcut + let canvasActualSizeContext = KeyboardShortcutSettings.Action.canvasZoomReset.shortcutContext + let canvasActualSizeWhen = KeyboardShortcutSettings.effectiveWhenClause(for: .canvasZoomReset) + var backgroundCanvasContext = ShortcutFocusState(browser: false, markdown: false, sidebar: false).context + backgroundCanvasContext.setBool(ShortcutContextKnownKey.workspaceCanvasLayout.rawValue, true) + var browserCanvasContext = ShortcutFocusState(browser: true, markdown: false, sidebar: false).context + browserCanvasContext.setBool(ShortcutContextKnownKey.workspaceCanvasLayout.rawValue, true) + var markdownCanvasContext = ShortcutFocusState(browser: false, markdown: true, sidebar: false).context + markdownCanvasContext.setBool(ShortcutContextKnownKey.workspaceCanvasLayout.rawValue, true) + + #expect(canvasActualSize == StoredShortcut(key: "0", command: true, shift: false, option: false, control: false)) + #expect(browserActualSize == canvasActualSize) + #expect(markdownActualSize == canvasActualSize) + #expect(canvasActualSizeContext == .canvasLayoutOutsideFocusedContent) + #expect(canvasActualSizeWhen.evaluate(backgroundCanvasContext)) + #expect(!canvasActualSizeWhen.evaluate(browserCanvasContext)) + #expect(!canvasActualSizeWhen.evaluate(markdownCanvasContext)) + #expect(!KeyboardShortcutSettings.Action.browserZoomReset.conflicts( + with: canvasActualSize, + proposedAction: .canvasZoomReset, + configuredShortcut: browserActualSize + )) + #expect(!KeyboardShortcutSettings.Action.markdownZoomReset.conflicts( + with: canvasActualSize, + proposedAction: .canvasZoomReset, + configuredShortcut: markdownActualSize + )) + } +} + +@MainActor +@Suite(.serialized) +struct CanvasShortcutRoutingFeedbackTests { + @Test func canvasSurfaceDigitsWinOverRightSidebarModeDigitsInCanvasMode() throws { + try withIsolatedShortcutSettings { + let appDelegate = try #require(AppDelegate.shared) + let windowId = appDelegate.createMainWindow() + defer { closeWindow(withId: windowId) } + + let window = try #require(mainWindow(for: windowId)) + let manager = try #require(appDelegate.tabManagerFor(windowId: windowId)) + let workspace = try #require(manager.selectedWorkspace) + let firstPanelId = try #require(workspace.focusedPanelId) + let event = try #require(makeKeyDownEvent(key: "1", keyCode: 18, windowNumber: window.windowNumber)) + + window.makeKeyAndOrderFront(nil) + workspace.setLayoutMode(.canvas) + let secondPanelId = try #require(workspace.openNewCanvasPane(type: .terminal, focus: true)) + #expect(workspace.focusedPanelId == secondPanelId) + + appDelegate.noteRightSidebarKeyboardFocusIntent(mode: .sessions, in: window) + let fileExplorerState = try #require(appDelegate.fileExplorerState) + fileExplorerState.mode = .sessions + +#if DEBUG + #expect(appDelegate.debugHandleCustomShortcut(event: event)) +#else + Issue.record("debugHandleCustomShortcut is only available in DEBUG") +#endif + + #expect(workspace.focusedPanelId == firstPanelId) + #expect( + fileExplorerState.mode == .sessions, + "Ctrl+1 should select the first Canvas surface instead of switching the right sidebar to Files in canvas mode" + ) + } + } + + @Test func directionalFocusShortcutInCanvasRevealsTargetPane() throws { + try withTemporaryShortcut(action: .focusRight) { + let appDelegate = try #require(AppDelegate.shared) + let windowId = appDelegate.createMainWindow() + defer { closeWindow(withId: windowId) } + + let window = try #require(mainWindow(for: windowId)) + let manager = try #require(appDelegate.tabManagerFor(windowId: windowId)) + let workspace = try #require(manager.selectedWorkspace) + let firstPanelId = try #require(workspace.focusedPanelId) + let event = try #require(makeKeyDownEvent( + key: "→", + modifiers: [.command, .option], + keyCode: 124, + windowNumber: window.windowNumber + )) + + window.makeKeyAndOrderFront(nil) + workspace.setLayoutMode(.canvas) + let secondPanelId = try #require(workspace.openNewCanvasPane(type: .terminal, focus: true, direction: .right)) + let viewport = CanvasRoutingViewportSpy() + workspace.canvasModel.viewport = viewport + workspace.focusPanel(firstPanelId) + +#if DEBUG + #expect(appDelegate.debugHandleCustomShortcut(event: event)) +#else + Issue.record("debugHandleCustomShortcut is only available in DEBUG") +#endif + + #expect(workspace.focusedPanelId == secondPanelId) + #expect(viewport.revealedPanelIds.last == secondPanelId) + } + } + + @Test func cmdZeroInCanvasResetsCanvasZoom() throws { + try withTemporaryShortcut(action: .canvasZoomReset) { + let appDelegate = try #require(AppDelegate.shared) + let windowId = appDelegate.createMainWindow() + defer { closeWindow(withId: windowId) } + + let window = try #require(mainWindow(for: windowId)) + let manager = try #require(appDelegate.tabManagerFor(windowId: windowId)) + let workspace = try #require(manager.selectedWorkspace) + let event = try #require(makeKeyDownEvent( + key: "0", + modifiers: [.command], + keyCode: 29, + windowNumber: window.windowNumber + )) + + window.makeKeyAndOrderFront(nil) + workspace.setLayoutMode(.canvas) + let viewport = CanvasRoutingViewportSpy() + workspace.canvasModel.viewport = viewport + +#if DEBUG + #expect(appDelegate.debugHandleCustomShortcut(event: event)) +#else + Issue.record("debugHandleCustomShortcut is only available in DEBUG") +#endif + + #expect(viewport.resetZoomCount == 1) + } + } + + private func makeKeyDownEvent( + key: String, + modifiers: NSEvent.ModifierFlags = [.control], + keyCode: UInt16, + windowNumber: Int + ) -> NSEvent? { + NSEvent.keyEvent( + with: .keyDown, + location: .zero, + modifierFlags: modifiers, + timestamp: ProcessInfo.processInfo.systemUptime, + windowNumber: windowNumber, + context: nil, + characters: key, + charactersIgnoringModifiers: key, + isARepeat: false, + keyCode: keyCode + ) + } + + private func withTemporaryShortcut(action: KeyboardShortcutSettings.Action, _ body: () throws -> Void) rethrows { + let hadPersistedShortcut = UserDefaults.standard.object(forKey: action.defaultsKey) != nil + let originalShortcut = KeyboardShortcutSettings.shortcut(for: action) + defer { + hadPersistedShortcut ? KeyboardShortcutSettings.setShortcut(originalShortcut, for: action) : KeyboardShortcutSettings.resetShortcut(for: action) +#if DEBUG + AppDelegate.shared?.debugResetShortcutRoutingStateForTesting(clearFocusedWindowOverride: false) +#endif + } + KeyboardShortcutSettings.setShortcut(action.defaultShortcut, for: action) +#if DEBUG + AppDelegate.shared?.debugResetShortcutRoutingStateForTesting(clearFocusedWindowOverride: false) +#endif + try body() + } + + private func withIsolatedShortcutSettings(_ body: () throws -> Void) rethrows { + let actions = Set(KeyboardShortcutSettings.Action.allCases.filter { UserDefaults.standard.object(forKey: $0.defaultsKey) != nil }) + let saved = Dictionary(uniqueKeysWithValues: actions.map { ($0, KeyboardShortcutSettings.shortcut(for: $0)) }) + let originalStore = KeyboardShortcutSettings.installIsolatedTestFileStore(prefix: "cmux-canvas-shortcut-routing") + KeyboardShortcutSettings.resetAll() +#if DEBUG + AppDelegate.shared?.debugResetShortcutRoutingStateForTesting(clearFocusedWindowOverride: false) +#endif + defer { + KeyboardShortcutSettings.settingsFileStore = originalStore + for action in KeyboardShortcutSettings.Action.allCases { + if actions.contains(action), let shortcut = saved[action] { + KeyboardShortcutSettings.setShortcut(shortcut, for: action) + } else { + KeyboardShortcutSettings.resetShortcut(for: action) + } + } +#if DEBUG + AppDelegate.shared?.debugResetShortcutRoutingStateForTesting(clearFocusedWindowOverride: false) +#endif + } + try body() + } + + private func mainWindow(for windowId: UUID) -> NSWindow? { + AppDelegate.shared?.windowForMainWindowId(windowId) + } + + private func closeWindow(withId windowId: UUID) { + mainWindow(for: windowId)?.close() + } +} diff --git a/skills/cmux-settings/references/shortcut-actions.md b/skills/cmux-settings/references/shortcut-actions.md index 9337e874bc91..5f1ad2100149 100644 --- a/skills/cmux-settings/references/shortcut-actions.md +++ b/skills/cmux-settings/references/shortcut-actions.md @@ -55,6 +55,24 @@ Values for `shortcuts.bindings.`: - `shortcuts.bindings.toggleSplitZoom` - `shortcuts.bindings.equalizeSplits` +## Canvas + +- `shortcuts.bindings.toggleCanvasLayout` +- `shortcuts.bindings.canvasRevealFocusedPane` +- `shortcuts.bindings.canvasOverview` +- `shortcuts.bindings.canvasZoomIn` +- `shortcuts.bindings.canvasZoomOut` +- `shortcuts.bindings.canvasZoomReset` +- `shortcuts.bindings.canvasTidy` +- `shortcuts.bindings.canvasAlignLeft` +- `shortcuts.bindings.canvasAlignRight` +- `shortcuts.bindings.canvasAlignTop` +- `shortcuts.bindings.canvasAlignBottom` +- `shortcuts.bindings.canvasEqualizeWidths` +- `shortcuts.bindings.canvasEqualizeHeights` +- `shortcuts.bindings.canvasDistributeHorizontally` +- `shortcuts.bindings.canvasDistributeVertically` + ## Command palette - `shortcuts.bindings.commandPalette` diff --git a/web/app/[locale]/docs/configuration/page.tsx b/web/app/[locale]/docs/configuration/page.tsx index e08967e88771..afe437022245 100644 --- a/web/app/[locale]/docs/configuration/page.tsx +++ b/web/app/[locale]/docs/configuration/page.tsx @@ -475,7 +475,8 @@ working-directory = ~/code`}
  • sidebarFocus, browserFocus, markdownFocus,{" "} terminalFocus, commandPaletteVisible,{" "} - terminalFindVisible — {t("shortcutsWhenBooleanKeys")} + terminalFindVisible, workspaceCanvasLayout —{" "} + {t("shortcutsWhenBooleanKeys")}
  • sidebarMode (files, find, sessions,{" "} diff --git a/web/data/cmux-shortcuts.ts b/web/data/cmux-shortcuts.ts index 2e17cdf787c7..bdf026843488 100644 --- a/web/data/cmux-shortcuts.ts +++ b/web/data/cmux-shortcuts.ts @@ -290,7 +290,7 @@ export const shortcutCategories: ShortcutCategory[] = [ { id: "canvasOverview", combos: [["⌃", "⌘", "O"]], description: { en: "Toggle overview zoom", ja: "全体表示を切り替え" } }, { id: "canvasZoomIn", combos: [["⌥", "⌘", "="]], description: { en: "Zoom in", ja: "拡大" } }, { id: "canvasZoomOut", combos: [["⌥", "⌘", "-"]], description: { en: "Zoom out", ja: "縮小" } }, - { id: "canvasZoomReset", combos: [["⌥", "⌘", "0"]], description: { en: "Actual size", ja: "実寸表示" } }, + { id: "canvasZoomReset", combos: [["⌘", "0"]], description: { en: "Actual size", ja: "実寸表示" } }, { id: "canvasTidy", combos: [["⌃", "⌘", "T"]], description: { en: "Tidy panes into a grid", ja: "ペインをグリッドに整列" } }, ], }, diff --git a/web/data/cmux.schema.json b/web/data/cmux.schema.json index cfbe5d697038..4e640706037b 100644 --- a/web/data/cmux.schema.json +++ b/web/data/cmux.schema.json @@ -1269,6 +1269,21 @@ "equalizeSplits", "splitBrowserRight", "splitBrowserDown", + "toggleCanvasLayout", + "canvasRevealFocusedPane", + "canvasOverview", + "canvasZoomIn", + "canvasZoomOut", + "canvasZoomReset", + "canvasTidy", + "canvasAlignLeft", + "canvasAlignRight", + "canvasAlignTop", + "canvasAlignBottom", + "canvasEqualizeWidths", + "canvasEqualizeHeights", + "canvasDistributeHorizontally", + "canvasDistributeVertically", "toggleFileExplorer", "fileExplorerOpenSelection", "fileExplorerOpenSelectionFinderAlias", @@ -1333,7 +1348,7 @@ "when": { "type": "object", "default": {}, - "description": "Optional per-action context predicates (VS Code-style `when` clauses), keyed by cmux action id. Each value is a boolean expression over context keys combined with !, &&, ||, and parentheses. Boolean keys: sidebarFocus, browserFocus, markdownFocus, terminalFocus, commandPaletteVisible, terminalFindVisible. Typed keys support comparisons: the string sidebarMode (files, find, sessions, feed, or dock) and the integers paneCount and workspaceCount. Comparison operators are ==, !=, =~ (regex), <, <=, >, >=, and `in [a, b]`; an unknown or absent key reads as false. The boolean literals true and false are also accepted; `key == false` is the same as `!key`. The action's shortcut only fires (and only conflicts with other shortcuts) when the clause holds. Examples: { \"selectWorkspaceByNumber\": \"!sidebarFocus\" } selects workspaces with Ctrl+1–9 everywhere except when the right sidebar is focused; { \"selectSurfaceByNumber\": \"sidebarMode == 'find' && paneCount > 1\" } scopes a binding to the Find sidebar when the workspace has multiple panes.", + "description": "Optional per-action context predicates (VS Code-style `when` clauses), keyed by cmux action id. Each value is a boolean expression over context keys combined with !, &&, ||, and parentheses. Boolean keys: sidebarFocus, browserFocus, markdownFocus, terminalFocus, commandPaletteVisible, terminalFindVisible, workspaceCanvasLayout. Typed keys support comparisons: the string sidebarMode (files, find, sessions, feed, or dock) and the integers paneCount and workspaceCount. Comparison operators are ==, !=, =~ (regex), <, <=, >, >=, and `in [a, b]`; an unknown or absent key reads as false. The boolean literals true and false are also accepted; `key == false` is the same as `!key`. The action's shortcut only fires (and only conflicts with other shortcuts) when the clause holds. Examples: { \"selectWorkspaceByNumber\": \"!sidebarFocus\" } selects workspaces with Ctrl+1–9 everywhere except when the right sidebar is focused; { \"selectSurfaceByNumber\": \"sidebarMode == 'find' && paneCount > 1\" } scopes a binding to the Find sidebar when the workspace has multiple panes.", "descriptionKey": "schemaDescriptions.shortcuts.when", "additionalProperties": { "type": "string" diff --git a/web/messages/ar.json b/web/messages/ar.json index 407b9d8b57bc..7d8e66a1da8d 100644 --- a/web/messages/ar.json +++ b/web/messages/ar.json @@ -846,7 +846,7 @@ "defaultLayout": "Default layout for newly opened diff viewers. The toolbar layout toggle persists the user's last choice; an explicit `cmux diff --layout` option overrides both." }, "shortcuts": { - "when": "مسندات سياق اختيارية لكل إجراء (عبارات `when` على نمط VS Code)، مفهرسة بمعرّف إجراء cmux. كل قيمة هي تعبير منطقي فوق مفاتيح السياق، مدمجة باستخدام ! و && و || والأقواس. المفاتيح المنطقية: sidebarFocus و browserFocus و markdownFocus و terminalFocus و commandPaletteVisible و terminalFindVisible. تدعم المفاتيح المصنّفة المقارنات: السلسلة النصية sidebarMode (files أو find أو sessions أو feed أو dock) والعددان الصحيحان paneCount و workspaceCount. عوامل المقارنة هي == و != و =~ (تعبير نمطي) و < و <= و > و >= و `in [a, b]`؛ المفتاح غير المعروف أو الغائب يُقرأ على أنه false. تُقبل أيضًا القيمتان المنطقيتان الحرفيتان true و false؛ و`key == false` تكافئ `!key`. لا يعمل اختصار الإجراء (ولا يتعارض مع اختصارات أخرى) إلا عندما تكون العبارة صحيحة. أمثلة: { \"selectWorkspaceByNumber\": \"!sidebarFocus\" } يحدد مساحات العمل بواسطة Ctrl+1–9 في كل مكان ما عدا عندما يكون الشريط الجانبي الأيمن مركزًا؛ { \"selectSurfaceByNumber\": \"sidebarMode == 'find' && paneCount > 1\" } يقصر الارتباط على الشريط الجانبي للبحث عندما تحتوي مساحة العمل على عدة ألواح.", + "when": "مسندات سياق اختيارية لكل إجراء (عبارات `when` على نمط VS Code)، مفهرسة بمعرّف إجراء cmux. كل قيمة هي تعبير منطقي فوق مفاتيح السياق، مدمجة باستخدام ! و && و || والأقواس. المفاتيح المنطقية: sidebarFocus و browserFocus و markdownFocus و terminalFocus و commandPaletteVisible و terminalFindVisible و workspaceCanvasLayout. تدعم المفاتيح المصنّفة المقارنات: السلسلة النصية sidebarMode (files أو find أو sessions أو feed أو dock) والعددان الصحيحان paneCount و workspaceCount. عوامل المقارنة هي == و != و =~ (تعبير نمطي) و < و <= و > و >= و `in [a, b]`؛ المفتاح غير المعروف أو الغائب يُقرأ على أنه false. تُقبل أيضًا القيمتان المنطقيتان الحرفيتان true و false؛ و`key == false` تكافئ `!key`. لا يعمل اختصار الإجراء (ولا يتعارض مع اختصارات أخرى) إلا عندما تكون العبارة صحيحة. أمثلة: { \"selectWorkspaceByNumber\": \"!sidebarFocus\" } يحدد مساحات العمل بواسطة Ctrl+1–9 في كل مكان ما عدا عندما يكون الشريط الجانبي الأيمن مركزًا؛ { \"selectSurfaceByNumber\": \"sidebarMode == 'find' && paneCount > 1\" } يقصر الارتباط على الشريط الجانبي للبحث عندما تحتوي مساحة العمل على عدة ألواح.", "showModifierHoldHints": "إظهار شرائح تلميحات الاختصارات أثناء الضغط باستمرار على Cmd أو Control." } }, diff --git a/web/messages/bs.json b/web/messages/bs.json index 516953fae12a..80f4d1da3a67 100644 --- a/web/messages/bs.json +++ b/web/messages/bs.json @@ -846,7 +846,7 @@ "defaultLayout": "Default layout for newly opened diff viewers. The toolbar layout toggle persists the user's last choice; an explicit `cmux diff --layout` option overrides both." }, "shortcuts": { - "when": "Opcionalni kontekstni predikati po radnji (VS Code stil `when` klauzula), indeksirani cmux ID-jem radnje. Svaka vrijednost je Booleov izraz nad ključevima konteksta, kombinovan sa !, &&, || i zagradama. Booleovi ključevi: sidebarFocus, browserFocus, markdownFocus, terminalFocus, commandPaletteVisible, terminalFindVisible. Tipizirani ključevi podržavaju poređenja: string sidebarMode (files, find, sessions, feed ili dock) i cijeli brojevi paneCount i workspaceCount. Operatori poređenja su ==, !=, =~ (regularni izraz), <, <=, >, >= i `in [a, b]`; nepoznat ili odsutan ključ čita se kao false. Prihvataju se i Booleovi literali true i false; `key == false` je isto što i `!key`. Prečica radnje se pokreće (i sukobljava s drugim prečicama) samo kada je klauzula tačna. Primjeri: { \"selectWorkspaceByNumber\": \"!sidebarFocus\" } bira radne prostore pomoću Ctrl+1–9 svuda osim kada je fokusiran desni bočni panel; { \"selectSurfaceByNumber\": \"sidebarMode == 'find' && paneCount > 1\" } ograničava vezivanje na bočni panel za pretragu kada radni prostor ima više okana.", + "when": "Opcionalni kontekstni predikati po radnji (VS Code stil `when` klauzula), indeksirani cmux ID-jem radnje. Svaka vrijednost je Booleov izraz nad ključevima konteksta, kombinovan sa !, &&, || i zagradama. Booleovi ključevi: sidebarFocus, browserFocus, markdownFocus, terminalFocus, commandPaletteVisible, terminalFindVisible, workspaceCanvasLayout. Tipizirani ključevi podržavaju poređenja: string sidebarMode (files, find, sessions, feed ili dock) i cijeli brojevi paneCount i workspaceCount. Operatori poređenja su ==, !=, =~ (regularni izraz), <, <=, >, >= i `in [a, b]`; nepoznat ili odsutan ključ čita se kao false. Prihvataju se i Booleovi literali true i false; `key == false` je isto što i `!key`. Prečica radnje se pokreće (i sukobljava s drugim prečicama) samo kada je klauzula tačna. Primjeri: { \"selectWorkspaceByNumber\": \"!sidebarFocus\" } bira radne prostore pomoću Ctrl+1–9 svuda osim kada je fokusiran desni bočni panel; { \"selectSurfaceByNumber\": \"sidebarMode == 'find' && paneCount > 1\" } ograničava vezivanje na bočni panel za pretragu kada radni prostor ima više okana.", "showModifierHoldHints": "Prikaži oznake s prečicama dok držite Cmd ili Control." } }, diff --git a/web/messages/da.json b/web/messages/da.json index 06f13b0d8639..4276708a40eb 100644 --- a/web/messages/da.json +++ b/web/messages/da.json @@ -846,7 +846,7 @@ "defaultLayout": "Default layout for newly opened diff viewers. The toolbar layout toggle persists the user's last choice; an explicit `cmux diff --layout` option overrides both." }, "shortcuts": { - "when": "Valgfri kontekstprædikater pr. handling (VS Code-lignende `when`-klausuler), nøgleindekseret efter cmux-handlings-id. Hver værdi er et boolesk udtryk over kontekstnøgler, kombineret med !, &&, || og parenteser. Boolske nøgler: sidebarFocus, browserFocus, markdownFocus, terminalFocus, commandPaletteVisible, terminalFindVisible. Typede nøgler understøtter sammenligninger: strengen sidebarMode (files, find, sessions, feed eller dock) og heltallene paneCount og workspaceCount. Sammenligningsoperatorerne er ==, !=, =~ (regulært udtryk), <, <=, >, >= og `in [a, b]`; en ukendt eller manglende nøgle læses som false. De boolske litteraler true og false accepteres også; `key == false` svarer til `!key`. Handlingens genvej udløses kun (og konflikter kun med andre genveje), når klausulen er sand. Eksempler: { \"selectWorkspaceByNumber\": \"!sidebarFocus\" } vælger workspaces med Ctrl+1–9 overalt undtagen når højre sidebjælke har fokus; { \"selectSurfaceByNumber\": \"sidebarMode == 'find' && paneCount > 1\" } begrænser en binding til Find-sidebjælken, når workspacet har flere paneler.", + "when": "Valgfri kontekstprædikater pr. handling (VS Code-lignende `when`-klausuler), nøgleindekseret efter cmux-handlings-id. Hver værdi er et boolesk udtryk over kontekstnøgler, kombineret med !, &&, || og parenteser. Boolske nøgler: sidebarFocus, browserFocus, markdownFocus, terminalFocus, commandPaletteVisible, terminalFindVisible, workspaceCanvasLayout. Typede nøgler understøtter sammenligninger: strengen sidebarMode (files, find, sessions, feed eller dock) og heltallene paneCount og workspaceCount. Sammenligningsoperatorerne er ==, !=, =~ (regulært udtryk), <, <=, >, >= og `in [a, b]`; en ukendt eller manglende nøgle læses som false. De boolske litteraler true og false accepteres også; `key == false` svarer til `!key`. Handlingens genvej udløses kun (og konflikter kun med andre genveje), når klausulen er sand. Eksempler: { \"selectWorkspaceByNumber\": \"!sidebarFocus\" } vælger workspaces med Ctrl+1–9 overalt undtagen når højre sidebjælke har fokus; { \"selectSurfaceByNumber\": \"sidebarMode == 'find' && paneCount > 1\" } begrænser en binding til Find-sidebjælken, når workspacet har flere paneler.", "showModifierHoldHints": "Vis genvejstipchips, mens Cmd eller Control holdes nede." } }, diff --git a/web/messages/de.json b/web/messages/de.json index 95cc420229e3..868a4633728f 100644 --- a/web/messages/de.json +++ b/web/messages/de.json @@ -846,7 +846,7 @@ "defaultLayout": "Default layout for newly opened diff viewers. The toolbar layout toggle persists the user's last choice; an explicit `cmux diff --layout` option overrides both." }, "shortcuts": { - "when": "Optionale Kontextprädikate pro Aktion (VS-Code-artige `when`-Klauseln), nach cmux-Aktions-ID indiziert. Jeder Wert ist ein boolescher Ausdruck über Kontextschlüssel, kombiniert mit !, &&, || und Klammern. Boolesche Schlüssel: sidebarFocus, browserFocus, markdownFocus, terminalFocus, commandPaletteVisible, terminalFindVisible. Typisierte Schlüssel unterstützen Vergleiche: die Zeichenkette sidebarMode (files, find, sessions, feed oder dock) sowie die Ganzzahlen paneCount und workspaceCount. Vergleichsoperatoren sind ==, !=, =~ (regulärer Ausdruck), <, <=, >, >= und `in [a, b]`; ein unbekannter oder fehlender Schlüssel gilt als false. Die booleschen Literale true und false werden ebenfalls akzeptiert; `key == false` entspricht `!key`. Der Kurzbefehl der Aktion wird nur ausgelöst (und kollidiert nur mit anderen Kurzbefehlen), wenn die Klausel erfüllt ist. Beispiele: { \"selectWorkspaceByNumber\": \"!sidebarFocus\" } wählt Workspaces mit Ctrl+1–9 überall aus, außer wenn die rechte Seitenleiste fokussiert ist; { \"selectSurfaceByNumber\": \"sidebarMode == 'find' && paneCount > 1\" } beschränkt eine Belegung auf die Suchen-Seitenleiste, wenn der Workspace mehrere Panes hat.", + "when": "Optionale Kontextprädikate pro Aktion (VS-Code-artige `when`-Klauseln), nach cmux-Aktions-ID indiziert. Jeder Wert ist ein boolescher Ausdruck über Kontextschlüssel, kombiniert mit !, &&, || und Klammern. Boolesche Schlüssel: sidebarFocus, browserFocus, markdownFocus, terminalFocus, commandPaletteVisible, terminalFindVisible, workspaceCanvasLayout. Typisierte Schlüssel unterstützen Vergleiche: die Zeichenkette sidebarMode (files, find, sessions, feed oder dock) sowie die Ganzzahlen paneCount und workspaceCount. Vergleichsoperatoren sind ==, !=, =~ (regulärer Ausdruck), <, <=, >, >= und `in [a, b]`; ein unbekannter oder fehlender Schlüssel gilt als false. Die booleschen Literale true und false werden ebenfalls akzeptiert; `key == false` entspricht `!key`. Der Kurzbefehl der Aktion wird nur ausgelöst (und kollidiert nur mit anderen Kurzbefehlen), wenn die Klausel erfüllt ist. Beispiele: { \"selectWorkspaceByNumber\": \"!sidebarFocus\" } wählt Workspaces mit Ctrl+1–9 überall aus, außer wenn die rechte Seitenleiste fokussiert ist; { \"selectSurfaceByNumber\": \"sidebarMode == 'find' && paneCount > 1\" } beschränkt eine Belegung auf die Suchen-Seitenleiste, wenn der Workspace mehrere Panes hat.", "showModifierHoldHints": "Zeigt Kurzbefehl-Hinweis-Chips an, während Cmd oder Control gedrückt gehalten wird." } }, diff --git a/web/messages/en.json b/web/messages/en.json index 96008031ec02..ee18b60428d3 100644 --- a/web/messages/en.json +++ b/web/messages/en.json @@ -1038,7 +1038,7 @@ "defaultLayout": "Default layout for newly opened diff viewers. The toolbar layout toggle persists the user's last choice; an explicit `cmux diff --layout` option overrides both." }, "shortcuts": { - "when": "Optional per-action context predicates (VS Code-style `when` clauses), keyed by cmux action id. Each value is a boolean expression over context keys combined with !, &&, ||, and parentheses. Boolean keys: sidebarFocus, browserFocus, markdownFocus, terminalFocus, commandPaletteVisible, terminalFindVisible. Typed keys support comparisons: the string sidebarMode (files, find, sessions, feed, or dock) and the integers paneCount and workspaceCount. Comparison operators are ==, !=, =~ (regex), <, <=, >, >=, and `in [a, b]`; an unknown or absent key reads as false. The boolean literals true and false are also accepted; `key == false` is the same as `!key`. The action's shortcut only fires (and only conflicts with other shortcuts) when the clause holds. Examples: { \"selectWorkspaceByNumber\": \"!sidebarFocus\" } selects workspaces with Ctrl+1–9 everywhere except when the right sidebar is focused; { \"selectSurfaceByNumber\": \"sidebarMode == 'find' && paneCount > 1\" } scopes a binding to the Find sidebar when the workspace has multiple panes.", + "when": "Optional per-action context predicates (VS Code-style `when` clauses), keyed by cmux action id. Each value is a boolean expression over context keys combined with !, &&, ||, and parentheses. Boolean keys: sidebarFocus, browserFocus, markdownFocus, terminalFocus, commandPaletteVisible, terminalFindVisible, workspaceCanvasLayout. Typed keys support comparisons: the string sidebarMode (files, find, sessions, feed, or dock) and the integers paneCount and workspaceCount. Comparison operators are ==, !=, =~ (regex), <, <=, >, >=, and `in [a, b]`; an unknown or absent key reads as false. The boolean literals true and false are also accepted; `key == false` is the same as `!key`. The action's shortcut only fires (and only conflicts with other shortcuts) when the clause holds. Examples: { \"selectWorkspaceByNumber\": \"!sidebarFocus\" } selects workspaces with Ctrl+1–9 everywhere except when the right sidebar is focused; { \"selectSurfaceByNumber\": \"sidebarMode == 'find' && paneCount > 1\" } scopes a binding to the Find sidebar when the workspace has multiple panes.", "showModifierHoldHints": "Show shortcut-hint chips while holding Cmd or Control." } }, diff --git a/web/messages/es.json b/web/messages/es.json index 1dfe0eab6e41..2ed379e1c883 100644 --- a/web/messages/es.json +++ b/web/messages/es.json @@ -846,7 +846,7 @@ "defaultLayout": "Default layout for newly opened diff viewers. The toolbar layout toggle persists the user's last choice; an explicit `cmux diff --layout` option overrides both." }, "shortcuts": { - "when": "Predicados de contexto opcionales por acción (cláusulas `when` al estilo de VS Code), indexados por el id de acción de cmux. Cada valor es una expresión booleana sobre claves de contexto, combinada con !, &&, || y paréntesis. Claves booleanas: sidebarFocus, browserFocus, markdownFocus, terminalFocus, commandPaletteVisible, terminalFindVisible. Las claves tipadas admiten comparaciones: la cadena sidebarMode (files, find, sessions, feed o dock) y los enteros paneCount y workspaceCount. Los operadores de comparación son ==, !=, =~ (expresión regular), <, <=, >, >= e `in [a, b]`; una clave desconocida o ausente se evalúa como false. También se aceptan los literales booleanos true y false; `key == false` equivale a `!key`. El atajo de la acción solo se activa (y solo entra en conflicto con otros atajos) cuando la cláusula se cumple. Ejemplos: { \"selectWorkspaceByNumber\": \"!sidebarFocus\" } selecciona espacios de trabajo con Ctrl+1–9 en todas partes excepto cuando la barra lateral derecha tiene el foco; { \"selectSurfaceByNumber\": \"sidebarMode == 'find' && paneCount > 1\" } limita una asignación a la barra lateral de búsqueda cuando el espacio de trabajo tiene varios paneles.", + "when": "Predicados de contexto opcionales por acción (cláusulas `when` al estilo de VS Code), indexados por el id de acción de cmux. Cada valor es una expresión booleana sobre claves de contexto, combinada con !, &&, || y paréntesis. Claves booleanas: sidebarFocus, browserFocus, markdownFocus, terminalFocus, commandPaletteVisible, terminalFindVisible, workspaceCanvasLayout. Las claves tipadas admiten comparaciones: la cadena sidebarMode (files, find, sessions, feed o dock) y los enteros paneCount y workspaceCount. Los operadores de comparación son ==, !=, =~ (expresión regular), <, <=, >, >= e `in [a, b]`; una clave desconocida o ausente se evalúa como false. También se aceptan los literales booleanos true y false; `key == false` equivale a `!key`. El atajo de la acción solo se activa (y solo entra en conflicto con otros atajos) cuando la cláusula se cumple. Ejemplos: { \"selectWorkspaceByNumber\": \"!sidebarFocus\" } selecciona espacios de trabajo con Ctrl+1–9 en todas partes excepto cuando la barra lateral derecha tiene el foco; { \"selectSurfaceByNumber\": \"sidebarMode == 'find' && paneCount > 1\" } limita una asignación a la barra lateral de búsqueda cuando el espacio de trabajo tiene varios paneles.", "showModifierHoldHints": "Muestra chips de sugerencias de atajos mientras mantienes pulsado Cmd o Control." } }, diff --git a/web/messages/fr.json b/web/messages/fr.json index c15850c14ff3..0c89f8b9ba2c 100644 --- a/web/messages/fr.json +++ b/web/messages/fr.json @@ -846,7 +846,7 @@ "defaultLayout": "Default layout for newly opened diff viewers. The toolbar layout toggle persists the user's last choice; an explicit `cmux diff --layout` option overrides both." }, "shortcuts": { - "when": "Prédicats de contexte facultatifs par action (clauses `when` de style VS Code), indexés par identifiant d'action cmux. Chaque valeur est une expression booléenne sur des clés de contexte, combinée avec !, &&, || et des parenthèses. Clés booléennes : sidebarFocus, browserFocus, markdownFocus, terminalFocus, commandPaletteVisible, terminalFindVisible. Les clés typées acceptent des comparaisons : la chaîne sidebarMode (files, find, sessions, feed ou dock) et les entiers paneCount et workspaceCount. Les opérateurs de comparaison sont ==, !=, =~ (expression régulière), <, <=, >, >= et `in [a, b]` ; une clé inconnue ou absente vaut false. Les littéraux booléens true et false sont également acceptés ; `key == false` équivaut à `!key`. Le raccourci de l'action ne se déclenche (et n'entre en conflit avec d'autres raccourcis) que lorsque la clause est vraie. Exemples : { \"selectWorkspaceByNumber\": \"!sidebarFocus\" } sélectionne les espaces de travail avec Ctrl+1–9 partout sauf lorsque la barre latérale droite a le focus ; { \"selectSurfaceByNumber\": \"sidebarMode == 'find' && paneCount > 1\" } limite une liaison à la barre latérale Rechercher lorsque l'espace de travail comporte plusieurs panneaux.", + "when": "Prédicats de contexte facultatifs par action (clauses `when` de style VS Code), indexés par identifiant d'action cmux. Chaque valeur est une expression booléenne sur des clés de contexte, combinée avec !, &&, || et des parenthèses. Clés booléennes : sidebarFocus, browserFocus, markdownFocus, terminalFocus, commandPaletteVisible, terminalFindVisible, workspaceCanvasLayout. Les clés typées acceptent des comparaisons : la chaîne sidebarMode (files, find, sessions, feed ou dock) et les entiers paneCount et workspaceCount. Les opérateurs de comparaison sont ==, !=, =~ (expression régulière), <, <=, >, >= et `in [a, b]` ; une clé inconnue ou absente vaut false. Les littéraux booléens true et false sont également acceptés ; `key == false` équivaut à `!key`. Le raccourci de l'action ne se déclenche (et n'entre en conflit avec d'autres raccourcis) que lorsque la clause est vraie. Exemples : { \"selectWorkspaceByNumber\": \"!sidebarFocus\" } sélectionne les espaces de travail avec Ctrl+1–9 partout sauf lorsque la barre latérale droite a le focus ; { \"selectSurfaceByNumber\": \"sidebarMode == 'find' && paneCount > 1\" } limite une liaison à la barre latérale Rechercher lorsque l'espace de travail comporte plusieurs panneaux.", "showModifierHoldHints": "Affiche les pastilles d’indication de raccourcis pendant que Cmd ou Control est maintenu enfoncé." } }, diff --git a/web/messages/it.json b/web/messages/it.json index 7872e5dc3603..b84e4c5a0e16 100644 --- a/web/messages/it.json +++ b/web/messages/it.json @@ -846,7 +846,7 @@ "defaultLayout": "Default layout for newly opened diff viewers. The toolbar layout toggle persists the user's last choice; an explicit `cmux diff --layout` option overrides both." }, "shortcuts": { - "when": "Predicati di contesto opzionali per azione (clausole `when` in stile VS Code), indicizzati per id azione cmux. Ogni valore è un'espressione booleana sulle chiavi di contesto, combinata con !, &&, || e parentesi. Chiavi booleane: sidebarFocus, browserFocus, markdownFocus, terminalFocus, commandPaletteVisible, terminalFindVisible. Le chiavi tipizzate supportano i confronti: la stringa sidebarMode (files, find, sessions, feed o dock) e gli interi paneCount e workspaceCount. Gli operatori di confronto sono ==, !=, =~ (espressione regolare), <, <=, >, >= e `in [a, b]`; una chiave sconosciuta o assente vale false. Sono accettati anche i letterali booleani true e false; `key == false` equivale a `!key`. La scorciatoia dell'azione si attiva (ed entra in conflitto con altre scorciatoie) solo quando la clausola è vera. Esempi: { \"selectWorkspaceByNumber\": \"!sidebarFocus\" } seleziona gli spazi di lavoro con Ctrl+1–9 ovunque tranne quando la barra laterale destra ha il focus; { \"selectSurfaceByNumber\": \"sidebarMode == 'find' && paneCount > 1\" } limita un'associazione alla barra laterale Trova quando lo spazio di lavoro ha più riquadri.", + "when": "Predicati di contesto opzionali per azione (clausole `when` in stile VS Code), indicizzati per id azione cmux. Ogni valore è un'espressione booleana sulle chiavi di contesto, combinata con !, &&, || e parentesi. Chiavi booleane: sidebarFocus, browserFocus, markdownFocus, terminalFocus, commandPaletteVisible, terminalFindVisible, workspaceCanvasLayout. Le chiavi tipizzate supportano i confronti: la stringa sidebarMode (files, find, sessions, feed o dock) e gli interi paneCount e workspaceCount. Gli operatori di confronto sono ==, !=, =~ (espressione regolare), <, <=, >, >= e `in [a, b]`; una chiave sconosciuta o assente vale false. Sono accettati anche i letterali booleani true e false; `key == false` equivale a `!key`. La scorciatoia dell'azione si attiva (ed entra in conflitto con altre scorciatoie) solo quando la clausola è vera. Esempi: { \"selectWorkspaceByNumber\": \"!sidebarFocus\" } seleziona gli spazi di lavoro con Ctrl+1–9 ovunque tranne quando la barra laterale destra ha il focus; { \"selectSurfaceByNumber\": \"sidebarMode == 'find' && paneCount > 1\" } limita un'associazione alla barra laterale Trova quando lo spazio di lavoro ha più riquadri.", "showModifierHoldHints": "Mostra i chip dei suggerimenti delle scorciatoie mentre tieni premuto Cmd o Control." } }, diff --git a/web/messages/ja.json b/web/messages/ja.json index 0105e1d4458c..e3c154331ff5 100644 --- a/web/messages/ja.json +++ b/web/messages/ja.json @@ -997,7 +997,7 @@ "defaultLayout": "新しく開く diff ビューアの既定レイアウトです。ツールバーのレイアウト切り替えはユーザーの最後の選択を保持し、明示的な `cmux diff --layout` オプションはその両方を上書きします。" }, "shortcuts": { - "when": "アクションごとの任意のコンテキスト述語(VS Code スタイルの `when` 句)です。cmux のアクション ID をキーにします。各値は、コンテキストキーを !、&&、||、括弧で組み合わせた boolean 式です。boolean キー: sidebarFocus、browserFocus、markdownFocus、terminalFocus、commandPaletteVisible、terminalFindVisible。型付きキーは比較できます: 文字列 sidebarMode(files、find、sessions、feed、dock のいずれか)と整数 paneCount、workspaceCount です。比較演算子は ==、!=、=~(正規表現)、<、<=、>、>=、および `in [a, b]` です。未知または存在しないキーは false として扱われます。 boolean リテラル true および false も使えます。`key == false` は `!key` と同じです。その句が成立するときだけアクションのショートカットが発火し、他のショートカットとの競合もそのときだけ扱われます。例: { \"selectWorkspaceByNumber\": \"!sidebarFocus\" } は、右サイドバーにフォーカスがある場合を除いて Ctrl+1–9 でワークスペースを選択します。{ \"selectSurfaceByNumber\": \"sidebarMode == 'find' && paneCount > 1\" } は、ワークスペースに複数のペインがあるときに Find サイドバーへバインドを絞り込みます。", + "when": "アクションごとの任意のコンテキスト述語(VS Code スタイルの `when` 句)です。cmux のアクション ID をキーにします。各値は、コンテキストキーを !、&&、||、括弧で組み合わせた boolean 式です。boolean キー: sidebarFocus、browserFocus、markdownFocus、terminalFocus、commandPaletteVisible、terminalFindVisible、workspaceCanvasLayout。型付きキーは比較できます: 文字列 sidebarMode(files、find、sessions、feed、dock のいずれか)と整数 paneCount、workspaceCount です。比較演算子は ==、!=、=~(正規表現)、<、<=、>、>=、および `in [a, b]` です。未知または存在しないキーは false として扱われます。 boolean リテラル true および false も使えます。`key == false` は `!key` と同じです。その句が成立するときだけアクションのショートカットが発火し、他のショートカットとの競合もそのときだけ扱われます。例: { \"selectWorkspaceByNumber\": \"!sidebarFocus\" } は、右サイドバーにフォーカスがある場合を除いて Ctrl+1–9 でワークスペースを選択します。{ \"selectSurfaceByNumber\": \"sidebarMode == 'find' && paneCount > 1\" } は、ワークスペースに複数のペインがあるときに Find サイドバーへバインドを絞り込みます。", "showModifierHoldHints": "Cmd または Control を押し続けている間、ショートカットヒントのチップを表示します。" } }, diff --git a/web/messages/km.json b/web/messages/km.json index eadf811462b9..de997ade0b72 100644 --- a/web/messages/km.json +++ b/web/messages/km.json @@ -846,7 +846,7 @@ "defaultLayout": "Default layout for newly opened diff viewers. The toolbar layout toggle persists the user's last choice; an explicit `cmux diff --layout` option overrides both." }, "shortcuts": { - "when": "លក្ខខណ្ឌបរិបទជាជម្រើសសម្រាប់សកម្មភាពនីមួយៗ (ឃ្លា `when` រចនាប័ទ្ម VS Code) ដែលដាក់សន្ទស្សន៍តាមលេខសម្គាល់សកម្មភាព cmux។ តម្លៃនីមួយៗគឺជាកន្សោម boolean លើគន្លឹះបរិបទ ដែលផ្សំជាមួយ !, &&, || និងវង់ក្រចក។ គន្លឹះ boolean: sidebarFocus, browserFocus, markdownFocus, terminalFocus, commandPaletteVisible, terminalFindVisible។ គន្លឹះមានប្រភេទគាំទ្រការប្រៀបធៀប: ខ្សែអក្សរ sidebarMode (files, find, sessions, feed ឬ dock) និងចំនួនគត់ paneCount និង workspaceCount។ សញ្ញាប្រៀបធៀបគឺ ==, !=, =~ (regex), <, <=, >, >= និង `in [a, b]`; គន្លឹះមិនស្គាល់ ឬមិនមាននឹងត្រូវអានជា false។ អក្សរ boolean true និង false ក៏អាចប្រើបានដែរ; `key == false` ដូចគ្នានឹង `!key`។ ផ្លូវកាត់របស់សកម្មភាពនឹងដំណើរការ (និងទាស់ជាមួយផ្លូវកាត់ផ្សេងទៀត) តែពេលឃ្លានេះពិតប៉ុណ្ណោះ។ ឧទាហរណ៍: { \"selectWorkspaceByNumber\": \"!sidebarFocus\" } ជ្រើសរើស workspace ដោយ Ctrl+1–9 នៅគ្រប់ទីកន្លែង លើកលែងពេលរបារចំហៀងស្តាំមាន focus; { \"selectSurfaceByNumber\": \"sidebarMode == 'find' && paneCount > 1\" } កំណត់ការចងទៅរបារចំហៀង Find ពេល workspace មានផ្ទាំងច្រើន។", + "when": "លក្ខខណ្ឌបរិបទជាជម្រើសសម្រាប់សកម្មភាពនីមួយៗ (ឃ្លា `when` រចនាប័ទ្ម VS Code) ដែលដាក់សន្ទស្សន៍តាមលេខសម្គាល់សកម្មភាព cmux។ តម្លៃនីមួយៗគឺជាកន្សោម boolean លើគន្លឹះបរិបទ ដែលផ្សំជាមួយ !, &&, || និងវង់ក្រចក។ គន្លឹះ boolean: sidebarFocus, browserFocus, markdownFocus, terminalFocus, commandPaletteVisible, terminalFindVisible, workspaceCanvasLayout។ គន្លឹះមានប្រភេទគាំទ្រការប្រៀបធៀប: ខ្សែអក្សរ sidebarMode (files, find, sessions, feed ឬ dock) និងចំនួនគត់ paneCount និង workspaceCount។ សញ្ញាប្រៀបធៀបគឺ ==, !=, =~ (regex), <, <=, >, >= និង `in [a, b]`; គន្លឹះមិនស្គាល់ ឬមិនមាននឹងត្រូវអានជា false។ អក្សរ boolean true និង false ក៏អាចប្រើបានដែរ; `key == false` ដូចគ្នានឹង `!key`។ ផ្លូវកាត់របស់សកម្មភាពនឹងដំណើរការ (និងទាស់ជាមួយផ្លូវកាត់ផ្សេងទៀត) តែពេលឃ្លានេះពិតប៉ុណ្ណោះ។ ឧទាហរណ៍: { \"selectWorkspaceByNumber\": \"!sidebarFocus\" } ជ្រើសរើស workspace ដោយ Ctrl+1–9 នៅគ្រប់ទីកន្លែង លើកលែងពេលរបារចំហៀងស្តាំមាន focus; { \"selectSurfaceByNumber\": \"sidebarMode == 'find' && paneCount > 1\" } កំណត់ការចងទៅរបារចំហៀង Find ពេល workspace មានផ្ទាំងច្រើន។", "showModifierHoldHints": "បង្ហាញបន្ទះណែនាំផ្លូវកាត់ ខណៈពេលចុច Cmd ឬ Control ជាប់។" } }, diff --git a/web/messages/ko.json b/web/messages/ko.json index 4d10f77a6b92..6909d3636a59 100644 --- a/web/messages/ko.json +++ b/web/messages/ko.json @@ -846,7 +846,7 @@ "defaultLayout": "Default layout for newly opened diff viewers. The toolbar layout toggle persists the user's last choice; an explicit `cmux diff --layout` option overrides both." }, "shortcuts": { - "when": "액션별 선택적 컨텍스트 조건자(VS Code 스타일 `when` 절)이며 cmux 액션 ID를 키로 사용합니다. 각 값은 컨텍스트 키를 !, &&, || 및 괄호로 조합한 불리언 표현식입니다. 불리언 키: sidebarFocus, browserFocus, markdownFocus, terminalFocus, commandPaletteVisible, terminalFindVisible. 타입이 있는 키는 비교를 지원합니다: 문자열 sidebarMode(files, find, sessions, feed, dock 중 하나)와 정수 paneCount, workspaceCount입니다. 비교 연산자는 ==, !=, =~(정규식), <, <=, >, >=, `in [a, b]`이며, 알 수 없거나 존재하지 않는 키는 false로 평가됩니다. 불리언 리터럴 true와 false도 사용할 수 있으며, `key == false`는 `!key`와 같습니다. 액션의 단축키는 이 절이 참일 때만 실행되며, 다른 단축키와의 충돌도 그때만 발생합니다. 예: { \"selectWorkspaceByNumber\": \"!sidebarFocus\" }는 오른쪽 사이드바에 포커스가 있을 때를 제외한 모든 곳에서 Ctrl+1–9로 워크스페이스를 선택합니다. { \"selectSurfaceByNumber\": \"sidebarMode == 'find' && paneCount > 1\" }는 워크스페이스에 패널이 여러 개 있을 때 바인딩을 찾기 사이드바로 한정합니다.", + "when": "액션별 선택적 컨텍스트 조건자(VS Code 스타일 `when` 절)이며 cmux 액션 ID를 키로 사용합니다. 각 값은 컨텍스트 키를 !, &&, || 및 괄호로 조합한 불리언 표현식입니다. 불리언 키: sidebarFocus, browserFocus, markdownFocus, terminalFocus, commandPaletteVisible, terminalFindVisible, workspaceCanvasLayout. 타입이 있는 키는 비교를 지원합니다: 문자열 sidebarMode(files, find, sessions, feed, dock 중 하나)와 정수 paneCount, workspaceCount입니다. 비교 연산자는 ==, !=, =~(정규식), <, <=, >, >=, `in [a, b]`이며, 알 수 없거나 존재하지 않는 키는 false로 평가됩니다. 불리언 리터럴 true와 false도 사용할 수 있으며, `key == false`는 `!key`와 같습니다. 액션의 단축키는 이 절이 참일 때만 실행되며, 다른 단축키와의 충돌도 그때만 발생합니다. 예: { \"selectWorkspaceByNumber\": \"!sidebarFocus\" }는 오른쪽 사이드바에 포커스가 있을 때를 제외한 모든 곳에서 Ctrl+1–9로 워크스페이스를 선택합니다. { \"selectSurfaceByNumber\": \"sidebarMode == 'find' && paneCount > 1\" }는 워크스페이스에 패널이 여러 개 있을 때 바인딩을 찾기 사이드바로 한정합니다.", "showModifierHoldHints": "Cmd 또는 Control을 누르고 있는 동안 단축키 힌트 칩을 표시합니다." } }, diff --git a/web/messages/no.json b/web/messages/no.json index 0b4e6ab2aea2..0a24be3fc853 100644 --- a/web/messages/no.json +++ b/web/messages/no.json @@ -846,7 +846,7 @@ "defaultLayout": "Default layout for newly opened diff viewers. The toolbar layout toggle persists the user's last choice; an explicit `cmux diff --layout` option overrides both." }, "shortcuts": { - "when": "Valgfrie kontekstpredikater per handling (VS Code-lignende `when`-klausuler), indeksert etter cmux-handlings-id. Hver verdi er et boolsk uttrykk over kontekstnøkler, kombinert med !, &&, || og parenteser. Boolske nøkler: sidebarFocus, browserFocus, markdownFocus, terminalFocus, commandPaletteVisible, terminalFindVisible. Typede nøkler støtter sammenligninger: strengen sidebarMode (files, find, sessions, feed eller dock) og heltallene paneCount og workspaceCount. Sammenligningsoperatorene er ==, !=, =~ (regulært uttrykk), <, <=, >, >= og `in [a, b]`; en ukjent eller fraværende nøkkel leses som false. De boolske literalene true og false godtas også; `key == false` tilsvarer `!key`. Handlingens snarvei utløses bare (og kolliderer bare med andre snarveier) når klausulen er sann. Eksempler: { \"selectWorkspaceByNumber\": \"!sidebarFocus\" } velger arbeidsområder med Ctrl+1–9 overalt unntatt når høyre sidelinje har fokus; { \"selectSurfaceByNumber\": \"sidebarMode == 'find' && paneCount > 1\" } begrenser en binding til Finn-sidelinjen når arbeidsområdet har flere paneler.", + "when": "Valgfrie kontekstpredikater per handling (VS Code-lignende `when`-klausuler), indeksert etter cmux-handlings-id. Hver verdi er et boolsk uttrykk over kontekstnøkler, kombinert med !, &&, || og parenteser. Boolske nøkler: sidebarFocus, browserFocus, markdownFocus, terminalFocus, commandPaletteVisible, terminalFindVisible, workspaceCanvasLayout. Typede nøkler støtter sammenligninger: strengen sidebarMode (files, find, sessions, feed eller dock) og heltallene paneCount og workspaceCount. Sammenligningsoperatorene er ==, !=, =~ (regulært uttrykk), <, <=, >, >= og `in [a, b]`; en ukjent eller fraværende nøkkel leses som false. De boolske literalene true og false godtas også; `key == false` tilsvarer `!key`. Handlingens snarvei utløses bare (og kolliderer bare med andre snarveier) når klausulen er sann. Eksempler: { \"selectWorkspaceByNumber\": \"!sidebarFocus\" } velger arbeidsområder med Ctrl+1–9 overalt unntatt når høyre sidelinje har fokus; { \"selectSurfaceByNumber\": \"sidebarMode == 'find' && paneCount > 1\" } begrenser en binding til Finn-sidelinjen når arbeidsområdet har flere paneler.", "showModifierHoldHints": "Vis snarveishint-brikker mens Cmd eller Control holdes nede." } }, diff --git a/web/messages/pl.json b/web/messages/pl.json index 8d9e7db7c90a..95babd0704a9 100644 --- a/web/messages/pl.json +++ b/web/messages/pl.json @@ -846,7 +846,7 @@ "defaultLayout": "Default layout for newly opened diff viewers. The toolbar layout toggle persists the user's last choice; an explicit `cmux diff --layout` option overrides both." }, "shortcuts": { - "when": "Opcjonalne predykaty kontekstu dla poszczególnych akcji (klauzule `when` w stylu VS Code), indeksowane według identyfikatora akcji cmux. Każda wartość jest wyrażeniem logicznym nad kluczami kontekstu, łączonym za pomocą !, &&, || oraz nawiasów. Klucze logiczne: sidebarFocus, browserFocus, markdownFocus, terminalFocus, commandPaletteVisible, terminalFindVisible. Klucze typowane obsługują porównania: łańcuch sidebarMode (files, find, sessions, feed lub dock) oraz liczby całkowite paneCount i workspaceCount. Operatory porównania to ==, !=, =~ (wyrażenie regularne), <, <=, >, >= oraz `in [a, b]`; nieznany lub nieobecny klucz jest traktowany jako false. Akceptowane są też literały logiczne true i false; `key == false` jest równoważne `!key`. Skrót akcji uruchamia się (i koliduje z innymi skrótami) tylko wtedy, gdy klauzula jest spełniona. Przykłady: { \"selectWorkspaceByNumber\": \"!sidebarFocus\" } wybiera obszary robocze za pomocą Ctrl+1–9 wszędzie poza sytuacją, gdy fokus ma prawy pasek boczny; { \"selectSurfaceByNumber\": \"sidebarMode == 'find' && paneCount > 1\" } ogranicza przypisanie do paska bocznego wyszukiwania, gdy obszar roboczy ma wiele paneli.", + "when": "Opcjonalne predykaty kontekstu dla poszczególnych akcji (klauzule `when` w stylu VS Code), indeksowane według identyfikatora akcji cmux. Każda wartość jest wyrażeniem logicznym nad kluczami kontekstu, łączonym za pomocą !, &&, || oraz nawiasów. Klucze logiczne: sidebarFocus, browserFocus, markdownFocus, terminalFocus, commandPaletteVisible, terminalFindVisible, workspaceCanvasLayout. Klucze typowane obsługują porównania: łańcuch sidebarMode (files, find, sessions, feed lub dock) oraz liczby całkowite paneCount i workspaceCount. Operatory porównania to ==, !=, =~ (wyrażenie regularne), <, <=, >, >= oraz `in [a, b]`; nieznany lub nieobecny klucz jest traktowany jako false. Akceptowane są też literały logiczne true i false; `key == false` jest równoważne `!key`. Skrót akcji uruchamia się (i koliduje z innymi skrótami) tylko wtedy, gdy klauzula jest spełniona. Przykłady: { \"selectWorkspaceByNumber\": \"!sidebarFocus\" } wybiera obszary robocze za pomocą Ctrl+1–9 wszędzie poza sytuacją, gdy fokus ma prawy pasek boczny; { \"selectSurfaceByNumber\": \"sidebarMode == 'find' && paneCount > 1\" } ogranicza przypisanie do paska bocznego wyszukiwania, gdy obszar roboczy ma wiele paneli.", "showModifierHoldHints": "Pokazuj etykiety podpowiedzi skrótów podczas przytrzymywania Cmd lub Control." } }, diff --git a/web/messages/pt-BR.json b/web/messages/pt-BR.json index 11c7675abfab..e915b116ecfb 100644 --- a/web/messages/pt-BR.json +++ b/web/messages/pt-BR.json @@ -846,7 +846,7 @@ "defaultLayout": "Default layout for newly opened diff viewers. The toolbar layout toggle persists the user's last choice; an explicit `cmux diff --layout` option overrides both." }, "shortcuts": { - "when": "Predicados de contexto opcionais por ação (cláusulas `when` no estilo do VS Code), indexados pelo id da ação do cmux. Cada valor é uma expressão booleana sobre chaves de contexto, combinada com !, &&, || e parênteses. Chaves booleanas: sidebarFocus, browserFocus, markdownFocus, terminalFocus, commandPaletteVisible, terminalFindVisible. Chaves tipadas aceitam comparações: a string sidebarMode (files, find, sessions, feed ou dock) e os inteiros paneCount e workspaceCount. Os operadores de comparação são ==, !=, =~ (expressão regular), <, <=, >, >= e `in [a, b]`; uma chave desconhecida ou ausente é avaliada como false. Os literais booleanos true e false também são aceitos; `key == false` equivale a `!key`. O atalho da ação só dispara (e só conflita com outros atalhos) quando a cláusula é verdadeira. Exemplos: { \"selectWorkspaceByNumber\": \"!sidebarFocus\" } seleciona workspaces com Ctrl+1–9 em todos os lugares, exceto quando a barra lateral direita está em foco; { \"selectSurfaceByNumber\": \"sidebarMode == 'find' && paneCount > 1\" } restringe uma atribuição à barra lateral Localizar quando o workspace tem vários painéis.", + "when": "Predicados de contexto opcionais por ação (cláusulas `when` no estilo do VS Code), indexados pelo id da ação do cmux. Cada valor é uma expressão booleana sobre chaves de contexto, combinada com !, &&, || e parênteses. Chaves booleanas: sidebarFocus, browserFocus, markdownFocus, terminalFocus, commandPaletteVisible, terminalFindVisible, workspaceCanvasLayout. Chaves tipadas aceitam comparações: a string sidebarMode (files, find, sessions, feed ou dock) e os inteiros paneCount e workspaceCount. Os operadores de comparação são ==, !=, =~ (expressão regular), <, <=, >, >= e `in [a, b]`; uma chave desconhecida ou ausente é avaliada como false. Os literais booleanos true e false também são aceitos; `key == false` equivale a `!key`. O atalho da ação só dispara (e só conflita com outros atalhos) quando a cláusula é verdadeira. Exemplos: { \"selectWorkspaceByNumber\": \"!sidebarFocus\" } seleciona workspaces com Ctrl+1–9 em todos os lugares, exceto quando a barra lateral direita está em foco; { \"selectSurfaceByNumber\": \"sidebarMode == 'find' && paneCount > 1\" } restringe uma atribuição à barra lateral Localizar quando o workspace tem vários painéis.", "showModifierHoldHints": "Mostra chips de dica de atalhos enquanto Cmd ou Control estiver pressionado." } }, diff --git a/web/messages/ru.json b/web/messages/ru.json index 6403de9e83dd..8f7ae5248b4c 100644 --- a/web/messages/ru.json +++ b/web/messages/ru.json @@ -846,7 +846,7 @@ "defaultLayout": "Default layout for newly opened diff viewers. The toolbar layout toggle persists the user's last choice; an explicit `cmux diff --layout` option overrides both." }, "shortcuts": { - "when": "Необязательные контекстные предикаты для каждой операции (условия `when` в стиле VS Code), заданные по идентификатору операции cmux. Каждое значение — булево выражение над ключами контекста, объединенными с помощью !, &&, || и скобок. Булевы ключи: sidebarFocus, browserFocus, markdownFocus, terminalFocus, commandPaletteVisible, terminalFindVisible. Типизированные ключи поддерживают сравнения: строка sidebarMode (files, find, sessions, feed или dock) и целые числа paneCount и workspaceCount. Операторы сравнения: ==, !=, =~ (регулярное выражение), <, <=, >, >= и `in [a, b]`; неизвестный или отсутствующий ключ считается false. Также принимаются булевы литералы true и false; `key == false` эквивалентно `!key`. Сочетание клавиш операции срабатывает (и конфликтует с другими сочетаниями) только когда условие истинно. Примеры: { \"selectWorkspaceByNumber\": \"!sidebarFocus\" } выбирает рабочие области с помощью Ctrl+1–9 везде, кроме случаев, когда в фокусе правая боковая панель; { \"selectSurfaceByNumber\": \"sidebarMode == 'find' && paneCount > 1\" } ограничивает привязку боковой панелью поиска, когда в рабочей области несколько панелей.", + "when": "Необязательные контекстные предикаты для каждой операции (условия `when` в стиле VS Code), заданные по идентификатору операции cmux. Каждое значение — булево выражение над ключами контекста, объединенными с помощью !, &&, || и скобок. Булевы ключи: sidebarFocus, browserFocus, markdownFocus, terminalFocus, commandPaletteVisible, terminalFindVisible, workspaceCanvasLayout. Типизированные ключи поддерживают сравнения: строка sidebarMode (files, find, sessions, feed или dock) и целые числа paneCount и workspaceCount. Операторы сравнения: ==, !=, =~ (регулярное выражение), <, <=, >, >= и `in [a, b]`; неизвестный или отсутствующий ключ считается false. Также принимаются булевы литералы true и false; `key == false` эквивалентно `!key`. Сочетание клавиш операции срабатывает (и конфликтует с другими сочетаниями) только когда условие истинно. Примеры: { \"selectWorkspaceByNumber\": \"!sidebarFocus\" } выбирает рабочие области с помощью Ctrl+1–9 везде, кроме случаев, когда в фокусе правая боковая панель; { \"selectSurfaceByNumber\": \"sidebarMode == 'find' && paneCount > 1\" } ограничивает привязку боковой панелью поиска, когда в рабочей области несколько панелей.", "showModifierHoldHints": "Показывать подсказки сочетаний клавиш, пока удерживается Cmd или Control." } }, diff --git a/web/messages/th.json b/web/messages/th.json index 0929acd2a958..07bd4a663cc1 100644 --- a/web/messages/th.json +++ b/web/messages/th.json @@ -846,7 +846,7 @@ "defaultLayout": "Default layout for newly opened diff viewers. The toolbar layout toggle persists the user's last choice; an explicit `cmux diff --layout` option overrides both." }, "shortcuts": { - "when": "เพรดิเคตบริบทแบบไม่บังคับสำหรับแต่ละแอ็กชัน (คลอส `when` แบบ VS Code) โดยใช้ id แอ็กชันของ cmux เป็นคีย์ แต่ละค่าเป็นนิพจน์บูลีนเหนือคีย์บริบท รวมด้วย !, &&, || และวงเล็บ คีย์บูลีน: sidebarFocus, browserFocus, markdownFocus, terminalFocus, commandPaletteVisible, terminalFindVisible คีย์แบบมีชนิดรองรับการเปรียบเทียบ: สตริง sidebarMode (files, find, sessions, feed หรือ dock) และจำนวนเต็ม paneCount กับ workspaceCount ตัวดำเนินการเปรียบเทียบคือ ==, !=, =~ (นิพจน์ปรกติ), <, <=, >, >= และ `in [a, b]` คีย์ที่ไม่รู้จักหรือไม่มีอยู่จะถูกอ่านเป็น false ยอมรับลิเทอรัลบูลีน true และ false ด้วย โดย `key == false` มีความหมายเดียวกับ `!key` ทางลัดของแอ็กชันจะทำงาน (และจะชนกับทางลัดอื่น) เฉพาะเมื่อคลอสเป็นจริงเท่านั้น ตัวอย่าง: { \"selectWorkspaceByNumber\": \"!sidebarFocus\" } เลือกเวิร์กสเปซด้วย Ctrl+1–9 ได้ทุกที่ยกเว้นเมื่อแถบด้านข้างขวาอยู่ในโฟกัส; { \"selectSurfaceByNumber\": \"sidebarMode == 'find' && paneCount > 1\" } จำกัดการผูกไว้กับแถบด้านข้างค้นหาเมื่อเวิร์กสเปซมีหลายเพน", + "when": "เพรดิเคตบริบทแบบไม่บังคับสำหรับแต่ละแอ็กชัน (คลอส `when` แบบ VS Code) โดยใช้ id แอ็กชันของ cmux เป็นคีย์ แต่ละค่าเป็นนิพจน์บูลีนเหนือคีย์บริบท รวมด้วย !, &&, || และวงเล็บ คีย์บูลีน: sidebarFocus, browserFocus, markdownFocus, terminalFocus, commandPaletteVisible, terminalFindVisible, workspaceCanvasLayout คีย์แบบมีชนิดรองรับการเปรียบเทียบ: สตริง sidebarMode (files, find, sessions, feed หรือ dock) และจำนวนเต็ม paneCount กับ workspaceCount ตัวดำเนินการเปรียบเทียบคือ ==, !=, =~ (นิพจน์ปรกติ), <, <=, >, >= และ `in [a, b]` คีย์ที่ไม่รู้จักหรือไม่มีอยู่จะถูกอ่านเป็น false ยอมรับลิเทอรัลบูลีน true และ false ด้วย โดย `key == false` มีความหมายเดียวกับ `!key` ทางลัดของแอ็กชันจะทำงาน (และจะชนกับทางลัดอื่น) เฉพาะเมื่อคลอสเป็นจริงเท่านั้น ตัวอย่าง: { \"selectWorkspaceByNumber\": \"!sidebarFocus\" } เลือกเวิร์กสเปซด้วย Ctrl+1–9 ได้ทุกที่ยกเว้นเมื่อแถบด้านข้างขวาอยู่ในโฟกัส; { \"selectSurfaceByNumber\": \"sidebarMode == 'find' && paneCount > 1\" } จำกัดการผูกไว้กับแถบด้านข้างค้นหาเมื่อเวิร์กสเปซมีหลายเพน", "showModifierHoldHints": "แสดงชิปคำใบ้ปุ่มลัดขณะกด Cmd หรือ Control ค้างไว้" } }, diff --git a/web/messages/tr.json b/web/messages/tr.json index e2fcd2b5fb3a..b7357c971d9c 100644 --- a/web/messages/tr.json +++ b/web/messages/tr.json @@ -846,7 +846,7 @@ "defaultLayout": "Default layout for newly opened diff viewers. The toolbar layout toggle persists the user's last choice; an explicit `cmux diff --layout` option overrides both." }, "shortcuts": { - "when": "Her eylem için isteğe bağlı bağlam koşulları (VS Code tarzı `when` koşulları), cmux eylem id'sine göre anahtarlanır. Her değer, bağlam anahtarları üzerinde !, &&, || ve parantezlerle birleştirilen bir boolean ifadesidir. Boolean anahtarlar: sidebarFocus, browserFocus, markdownFocus, terminalFocus, commandPaletteVisible, terminalFindVisible. Türlü anahtarlar karşılaştırmaları destekler: sidebarMode dizesi (files, find, sessions, feed veya dock) ile paneCount ve workspaceCount tamsayıları. Karşılaştırma işleçleri ==, !=, =~ (düzenli ifade), <, <=, >, >= ve `in [a, b]` şeklindedir; bilinmeyen veya bulunmayan bir anahtar false kabul edilir. true ve false boolean sabitleri de kabul edilir; `key == false`, `!key` ile aynıdır. Eylemin kısayolu yalnızca koşul doğru olduğunda çalışır (ve diğer kısayollarla yalnızca o durumda çakışır). Örnekler: { \"selectWorkspaceByNumber\": \"!sidebarFocus\" } sağ kenar çubuğu odaktayken hariç her yerde Ctrl+1–9 ile çalışma alanlarını seçer; { \"selectSurfaceByNumber\": \"sidebarMode == 'find' && paneCount > 1\" } çalışma alanında birden çok bölme varken bir atamayı Bul kenar çubuğuyla sınırlar.", + "when": "Her eylem için isteğe bağlı bağlam koşulları (VS Code tarzı `when` koşulları), cmux eylem id'sine göre anahtarlanır. Her değer, bağlam anahtarları üzerinde !, &&, || ve parantezlerle birleştirilen bir boolean ifadesidir. Boolean anahtarlar: sidebarFocus, browserFocus, markdownFocus, terminalFocus, commandPaletteVisible, terminalFindVisible, workspaceCanvasLayout. Türlü anahtarlar karşılaştırmaları destekler: sidebarMode dizesi (files, find, sessions, feed veya dock) ile paneCount ve workspaceCount tamsayıları. Karşılaştırma işleçleri ==, !=, =~ (düzenli ifade), <, <=, >, >= ve `in [a, b]` şeklindedir; bilinmeyen veya bulunmayan bir anahtar false kabul edilir. true ve false boolean sabitleri de kabul edilir; `key == false`, `!key` ile aynıdır. Eylemin kısayolu yalnızca koşul doğru olduğunda çalışır (ve diğer kısayollarla yalnızca o durumda çakışır). Örnekler: { \"selectWorkspaceByNumber\": \"!sidebarFocus\" } sağ kenar çubuğu odaktayken hariç her yerde Ctrl+1–9 ile çalışma alanlarını seçer; { \"selectSurfaceByNumber\": \"sidebarMode == 'find' && paneCount > 1\" } çalışma alanında birden çok bölme varken bir atamayı Bul kenar çubuğuyla sınırlar.", "showModifierHoldHints": "Cmd veya Control basılı tutulurken kısayol ipucu rozetlerini göster." } }, diff --git a/web/messages/uk.json b/web/messages/uk.json index 9a12cbbb7e10..95a4f83118c2 100644 --- a/web/messages/uk.json +++ b/web/messages/uk.json @@ -846,7 +846,7 @@ "defaultLayout": "Default layout for newly opened diff viewers. The toolbar layout toggle persists the user's last choice; an explicit `cmux diff --layout` option overrides both." }, "shortcuts": { - "when": "Необов'язкові контекстні предикати для кожної дії (клаузи `when` у стилі VS Code), індексовані за ідентифікатором дії cmux. Кожне значення — булевий вираз над ключами контексту, поєднаними за допомогою !, &&, || та дужок. Булеві ключі: sidebarFocus, browserFocus, markdownFocus, terminalFocus, commandPaletteVisible, terminalFindVisible. Типізовані ключі підтримують порівняння: рядок sidebarMode (files, find, sessions, feed або dock) і цілі числа paneCount та workspaceCount. Оператори порівняння: ==, !=, =~ (регулярний вираз), <, <=, >, >= та `in [a, b]`; невідомий або відсутній ключ вважається false. Також приймаються булеві літерали true і false; `key == false` еквівалентно `!key`. Скорочення дії спрацьовує (і конфліктує з іншими скороченнями) лише тоді, коли клауза істинна. Приклади: { \"selectWorkspaceByNumber\": \"!sidebarFocus\" } вибирає робочі простори за допомогою Ctrl+1–9 всюди, крім випадків, коли у фокусі права бічна панель; { \"selectSurfaceByNumber\": \"sidebarMode == 'find' && paneCount > 1\" } обмежує прив'язку бічною панеллю пошуку, коли в робочому просторі кілька панелей.", + "when": "Необов'язкові контекстні предикати для кожної дії (клаузи `when` у стилі VS Code), індексовані за ідентифікатором дії cmux. Кожне значення — булевий вираз над ключами контексту, поєднаними за допомогою !, &&, || та дужок. Булеві ключі: sidebarFocus, browserFocus, markdownFocus, terminalFocus, commandPaletteVisible, terminalFindVisible, workspaceCanvasLayout. Типізовані ключі підтримують порівняння: рядок sidebarMode (files, find, sessions, feed або dock) і цілі числа paneCount та workspaceCount. Оператори порівняння: ==, !=, =~ (регулярний вираз), <, <=, >, >= та `in [a, b]`; невідомий або відсутній ключ вважається false. Також приймаються булеві літерали true і false; `key == false` еквівалентно `!key`. Скорочення дії спрацьовує (і конфліктує з іншими скороченнями) лише тоді, коли клауза істинна. Приклади: { \"selectWorkspaceByNumber\": \"!sidebarFocus\" } вибирає робочі простори за допомогою Ctrl+1–9 всюди, крім випадків, коли у фокусі права бічна панель; { \"selectSurfaceByNumber\": \"sidebarMode == 'find' && paneCount > 1\" } обмежує прив'язку бічною панеллю пошуку, коли в робочому просторі кілька панелей.", "showModifierHoldHints": "Показувати підказки комбінацій клавіш, доки утримується Cmd або Control." } }, diff --git a/web/messages/zh-CN.json b/web/messages/zh-CN.json index e0c1511236d5..b9856c2c2f30 100644 --- a/web/messages/zh-CN.json +++ b/web/messages/zh-CN.json @@ -846,7 +846,7 @@ "defaultLayout": "Default layout for newly opened diff viewers. The toolbar layout toggle persists the user's last choice; an explicit `cmux diff --layout` option overrides both." }, "shortcuts": { - "when": "每个操作可选的上下文谓词(VS Code 风格的 `when` 子句),按 cmux 操作 id 索引。每个值都是基于上下文键的布尔表达式,可用 !、&&、|| 和括号组合。布尔键:sidebarFocus、browserFocus、markdownFocus、terminalFocus、commandPaletteVisible、terminalFindVisible。类型化键支持比较:字符串 sidebarMode(files、find、sessions、feed 或 dock)以及整数 paneCount 和 workspaceCount。比较运算符为 ==、!=、=~(正则表达式)、<、<=、>、>= 和 `in [a, b]`;未知或不存在的键按 false 处理。也接受布尔字面量 true 和 false;`key == false` 等同于 `!key`。只有当子句成立时,该操作的快捷键才会触发,也只会在这种情况下与其他快捷键冲突。示例:{ \"selectWorkspaceByNumber\": \"!sidebarFocus\" } 让 Ctrl+1–9 在右侧边栏未聚焦时选择工作区;{ \"selectSurfaceByNumber\": \"sidebarMode == 'find' && paneCount > 1\" } 在工作区有多个窗格时将绑定限定在查找侧边栏。", + "when": "每个操作可选的上下文谓词(VS Code 风格的 `when` 子句),按 cmux 操作 id 索引。每个值都是基于上下文键的布尔表达式,可用 !、&&、|| 和括号组合。布尔键:sidebarFocus、browserFocus、markdownFocus、terminalFocus、commandPaletteVisible、terminalFindVisible、workspaceCanvasLayout。类型化键支持比较:字符串 sidebarMode(files、find、sessions、feed 或 dock)以及整数 paneCount 和 workspaceCount。比较运算符为 ==、!=、=~(正则表达式)、<、<=、>、>= 和 `in [a, b]`;未知或不存在的键按 false 处理。也接受布尔字面量 true 和 false;`key == false` 等同于 `!key`。只有当子句成立时,该操作的快捷键才会触发,也只会在这种情况下与其他快捷键冲突。示例:{ \"selectWorkspaceByNumber\": \"!sidebarFocus\" } 让 Ctrl+1–9 在右侧边栏未聚焦时选择工作区;{ \"selectSurfaceByNumber\": \"sidebarMode == 'find' && paneCount > 1\" } 在工作区有多个窗格时将绑定限定在查找侧边栏。", "showModifierHoldHints": "在按住 Cmd 或 Control 时显示快捷键提示标签。" } }, diff --git a/web/messages/zh-TW.json b/web/messages/zh-TW.json index 2e69844164c7..8c81627b00ca 100644 --- a/web/messages/zh-TW.json +++ b/web/messages/zh-TW.json @@ -846,7 +846,7 @@ "defaultLayout": "Default layout for newly opened diff viewers. The toolbar layout toggle persists the user's last choice; an explicit `cmux diff --layout` option overrides both." }, "shortcuts": { - "when": "每個動作可選的內容述詞(VS Code 風格的 `when` 子句),依 cmux 動作 id 建立索引。每個值都是基於內容鍵的布林運算式,可用 !、&&、|| 與括號組合。布林鍵:sidebarFocus、browserFocus、markdownFocus、terminalFocus、commandPaletteVisible、terminalFindVisible。型別化鍵支援比較:字串 sidebarMode(files、find、sessions、feed 或 dock)以及整數 paneCount 與 workspaceCount。比較運算子為 ==、!=、=~(正規表示式)、<、<=、>、>= 和 `in [a, b]`;未知或不存在的鍵視為 false。也接受布林常值 true 和 false;`key == false` 等同於 `!key`。只有在子句成立時,該動作的快速鍵才會觸發,也只會在這種情況下與其他快速鍵衝突。範例:{ \"selectWorkspaceByNumber\": \"!sidebarFocus\" } 讓 Ctrl+1–9 在右側邊欄未聚焦時選取工作區;{ \"selectSurfaceByNumber\": \"sidebarMode == 'find' && paneCount > 1\" } 在工作區有多個窗格時,將綁定限制在尋找側邊欄。", + "when": "每個動作可選的內容述詞(VS Code 風格的 `when` 子句),依 cmux 動作 id 建立索引。每個值都是基於內容鍵的布林運算式,可用 !、&&、|| 與括號組合。布林鍵:sidebarFocus、browserFocus、markdownFocus、terminalFocus、commandPaletteVisible、terminalFindVisible、workspaceCanvasLayout。型別化鍵支援比較:字串 sidebarMode(files、find、sessions、feed 或 dock)以及整數 paneCount 與 workspaceCount。比較運算子為 ==、!=、=~(正規表示式)、<、<=、>、>= 和 `in [a, b]`;未知或不存在的鍵視為 false。也接受布林常值 true 和 false;`key == false` 等同於 `!key`。只有在子句成立時,該動作的快速鍵才會觸發,也只會在這種情況下與其他快速鍵衝突。範例:{ \"selectWorkspaceByNumber\": \"!sidebarFocus\" } 讓 Ctrl+1–9 在右側邊欄未聚焦時選取工作區;{ \"selectSurfaceByNumber\": \"sidebarMode == 'find' && paneCount > 1\" } 在工作區有多個窗格時,將綁定限制在尋找側邊欄。", "showModifierHoldHints": "按住 Cmd 或 Control 時顯示快捷鍵提示標籤。" } },