From f44e9367dd026d6d7b9f851f4736ed3c325fc213 Mon Sep 17 00:00:00 2001 From: Aziz Albahar Date: Fri, 12 Jun 2026 22:41:38 -0700 Subject: [PATCH 1/7] Open file explorer selection from keyboard --- .../Values/ShortcutAction+Defaults.swift | 1 + .../CmuxSettings/Values/ShortcutAction.swift | 7 +- .../Sections/KeyboardShortcutsSection.swift | 5 +- Resources/Localizable.xcstrings | 17 +++ Sources/FileExplorerView.swift | 62 +++++++-- Sources/KeyboardShortcutContext.swift | 3 +- Sources/KeyboardShortcutSettings.swift | 5 + cmuxTests/FileExplorerStoreTests.swift | 118 ++++++++++++++++++ cmuxTests/WorkspaceUnitTests.swift | 12 ++ 9 files changed, 219 insertions(+), 11 deletions(-) diff --git a/Packages/CmuxSettings/Sources/CmuxSettings/Values/ShortcutAction+Defaults.swift b/Packages/CmuxSettings/Sources/CmuxSettings/Values/ShortcutAction+Defaults.swift index 5a6a3e32618..25a0ce24a5f 100644 --- a/Packages/CmuxSettings/Sources/CmuxSettings/Values/ShortcutAction+Defaults.swift +++ b/Packages/CmuxSettings/Sources/CmuxSettings/Values/ShortcutAction+Defaults.swift @@ -86,6 +86,7 @@ extension ShortcutAction { case .attachTextBoxFile: return ShortcutStroke(key: "a", command: true, shift: true, option: true) case .sendCtrlFToTerminal: return nil case .toggleRightSidebar: return ShortcutStroke(key: "b", command: true, option: true) + case .openFileExplorerSelection: return ShortcutStroke(key: "↓", command: true) case .openDiffViewer: return ShortcutStroke(key: "d", command: true, shift: true, control: true) case .saveFilePreview: return ShortcutStroke(key: "s", command: true) case .openBrowser: return ShortcutStroke(key: "l", command: true, shift: true) diff --git a/Packages/CmuxSettings/Sources/CmuxSettings/Values/ShortcutAction.swift b/Packages/CmuxSettings/Sources/CmuxSettings/Values/ShortcutAction.swift index 9d8fe70ad3d..ea58558cc11 100644 --- a/Packages/CmuxSettings/Sources/CmuxSettings/Values/ShortcutAction.swift +++ b/Packages/CmuxSettings/Sources/CmuxSettings/Values/ShortcutAction.swift @@ -83,6 +83,7 @@ public enum ShortcutAction: String, CaseIterable, Sendable, Hashable, SettingCod case splitBrowserRight case splitBrowserDown case toggleRightSidebar = "toggleFileExplorer" + case openFileExplorerSelection // MARK: Browser & Find case openDiffViewer @@ -163,7 +164,7 @@ extension ShortcutAction { return .navigation case .focusLeft, .focusRight, .focusUp, .focusDown, .splitRight, .splitDown, .toggleSplitZoom, .equalizeSplits, .splitBrowserRight, .splitBrowserDown, - .toggleRightSidebar: + .toggleRightSidebar, .openFileExplorerSelection: return .panes case .openDiffViewer, .saveFilePreview, .openBrowser, .focusBrowserAddressBar, .browserBack, .browserForward, .browserReload, .browserZoomIn, .browserZoomOut, @@ -227,6 +228,8 @@ extension ShortcutAction { case .switchRightSidebarToFiles, .switchRightSidebarToFind, .switchRightSidebarToSessions, .switchRightSidebarToFeed, .switchRightSidebarToDock: return .atom(.sidebarFocus) + case .openFileExplorerSelection: + return .atom(.sidebarFocus) case .renameTab, .renameWorkspace: return .and(.not(.atom(.browserFocus)), .not(.atom(.sidebarFocus))) case .sendCtrlFToTerminal: @@ -334,6 +337,8 @@ extension ShortcutAction { case .splitBrowserRight: return "Split Browser Right" case .splitBrowserDown: return "Split Browser Down" case .toggleRightSidebar: return "Toggle Right Sidebar" + case .openFileExplorerSelection: + return String(localized: "shortcut.openFileExplorerSelection.label", defaultValue: "File Explorer: Open Selection") case .openDiffViewer: return "Open Diff Viewer" case .saveFilePreview: return "Save File Preview" case .openBrowser: return "Open Browser" diff --git a/Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/KeyboardShortcutsSection.swift b/Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/KeyboardShortcutsSection.swift index bf9bed33659..b491d2fd090 100644 --- a/Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/KeyboardShortcutsSection.swift +++ b/Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/KeyboardShortcutsSection.swift @@ -295,8 +295,8 @@ public struct KeyboardShortcutsSection: View { /// Mirrors legacy `KeyboardShortcutSettings.settingsVisibleActions`: /// filters out `.showHideAllWindows` (owned by Global Hotkey section) - /// then re-orders so `focusRightSidebar`, `toggleRightSidebar`, and - /// `findInDirectory` sit immediately after `markOldestUnreadAndJumpNext` + /// then re-orders so `focusRightSidebar`, `toggleRightSidebar`, + /// `openFileExplorerSelection`, and `findInDirectory` sit immediately after `markOldestUnreadAndJumpNext` /// or `jumpToUnread` (the unread navigation cluster), so colocated /// sidebar/find shortcuts appear together in the settings UI. private static var settingsVisibleActions: [ShortcutAction] { @@ -304,6 +304,7 @@ public struct KeyboardShortcutsSection: View { let colocated: [ShortcutAction] = [ .focusRightSidebar, .toggleRightSidebar, + .openFileExplorerSelection, .findInDirectory, ].filter(base.contains) let colocatedSet = Set(colocated) diff --git a/Resources/Localizable.xcstrings b/Resources/Localizable.xcstrings index c6b05c69f19..972e79e80bf 100644 --- a/Resources/Localizable.xcstrings +++ b/Resources/Localizable.xcstrings @@ -1927,6 +1927,23 @@ } } }, + "shortcut.openFileExplorerSelection.label": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "File Explorer: Open Selection" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ファイルエクスプローラ: 選択項目を開く" + } + } + } + }, "command.markdownZoomIn.title": { "extractionState": "manual", "localizations": { diff --git a/Sources/FileExplorerView.swift b/Sources/FileExplorerView.swift index 8e52031d41e..7f16d5e31e7 100644 --- a/Sources/FileExplorerView.swift +++ b/Sources/FileExplorerView.swift @@ -3,6 +3,25 @@ import Bonsplit import Combine import SwiftUI +enum FileExplorerKeyboardActivation { + static func isDefaultOpenEvent(_ event: NSEvent) -> Bool { + guard event.type == .keyDown else { return false } + let flags = event.modifierFlags.intersection(.deviceIndependentFlagsMask) + .subtracting([.numericPad, .function, .capsLock]) + guard flags.intersection([.command, .control, .option, .shift]).isEmpty else { + return false + } + return event.keyCode == 36 || event.keyCode == 76 + } + + static func matchesOpenSelectionShortcut( + _ event: NSEvent, + shortcutForAction: (KeyboardShortcutSettings.Action) -> StoredShortcut = KeyboardShortcutSettings.shortcut(for:) + ) -> Bool { + isDefaultOpenEvent(event) || shortcutForAction(.openFileExplorerSelection).matches(event: event) + } +} + #if DEBUG private func fileExplorerDebugResponder(_ responder: NSResponder?) -> String { guard let responder else { return "nil" } @@ -604,20 +623,34 @@ struct FileExplorerPanelView: NSViewRepresentable { @objc func handleDoubleClick(_ sender: NSOutlineView) { let row = sender.clickedRow >= 0 ? sender.clickedRow : sender.selectedRow + _ = activateNode(at: row, in: sender) + } + + @discardableResult + func openSelectedItem(in outlineView: NSOutlineView) -> Bool { + guard let row = resolvedSelectionRow(in: outlineView) else { return false } + return activateNode(at: row, in: outlineView) + } + + @discardableResult + private func activateNode(at row: Int, in outlineView: NSOutlineView) -> Bool { guard row >= 0, - let node = sender.item(atRow: row) as? FileExplorerNode else { return } + let node = outlineView.item(atRow: row) as? FileExplorerNode else { return false } + + selectRow(row, in: outlineView, scroll: false) if node.isDirectory { - if sender.isItemExpanded(node) { - sender.collapseItem(node) - } else if sender.isExpandable(node) { - sender.expandItem(node) + if outlineView.isItemExpanded(node) { + outlineView.collapseItem(node) + } else if outlineView.isExpandable(node) { + outlineView.expandItem(node) } - return + return true } - guard store.provider is LocalFileExplorerProvider else { return } + guard store.provider is LocalFileExplorerProvider else { return true } onOpenFilePreview(node.path) + return true } // MARK: - Context Menu (NSMenuDelegate) @@ -2119,6 +2152,10 @@ final class FileExplorerNSOutlineView: NSOutlineView { } } + if handleOpenSelectionShortcut(event) { + return + } + if quickSearchActive, handleQuickSearchKey(event) { return } @@ -2147,6 +2184,9 @@ final class FileExplorerNSOutlineView: NSOutlineView { } override func performKeyEquivalent(with event: NSEvent) -> Bool { + if handleOpenSelectionShortcut(event) { + return true + } if quickSearchActive, handleQuickSearchKey(event) { return true } @@ -2223,6 +2263,14 @@ final class FileExplorerNSOutlineView: NSOutlineView { dataSource as? FileExplorerPanelView.Coordinator } + private func handleOpenSelectionShortcut(_ event: NSEvent) -> Bool { + guard FileExplorerKeyboardActivation.matchesOpenSelectionShortcut(event) else { + return false + } + endQuickSearch() + return fileExplorerCoordinator?.openSelectedItem(in: self) ?? false + } + private func beginQuickSearch() { quickSearchActive = true quickSearchQuery = "" diff --git a/Sources/KeyboardShortcutContext.swift b/Sources/KeyboardShortcutContext.swift index 35d936a340d..de93e525221 100644 --- a/Sources/KeyboardShortcutContext.swift +++ b/Sources/KeyboardShortcutContext.swift @@ -125,7 +125,8 @@ extension KeyboardShortcutSettings.Action { .diffViewerScrollToTop, .diffViewerOpenFileSearch: return .browserPanel - case .switchRightSidebarToFiles, .switchRightSidebarToFind, .switchRightSidebarToSessions, .switchRightSidebarToFeed, .switchRightSidebarToDock: + case .switchRightSidebarToFiles, .switchRightSidebarToFind, .switchRightSidebarToSessions, .switchRightSidebarToFeed, .switchRightSidebarToDock, + .openFileExplorerSelection: return .rightSidebarFocus case .renameTab, .renameWorkspace, .sendCtrlFToTerminal: return .nonBrowserPanel diff --git a/Sources/KeyboardShortcutSettings.swift b/Sources/KeyboardShortcutSettings.swift index c0c7cb1ae73..f98483122dc 100644 --- a/Sources/KeyboardShortcutSettings.swift +++ b/Sources/KeyboardShortcutSettings.swift @@ -31,6 +31,7 @@ enum KeyboardShortcutSettings { let colocatedSidebarActions = [ .focusRightSidebar, .toggleRightSidebar, + .openFileExplorerSelection, .findInDirectory, ].filter(actions.contains) let actionSet = Set(colocatedSidebarActions) @@ -130,6 +131,7 @@ enum KeyboardShortcutSettings { // File Explorer case toggleRightSidebar = "toggleFileExplorer" + case openFileExplorerSelection // Panels case saveFilePreview @@ -228,6 +230,7 @@ enum KeyboardShortcutSettings { case .splitBrowserRight: return String(localized: "shortcut.splitBrowserRight.label", defaultValue: "Split Browser Right") case .splitBrowserDown: return String(localized: "shortcut.splitBrowserDown.label", defaultValue: "Split Browser Down") case .toggleRightSidebar: return String(localized: "shortcut.toggleRightSidebar.label", defaultValue: "Toggle Right Sidebar") + case .openFileExplorerSelection: return String(localized: "shortcut.openFileExplorerSelection.label", defaultValue: "File Explorer: Open Selection") case .saveFilePreview: return String(localized: "shortcut.saveFilePreview.label", defaultValue: "Save File Preview") case .openBrowser: return String(localized: "shortcut.openBrowser.label", defaultValue: "Open Browser") case .focusBrowserAddressBar: return String(localized: "command.browserFocusAddressBar.title", defaultValue: "Focus Address Bar") @@ -419,6 +422,8 @@ enum KeyboardShortcutSettings { return StoredShortcut(key: "1", command: true, shift: false, option: false, control: false) case .toggleRightSidebar: return StoredShortcut(key: "b", command: true, shift: false, option: true, control: false) + case .openFileExplorerSelection: + return StoredShortcut(key: "↓", command: true, shift: false, option: false, control: false) case .saveFilePreview: return StoredShortcut(key: "s", command: true, shift: false, option: false, control: false) case .openBrowser: diff --git a/cmuxTests/FileExplorerStoreTests.swift b/cmuxTests/FileExplorerStoreTests.swift index 6b980aab18e..431200221ff 100644 --- a/cmuxTests/FileExplorerStoreTests.swift +++ b/cmuxTests/FileExplorerStoreTests.swift @@ -603,6 +603,124 @@ struct FileExplorerStoreTests { } } +@MainActor +@Suite("File explorer keyboard activation") +struct FileExplorerKeyboardActivationTests { + private final class OpenProbe { + var openedPaths: [String] = [] + } + + @Test + func selectedLocalFileOpensThroughSharedActivationPath() { + let file = FileExplorerNode(name: "README.md", path: "/tmp/project/README.md", isDirectory: false) + let probe = OpenProbe() + let (coordinator, outlineView) = makeOutline(nodes: [file], probe: probe) + + outlineView.selectRowIndexes(IndexSet(integer: 0), byExtendingSelection: false) + coordinator.store.select(node: file) + + #expect(coordinator.openSelectedItem(in: outlineView)) + #expect(probe.openedPaths == [file.path]) + } + + @Test + func selectedFolderTogglesExpansionThroughSharedActivationPath() { + let folder = FileExplorerNode(name: "Sources", path: "/tmp/project/Sources", isDirectory: true) + folder.children = [ + FileExplorerNode(name: "App.swift", path: "/tmp/project/Sources/App.swift", isDirectory: false) + ] + let probe = OpenProbe() + let (coordinator, outlineView) = makeOutline(nodes: [folder], probe: probe) + + outlineView.selectRowIndexes(IndexSet(integer: 0), byExtendingSelection: false) + coordinator.store.select(node: folder) + + #expect(coordinator.openSelectedItem(in: outlineView)) + #expect(outlineView.isItemExpanded(folder)) + #expect(coordinator.store.isExpanded(folder)) + + #expect(coordinator.openSelectedItem(in: outlineView)) + #expect(!outlineView.isItemExpanded(folder)) + #expect(!coordinator.store.isExpanded(folder)) + #expect(probe.openedPaths.isEmpty) + } + + @Test + func returnEnterAndConfiguredShortcutMatchOpenSelection() { + let returnEvent = makeKeyEvent(characters: "\r", keyCode: 36) + let keypadEnterEvent = makeKeyEvent(characters: "\r", keyCode: 76) + let downArrow = String(UnicodeScalar(NSDownArrowFunctionKey)!) + let commandDownEvent = makeKeyEvent( + modifierFlags: .command, + characters: downArrow, + keyCode: 125 + ) + let plainDownEvent = makeKeyEvent(characters: downArrow, keyCode: 125) + + #expect(FileExplorerKeyboardActivation.isDefaultOpenEvent(returnEvent)) + #expect(FileExplorerKeyboardActivation.isDefaultOpenEvent(keypadEnterEvent)) + #expect( + FileExplorerKeyboardActivation.matchesOpenSelectionShortcut( + commandDownEvent, + shortcutForAction: { action in + #expect(action == .openFileExplorerSelection) + return action.defaultShortcut + } + ) + ) + #expect(!FileExplorerKeyboardActivation.matchesOpenSelectionShortcut(plainDownEvent)) + } + + private func makeOutline( + nodes: [FileExplorerNode], + probe: OpenProbe + ) -> (FileExplorerPanelView.Coordinator, FileExplorerNSOutlineView) { + let store = FileExplorerStore() + store.provider = LocalFileExplorerProvider() + store.rootPath = "/tmp/project" + store.rootNodes = nodes + + let coordinator = FileExplorerPanelView.Coordinator( + store: store, + state: FileExplorerState(), + onOpenFilePreview: { probe.openedPaths.append($0) } + ) + + let outlineView = FileExplorerNSOutlineView() + let column = NSTableColumn(identifier: NSUserInterfaceItemIdentifier("name")) + outlineView.addTableColumn(column) + outlineView.outlineTableColumn = column + outlineView.dataSource = coordinator + outlineView.delegate = coordinator + coordinator.outlineView = outlineView + outlineView.reloadData() + + return (coordinator, outlineView) + } + + private func makeKeyEvent( + modifierFlags: NSEvent.ModifierFlags = [], + characters: String, + keyCode: UInt16 + ) -> NSEvent { + guard let event = NSEvent.keyEvent( + with: .keyDown, + location: .zero, + modifierFlags: modifierFlags, + timestamp: ProcessInfo.processInfo.systemUptime, + windowNumber: 0, + context: nil, + characters: characters, + charactersIgnoringModifiers: characters, + isARepeat: false, + keyCode: keyCode + ) else { + fatalError("Failed to construct key event") + } + return event + } +} + @MainActor @Suite(.serialized) struct FileSearchControllerTests { diff --git a/cmuxTests/WorkspaceUnitTests.swift b/cmuxTests/WorkspaceUnitTests.swift index 5fde169a3e6..1e1972b30fe 100644 --- a/cmuxTests/WorkspaceUnitTests.swift +++ b/cmuxTests/WorkspaceUnitTests.swift @@ -425,6 +425,10 @@ final class WorkspaceRenameShortcutDefaultsTests: XCTestCase { KeyboardShortcutSettings.Action.toggleRightSidebar.label, String(localized: "shortcut.toggleRightSidebar.label", defaultValue: "Toggle Right Sidebar") ) + XCTAssertEqual( + KeyboardShortcutSettings.Action.openFileExplorerSelection.label, + String(localized: "shortcut.openFileExplorerSelection.label", defaultValue: "File Explorer: Open Selection") + ) let toggleRightSidebar = KeyboardShortcutSettings.Action.toggleRightSidebar.defaultShortcut XCTAssertEqual(toggleRightSidebar.key, "b") @@ -433,6 +437,13 @@ final class WorkspaceRenameShortcutDefaultsTests: XCTestCase { XCTAssertTrue(toggleRightSidebar.option) XCTAssertFalse(toggleRightSidebar.control) + let openSelection = KeyboardShortcutSettings.Action.openFileExplorerSelection.defaultShortcut + XCTAssertEqual(openSelection.key, "↓") + XCTAssertTrue(openSelection.command) + XCTAssertFalse(openSelection.shift) + XCTAssertFalse(openSelection.option) + XCTAssertFalse(openSelection.control) + let focusRightSidebar = KeyboardShortcutSettings.Action.focusRightSidebar.defaultShortcut XCTAssertEqual(focusRightSidebar.key, "e") XCTAssertTrue(focusRightSidebar.command) @@ -509,6 +520,7 @@ final class WorkspaceRenameShortcutDefaultsTests: XCTestCase { let expectedActions: [KeyboardShortcutSettings.Action] = [ .focusRightSidebar, .toggleRightSidebar, + .openFileExplorerSelection, .findInDirectory, ] From 3cd5ab3b6dc5bd993e044b98b4e935a0d6842f3b Mon Sep 17 00:00:00 2001 From: Aziz Albahar Date: Fri, 12 Jun 2026 22:41:46 -0700 Subject: [PATCH 2/7] Document file explorer open shortcut --- skills/cmux-settings/references/shortcut-actions.md | 1 + web/app/[locale]/docs/configuration/page.tsx | 1 + web/app/[locale]/docs/keyboard-shortcuts/page.tsx | 1 + web/data/cmux-shortcuts.ts | 9 +++++++++ web/data/cmux.schema.json | 1 + 5 files changed, 13 insertions(+) diff --git a/skills/cmux-settings/references/shortcut-actions.md b/skills/cmux-settings/references/shortcut-actions.md index 4d9f0316f1c..d26899fe42e 100644 --- a/skills/cmux-settings/references/shortcut-actions.md +++ b/skills/cmux-settings/references/shortcut-actions.md @@ -108,5 +108,6 @@ Values for `shortcuts.bindings.`: ## Files and React Grab - `shortcuts.bindings.toggleFileExplorer` +- `shortcuts.bindings.openFileExplorerSelection` - `shortcuts.bindings.saveFilePreview` - `shortcuts.bindings.toggleReactGrab` diff --git a/web/app/[locale]/docs/configuration/page.tsx b/web/app/[locale]/docs/configuration/page.tsx index 446f5d55676..d9469213fc7 100644 --- a/web/app/[locale]/docs/configuration/page.tsx +++ b/web/app/[locale]/docs/configuration/page.tsx @@ -128,6 +128,7 @@ function buildSettingsFileExample(t: ConfigurationTranslation) { // "bindings": { // "toggleSidebar": "cmd+b", // "toggleFileExplorer": "cmd+opt+b", + // "openFileExplorerSelection": "cmd+down", // "newTab": ["ctrl+b", "c"], // "commandPalettePrevious": null // } diff --git a/web/app/[locale]/docs/keyboard-shortcuts/page.tsx b/web/app/[locale]/docs/keyboard-shortcuts/page.tsx index 3c1d6a17064..22c18aedab3 100644 --- a/web/app/[locale]/docs/keyboard-shortcuts/page.tsx +++ b/web/app/[locale]/docs/keyboard-shortcuts/page.tsx @@ -14,6 +14,7 @@ const shortcutChordExample = `{ "showNotifications": ["ctrl+b", "i"], "toggleSidebar": "cmd+b", "toggleFileExplorer": "cmd+opt+b", + "openFileExplorerSelection": "cmd+down", "splitRight": "", "commandPalettePrevious": null } diff --git a/web/data/cmux-shortcuts.ts b/web/data/cmux-shortcuts.ts index 7d2be418adf..28acac7f141 100644 --- a/web/data/cmux-shortcuts.ts +++ b/web/data/cmux-shortcuts.ts @@ -116,6 +116,15 @@ export const shortcutCategories: ShortcutCategory[] = [ { id: "renameWorkspace", combos: [["⌘", "⇧", "R"]], description: { en: "Rename workspace", ja: "ワークスペース名を変更" } }, { id: "editWorkspaceDescription", combos: [["⌥", "⌘", "E"]], description: { en: "Edit workspace description", ja: "ワークスペースの説明を編集" } }, { id: "focusRightSidebar", combos: [["⌘", "⇧", "E"]], description: { en: "Toggle right-sidebar focus", ja: "右サイドバーのフォーカスを切り替え" } }, + { + id: "openFileExplorerSelection", + combos: [["⌘", "↓"]], + description: { en: "Open selected file explorer item", ja: "ファイルエクスプローラの選択項目を開く" }, + note: { + en: "Return and keypad Enter also open the selected file while the file explorer is focused.", + ja: "ファイルエクスプローラにフォーカスがある間は、Return とテンキー Enter でも選択中のファイルを開けます。", + }, + }, { id: "navigateRightSidebarRows", combos: [["J / K"], ["⌃", "N / P"], ["H / L"]], diff --git a/web/data/cmux.schema.json b/web/data/cmux.schema.json index b4c017db991..4cc51d979ac 100644 --- a/web/data/cmux.schema.json +++ b/web/data/cmux.schema.json @@ -1176,6 +1176,7 @@ "splitBrowserRight", "splitBrowserDown", "toggleFileExplorer", + "openFileExplorerSelection", "saveFilePreview", "openBrowser", "focusBrowserAddressBar", From a7693bb7543820c295ae0b9bd048585d501e8706 Mon Sep 17 00:00:00 2001 From: Aziz Albahar Date: Fri, 12 Jun 2026 22:52:01 -0700 Subject: [PATCH 3/7] Split file explorer keyboard code for budget --- .../Sections/KeyboardShortcutsSection.swift | 3 +- Sources/FileExplorerOutlineView.swift | 197 +++++++++++++++++ Sources/FileExplorerView.swift | 198 ------------------ Sources/KeyboardShortcutSettings.swift | 9 +- cmux.xcodeproj/project.pbxproj | 8 + .../FileExplorerKeyboardActivationTests.swift | 162 ++++++++++++++ cmuxTests/FileExplorerStoreTests.swift | 118 ----------- cmuxTests/WorkspaceUnitTests.swift | 34 --- 8 files changed, 370 insertions(+), 359 deletions(-) create mode 100644 Sources/FileExplorerOutlineView.swift create mode 100644 cmuxTests/FileExplorerKeyboardActivationTests.swift diff --git a/Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/KeyboardShortcutsSection.swift b/Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/KeyboardShortcutsSection.swift index b491d2fd090..ad985691d17 100644 --- a/Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/KeyboardShortcutsSection.swift +++ b/Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/KeyboardShortcutsSection.swift @@ -295,8 +295,7 @@ public struct KeyboardShortcutsSection: View { /// Mirrors legacy `KeyboardShortcutSettings.settingsVisibleActions`: /// filters out `.showHideAllWindows` (owned by Global Hotkey section) - /// then re-orders so `focusRightSidebar`, `toggleRightSidebar`, - /// `openFileExplorerSelection`, and `findInDirectory` sit immediately after `markOldestUnreadAndJumpNext` + /// then re-orders the right-sidebar/find actions after `markOldestUnreadAndJumpNext` /// or `jumpToUnread` (the unread navigation cluster), so colocated /// sidebar/find shortcuts appear together in the settings UI. private static var settingsVisibleActions: [ShortcutAction] { diff --git a/Sources/FileExplorerOutlineView.swift b/Sources/FileExplorerOutlineView.swift new file mode 100644 index 00000000000..f27cefe7fda --- /dev/null +++ b/Sources/FileExplorerOutlineView.swift @@ -0,0 +1,197 @@ +import AppKit + +enum FileExplorerKeyboardActivation { + static func isDefaultOpenEvent(_ event: NSEvent) -> Bool { + guard event.type == .keyDown else { return false } + let flags = event.modifierFlags.intersection(.deviceIndependentFlagsMask) + .subtracting([.numericPad, .function, .capsLock]) + guard flags.intersection([.command, .control, .option, .shift]).isEmpty else { + return false + } + return event.keyCode == 36 || event.keyCode == 76 + } + + static func matchesOpenSelectionShortcut( + _ event: NSEvent, + shortcutForAction: (KeyboardShortcutSettings.Action) -> StoredShortcut = KeyboardShortcutSettings.shortcut(for:) + ) -> Bool { + isDefaultOpenEvent(event) || shortcutForAction(.openFileExplorerSelection).matches(event: event) + } +} + +/// NSOutlineView subclass that disables expand/collapse animations and adds leading margin. +final class FileExplorerNSOutlineView: NSOutlineView { + /// Leading margin applied to disclosure triangles and content. + static let leadingMargin: CGFloat = 8 + var onQuickSearchChanged: ((String?) -> Void)? + private var quickSearchActive = false + private var quickSearchQuery = "" + + override func keyDown(with event: NSEvent) { + if let mode = AppDelegate.shared?.rightSidebarModeShortcut(for: event) { + if fileExplorerCoordinator?.handleModeShortcut(mode, in: window) == true { + return + } + } + + if handleOpenSelectionShortcut(event) { + return + } + + if quickSearchActive, handleQuickSearchKey(event) { + return + } + + if let delta = RightSidebarKeyboardNavigation.moveDelta(for: event) { + endQuickSearch() + fileExplorerCoordinator?.moveSelection(in: self, by: delta) + return + } + + if let action = RightSidebarKeyboardNavigation.disclosureAction(for: event) { + endQuickSearch() + fileExplorerCoordinator?.performDisclosureAction(action, in: self) + return + } + + if RightSidebarKeyboardNavigation.isPlainSlash(event) { + beginQuickSearch() + return + } + + if RightSidebarKeyboardNavigation.isPlainPrintableText(event) { + return + } + super.keyDown(with: event) + } + + override func performKeyEquivalent(with event: NSEvent) -> Bool { + if handleOpenSelectionShortcut(event) { + return true + } + if quickSearchActive, handleQuickSearchKey(event) { + return true + } + if let delta = RightSidebarKeyboardNavigation.moveDelta(for: event) { + endQuickSearch() + fileExplorerCoordinator?.moveSelection(in: self, by: delta) + return true + } + if let action = RightSidebarKeyboardNavigation.disclosureAction(for: event) { + endQuickSearch() + fileExplorerCoordinator?.performDisclosureAction(action, in: self) + return true + } + return super.performKeyEquivalent(with: event) + } + + override func becomeFirstResponder() -> Bool { + let result = super.becomeFirstResponder() + if result { + redrawVisibleRows() + } + return result + } + + override func resignFirstResponder() -> Bool { + let result = super.resignFirstResponder() + if result { + endQuickSearch() + redrawVisibleRows() + } + return result + } + + override func expandItem(_ item: Any?, expandChildren: Bool) { + NSAnimationContext.beginGrouping() + NSAnimationContext.current.duration = 0 + super.expandItem(item, expandChildren: expandChildren) + NSAnimationContext.endGrouping() + } + + override func collapseItem(_ item: Any?, collapseChildren: Bool) { + NSAnimationContext.beginGrouping() + NSAnimationContext.current.duration = 0 + super.collapseItem(item, collapseChildren: collapseChildren) + NSAnimationContext.endGrouping() + } + + override func frameOfOutlineCell(atRow row: Int) -> NSRect { + var frame = super.frameOfOutlineCell(atRow: row) + frame.origin.x += Self.leadingMargin + return frame + } + + override func frameOfCell(atColumn column: Int, row: Int) -> NSRect { + var frame = super.frameOfCell(atColumn: column, row: row) + let cellShift: CGFloat = Self.leadingMargin - 6 + frame.origin.x += cellShift + frame.size.width -= cellShift + return frame + } + + private func redrawVisibleRows() { + setNeedsDisplay(bounds) + let visibleRows = rows(in: visibleRect) + guard visibleRows.location != NSNotFound else { return } + let upperBound = min(visibleRows.location + visibleRows.length, numberOfRows) + guard visibleRows.location < upperBound else { return } + for row in visibleRows.location.. Bool { + guard FileExplorerKeyboardActivation.matchesOpenSelectionShortcut(event) else { + return false + } + endQuickSearch() + return fileExplorerCoordinator?.openSelectedItem(in: self) ?? false + } + + private func beginQuickSearch() { + quickSearchActive = true + quickSearchQuery = "" + onQuickSearchChanged?(quickSearchQuery) + } + + private func endQuickSearch() { + guard quickSearchActive || !quickSearchQuery.isEmpty else { return } + quickSearchActive = false + quickSearchQuery = "" + onQuickSearchChanged?(nil) + } + + private func handleQuickSearchKey(_ event: NSEvent) -> Bool { + if event.keyCode == 53 { + endQuickSearch() + return true + } + if event.keyCode == 36 || event.keyCode == 76 { + endQuickSearch() + return true + } + if event.keyCode == 51 { + if !quickSearchQuery.isEmpty { + quickSearchQuery.removeLast() + onQuickSearchChanged?(quickSearchQuery) + fileExplorerCoordinator?.selectBestQuickSearchMatch(in: self, query: quickSearchQuery) + } + return true + } + guard RightSidebarKeyboardNavigation.isPlainPrintableText(event) else { + return false + } + guard let text = event.charactersIgnoringModifiers, !text.isEmpty else { + return true + } + quickSearchQuery += text + onQuickSearchChanged?(quickSearchQuery) + fileExplorerCoordinator?.selectBestQuickSearchMatch(in: self, query: quickSearchQuery) + return true + } +} diff --git a/Sources/FileExplorerView.swift b/Sources/FileExplorerView.swift index 7f16d5e31e7..802979f54cc 100644 --- a/Sources/FileExplorerView.swift +++ b/Sources/FileExplorerView.swift @@ -3,25 +3,6 @@ import Bonsplit import Combine import SwiftUI -enum FileExplorerKeyboardActivation { - static func isDefaultOpenEvent(_ event: NSEvent) -> Bool { - guard event.type == .keyDown else { return false } - let flags = event.modifierFlags.intersection(.deviceIndependentFlagsMask) - .subtracting([.numericPad, .function, .capsLock]) - guard flags.intersection([.command, .control, .option, .shift]).isEmpty else { - return false - } - return event.keyCode == 36 || event.keyCode == 76 - } - - static func matchesOpenSelectionShortcut( - _ event: NSEvent, - shortcutForAction: (KeyboardShortcutSettings.Action) -> StoredShortcut = KeyboardShortcutSettings.shortcut(for:) - ) -> Bool { - isDefaultOpenEvent(event) || shortcutForAction(.openFileExplorerSelection).matches(event: event) - } -} - #if DEBUG private func fileExplorerDebugResponder(_ responder: NSResponder?) -> String { guard let responder else { return "nil" } @@ -2135,185 +2116,6 @@ final class FileExplorerCellView: NSTableCellView { } } -// MARK: - Non-Animating Outline View - -/// NSOutlineView subclass that disables expand/collapse animations and adds leading margin. -final class FileExplorerNSOutlineView: NSOutlineView { - /// Leading margin applied to disclosure triangles and content. - static let leadingMargin: CGFloat = 8 - var onQuickSearchChanged: ((String?) -> Void)? - private var quickSearchActive = false - private var quickSearchQuery = "" - - override func keyDown(with event: NSEvent) { - if let mode = AppDelegate.shared?.rightSidebarModeShortcut(for: event) { - if fileExplorerCoordinator?.handleModeShortcut(mode, in: window) == true { - return - } - } - - if handleOpenSelectionShortcut(event) { - return - } - - if quickSearchActive, handleQuickSearchKey(event) { - return - } - - if let delta = RightSidebarKeyboardNavigation.moveDelta(for: event) { - endQuickSearch() - fileExplorerCoordinator?.moveSelection(in: self, by: delta) - return - } - - if let action = RightSidebarKeyboardNavigation.disclosureAction(for: event) { - endQuickSearch() - fileExplorerCoordinator?.performDisclosureAction(action, in: self) - return - } - - if RightSidebarKeyboardNavigation.isPlainSlash(event) { - beginQuickSearch() - return - } - - if RightSidebarKeyboardNavigation.isPlainPrintableText(event) { - return - } - super.keyDown(with: event) - } - - override func performKeyEquivalent(with event: NSEvent) -> Bool { - if handleOpenSelectionShortcut(event) { - return true - } - if quickSearchActive, handleQuickSearchKey(event) { - return true - } - if let delta = RightSidebarKeyboardNavigation.moveDelta(for: event) { - endQuickSearch() - fileExplorerCoordinator?.moveSelection(in: self, by: delta) - return true - } - if let action = RightSidebarKeyboardNavigation.disclosureAction(for: event) { - endQuickSearch() - fileExplorerCoordinator?.performDisclosureAction(action, in: self) - return true - } - return super.performKeyEquivalent(with: event) - } - - override func becomeFirstResponder() -> Bool { - let result = super.becomeFirstResponder() - if result { - redrawVisibleRows() - } - return result - } - - override func resignFirstResponder() -> Bool { - let result = super.resignFirstResponder() - if result { - endQuickSearch() - redrawVisibleRows() - } - return result - } - - override func expandItem(_ item: Any?, expandChildren: Bool) { - NSAnimationContext.beginGrouping() - NSAnimationContext.current.duration = 0 - super.expandItem(item, expandChildren: expandChildren) - NSAnimationContext.endGrouping() - } - - override func collapseItem(_ item: Any?, collapseChildren: Bool) { - NSAnimationContext.beginGrouping() - NSAnimationContext.current.duration = 0 - super.collapseItem(item, collapseChildren: collapseChildren) - NSAnimationContext.endGrouping() - } - - override func frameOfOutlineCell(atRow row: Int) -> NSRect { - var frame = super.frameOfOutlineCell(atRow: row) - frame.origin.x += Self.leadingMargin - return frame - } - - override func frameOfCell(atColumn column: Int, row: Int) -> NSRect { - var frame = super.frameOfCell(atColumn: column, row: row) - let cellShift: CGFloat = Self.leadingMargin - 6 - frame.origin.x += cellShift - frame.size.width -= cellShift - return frame - } - - private func redrawVisibleRows() { - setNeedsDisplay(bounds) - let visibleRows = rows(in: visibleRect) - guard visibleRows.location != NSNotFound else { return } - let upperBound = min(visibleRows.location + visibleRows.length, numberOfRows) - guard visibleRows.location < upperBound else { return } - for row in visibleRows.location.. Bool { - guard FileExplorerKeyboardActivation.matchesOpenSelectionShortcut(event) else { - return false - } - endQuickSearch() - return fileExplorerCoordinator?.openSelectedItem(in: self) ?? false - } - - private func beginQuickSearch() { - quickSearchActive = true - quickSearchQuery = "" - onQuickSearchChanged?(quickSearchQuery) - } - - private func endQuickSearch() { - guard quickSearchActive || !quickSearchQuery.isEmpty else { return } - quickSearchActive = false - quickSearchQuery = "" - onQuickSearchChanged?(nil) - } - - private func handleQuickSearchKey(_ event: NSEvent) -> Bool { - if event.keyCode == 53 { - endQuickSearch() - return true - } - if event.keyCode == 36 || event.keyCode == 76 { - endQuickSearch() - return true - } - if event.keyCode == 51 { - if !quickSearchQuery.isEmpty { - quickSearchQuery.removeLast() - onQuickSearchChanged?(quickSearchQuery) - fileExplorerCoordinator?.selectBestQuickSearchMatch(in: self, query: quickSearchQuery) - } - return true - } - guard RightSidebarKeyboardNavigation.isPlainPrintableText(event) else { - return false - } - guard let text = event.charactersIgnoringModifiers, !text.isEmpty else { - return true - } - quickSearchQuery += text - onQuickSearchChanged?(quickSearchQuery) - fileExplorerCoordinator?.selectBestQuickSearchMatch(in: self, query: quickSearchQuery) - return true - } -} - // MARK: - Row View final class FileExplorerRowView: NSTableRowView { diff --git a/Sources/KeyboardShortcutSettings.swift b/Sources/KeyboardShortcutSettings.swift index f98483122dc..a8451e8f1b0 100644 --- a/Sources/KeyboardShortcutSettings.swift +++ b/Sources/KeyboardShortcutSettings.swift @@ -412,11 +412,8 @@ enum KeyboardShortcutSettings { case .attachTextBoxFile: return StoredShortcut(key: "a", command: true, shift: true, option: true, control: false) case .sendCtrlFToTerminal: - // Unbound by default: this is a deliberate escape hatch for forwarding a - // control chord (e.g. Claude Code's Ctrl-F force-stop) to the focused - // terminal. Binding it to plain Ctrl-F would be self-referential, so users - // opt in via Settings; it stays reachable through the command palette and - // the `send_key ctrl-f` socket command. + // Unbound by default: users opt in via Settings when they need + // Ctrl-F forwarded to terminal apps such as Claude Code. return .unbound case .selectWorkspaceByNumber: return StoredShortcut(key: "1", command: true, shift: false, option: false, control: false) @@ -443,8 +440,6 @@ enum KeyboardShortcutSettings { case .browserZoomReset: return StoredShortcut(key: "0", command: true, shift: false, option: false, control: false) case .markdownZoomIn: - // Same chord as browser zoom, but scoped to the markdown panel - // context so the two never collide. return StoredShortcut(key: "=", command: true, shift: false, option: false, control: false) case .markdownZoomOut: return StoredShortcut(key: "-", command: true, shift: false, option: false, control: false) diff --git a/cmux.xcodeproj/project.pbxproj b/cmux.xcodeproj/project.pbxproj index 026dc12e411..bc6322e23a9 100644 --- a/cmux.xcodeproj/project.pbxproj +++ b/cmux.xcodeproj/project.pbxproj @@ -324,6 +324,8 @@ D0B10020A1B2C3D4E5F60001 /* FileDropOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B10021A1B2C3D4E5F60001 /* FileDropOverlayView.swift */; }; D0B10022A1B2C3D4E5F60001 /* FileDropOverlayViewHitTesting.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B10023A1B2C3D4E5F60001 /* FileDropOverlayViewHitTesting.swift */; }; D0B10018A1B2C3D4E5F60001 /* FileDropOverlayViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B10019A1B2C3D4E5F60001 /* FileDropOverlayViewTests.swift */; }; + FE002104 /* FileExplorerKeyboardActivationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE002004 /* FileExplorerKeyboardActivationTests.swift */; }; + FE001105 /* FileExplorerOutlineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE001005 /* FileExplorerOutlineView.swift */; }; FE002101 /* FileExplorerRootResolverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE002001 /* FileExplorerRootResolverTests.swift */; }; 9F8A6669D6F54F0EA8E8BEB8 /* FileExplorerRootSyncPolicyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 120A1EF846214C938416846E /* FileExplorerRootSyncPolicyTests.swift */; }; FE001103 /* FileExplorerSearchController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE001003 /* FileExplorerSearchController.swift */; }; @@ -1137,6 +1139,8 @@ D0B10021A1B2C3D4E5F60001 /* FileDropOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileDropOverlayView.swift; sourceTree = ""; }; D0B10023A1B2C3D4E5F60001 /* FileDropOverlayViewHitTesting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileDropOverlayViewHitTesting.swift; sourceTree = ""; }; D0B10019A1B2C3D4E5F60001 /* FileDropOverlayViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileDropOverlayViewTests.swift; sourceTree = ""; }; + FE002004 /* FileExplorerKeyboardActivationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileExplorerKeyboardActivationTests.swift; sourceTree = ""; }; + FE001005 /* FileExplorerOutlineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileExplorerOutlineView.swift; sourceTree = ""; }; FE002001 /* FileExplorerRootResolverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileExplorerRootResolverTests.swift; sourceTree = ""; }; 120A1EF846214C938416846E /* FileExplorerRootSyncPolicyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileExplorerRootSyncPolicyTests.swift; sourceTree = ""; }; FE001003 /* FileExplorerSearchController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileExplorerSearchController.swift; sourceTree = ""; }; @@ -2216,6 +2220,7 @@ B37A0000000000000000000A /* FileExplorerState.swift */, FE001003 /* FileExplorerSearchController.swift */, FE001004 /* FileExplorerTerminalPathInsertion.swift */, + FE001005 /* FileExplorerOutlineView.swift */, FE001002 /* FileExplorerView.swift */, FE003001 /* SessionIndexStore.swift */, B35750000000000000000007 /* SessionIndexRegisteredAgents.swift */, @@ -2477,6 +2482,7 @@ C0DEF0A40000000000000002 /* CmuxConfigContextMenuTests.swift */, E30750000000000000000005 /* CmuxConfigNamedColorTests.swift */, FE002001 /* FileExplorerRootResolverTests.swift */, + FE002004 /* FileExplorerKeyboardActivationTests.swift */, FE002002 /* FileExplorerStoreTests.swift */, FE002003 /* FileSearchRipgrepParserTests.swift */, 9BD519CFE74530A46DF0925D /* MobileHostAuthorizationTests.swift */, @@ -3049,6 +3055,7 @@ D0B1001AA1B2C3D4E5F60001 /* FileDropHintBadgeView.swift in Sources */, D0B10020A1B2C3D4E5F60001 /* FileDropOverlayView.swift in Sources */, D0B10022A1B2C3D4E5F60001 /* FileDropOverlayViewHitTesting.swift in Sources */, + FE001105 /* FileExplorerOutlineView.swift in Sources */, FE001103 /* FileExplorerSearchController.swift in Sources */, B37A00000000000000000009 /* FileExplorerState.swift in Sources */, FE001101 /* FileExplorerStore.swift in Sources */, @@ -3513,6 +3520,7 @@ FEED49850000000000000001 /* FeedEventClassificationTests.swift in Sources */, FEEDC1A50000000000000003 /* FeedEventClassifier.swift in Sources */, D0B10018A1B2C3D4E5F60001 /* FileDropOverlayViewTests.swift in Sources */, + FE002104 /* FileExplorerKeyboardActivationTests.swift in Sources */, FE002101 /* FileExplorerRootResolverTests.swift in Sources */, 9F8A6669D6F54F0EA8E8BEB8 /* FileExplorerRootSyncPolicyTests.swift in Sources */, B37A0000000000000000000B /* FileExplorerStateModePersistenceTests.swift in Sources */, diff --git a/cmuxTests/FileExplorerKeyboardActivationTests.swift b/cmuxTests/FileExplorerKeyboardActivationTests.swift new file mode 100644 index 00000000000..eed1f4ad24c --- /dev/null +++ b/cmuxTests/FileExplorerKeyboardActivationTests.swift @@ -0,0 +1,162 @@ +import AppKit +import Testing + +#if canImport(cmux_DEV) +@testable import cmux_DEV +#elseif canImport(cmux) +@testable import cmux +#endif + +@MainActor +@Suite("File explorer keyboard activation") +struct FileExplorerKeyboardActivationTests { + private final class OpenProbe { + var openedPaths: [String] = [] + } + + @Test + func selectedLocalFileOpensThroughSharedActivationPath() { + let file = FileExplorerNode(name: "README.md", path: "/tmp/project/README.md", isDirectory: false) + let probe = OpenProbe() + let (coordinator, outlineView) = makeOutline(nodes: [file], probe: probe) + + outlineView.selectRowIndexes(IndexSet(integer: 0), byExtendingSelection: false) + coordinator.store.select(node: file) + + #expect(coordinator.openSelectedItem(in: outlineView)) + #expect(probe.openedPaths == [file.path]) + } + + @Test + func selectedFolderTogglesExpansionThroughSharedActivationPath() { + let folder = FileExplorerNode(name: "Sources", path: "/tmp/project/Sources", isDirectory: true) + folder.children = [ + FileExplorerNode(name: "App.swift", path: "/tmp/project/Sources/App.swift", isDirectory: false) + ] + let probe = OpenProbe() + let (coordinator, outlineView) = makeOutline(nodes: [folder], probe: probe) + + outlineView.selectRowIndexes(IndexSet(integer: 0), byExtendingSelection: false) + coordinator.store.select(node: folder) + + #expect(coordinator.openSelectedItem(in: outlineView)) + #expect(outlineView.isItemExpanded(folder)) + #expect(coordinator.store.isExpanded(folder)) + + #expect(coordinator.openSelectedItem(in: outlineView)) + #expect(!outlineView.isItemExpanded(folder)) + #expect(!coordinator.store.isExpanded(folder)) + #expect(probe.openedPaths.isEmpty) + } + + @Test + func returnEnterAndConfiguredShortcutMatchOpenSelection() { + let returnEvent = makeKeyEvent(characters: "\r", keyCode: 36) + let keypadEnterEvent = makeKeyEvent(characters: "\r", keyCode: 76) + let downArrow = String(UnicodeScalar(NSDownArrowFunctionKey)!) + let commandDownEvent = makeKeyEvent( + modifierFlags: .command, + characters: downArrow, + keyCode: 125 + ) + let plainDownEvent = makeKeyEvent(characters: downArrow, keyCode: 125) + + #expect(FileExplorerKeyboardActivation.isDefaultOpenEvent(returnEvent)) + #expect(FileExplorerKeyboardActivation.isDefaultOpenEvent(keypadEnterEvent)) + #expect( + FileExplorerKeyboardActivation.matchesOpenSelectionShortcut( + commandDownEvent, + shortcutForAction: { action in + #expect(action == .openFileExplorerSelection) + return action.defaultShortcut + } + ) + ) + #expect(!FileExplorerKeyboardActivation.matchesOpenSelectionShortcut(plainDownEvent)) + } + + @Test + func openSelectionShortcutDefaultsAndMetadata() { + let action = KeyboardShortcutSettings.Action.openFileExplorerSelection + let shortcut = action.defaultShortcut + + #expect(action.label == String(localized: "shortcut.openFileExplorerSelection.label", defaultValue: "File Explorer: Open Selection")) + #expect(shortcut.key == "↓") + #expect(shortcut.command) + #expect(!shortcut.shift) + #expect(!shortcut.option) + #expect(!shortcut.control) + } + + @Test + func settingsVisibleActionsColocateRightSidebarFileExplorerAndFindShortcuts() { + let visibleActions = KeyboardShortcutSettings.settingsVisibleActions + let expectedActions: [KeyboardShortcutSettings.Action] = [ + .focusRightSidebar, + .toggleRightSidebar, + .openFileExplorerSelection, + .findInDirectory, + ] + + guard let startIndex = visibleActions.firstIndex(of: .focusRightSidebar) else { + Issue.record("Toggle Right Sidebar Focus should be visible in keyboard shortcut settings") + return + } + + let endIndex = startIndex + expectedActions.count + guard endIndex <= visibleActions.count else { + Issue.record("Expected shortcut settings to include the full right-sidebar shortcut run") + return + } + #expect(Array(visibleActions[startIndex.. (FileExplorerPanelView.Coordinator, FileExplorerNSOutlineView) { + let store = FileExplorerStore() + store.provider = LocalFileExplorerProvider() + store.rootPath = "/tmp/project" + store.rootNodes = nodes + + let coordinator = FileExplorerPanelView.Coordinator( + store: store, + state: FileExplorerState(), + onOpenFilePreview: { probe.openedPaths.append($0) } + ) + + let outlineView = FileExplorerNSOutlineView() + let column = NSTableColumn(identifier: NSUserInterfaceItemIdentifier("name")) + outlineView.addTableColumn(column) + outlineView.outlineTableColumn = column + outlineView.dataSource = coordinator + outlineView.delegate = coordinator + coordinator.outlineView = outlineView + outlineView.reloadData() + + return (coordinator, outlineView) + } + + private func makeKeyEvent( + modifierFlags: NSEvent.ModifierFlags = [], + characters: String, + keyCode: UInt16 + ) -> NSEvent { + guard let event = NSEvent.keyEvent( + with: .keyDown, + location: .zero, + modifierFlags: modifierFlags, + timestamp: ProcessInfo.processInfo.systemUptime, + windowNumber: 0, + context: nil, + characters: characters, + charactersIgnoringModifiers: characters, + isARepeat: false, + keyCode: keyCode + ) else { + fatalError("Failed to construct key event") + } + return event + } +} diff --git a/cmuxTests/FileExplorerStoreTests.swift b/cmuxTests/FileExplorerStoreTests.swift index 431200221ff..6b980aab18e 100644 --- a/cmuxTests/FileExplorerStoreTests.swift +++ b/cmuxTests/FileExplorerStoreTests.swift @@ -603,124 +603,6 @@ struct FileExplorerStoreTests { } } -@MainActor -@Suite("File explorer keyboard activation") -struct FileExplorerKeyboardActivationTests { - private final class OpenProbe { - var openedPaths: [String] = [] - } - - @Test - func selectedLocalFileOpensThroughSharedActivationPath() { - let file = FileExplorerNode(name: "README.md", path: "/tmp/project/README.md", isDirectory: false) - let probe = OpenProbe() - let (coordinator, outlineView) = makeOutline(nodes: [file], probe: probe) - - outlineView.selectRowIndexes(IndexSet(integer: 0), byExtendingSelection: false) - coordinator.store.select(node: file) - - #expect(coordinator.openSelectedItem(in: outlineView)) - #expect(probe.openedPaths == [file.path]) - } - - @Test - func selectedFolderTogglesExpansionThroughSharedActivationPath() { - let folder = FileExplorerNode(name: "Sources", path: "/tmp/project/Sources", isDirectory: true) - folder.children = [ - FileExplorerNode(name: "App.swift", path: "/tmp/project/Sources/App.swift", isDirectory: false) - ] - let probe = OpenProbe() - let (coordinator, outlineView) = makeOutline(nodes: [folder], probe: probe) - - outlineView.selectRowIndexes(IndexSet(integer: 0), byExtendingSelection: false) - coordinator.store.select(node: folder) - - #expect(coordinator.openSelectedItem(in: outlineView)) - #expect(outlineView.isItemExpanded(folder)) - #expect(coordinator.store.isExpanded(folder)) - - #expect(coordinator.openSelectedItem(in: outlineView)) - #expect(!outlineView.isItemExpanded(folder)) - #expect(!coordinator.store.isExpanded(folder)) - #expect(probe.openedPaths.isEmpty) - } - - @Test - func returnEnterAndConfiguredShortcutMatchOpenSelection() { - let returnEvent = makeKeyEvent(characters: "\r", keyCode: 36) - let keypadEnterEvent = makeKeyEvent(characters: "\r", keyCode: 76) - let downArrow = String(UnicodeScalar(NSDownArrowFunctionKey)!) - let commandDownEvent = makeKeyEvent( - modifierFlags: .command, - characters: downArrow, - keyCode: 125 - ) - let plainDownEvent = makeKeyEvent(characters: downArrow, keyCode: 125) - - #expect(FileExplorerKeyboardActivation.isDefaultOpenEvent(returnEvent)) - #expect(FileExplorerKeyboardActivation.isDefaultOpenEvent(keypadEnterEvent)) - #expect( - FileExplorerKeyboardActivation.matchesOpenSelectionShortcut( - commandDownEvent, - shortcutForAction: { action in - #expect(action == .openFileExplorerSelection) - return action.defaultShortcut - } - ) - ) - #expect(!FileExplorerKeyboardActivation.matchesOpenSelectionShortcut(plainDownEvent)) - } - - private func makeOutline( - nodes: [FileExplorerNode], - probe: OpenProbe - ) -> (FileExplorerPanelView.Coordinator, FileExplorerNSOutlineView) { - let store = FileExplorerStore() - store.provider = LocalFileExplorerProvider() - store.rootPath = "/tmp/project" - store.rootNodes = nodes - - let coordinator = FileExplorerPanelView.Coordinator( - store: store, - state: FileExplorerState(), - onOpenFilePreview: { probe.openedPaths.append($0) } - ) - - let outlineView = FileExplorerNSOutlineView() - let column = NSTableColumn(identifier: NSUserInterfaceItemIdentifier("name")) - outlineView.addTableColumn(column) - outlineView.outlineTableColumn = column - outlineView.dataSource = coordinator - outlineView.delegate = coordinator - coordinator.outlineView = outlineView - outlineView.reloadData() - - return (coordinator, outlineView) - } - - private func makeKeyEvent( - modifierFlags: NSEvent.ModifierFlags = [], - characters: String, - keyCode: UInt16 - ) -> NSEvent { - guard let event = NSEvent.keyEvent( - with: .keyDown, - location: .zero, - modifierFlags: modifierFlags, - timestamp: ProcessInfo.processInfo.systemUptime, - windowNumber: 0, - context: nil, - characters: characters, - charactersIgnoringModifiers: characters, - isARepeat: false, - keyCode: keyCode - ) else { - fatalError("Failed to construct key event") - } - return event - } -} - @MainActor @Suite(.serialized) struct FileSearchControllerTests { diff --git a/cmuxTests/WorkspaceUnitTests.swift b/cmuxTests/WorkspaceUnitTests.swift index 1e1972b30fe..3eaf4612a91 100644 --- a/cmuxTests/WorkspaceUnitTests.swift +++ b/cmuxTests/WorkspaceUnitTests.swift @@ -425,11 +425,6 @@ final class WorkspaceRenameShortcutDefaultsTests: XCTestCase { KeyboardShortcutSettings.Action.toggleRightSidebar.label, String(localized: "shortcut.toggleRightSidebar.label", defaultValue: "Toggle Right Sidebar") ) - XCTAssertEqual( - KeyboardShortcutSettings.Action.openFileExplorerSelection.label, - String(localized: "shortcut.openFileExplorerSelection.label", defaultValue: "File Explorer: Open Selection") - ) - let toggleRightSidebar = KeyboardShortcutSettings.Action.toggleRightSidebar.defaultShortcut XCTAssertEqual(toggleRightSidebar.key, "b") XCTAssertTrue(toggleRightSidebar.command) @@ -437,13 +432,6 @@ final class WorkspaceRenameShortcutDefaultsTests: XCTestCase { XCTAssertTrue(toggleRightSidebar.option) XCTAssertFalse(toggleRightSidebar.control) - let openSelection = KeyboardShortcutSettings.Action.openFileExplorerSelection.defaultShortcut - XCTAssertEqual(openSelection.key, "↓") - XCTAssertTrue(openSelection.command) - XCTAssertFalse(openSelection.shift) - XCTAssertFalse(openSelection.option) - XCTAssertFalse(openSelection.control) - let focusRightSidebar = KeyboardShortcutSettings.Action.focusRightSidebar.defaultShortcut XCTAssertEqual(focusRightSidebar.key, "e") XCTAssertTrue(focusRightSidebar.command) @@ -515,28 +503,6 @@ final class WorkspaceRenameShortcutDefaultsTests: XCTestCase { XCTAssertTrue(KeyboardShortcutSettings.settingsVisibleActions.contains(.markOldestUnreadAndJumpNext)) } - func testSettingsVisibleShortcutActionsColocateRightSidebarFileExplorerAndFindShortcuts() { - let visibleActions = KeyboardShortcutSettings.settingsVisibleActions - let expectedActions: [KeyboardShortcutSettings.Action] = [ - .focusRightSidebar, - .toggleRightSidebar, - .openFileExplorerSelection, - .findInDirectory, - ] - - guard let startIndex = visibleActions.firstIndex(of: .focusRightSidebar) else { - XCTFail("Toggle Right Sidebar Focus should be visible in keyboard shortcut settings") - return - } - - let endIndex = startIndex + expectedActions.count - guard endIndex <= visibleActions.count else { - XCTFail("Expected shortcut settings to include the full right-sidebar shortcut run") - return - } - XCTAssertEqual(Array(visibleActions[startIndex.. Date: Fri, 12 Jun 2026 22:56:03 -0700 Subject: [PATCH 4/7] Prefer outline row for file explorer activation --- Sources/FileExplorerView.swift | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/Sources/FileExplorerView.swift b/Sources/FileExplorerView.swift index 802979f54cc..f37e0eccceb 100644 --- a/Sources/FileExplorerView.swift +++ b/Sources/FileExplorerView.swift @@ -516,17 +516,28 @@ struct FileExplorerPanelView: NSViewRepresentable { } private func resolvedSelectionRow(in outlineView: NSOutlineView) -> Int? { + if let selected = selectedOutlineRow(in: outlineView) { + if store.selectedPath != selected.node.path { + store.select(node: selected.node) + } + return selected.row + } + if let selectedPath = store.selectedPath, let resolution = selectionResolution(for: selectedPath, in: outlineView) { return resolution.row } + + return nil + } + + private func selectedOutlineRow(in outlineView: NSOutlineView) -> (row: Int, node: FileExplorerNode)? { guard outlineView.selectedRow >= 0, outlineView.selectedRow < outlineView.numberOfRows, let node = outlineView.item(atRow: outlineView.selectedRow) as? FileExplorerNode else { return nil } - store.select(node: node) - return outlineView.selectedRow + return (outlineView.selectedRow, node) } private struct SelectionResolution { From d068303cfa1ee8e7e4d9924799922451a7715524 Mon Sep 17 00:00:00 2001 From: Aziz Albahar Date: Fri, 12 Jun 2026 23:02:00 -0700 Subject: [PATCH 5/7] Fix file explorer shortcut test assertion --- .../FileExplorerKeyboardActivationTests.swift | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/cmuxTests/FileExplorerKeyboardActivationTests.swift b/cmuxTests/FileExplorerKeyboardActivationTests.swift index eed1f4ad24c..78200e0c75a 100644 --- a/cmuxTests/FileExplorerKeyboardActivationTests.swift +++ b/cmuxTests/FileExplorerKeyboardActivationTests.swift @@ -60,18 +60,19 @@ struct FileExplorerKeyboardActivationTests { keyCode: 125 ) let plainDownEvent = makeKeyEvent(characters: downArrow, keyCode: 125) + var requestedShortcutAction: KeyboardShortcutSettings.Action? + let configuredShortcutMatches = FileExplorerKeyboardActivation.matchesOpenSelectionShortcut( + commandDownEvent, + shortcutForAction: { action in + requestedShortcutAction = action + return action.defaultShortcut + } + ) #expect(FileExplorerKeyboardActivation.isDefaultOpenEvent(returnEvent)) #expect(FileExplorerKeyboardActivation.isDefaultOpenEvent(keypadEnterEvent)) - #expect( - FileExplorerKeyboardActivation.matchesOpenSelectionShortcut( - commandDownEvent, - shortcutForAction: { action in - #expect(action == .openFileExplorerSelection) - return action.defaultShortcut - } - ) - ) + #expect(configuredShortcutMatches) + #expect(requestedShortcutAction == .openFileExplorerSelection) #expect(!FileExplorerKeyboardActivation.matchesOpenSelectionShortcut(plainDownEvent)) } From ea9e264661a490b7ba02a5baabc2b998ad7c8837 Mon Sep 17 00:00:00 2001 From: Aziz Albahar Date: Fri, 12 Jun 2026 23:11:49 -0700 Subject: [PATCH 6/7] Respect file explorer open shortcut context --- Resources/Localizable.xcstrings | 108 +++++++++++++++++++ Sources/FileExplorerOutlineView.swift | 22 +++- web/app/[locale]/docs/configuration/page.tsx | 6 +- web/app/[locale]/keyboard-shortcuts.tsx | 6 +- web/data/cmux-shortcuts.ts | 53 ++++++++- 5 files changed, 178 insertions(+), 17 deletions(-) diff --git a/Resources/Localizable.xcstrings b/Resources/Localizable.xcstrings index 972e79e80bf..a78989b7b8e 100644 --- a/Resources/Localizable.xcstrings +++ b/Resources/Localizable.xcstrings @@ -1930,17 +1930,125 @@ "shortcut.openFileExplorerSelection.label": { "extractionState": "manual", "localizations": { + "ar": { + "stringUnit": { + "state": "translated", + "value": "مستكشف الملفات: فتح التحديد" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Istraživač datoteka: otvori odabrano" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Filoversigt: Åbn markering" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Dateiexplorer: Auswahl öffnen" + } + }, "en": { "stringUnit": { "state": "translated", "value": "File Explorer: Open Selection" } }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Explorador de archivos: abrir selección" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Explorateur de fichiers : ouvrir la sélection" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Esplora file: apri selezione" + } + }, "ja": { "stringUnit": { "state": "translated", "value": "ファイルエクスプローラ: 選択項目を開く" } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "កម្មវិធីរុករកឯកសារ៖ បើកជម្រើស" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "파일 탐색기: 선택 항목 열기" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Filutforsker: Åpne markering" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Eksplorator plików: otwórz zaznaczenie" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Explorador de arquivos: abrir seleção" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Проводник файлов: открыть выбранное" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "ตัวสำรวจไฟล์: เปิดรายการที่เลือก" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Dosya Gezgini: Seçimi Aç" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Провідник файлів: відкрити вибране" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "文件浏览器:打开所选项" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "檔案瀏覽器:開啟所選項" + } } } }, diff --git a/Sources/FileExplorerOutlineView.swift b/Sources/FileExplorerOutlineView.swift index f27cefe7fda..aee46378dff 100644 --- a/Sources/FileExplorerOutlineView.swift +++ b/Sources/FileExplorerOutlineView.swift @@ -15,7 +15,17 @@ enum FileExplorerKeyboardActivation { _ event: NSEvent, shortcutForAction: (KeyboardShortcutSettings.Action) -> StoredShortcut = KeyboardShortcutSettings.shortcut(for:) ) -> Bool { - isDefaultOpenEvent(event) || shortcutForAction(.openFileExplorerSelection).matches(event: event) + isDefaultOpenEvent(event) || matchesConfiguredOpenSelectionShortcut( + event, + shortcutForAction: shortcutForAction + ) + } + + static func matchesConfiguredOpenSelectionShortcut( + _ event: NSEvent, + shortcutForAction: (KeyboardShortcutSettings.Action) -> StoredShortcut = KeyboardShortcutSettings.shortcut(for:) + ) -> Bool { + shortcutForAction(.openFileExplorerSelection).matches(event: event) } } @@ -146,11 +156,17 @@ final class FileExplorerNSOutlineView: NSOutlineView { } private func handleOpenSelectionShortcut(_ event: NSEvent) -> Bool { - guard FileExplorerKeyboardActivation.matchesOpenSelectionShortcut(event) else { + let isDefaultOpenEvent = FileExplorerKeyboardActivation.isDefaultOpenEvent(event) + guard isDefaultOpenEvent || FileExplorerKeyboardActivation.matchesConfiguredOpenSelectionShortcut(event) else { + return false + } + guard isDefaultOpenEvent || + (AppDelegate.shared?.shortcutWhenClauseAllows(action: .openFileExplorerSelection, event: event) ?? true) else { return false } endQuickSearch() - return fileExplorerCoordinator?.openSelectedItem(in: self) ?? false + _ = fileExplorerCoordinator?.openSelectedItem(in: self) + return true } private func beginQuickSearch() { diff --git a/web/app/[locale]/docs/configuration/page.tsx b/web/app/[locale]/docs/configuration/page.tsx index d9469213fc7..b4b2773842f 100644 --- a/web/app/[locale]/docs/configuration/page.tsx +++ b/web/app/[locale]/docs/configuration/page.tsx @@ -5,7 +5,7 @@ import { Link } from "../../../../i18n/navigation"; import { CodeBlock } from "../../components/code-block"; import { Callout } from "../../components/callout"; import settingsSchema from "../../../../data/cmux.schema.json"; -import { shortcutCategories, type LocalizedText } from "../../../../data/cmux-shortcuts"; +import { localizedText, shortcutCategories } from "../../../../data/cmux-shortcuts"; import { DocsHeading } from "../../components/docs-heading"; type SchemaProperty = { @@ -146,10 +146,6 @@ export async function generateMetadata({ params }: { params: Promise<{ locale: s }; } -function localizedText(text: LocalizedText, locale: string) { - return locale.startsWith("ja") ? text.ja : text.en; -} - function shortcutToConfig(shortcut: { combos: string[][]; configValue?: string }) { if (shortcut.configValue) return shortcut.configValue; return shortcutComboToConfig(shortcut.combos[0] ?? []); diff --git a/web/app/[locale]/keyboard-shortcuts.tsx b/web/app/[locale]/keyboard-shortcuts.tsx index 6072d4e037f..5a0f6d3c809 100644 --- a/web/app/[locale]/keyboard-shortcuts.tsx +++ b/web/app/[locale]/keyboard-shortcuts.tsx @@ -2,11 +2,7 @@ import { useMemo, useState } from "react"; import { useLocale, useTranslations } from "next-intl"; -import { shortcutCategories, type LocalizedText, type Shortcut } from "../../data/cmux-shortcuts"; - -function localizedText(text: LocalizedText, locale: string) { - return locale.startsWith("ja") ? text.ja : text.en; -} +import { localizedText, shortcutCategories, type Shortcut } from "../../data/cmux-shortcuts"; function normalize(s: string) { return s.toLowerCase().replace(/\s+/g, " ").trim(); diff --git a/web/data/cmux-shortcuts.ts b/web/data/cmux-shortcuts.ts index 28acac7f141..321a27acc34 100644 --- a/web/data/cmux-shortcuts.ts +++ b/web/data/cmux-shortcuts.ts @@ -1,7 +1,9 @@ +import type { Locale } from "../i18n/routing"; + export type LocalizedText = { en: string; ja: string; -}; +} & Partial, string>>; export type Shortcut = { id: string; @@ -18,6 +20,10 @@ export type ShortcutCategory = { shortcuts: Shortcut[]; }; +export function localizedText(text: LocalizedText, locale: string) { + return text[locale as Locale] ?? (locale.startsWith("ja") ? text.ja : text.en); +} + export const shortcutCategories: ShortcutCategory[] = [ { id: "app", @@ -119,10 +125,49 @@ export const shortcutCategories: ShortcutCategory[] = [ { id: "openFileExplorerSelection", combos: [["⌘", "↓"]], - description: { en: "Open selected file explorer item", ja: "ファイルエクスプローラの選択項目を開く" }, + description: { + en: "Open selected file explorer item", + ja: "ファイルエクスプローラの選択項目を開く", + "zh-CN": "打开文件浏览器中的所选项", + "zh-TW": "開啟檔案瀏覽器中的所選項", + ko: "파일 탐색기에서 선택한 항목 열기", + de: "Ausgewähltes Dateiexplorer-Element öffnen", + es: "Abrir el elemento seleccionado del explorador de archivos", + fr: "Ouvrir l'élément sélectionné dans l'explorateur de fichiers", + it: "Apri l'elemento selezionato nell'esplora file", + da: "Åbn det valgte element i filoversigten", + pl: "Otwórz zaznaczony element eksploratora plików", + ru: "Открыть выбранный элемент проводника файлов", + bs: "Otvori odabranu stavku istraživača datoteka", + ar: "فتح العنصر المحدد في مستكشف الملفات", + no: "Åpne valgt element i filutforskeren", + "pt-BR": "Abrir o item selecionado do explorador de arquivos", + th: "เปิดรายการที่เลือกในตัวสำรวจไฟล์", + tr: "Dosya gezgininde seçili öğeyi aç", + km: "បើកធាតុដែលបានជ្រើសក្នុងកម្មវិធីរុករកឯកសារ", + uk: "Відкрити вибраний елемент провідника файлів", + }, note: { - en: "Return and keypad Enter also open the selected file while the file explorer is focused.", - ja: "ファイルエクスプローラにフォーカスがある間は、Return とテンキー Enter でも選択中のファイルを開けます。", + en: "Return and keypad Enter also open files or toggle folders while the file explorer is focused.", + ja: "ファイルエクスプローラにフォーカスがある間は、Return とテンキー Enter でもファイルを開くかフォルダを展開/折りたたみできます。", + "zh-CN": "文件浏览器获得焦点时,Return 和数字键盘 Enter 也会打开文件或切换文件夹展开状态。", + "zh-TW": "檔案瀏覽器取得焦點時,Return 和數字鍵盤 Enter 也會開啟檔案或切換資料夾展開狀態。", + ko: "파일 탐색기에 포커스가 있을 때 Return 및 숫자 키패드 Enter도 파일을 열거나 폴더 펼침 상태를 전환합니다.", + de: "Wenn der Dateiexplorer fokussiert ist, öffnen Return und Ziffernblock-Enter ebenfalls Dateien oder klappen Ordner um.", + es: "Cuando el explorador de archivos tiene el foco, Return y Enter del teclado numérico también abren archivos o alternan carpetas.", + fr: "Lorsque l'explorateur de fichiers a le focus, Return et Entrée du pavé numérique ouvrent aussi les fichiers ou replient/déplient les dossiers.", + it: "Quando l'esplora file ha il focus, Return e Invio del tastierino aprono anche i file o espandono/comprimono le cartelle.", + da: "Når filoversigten har fokus, åbner Return og Enter på det numeriske tastatur også filer eller slår mapper til/fra.", + pl: "Gdy eksplorator plików ma fokus, Return i Enter z klawiatury numerycznej także otwierają pliki lub przełączają foldery.", + ru: "Когда проводник файлов в фокусе, Return и Enter на цифровой клавиатуре также открывают файлы или переключают папки.", + bs: "Kada je istraživač datoteka u fokusu, Return i Enter na numeričkoj tastaturi također otvaraju datoteke ili prebacuju mape.", + ar: "عندما يكون مستكشف الملفات في التركيز، يفتح Return وEnter في لوحة الأرقام الملفات أيضًا أو يبدلان حالة المجلدات.", + no: "Når filutforskeren har fokus, åpner Return og Enter på talltastaturet også filer eller veksler mapper.", + "pt-BR": "Quando o explorador de arquivos está em foco, Return e Enter do teclado numérico também abrem arquivos ou alternam pastas.", + th: "เมื่อตัวสำรวจไฟล์มีโฟกัส Return และ Enter บนแป้นตัวเลขจะเปิดไฟล์หรือสลับการขยายโฟลเดอร์ได้ด้วย", + tr: "Dosya gezgini odaktayken Return ve sayısal klavye Enter da dosyaları açar veya klasörleri açıp kapatır.", + km: "នៅពេលកម្មវិធីរុករកឯកសារមានផ្តោត Return និង Enter លើក្តារលេខក៏បើកឯកសារ ឬប្តូរថតឱ្យបង្ហាញ/លាក់បានដែរ។", + uk: "Коли провідник файлів у фокусі, Return і Enter на цифровій клавіатурі також відкривають файли або перемикають папки.", }, }, { From 54d53f411bd3314708c09a5845dc5dff17f707ad Mon Sep 17 00:00:00 2001 From: Aziz Albahar Date: Fri, 12 Jun 2026 23:26:12 -0700 Subject: [PATCH 7/7] Run CI display setup before AppKit startup probe --- .github/workflows/ci.yml | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a990f6624a5..00231e07b32 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -631,6 +631,17 @@ jobs: - name: Validate Swift warning budget run: python3 scripts/swift_warning_budget.py --log /tmp/cmux-build-output.txt + - name: Create virtual display + run: | + set -euo pipefail + clang -framework Foundation -framework CoreGraphics \ + -o /tmp/create-virtual-display scripts/create-virtual-display.m + /tmp/create-virtual-display & + VDISPLAY_PID=$! + echo "VDISPLAY_PID=$VDISPLAY_PID" >> "$GITHUB_ENV" + sleep 3 + kill -0 "$VDISPLAY_PID" + - name: Run CoreAnimation main-thread startup regression run: | set -euo pipefail @@ -650,17 +661,6 @@ jobs: CMUX_TAG="ci-ca-main-thread" \ ./scripts/verify-main-thread-ca-transactions.sh "$APP" - - name: Create virtual display - run: | - set -euo pipefail - clang -framework Foundation -framework CoreGraphics \ - -o /tmp/create-virtual-display scripts/create-virtual-display.m - /tmp/create-virtual-display & - VDISPLAY_PID=$! - echo "VDISPLAY_PID=$VDISPLAY_PID" >> "$GITHUB_ENV" - sleep 3 - kill -0 "$VDISPLAY_PID" - - name: Run workspace churn typing-lag regression run: | set -euo pipefail