From 90ecd13976003ad2e9aa39cb2619c944ae886651 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Wed, 13 May 2026 12:51:48 -0700 Subject: [PATCH 1/8] Make open-with palette commands bindable --- .../App/TerminalDirectoryOpenSupport.swift | 126 ++++++++++++++++++ Sources/AppDelegate.swift | 19 +++ ...ntentView+RightSidebarCommandPalette.swift | 4 + Sources/ContentView.swift | 37 +---- Sources/KeyboardShortcutSettings.swift | 46 +++++++ ...andPaletteShortcutCustomizationTests.swift | 34 +++++ web/data/cmux-shortcuts.ts | 32 +++++ web/data/cmux.schema.json | 15 +++ 8 files changed, 277 insertions(+), 36 deletions(-) diff --git a/Sources/App/TerminalDirectoryOpenSupport.swift b/Sources/App/TerminalDirectoryOpenSupport.swift index 81ad90d19248..ab136d1cb6d2 100644 --- a/Sources/App/TerminalDirectoryOpenSupport.swift +++ b/Sources/App/TerminalDirectoryOpenSupport.swift @@ -96,6 +96,12 @@ enum TerminalDirectoryOpenTarget: String, CaseIterable { Array(allCases) } + init?(commandPaletteCommandId: String) { + let prefix = "palette.terminalOpenDirectory." + guard commandPaletteCommandId.hasPrefix(prefix) else { return nil } + self.init(rawValue: String(commandPaletteCommandId.dropFirst(prefix.count))) + } + static func availableTargets(in environment: DetectionEnvironment = .live) -> Set { Set(commandPaletteShortcutTargets.filter { $0.isAvailable(in: environment) }) } @@ -293,6 +299,126 @@ enum TerminalDirectoryOpenTarget: String, CaseIterable { } } +@MainActor +enum TerminalDirectoryOpenLauncher { + static func currentDirectoryURL(in tabManager: TabManager?) -> URL? { + guard let workspace = tabManager?.selectedWorkspace else { return nil } + let focusedPanelDirectory = workspace.focusedPanelId.flatMap { workspace.panelDirectories[$0] } + for rawDirectory in [focusedPanelDirectory, workspace.currentDirectory] { + let trimmed = rawDirectory?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + guard !trimmed.isEmpty else { continue } + guard FileManager.default.fileExists(atPath: trimmed) else { continue } + return URL(fileURLWithPath: trimmed, isDirectory: true) + } + return nil + } + + static func openCurrentDirectory( + in target: TerminalDirectoryOpenTarget, + tabManager: TabManager? + ) -> Bool { + guard let directoryURL = currentDirectoryURL(in: tabManager) else { return false } + return openDirectory(directoryURL, in: target, tabManager: tabManager) + } + + static func openDirectory( + _ directoryURL: URL, + in target: TerminalDirectoryOpenTarget, + tabManager: TabManager? + ) -> Bool { + switch target { + case .finder: + NSWorkspace.shared.selectFile(nil, inFileViewerRootedAtPath: directoryURL.path) + return true + case .vscodeInline: + return AppDelegate.shared?.openDirectoryInInlineVSCode(directoryURL, tabManager: tabManager) ?? false + default: + guard let applicationURL = target.applicationURL() else { return false } + let configuration = NSWorkspace.OpenConfiguration() + NSWorkspace.shared.open([directoryURL], withApplicationAt: applicationURL, configuration: configuration) + return true + } + } +} + +extension KeyboardShortcutSettings.Action { + static var terminalDirectoryOpenActions: [Self] { + TerminalDirectoryOpenTarget.commandPaletteShortcutTargets.compactMap(terminalDirectoryOpenAction(for:)) + } + + static func terminalDirectoryOpenAction(for target: TerminalDirectoryOpenTarget) -> Self? { + switch target { + case .androidStudio: + return .terminalOpenDirectoryAndroidStudio + case .antigravity: + return .terminalOpenDirectoryAntigravity + case .cursor: + return .terminalOpenDirectoryCursor + case .finder: + return .terminalOpenDirectoryFinder + case .ghostty: + return .terminalOpenDirectoryGhostty + case .intellij: + return .terminalOpenDirectoryIntelliJ + case .iterm2: + return .terminalOpenDirectoryITerm2 + case .terminal: + return .terminalOpenDirectoryTerminal + case .tower: + return .terminalOpenDirectoryTower + case .vscode: + return .terminalOpenDirectoryVSCode + case .vscodeInline: + return .terminalOpenDirectoryVSCodeInline + case .warp: + return .terminalOpenDirectoryWarp + case .windsurf: + return .terminalOpenDirectoryWindsurf + case .xcode: + return .terminalOpenDirectoryXcode + case .zed: + return .terminalOpenDirectoryZed + } + } + + var terminalDirectoryOpenTarget: TerminalDirectoryOpenTarget? { + switch self { + case .terminalOpenDirectoryAndroidStudio: + return .androidStudio + case .terminalOpenDirectoryAntigravity: + return .antigravity + case .terminalOpenDirectoryCursor: + return .cursor + case .terminalOpenDirectoryFinder: + return .finder + case .terminalOpenDirectoryGhostty: + return .ghostty + case .terminalOpenDirectoryIntelliJ: + return .intellij + case .terminalOpenDirectoryITerm2: + return .iterm2 + case .terminalOpenDirectoryTerminal: + return .terminal + case .terminalOpenDirectoryTower: + return .tower + case .terminalOpenDirectoryVSCode: + return .vscode + case .terminalOpenDirectoryVSCodeInline: + return .vscodeInline + case .terminalOpenDirectoryWarp: + return .warp + case .terminalOpenDirectoryWindsurf: + return .windsurf + case .terminalOpenDirectoryXcode: + return .xcode + case .terminalOpenDirectoryZed: + return .zed + default: + return nil + } + } +} + enum VSCodeServeWebURLBuilder { static func extractWebUIURL(from output: String) -> URL? { let prefix = "Web UI available at " diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index 0afdc615a327..9fea5008c9ba 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -11502,6 +11502,10 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent return true } + if handleTerminalDirectoryOpenShortcut(event: event) { + return true + } + if handleConfiguredCmuxShortcut( event: event, actions: configuredCmuxShortcutActions, @@ -13056,6 +13060,21 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent ) } + private func handleTerminalDirectoryOpenShortcut(event: NSEvent) -> Bool { + for action in KeyboardShortcutSettings.Action.terminalDirectoryOpenActions { + guard matchConfiguredShortcut(event: event, action: action), + let target = action.terminalDirectoryOpenTarget else { + continue + } + let targetTabManager = preferredMainWindowContextForShortcutRouting(event: event)?.tabManager ?? tabManager + if !TerminalDirectoryOpenLauncher.openCurrentDirectory(in: target, tabManager: targetTabManager) { + NSSound.beep() + } + return true + } + return false + } + @discardableResult private func executeConfiguredCmuxAction( _ action: CmuxResolvedConfigAction, diff --git a/Sources/ContentView+RightSidebarCommandPalette.swift b/Sources/ContentView+RightSidebarCommandPalette.swift index 918bbffb0da7..afa8c2691a31 100644 --- a/Sources/ContentView+RightSidebarCommandPalette.swift +++ b/Sources/ContentView+RightSidebarCommandPalette.swift @@ -2,6 +2,10 @@ import AppKit extension ContentView { static func commandPaletteShortcutAction(forCommandID commandId: String) -> KeyboardShortcutSettings.Action? { + if let target = TerminalDirectoryOpenTarget(commandPaletteCommandId: commandId) { + return KeyboardShortcutSettings.Action.terminalDirectoryOpenAction(for: target) + } + if let rightSidebarModeAction = commandPaletteRightSidebarModeShortcutAction(forCommandID: commandId) { return rightSidebarModeAction } diff --git a/Sources/ContentView.swift b/Sources/ContentView.swift index d7962fa66af4..4996277ebb88 100644 --- a/Sources/ContentView.swift +++ b/Sources/ContentView.swift @@ -8847,27 +8847,7 @@ struct ContentView: View { } private func openFocusedDirectory(in target: TerminalDirectoryOpenTarget) -> Bool { - guard let directoryURL = focusedTerminalDirectoryURL() else { return false } - return openFocusedDirectory(directoryURL, in: target) - } - - private func openFocusedDirectory(_ directoryURL: URL, in target: TerminalDirectoryOpenTarget) -> Bool { - switch target { - case .finder: - NSWorkspace.shared.selectFile(nil, inFileViewerRootedAtPath: directoryURL.path) - return true - case .vscodeInline: - return openFocusedDirectoryInInlineVSCode(directoryURL) - default: - guard let applicationURL = target.applicationURL() else { return false } - let configuration = NSWorkspace.OpenConfiguration() - NSWorkspace.shared.open([directoryURL], withApplicationAt: applicationURL, configuration: configuration) - return true - } - } - - private func openFocusedDirectoryInInlineVSCode(_ directoryURL: URL) -> Bool { - AppDelegate.shared?.openDirectoryInInlineVSCode(directoryURL, tabManager: tabManager) ?? false + TerminalDirectoryOpenLauncher.openCurrentDirectory(in: target, tabManager: tabManager) } private func stopInlineVSCodeServeWeb() { @@ -8886,21 +8866,6 @@ struct ContentView: View { return true } - private func focusedTerminalDirectoryURL() -> URL? { - guard let workspace = tabManager.selectedWorkspace else { return nil } - let rawDirectory: String = { - if let focusedPanelId = workspace.focusedPanelId, - let directory = workspace.panelDirectories[focusedPanelId] { - return directory - } - return workspace.currentDirectory - }() - let trimmed = rawDirectory.trimmingCharacters(in: .whitespacesAndNewlines) - guard !trimmed.isEmpty else { return nil } - guard FileManager.default.fileExists(atPath: trimmed) else { return nil } - return URL(fileURLWithPath: trimmed, isDirectory: true) - } - #if DEBUG private func debugShortWorkspaceId(_ id: UUID?) -> String { guard let id else { return "nil" } diff --git a/Sources/KeyboardShortcutSettings.swift b/Sources/KeyboardShortcutSettings.swift index 2ea912c3767d..cbd6eb4c14a0 100644 --- a/Sources/KeyboardShortcutSettings.swift +++ b/Sources/KeyboardShortcutSettings.swift @@ -72,6 +72,21 @@ enum KeyboardShortcutSettings { case toggleSidebar case newTab case openFolder + case terminalOpenDirectoryAndroidStudio = "palette.terminalOpenDirectory.androidStudio" + case terminalOpenDirectoryAntigravity = "palette.terminalOpenDirectory.antigravity" + case terminalOpenDirectoryCursor = "palette.terminalOpenDirectory.cursor" + case terminalOpenDirectoryFinder = "palette.terminalOpenDirectory.finder" + case terminalOpenDirectoryGhostty = "palette.terminalOpenDirectory.ghostty" + case terminalOpenDirectoryIntelliJ = "palette.terminalOpenDirectory.intellij" + case terminalOpenDirectoryITerm2 = "palette.terminalOpenDirectory.iterm2" + case terminalOpenDirectoryTerminal = "palette.terminalOpenDirectory.terminal" + case terminalOpenDirectoryTower = "palette.terminalOpenDirectory.tower" + case terminalOpenDirectoryVSCode = "palette.terminalOpenDirectory.vscode" + case terminalOpenDirectoryVSCodeInline = "palette.terminalOpenDirectory.vscodeInline" + case terminalOpenDirectoryWarp = "palette.terminalOpenDirectory.warp" + case terminalOpenDirectoryWindsurf = "palette.terminalOpenDirectory.windsurf" + case terminalOpenDirectoryXcode = "palette.terminalOpenDirectory.xcode" + case terminalOpenDirectoryZed = "palette.terminalOpenDirectory.zed" case reopenPreviousSession case goToWorkspace case commandPalette @@ -155,6 +170,21 @@ enum KeyboardShortcutSettings { case .toggleSidebar: return String(localized: "shortcut.toggleLeftSidebar.label", defaultValue: "Toggle Left Sidebar") case .newTab: return String(localized: "shortcut.newWorkspace.label", defaultValue: "New Workspace") case .openFolder: return String(localized: "shortcut.openFolder.label", defaultValue: "Open Folder") + case .terminalOpenDirectoryAndroidStudio: return TerminalDirectoryOpenTarget.androidStudio.commandPaletteTitle + case .terminalOpenDirectoryAntigravity: return TerminalDirectoryOpenTarget.antigravity.commandPaletteTitle + case .terminalOpenDirectoryCursor: return TerminalDirectoryOpenTarget.cursor.commandPaletteTitle + case .terminalOpenDirectoryFinder: return TerminalDirectoryOpenTarget.finder.commandPaletteTitle + case .terminalOpenDirectoryGhostty: return TerminalDirectoryOpenTarget.ghostty.commandPaletteTitle + case .terminalOpenDirectoryIntelliJ: return TerminalDirectoryOpenTarget.intellij.commandPaletteTitle + case .terminalOpenDirectoryITerm2: return TerminalDirectoryOpenTarget.iterm2.commandPaletteTitle + case .terminalOpenDirectoryTerminal: return TerminalDirectoryOpenTarget.terminal.commandPaletteTitle + case .terminalOpenDirectoryTower: return TerminalDirectoryOpenTarget.tower.commandPaletteTitle + case .terminalOpenDirectoryVSCode: return TerminalDirectoryOpenTarget.vscode.commandPaletteTitle + case .terminalOpenDirectoryVSCodeInline: return TerminalDirectoryOpenTarget.vscodeInline.commandPaletteTitle + case .terminalOpenDirectoryWarp: return TerminalDirectoryOpenTarget.warp.commandPaletteTitle + case .terminalOpenDirectoryWindsurf: return TerminalDirectoryOpenTarget.windsurf.commandPaletteTitle + case .terminalOpenDirectoryXcode: return TerminalDirectoryOpenTarget.xcode.commandPaletteTitle + case .terminalOpenDirectoryZed: return TerminalDirectoryOpenTarget.zed.commandPaletteTitle case .reopenPreviousSession: return String(localized: "shortcut.reopenPreviousSession.label", defaultValue: "Reopen Previous Session") case .goToWorkspace: return String(localized: "menu.file.goToWorkspace", defaultValue: "Go to Workspace…") case .commandPalette: return String(localized: "menu.file.commandPalette", defaultValue: "Command Palette…") @@ -262,6 +292,22 @@ enum KeyboardShortcutSettings { return StoredShortcut(key: "n", command: true, shift: false, option: false, control: false) case .openFolder: return StoredShortcut(key: "o", command: true, shift: false, option: false, control: false) + case .terminalOpenDirectoryAndroidStudio, + .terminalOpenDirectoryAntigravity, + .terminalOpenDirectoryCursor, + .terminalOpenDirectoryFinder, + .terminalOpenDirectoryGhostty, + .terminalOpenDirectoryIntelliJ, + .terminalOpenDirectoryITerm2, + .terminalOpenDirectoryTerminal, + .terminalOpenDirectoryTower, + .terminalOpenDirectoryVSCode, + .terminalOpenDirectoryVSCodeInline, + .terminalOpenDirectoryWarp, + .terminalOpenDirectoryWindsurf, + .terminalOpenDirectoryXcode, + .terminalOpenDirectoryZed: + return .unbound case .reopenPreviousSession: return StoredShortcut(key: "o", command: true, shift: true, option: false, control: false) case .goToWorkspace: diff --git a/cmuxTests/CommandPaletteShortcutCustomizationTests.swift b/cmuxTests/CommandPaletteShortcutCustomizationTests.swift index c7364517404b..628d8e916e93 100644 --- a/cmuxTests/CommandPaletteShortcutCustomizationTests.swift +++ b/cmuxTests/CommandPaletteShortcutCustomizationTests.swift @@ -53,6 +53,40 @@ final class CommandPaletteShortcutCustomizationTests: XCTestCase { } } + func testTerminalDirectoryOpenPaletteCommandsExposeBindableShortcutActions() { + let actions = KeyboardShortcutSettings.Action.terminalDirectoryOpenActions + + XCTAssertEqual(actions.count, TerminalDirectoryOpenTarget.commandPaletteShortcutTargets.count) + for target in TerminalDirectoryOpenTarget.commandPaletteShortcutTargets { + let action = KeyboardShortcutSettings.Action.terminalDirectoryOpenAction(for: target) + XCTAssertEqual(action?.rawValue, target.commandPaletteCommandId) + XCTAssertEqual(ContentView.commandPaletteShortcutAction(forCommandID: target.commandPaletteCommandId), action) + XCTAssertEqual(action?.terminalDirectoryOpenTarget, target) + XCTAssertEqual(action?.defaultShortcut, .unbound) + XCTAssertTrue(action?.isPublicShortcutAction ?? false) + } + } + + func testSettingsFileParsesTerminalDirectoryOpenPaletteShortcutBinding() throws { + let settingsFileURL = settingsDirectoryURL.appendingPathComponent("cmux.json", isDirectory: false) + try """ + { + "shortcuts": { + "bindings": { + "palette.terminalOpenDirectory.vscode": "cmd+shift+v" + } + } + } + """.write(to: settingsFileURL, atomically: true, encoding: .utf8) + + KeyboardShortcutSettings.settingsFileStore.reload() + + XCTAssertEqual( + KeyboardShortcutSettings.shortcut(for: .terminalOpenDirectoryVSCode), + StoredShortcut(key: "v", command: true, shift: true, option: false, control: false) + ) + } + func testFieldEditorMoveCommandHonorsClearedCommandPalettePreviousShortcut() { guard let controlPEvent = makeKeyDownEvent( key: "\u{10}", diff --git a/web/data/cmux-shortcuts.ts b/web/data/cmux-shortcuts.ts index f7baffdb84df..0e44edefdb7e 100644 --- a/web/data/cmux-shortcuts.ts +++ b/web/data/cmux-shortcuts.ts @@ -17,6 +17,17 @@ export type ShortcutCategory = { shortcuts: Shortcut[]; }; +const unboundByDefaultNote: LocalizedText = { en: "unbound by default", ja: "デフォルトでは未割り当て" }; + +function openWithShortcut(id: string, en: string, ja: string): Shortcut { + return { + id, + combos: [], + description: { en, ja }, + note: unboundByDefaultNote, + }; +} + export const shortcutCategories: ShortcutCategory[] = [ { id: "app", @@ -180,6 +191,27 @@ export const shortcutCategories: ShortcutCategory[] = [ { id: "useSelectionForFind", combos: [["⌘", "E"]], description: { en: "Use selection for find", ja: "選択範囲で検索" } }, ], }, + { + id: "open-with", + titleKey: "terminal", + shortcuts: [ + openWithShortcut("palette.terminalOpenDirectory.androidStudio", "Open current directory in Android Studio", "現在のディレクトリをAndroid Studioで開く"), + openWithShortcut("palette.terminalOpenDirectory.antigravity", "Open current directory in Antigravity", "現在のディレクトリをAntigravityで開く"), + openWithShortcut("palette.terminalOpenDirectory.cursor", "Open current directory in Cursor", "現在のディレクトリをCursorで開く"), + openWithShortcut("palette.terminalOpenDirectory.finder", "Open current directory in Finder", "現在のディレクトリをFinderで開く"), + openWithShortcut("palette.terminalOpenDirectory.ghostty", "Open current directory in Ghostty", "現在のディレクトリをGhosttyで開く"), + openWithShortcut("palette.terminalOpenDirectory.intellij", "Open current directory in IntelliJ IDEA", "現在のディレクトリをIntelliJ IDEAで開く"), + openWithShortcut("palette.terminalOpenDirectory.iterm2", "Open current directory in iTerm2", "現在のディレクトリをiTerm2で開く"), + openWithShortcut("palette.terminalOpenDirectory.terminal", "Open current directory in Terminal", "現在のディレクトリをTerminalで開く"), + openWithShortcut("palette.terminalOpenDirectory.tower", "Open current directory in Tower", "現在のディレクトリをTowerで開く"), + openWithShortcut("palette.terminalOpenDirectory.vscode", "Open current directory in VS Code", "現在のディレクトリをVS Codeで開く"), + openWithShortcut("palette.terminalOpenDirectory.vscodeInline", "Open current directory in VS Code (Inline)", "現在のディレクトリをVS Code (Inline)で開く"), + openWithShortcut("palette.terminalOpenDirectory.warp", "Open current directory in Warp", "現在のディレクトリをWarpで開く"), + openWithShortcut("palette.terminalOpenDirectory.windsurf", "Open current directory in Windsurf", "現在のディレクトリをWindsurfで開く"), + openWithShortcut("palette.terminalOpenDirectory.xcode", "Open current directory in Xcode", "現在のディレクトリをXcodeで開く"), + openWithShortcut("palette.terminalOpenDirectory.zed", "Open current directory in Zed", "現在のディレクトリをZedで開く"), + ], + }, { id: "notifications", titleKey: "notifications", diff --git a/web/data/cmux.schema.json b/web/data/cmux.schema.json index 74b00371abf3..d03748722ce3 100644 --- a/web/data/cmux.schema.json +++ b/web/data/cmux.schema.json @@ -778,6 +778,21 @@ "toggleSidebar", "newTab", "openFolder", + "palette.terminalOpenDirectory.androidStudio", + "palette.terminalOpenDirectory.antigravity", + "palette.terminalOpenDirectory.cursor", + "palette.terminalOpenDirectory.finder", + "palette.terminalOpenDirectory.ghostty", + "palette.terminalOpenDirectory.intellij", + "palette.terminalOpenDirectory.iterm2", + "palette.terminalOpenDirectory.terminal", + "palette.terminalOpenDirectory.tower", + "palette.terminalOpenDirectory.vscode", + "palette.terminalOpenDirectory.vscodeInline", + "palette.terminalOpenDirectory.warp", + "palette.terminalOpenDirectory.windsurf", + "palette.terminalOpenDirectory.xcode", + "palette.terminalOpenDirectory.zed", "reopenPreviousSession", "goToWorkspace", "commandPalette", From 4832bc0cf6dbcd55a5a8b3f47387b841000a4212 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Wed, 13 May 2026 12:58:25 -0700 Subject: [PATCH 2/8] Address open-with directory launch review feedback --- Sources/App/TerminalDirectoryOpenSupport.swift | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/Sources/App/TerminalDirectoryOpenSupport.swift b/Sources/App/TerminalDirectoryOpenSupport.swift index ab136d1cb6d2..5adf527ab443 100644 --- a/Sources/App/TerminalDirectoryOpenSupport.swift +++ b/Sources/App/TerminalDirectoryOpenSupport.swift @@ -304,13 +304,15 @@ enum TerminalDirectoryOpenLauncher { static func currentDirectoryURL(in tabManager: TabManager?) -> URL? { guard let workspace = tabManager?.selectedWorkspace else { return nil } let focusedPanelDirectory = workspace.focusedPanelId.flatMap { workspace.panelDirectories[$0] } - for rawDirectory in [focusedPanelDirectory, workspace.currentDirectory] { - let trimmed = rawDirectory?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" - guard !trimmed.isEmpty else { continue } - guard FileManager.default.fileExists(atPath: trimmed) else { continue } - return URL(fileURLWithPath: trimmed, isDirectory: true) + let rawDirectory = focusedPanelDirectory ?? workspace.currentDirectory + let trimmed = rawDirectory.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmed.isEmpty else { return nil } + var isDirectory: ObjCBool = false + guard FileManager.default.fileExists(atPath: trimmed, isDirectory: &isDirectory), + isDirectory.boolValue else { + return nil } - return nil + return URL(fileURLWithPath: trimmed, isDirectory: true) } static func openCurrentDirectory( @@ -328,8 +330,7 @@ enum TerminalDirectoryOpenLauncher { ) -> Bool { switch target { case .finder: - NSWorkspace.shared.selectFile(nil, inFileViewerRootedAtPath: directoryURL.path) - return true + return NSWorkspace.shared.selectFile(nil, inFileViewerRootedAtPath: directoryURL.path) case .vscodeInline: return AppDelegate.shared?.openDirectoryInInlineVSCode(directoryURL, tabManager: tabManager) ?? false default: From b04c227a4c66075c29b5ab8aec9a24f9945f41ac Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Wed, 13 May 2026 13:41:06 -0700 Subject: [PATCH 3/8] Address open-with shortcut review feedback --- .../App/TerminalDirectoryOpenSupport.swift | 35 +------------------ web/data/cmux-shortcuts.ts | 2 +- web/messages/en.json | 1 + web/messages/ja.json | 1 + 4 files changed, 4 insertions(+), 35 deletions(-) diff --git a/Sources/App/TerminalDirectoryOpenSupport.swift b/Sources/App/TerminalDirectoryOpenSupport.swift index 5adf527ab443..2801943f693f 100644 --- a/Sources/App/TerminalDirectoryOpenSupport.swift +++ b/Sources/App/TerminalDirectoryOpenSupport.swift @@ -383,40 +383,7 @@ extension KeyboardShortcutSettings.Action { } var terminalDirectoryOpenTarget: TerminalDirectoryOpenTarget? { - switch self { - case .terminalOpenDirectoryAndroidStudio: - return .androidStudio - case .terminalOpenDirectoryAntigravity: - return .antigravity - case .terminalOpenDirectoryCursor: - return .cursor - case .terminalOpenDirectoryFinder: - return .finder - case .terminalOpenDirectoryGhostty: - return .ghostty - case .terminalOpenDirectoryIntelliJ: - return .intellij - case .terminalOpenDirectoryITerm2: - return .iterm2 - case .terminalOpenDirectoryTerminal: - return .terminal - case .terminalOpenDirectoryTower: - return .tower - case .terminalOpenDirectoryVSCode: - return .vscode - case .terminalOpenDirectoryVSCodeInline: - return .vscodeInline - case .terminalOpenDirectoryWarp: - return .warp - case .terminalOpenDirectoryWindsurf: - return .windsurf - case .terminalOpenDirectoryXcode: - return .xcode - case .terminalOpenDirectoryZed: - return .zed - default: - return nil - } + TerminalDirectoryOpenTarget(commandPaletteCommandId: rawValue) } } diff --git a/web/data/cmux-shortcuts.ts b/web/data/cmux-shortcuts.ts index 0e44edefdb7e..085f090e885a 100644 --- a/web/data/cmux-shortcuts.ts +++ b/web/data/cmux-shortcuts.ts @@ -193,7 +193,7 @@ export const shortcutCategories: ShortcutCategory[] = [ }, { id: "open-with", - titleKey: "terminal", + titleKey: "openWith", shortcuts: [ openWithShortcut("palette.terminalOpenDirectory.androidStudio", "Open current directory in Android Studio", "現在のディレクトリをAndroid Studioで開く"), openWithShortcut("palette.terminalOpenDirectory.antigravity", "Open current directory in Antigravity", "現在のディレクトリをAntigravityで開く"), diff --git a/web/messages/en.json b/web/messages/en.json index 729227e6bbf1..1626995a3449 100644 --- a/web/messages/en.json +++ b/web/messages/en.json @@ -541,6 +541,7 @@ "notifications": "Notifications", "find": "Find", "terminal": "Terminal", + "openWith": "Open With", "window": "Window" }, "sc": { diff --git a/web/messages/ja.json b/web/messages/ja.json index 103134d2ff5d..59508c8297ec 100644 --- a/web/messages/ja.json +++ b/web/messages/ja.json @@ -433,6 +433,7 @@ "notifications": "通知", "find": "検索", "terminal": "ターミナル", + "openWith": "アプリで開く", "window": "ウィンドウ" }, "sc": { From 4aacabce1ae4460654e067e14f2c04f1e126dda1 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Thu, 14 May 2026 16:54:31 -0700 Subject: [PATCH 4/8] Refine open-with shortcut routing --- GhosttyTabs.xcodeproj/project.pbxproj | 8 ++ ...ortcutSettings+TerminalDirectoryOpen.swift | 58 ++++++++++++ .../App/TerminalDirectoryOpenLauncher.swift | 45 ++++++++++ .../App/TerminalDirectoryOpenSupport.swift | 88 ------------------- Sources/AppDelegate.swift | 7 +- ...andPaletteShortcutCustomizationTests.swift | 11 ++- 6 files changed, 121 insertions(+), 96 deletions(-) create mode 100644 Sources/App/KeyboardShortcutSettings+TerminalDirectoryOpen.swift create mode 100644 Sources/App/TerminalDirectoryOpenLauncher.swift diff --git a/GhosttyTabs.xcodeproj/project.pbxproj b/GhosttyTabs.xcodeproj/project.pbxproj index bfb8b23e6750..af4f9a16176c 100644 --- a/GhosttyTabs.xcodeproj/project.pbxproj +++ b/GhosttyTabs.xcodeproj/project.pbxproj @@ -156,6 +156,8 @@ B84BD7AD94EE485B8DDAB6FF /* GhosttySurfaceConfigurationRefresh.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DAB808A8EC74C40B95F7672 /* GhosttySurfaceConfigurationRefresh.swift */; }; C4041001000000000000001B /* CommandClickFileOpenRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4041001000000000000001A /* CommandClickFileOpenRouter.swift */; }; 0D56BE882EAD4B67AC44F96D /* TerminalDirectoryOpenSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B8E2E03F4A64C61B729CF19 /* TerminalDirectoryOpenSupport.swift */; }; + C41110010000000000000001 /* TerminalDirectoryOpenLauncher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41110010000000000000002 /* TerminalDirectoryOpenLauncher.swift */; }; + C41110020000000000000001 /* KeyboardShortcutSettings+TerminalDirectoryOpen.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41110020000000000000002 /* KeyboardShortcutSettings+TerminalDirectoryOpen.swift */; }; A72C9F4179B54DF38E99A021 /* CmuxCLIPathInstaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A4FE96C3F394FC6A6D4B018 /* CmuxCLIPathInstaller.swift */; }; E8BA79E246754A8B99A0B823 /* ScreenIdentity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47D5AA7D29C94F5CA865B2BF /* ScreenIdentity.swift */; }; C1713002C1713002C1713002 /* CommandPaletteShortcutRouting.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1713001C1713001C1713001 /* CommandPaletteShortcutRouting.swift */; }; @@ -648,6 +650,8 @@ 38A09EB2E92203B2E95923A7 /* CommandPaletteSearch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandPalette/CommandPaletteSearch.swift; sourceTree = ""; }; 9DAB808A8EC74C40B95F7672 /* GhosttySurfaceConfigurationRefresh.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/GhosttySurfaceConfigurationRefresh.swift; sourceTree = ""; }; 6B8E2E03F4A64C61B729CF19 /* TerminalDirectoryOpenSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/TerminalDirectoryOpenSupport.swift; sourceTree = ""; }; + C41110010000000000000002 /* TerminalDirectoryOpenLauncher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/TerminalDirectoryOpenLauncher.swift; sourceTree = ""; }; + C41110020000000000000002 /* KeyboardShortcutSettings+TerminalDirectoryOpen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "App/KeyboardShortcutSettings+TerminalDirectoryOpen.swift"; sourceTree = ""; }; 8A4FE96C3F394FC6A6D4B018 /* CmuxCLIPathInstaller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/CmuxCLIPathInstaller.swift; sourceTree = ""; }; 47D5AA7D29C94F5CA865B2BF /* ScreenIdentity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/ScreenIdentity.swift; sourceTree = ""; }; C1713001C1713001C1713001 /* CommandPaletteShortcutRouting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/CommandPaletteShortcutRouting.swift; sourceTree = ""; }; @@ -1169,6 +1173,8 @@ 38A09EB2E92203B2E95923A7 /* CommandPaletteSearch.swift */, 9DAB808A8EC74C40B95F7672 /* GhosttySurfaceConfigurationRefresh.swift */, 6B8E2E03F4A64C61B729CF19 /* TerminalDirectoryOpenSupport.swift */, + C41110010000000000000002 /* TerminalDirectoryOpenLauncher.swift */, + C41110020000000000000002 /* KeyboardShortcutSettings+TerminalDirectoryOpen.swift */, 8A4FE96C3F394FC6A6D4B018 /* CmuxCLIPathInstaller.swift */, 47D5AA7D29C94F5CA865B2BF /* ScreenIdentity.swift */, C1713001C1713001C1713001 /* CommandPaletteShortcutRouting.swift */, @@ -1845,6 +1851,8 @@ A8CBA43C2DA5AB9E3A1E65A4 /* CommandPaletteSearch.swift in Sources */, B84BD7AD94EE485B8DDAB6FF /* GhosttySurfaceConfigurationRefresh.swift in Sources */, 0D56BE882EAD4B67AC44F96D /* TerminalDirectoryOpenSupport.swift in Sources */, + C41110010000000000000001 /* TerminalDirectoryOpenLauncher.swift in Sources */, + C41110020000000000000001 /* KeyboardShortcutSettings+TerminalDirectoryOpen.swift in Sources */, A72C9F4179B54DF38E99A021 /* CmuxCLIPathInstaller.swift in Sources */, E8BA79E246754A8B99A0B823 /* ScreenIdentity.swift in Sources */, C1713002C1713002C1713002 /* CommandPaletteShortcutRouting.swift in Sources */, diff --git a/Sources/App/KeyboardShortcutSettings+TerminalDirectoryOpen.swift b/Sources/App/KeyboardShortcutSettings+TerminalDirectoryOpen.swift new file mode 100644 index 000000000000..172b1bcde395 --- /dev/null +++ b/Sources/App/KeyboardShortcutSettings+TerminalDirectoryOpen.swift @@ -0,0 +1,58 @@ +enum TerminalDirectoryOpenShortcutBindings { + typealias Binding = (target: TerminalDirectoryOpenTarget, action: KeyboardShortcutSettings.Action) + + static let all: [Binding] = TerminalDirectoryOpenTarget.commandPaletteShortcutTargets.map { target in + (target, KeyboardShortcutSettings.Action.terminalDirectoryOpenAction(for: target)) + } + + static let actions: [KeyboardShortcutSettings.Action] = all.map { $0.action } +} + +extension KeyboardShortcutSettings.Action { + static var terminalDirectoryOpenActions: [Self] { + TerminalDirectoryOpenShortcutBindings.actions + } + + static var terminalDirectoryOpenShortcutBindings: [TerminalDirectoryOpenShortcutBindings.Binding] { + TerminalDirectoryOpenShortcutBindings.all + } + + static func terminalDirectoryOpenAction(for target: TerminalDirectoryOpenTarget) -> Self { + switch target { + case .androidStudio: + return .terminalOpenDirectoryAndroidStudio + case .antigravity: + return .terminalOpenDirectoryAntigravity + case .cursor: + return .terminalOpenDirectoryCursor + case .finder: + return .terminalOpenDirectoryFinder + case .ghostty: + return .terminalOpenDirectoryGhostty + case .intellij: + return .terminalOpenDirectoryIntelliJ + case .iterm2: + return .terminalOpenDirectoryITerm2 + case .terminal: + return .terminalOpenDirectoryTerminal + case .tower: + return .terminalOpenDirectoryTower + case .vscode: + return .terminalOpenDirectoryVSCode + case .vscodeInline: + return .terminalOpenDirectoryVSCodeInline + case .warp: + return .terminalOpenDirectoryWarp + case .windsurf: + return .terminalOpenDirectoryWindsurf + case .xcode: + return .terminalOpenDirectoryXcode + case .zed: + return .terminalOpenDirectoryZed + } + } + + var terminalDirectoryOpenTarget: TerminalDirectoryOpenTarget? { + TerminalDirectoryOpenShortcutBindings.all.first { $0.action == self }?.target + } +} diff --git a/Sources/App/TerminalDirectoryOpenLauncher.swift b/Sources/App/TerminalDirectoryOpenLauncher.swift new file mode 100644 index 000000000000..5864205f7784 --- /dev/null +++ b/Sources/App/TerminalDirectoryOpenLauncher.swift @@ -0,0 +1,45 @@ +import AppKit +import Foundation + +@MainActor +enum TerminalDirectoryOpenLauncher { + static func currentDirectoryURL(in tabManager: TabManager?) -> URL? { + guard let workspace = tabManager?.selectedWorkspace else { return nil } + let focusedPanelDirectory = workspace.focusedPanelId.flatMap { workspace.panelDirectories[$0] } + let rawDirectory = focusedPanelDirectory ?? workspace.currentDirectory + let trimmed = rawDirectory.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmed.isEmpty else { return nil } + var isDirectory: ObjCBool = false + guard FileManager.default.fileExists(atPath: trimmed, isDirectory: &isDirectory), + isDirectory.boolValue else { + return nil + } + return URL(fileURLWithPath: trimmed, isDirectory: true) + } + + static func openCurrentDirectory( + in target: TerminalDirectoryOpenTarget, + tabManager: TabManager? + ) -> Bool { + guard let directoryURL = currentDirectoryURL(in: tabManager) else { return false } + return openDirectory(directoryURL, in: target, tabManager: tabManager) + } + + static func openDirectory( + _ directoryURL: URL, + in target: TerminalDirectoryOpenTarget, + tabManager: TabManager? + ) -> Bool { + switch target { + case .finder: + return NSWorkspace.shared.selectFile(nil, inFileViewerRootedAtPath: directoryURL.path) + case .vscodeInline: + return AppDelegate.shared?.openDirectoryInInlineVSCode(directoryURL, tabManager: tabManager) ?? false + default: + guard let applicationURL = target.applicationURL() else { return false } + let configuration = NSWorkspace.OpenConfiguration() + NSWorkspace.shared.open([directoryURL], withApplicationAt: applicationURL, configuration: configuration) + return true + } + } +} diff --git a/Sources/App/TerminalDirectoryOpenSupport.swift b/Sources/App/TerminalDirectoryOpenSupport.swift index 2801943f693f..57116299e30c 100644 --- a/Sources/App/TerminalDirectoryOpenSupport.swift +++ b/Sources/App/TerminalDirectoryOpenSupport.swift @@ -299,94 +299,6 @@ enum TerminalDirectoryOpenTarget: String, CaseIterable { } } -@MainActor -enum TerminalDirectoryOpenLauncher { - static func currentDirectoryURL(in tabManager: TabManager?) -> URL? { - guard let workspace = tabManager?.selectedWorkspace else { return nil } - let focusedPanelDirectory = workspace.focusedPanelId.flatMap { workspace.panelDirectories[$0] } - let rawDirectory = focusedPanelDirectory ?? workspace.currentDirectory - let trimmed = rawDirectory.trimmingCharacters(in: .whitespacesAndNewlines) - guard !trimmed.isEmpty else { return nil } - var isDirectory: ObjCBool = false - guard FileManager.default.fileExists(atPath: trimmed, isDirectory: &isDirectory), - isDirectory.boolValue else { - return nil - } - return URL(fileURLWithPath: trimmed, isDirectory: true) - } - - static func openCurrentDirectory( - in target: TerminalDirectoryOpenTarget, - tabManager: TabManager? - ) -> Bool { - guard let directoryURL = currentDirectoryURL(in: tabManager) else { return false } - return openDirectory(directoryURL, in: target, tabManager: tabManager) - } - - static func openDirectory( - _ directoryURL: URL, - in target: TerminalDirectoryOpenTarget, - tabManager: TabManager? - ) -> Bool { - switch target { - case .finder: - return NSWorkspace.shared.selectFile(nil, inFileViewerRootedAtPath: directoryURL.path) - case .vscodeInline: - return AppDelegate.shared?.openDirectoryInInlineVSCode(directoryURL, tabManager: tabManager) ?? false - default: - guard let applicationURL = target.applicationURL() else { return false } - let configuration = NSWorkspace.OpenConfiguration() - NSWorkspace.shared.open([directoryURL], withApplicationAt: applicationURL, configuration: configuration) - return true - } - } -} - -extension KeyboardShortcutSettings.Action { - static var terminalDirectoryOpenActions: [Self] { - TerminalDirectoryOpenTarget.commandPaletteShortcutTargets.compactMap(terminalDirectoryOpenAction(for:)) - } - - static func terminalDirectoryOpenAction(for target: TerminalDirectoryOpenTarget) -> Self? { - switch target { - case .androidStudio: - return .terminalOpenDirectoryAndroidStudio - case .antigravity: - return .terminalOpenDirectoryAntigravity - case .cursor: - return .terminalOpenDirectoryCursor - case .finder: - return .terminalOpenDirectoryFinder - case .ghostty: - return .terminalOpenDirectoryGhostty - case .intellij: - return .terminalOpenDirectoryIntelliJ - case .iterm2: - return .terminalOpenDirectoryITerm2 - case .terminal: - return .terminalOpenDirectoryTerminal - case .tower: - return .terminalOpenDirectoryTower - case .vscode: - return .terminalOpenDirectoryVSCode - case .vscodeInline: - return .terminalOpenDirectoryVSCodeInline - case .warp: - return .terminalOpenDirectoryWarp - case .windsurf: - return .terminalOpenDirectoryWindsurf - case .xcode: - return .terminalOpenDirectoryXcode - case .zed: - return .terminalOpenDirectoryZed - } - } - - var terminalDirectoryOpenTarget: TerminalDirectoryOpenTarget? { - TerminalDirectoryOpenTarget(commandPaletteCommandId: rawValue) - } -} - enum VSCodeServeWebURLBuilder { static func extractWebUIURL(from output: String) -> URL? { let prefix = "Web UI available at " diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index 9fea5008c9ba..1c504bad4517 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -13061,13 +13061,12 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent } private func handleTerminalDirectoryOpenShortcut(event: NSEvent) -> Bool { - for action in KeyboardShortcutSettings.Action.terminalDirectoryOpenActions { - guard matchConfiguredShortcut(event: event, action: action), - let target = action.terminalDirectoryOpenTarget else { + for binding in KeyboardShortcutSettings.Action.terminalDirectoryOpenShortcutBindings { + guard matchConfiguredShortcut(event: event, action: binding.action) else { continue } let targetTabManager = preferredMainWindowContextForShortcutRouting(event: event)?.tabManager ?? tabManager - if !TerminalDirectoryOpenLauncher.openCurrentDirectory(in: target, tabManager: targetTabManager) { + if !TerminalDirectoryOpenLauncher.openCurrentDirectory(in: binding.target, tabManager: targetTabManager) { NSSound.beep() } return true diff --git a/cmuxTests/CommandPaletteShortcutCustomizationTests.swift b/cmuxTests/CommandPaletteShortcutCustomizationTests.swift index 628d8e916e93..addaf9d0a5bc 100644 --- a/cmuxTests/CommandPaletteShortcutCustomizationTests.swift +++ b/cmuxTests/CommandPaletteShortcutCustomizationTests.swift @@ -55,15 +55,18 @@ final class CommandPaletteShortcutCustomizationTests: XCTestCase { func testTerminalDirectoryOpenPaletteCommandsExposeBindableShortcutActions() { let actions = KeyboardShortcutSettings.Action.terminalDirectoryOpenActions + let bindings = KeyboardShortcutSettings.Action.terminalDirectoryOpenShortcutBindings XCTAssertEqual(actions.count, TerminalDirectoryOpenTarget.commandPaletteShortcutTargets.count) + XCTAssertEqual(bindings.map { $0.action }, actions) + XCTAssertEqual(bindings.map { $0.target }, TerminalDirectoryOpenTarget.commandPaletteShortcutTargets) for target in TerminalDirectoryOpenTarget.commandPaletteShortcutTargets { let action = KeyboardShortcutSettings.Action.terminalDirectoryOpenAction(for: target) - XCTAssertEqual(action?.rawValue, target.commandPaletteCommandId) + XCTAssertEqual(action.rawValue, target.commandPaletteCommandId) XCTAssertEqual(ContentView.commandPaletteShortcutAction(forCommandID: target.commandPaletteCommandId), action) - XCTAssertEqual(action?.terminalDirectoryOpenTarget, target) - XCTAssertEqual(action?.defaultShortcut, .unbound) - XCTAssertTrue(action?.isPublicShortcutAction ?? false) + XCTAssertEqual(action.terminalDirectoryOpenTarget, target) + XCTAssertEqual(action.defaultShortcut, .unbound) + XCTAssertTrue(action.isPublicShortcutAction) } } From 77a7fcc50f9930c72ca6613f002cd26222b99804 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Mon, 18 May 2026 00:09:28 -0700 Subject: [PATCH 5/8] fix: restore open-with directory fallback --- .../App/TerminalDirectoryOpenLauncher.swift | 22 ++++++++++++------- ...andPaletteShortcutCustomizationTests.swift | 14 ++++++++++++ 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/Sources/App/TerminalDirectoryOpenLauncher.swift b/Sources/App/TerminalDirectoryOpenLauncher.swift index 5864205f7784..4d9735d6720a 100644 --- a/Sources/App/TerminalDirectoryOpenLauncher.swift +++ b/Sources/App/TerminalDirectoryOpenLauncher.swift @@ -6,15 +6,21 @@ enum TerminalDirectoryOpenLauncher { static func currentDirectoryURL(in tabManager: TabManager?) -> URL? { guard let workspace = tabManager?.selectedWorkspace else { return nil } let focusedPanelDirectory = workspace.focusedPanelId.flatMap { workspace.panelDirectories[$0] } - let rawDirectory = focusedPanelDirectory ?? workspace.currentDirectory - let trimmed = rawDirectory.trimmingCharacters(in: .whitespacesAndNewlines) - guard !trimmed.isEmpty else { return nil } - var isDirectory: ObjCBool = false - guard FileManager.default.fileExists(atPath: trimmed, isDirectory: &isDirectory), - isDirectory.boolValue else { - return nil + return firstValidDirectoryURL(in: [focusedPanelDirectory, workspace.currentDirectory]) + } + + static func firstValidDirectoryURL(in rawDirectories: [String?]) -> URL? { + for rawDirectory in rawDirectories { + let trimmed = rawDirectory?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + guard !trimmed.isEmpty else { continue } + var isDirectory: ObjCBool = false + guard FileManager.default.fileExists(atPath: trimmed, isDirectory: &isDirectory), + isDirectory.boolValue else { + continue + } + return URL(fileURLWithPath: trimmed, isDirectory: true) } - return URL(fileURLWithPath: trimmed, isDirectory: true) + return nil } static func openCurrentDirectory( diff --git a/cmuxTests/CommandPaletteShortcutCustomizationTests.swift b/cmuxTests/CommandPaletteShortcutCustomizationTests.swift index addaf9d0a5bc..6ec9ae34c447 100644 --- a/cmuxTests/CommandPaletteShortcutCustomizationTests.swift +++ b/cmuxTests/CommandPaletteShortcutCustomizationTests.swift @@ -90,6 +90,20 @@ final class CommandPaletteShortcutCustomizationTests: XCTestCase { ) } + func testTerminalDirectoryOpenLauncherFallsBackToWorkspaceDirectoryWhenFocusedDirectoryIsStale() throws { + let workspaceDirectory = settingsDirectoryURL.appendingPathComponent("workspace", isDirectory: true) + try FileManager.default.createDirectory(at: workspaceDirectory, withIntermediateDirectories: true) + + let resolvedURL = TerminalDirectoryOpenLauncher.firstValidDirectoryURL( + in: [ + settingsDirectoryURL.appendingPathComponent("missing", isDirectory: true).path, + workspaceDirectory.path, + ] + ) + + XCTAssertEqual(resolvedURL?.standardizedFileURL, workspaceDirectory.standardizedFileURL) + } + func testFieldEditorMoveCommandHonorsClearedCommandPalettePreviousShortcut() { guard let controlPEvent = makeKeyDownEvent( key: "\u{10}", From 8c804de4b6e67eab5b4648fea62752d1e9e6d340 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Mon, 18 May 2026 19:10:54 -0700 Subject: [PATCH 6/8] fix: address terminal open shortcut review --- .../App/TerminalDirectoryOpenLauncher.swift | 44 ++++++++-- Sources/AppDelegate.swift | 16 +++- Sources/ContentView.swift | 8 +- ...andPaletteShortcutCustomizationTests.swift | 85 +++++++++++++++++++ 4 files changed, 146 insertions(+), 7 deletions(-) diff --git a/Sources/App/TerminalDirectoryOpenLauncher.swift b/Sources/App/TerminalDirectoryOpenLauncher.swift index 4d9735d6720a..038f8000fdc2 100644 --- a/Sources/App/TerminalDirectoryOpenLauncher.swift +++ b/Sources/App/TerminalDirectoryOpenLauncher.swift @@ -3,6 +3,14 @@ import Foundation @MainActor enum TerminalDirectoryOpenLauncher { + typealias ApplicationOpenCompletion = @MainActor (Error?) -> Void + typealias ApplicationOpenHandler = ( + _ urls: [URL], + _ applicationURL: URL, + _ configuration: NSWorkspace.OpenConfiguration, + _ completion: @escaping ApplicationOpenCompletion + ) -> Void + static func currentDirectoryURL(in tabManager: TabManager?) -> URL? { guard let workspace = tabManager?.selectedWorkspace else { return nil } let focusedPanelDirectory = workspace.focusedPanelId.flatMap { workspace.panelDirectories[$0] } @@ -25,16 +33,25 @@ enum TerminalDirectoryOpenLauncher { static func openCurrentDirectory( in target: TerminalDirectoryOpenTarget, - tabManager: TabManager? + tabManager: TabManager?, + onOpenFailure: ApplicationOpenCompletion? = nil ) -> Bool { guard let directoryURL = currentDirectoryURL(in: tabManager) else { return false } - return openDirectory(directoryURL, in: target, tabManager: tabManager) + return openDirectory( + directoryURL, + in: target, + tabManager: tabManager, + onOpenFailure: onOpenFailure + ) } static func openDirectory( _ directoryURL: URL, in target: TerminalDirectoryOpenTarget, - tabManager: TabManager? + tabManager: TabManager?, + onOpenFailure: ApplicationOpenCompletion? = nil, + applicationURLProvider: (TerminalDirectoryOpenTarget) -> URL? = { $0.applicationURL() }, + openWithApplication: ApplicationOpenHandler? = nil ) -> Bool { switch target { case .finder: @@ -42,10 +59,27 @@ enum TerminalDirectoryOpenLauncher { case .vscodeInline: return AppDelegate.shared?.openDirectoryInInlineVSCode(directoryURL, tabManager: tabManager) ?? false default: - guard let applicationURL = target.applicationURL() else { return false } + guard let applicationURL = applicationURLProvider(target) else { return false } let configuration = NSWorkspace.OpenConfiguration() - NSWorkspace.shared.open([directoryURL], withApplicationAt: applicationURL, configuration: configuration) + let openWithApplication = openWithApplication ?? openURLsWithApplication + openWithApplication([directoryURL], applicationURL, configuration) { error in + guard error != nil else { return } + onOpenFailure?(error) + } return true } } + + private static func openURLsWithApplication( + _ urls: [URL], + applicationURL: URL, + configuration: NSWorkspace.OpenConfiguration, + completion: @escaping ApplicationOpenCompletion + ) { + NSWorkspace.shared.open(urls, withApplicationAt: applicationURL, configuration: configuration) { _, error in + Task { @MainActor in + completion(error) + } + } + } } diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index 962299d7db2a..25abeb0b0bcd 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -13344,12 +13344,26 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent } private func handleTerminalDirectoryOpenShortcut(event: NSEvent) -> Bool { + if activeConfiguredShortcutChordPrefixForCurrentEvent == nil, + armConfiguredShortcutChordIfNeeded( + event: event, + actions: KeyboardShortcutSettings.Action.terminalDirectoryOpenActions + ) { + return true + } + for binding in KeyboardShortcutSettings.Action.terminalDirectoryOpenShortcutBindings { guard matchConfiguredShortcut(event: event, action: binding.action) else { continue } let targetTabManager = preferredMainWindowContextForShortcutRouting(event: event)?.tabManager ?? tabManager - if !TerminalDirectoryOpenLauncher.openCurrentDirectory(in: binding.target, tabManager: targetTabManager) { + if !TerminalDirectoryOpenLauncher.openCurrentDirectory( + in: binding.target, + tabManager: targetTabManager, + onOpenFailure: { _ in + NSSound.beep() + } + ) { NSSound.beep() } return true diff --git a/Sources/ContentView.swift b/Sources/ContentView.swift index 2f2199eb338d..d3a18f75a2c6 100644 --- a/Sources/ContentView.swift +++ b/Sources/ContentView.swift @@ -9142,7 +9142,13 @@ struct ContentView: View { } private func openFocusedDirectory(in target: TerminalDirectoryOpenTarget) -> Bool { - TerminalDirectoryOpenLauncher.openCurrentDirectory(in: target, tabManager: tabManager) + TerminalDirectoryOpenLauncher.openCurrentDirectory( + in: target, + tabManager: tabManager, + onOpenFailure: { _ in + NSSound.beep() + } + ) } private func stopInlineVSCodeServeWeb() { diff --git a/cmuxTests/CommandPaletteShortcutCustomizationTests.swift b/cmuxTests/CommandPaletteShortcutCustomizationTests.swift index 6ec9ae34c447..8c9443fda73c 100644 --- a/cmuxTests/CommandPaletteShortcutCustomizationTests.swift +++ b/cmuxTests/CommandPaletteShortcutCustomizationTests.swift @@ -104,6 +104,47 @@ final class CommandPaletteShortcutCustomizationTests: XCTestCase { XCTAssertEqual(resolvedURL?.standardizedFileURL, workspaceDirectory.standardizedFileURL) } + func testTerminalDirectoryOpenLauncherReportsApplicationLaunchCompletionFailures() throws { + let directoryURL = settingsDirectoryURL.appendingPathComponent("workspace", isDirectory: true) + try FileManager.default.createDirectory(at: directoryURL, withIntermediateDirectories: true) + let applicationURL = URL(fileURLWithPath: "/Applications/Example.app", isDirectory: true) + let expectedError = NSError(domain: "cmuxTests", code: 42) + var capturedURLs: [URL] = [] + var capturedApplicationURL: URL? + var launchCompletion: TerminalDirectoryOpenLauncher.ApplicationOpenCompletion? + var reportedErrors: [NSError] = [] + + let opened = TerminalDirectoryOpenLauncher.openDirectory( + directoryURL, + in: .vscode, + tabManager: nil, + onOpenFailure: { error in + reportedErrors.append(error as NSError) + }, + applicationURLProvider: { target in + XCTAssertEqual(target, .vscode) + return applicationURL + }, + openWithApplication: { urls, applicationURL, _, completion in + capturedURLs = urls + capturedApplicationURL = applicationURL + launchCompletion = completion + } + ) + + XCTAssertTrue(opened) + XCTAssertEqual(capturedURLs, [directoryURL]) + XCTAssertEqual(capturedApplicationURL, applicationURL) + XCTAssertTrue(reportedErrors.isEmpty) + + launchCompletion?(nil) + XCTAssertTrue(reportedErrors.isEmpty) + + launchCompletion?(expectedError) + XCTAssertEqual(reportedErrors.map(\.domain), [expectedError.domain]) + XCTAssertEqual(reportedErrors.map(\.code), [expectedError.code]) + } + func testFieldEditorMoveCommandHonorsClearedCommandPalettePreviousShortcut() { guard let controlPEvent = makeKeyDownEvent( key: "\u{10}", @@ -428,6 +469,50 @@ final class CommandPaletteShortcutCustomizationTests: XCTestCase { } } + func testChordedTerminalDirectoryOpenShortcutConsumesPrefix() { + guard let appDelegate = AppDelegate.shared else { + XCTFail("Expected AppDelegate.shared") + return + } + + let windowId = appDelegate.createMainWindow() + defer { closeWindow(withId: windowId) } + + guard let window = window(withId: windowId) else { + XCTFail("Expected test window") + return + } + + window.makeKeyAndOrderFront(nil) + RunLoop.main.run(until: Date(timeIntervalSinceNow: 0.05)) + + withTemporaryCommandPaletteShortcut(.terminalOpenDirectoryFinder) { + KeyboardShortcutSettings.setShortcut( + StoredShortcut(key: "b", command: false, shift: false, option: false, control: true, chordKey: "o"), + for: .terminalOpenDirectoryFinder + ) + + guard let prefixEvent = makeKeyDownEvent( + key: "b", + modifiers: [.control], + keyCode: 11, + windowNumber: window.windowNumber + ) else { + XCTFail("Failed to construct terminal-open chord prefix event") + return + } + + #if DEBUG + XCTAssertTrue( + appDelegate.debugHandleCustomShortcut(event: prefixEvent), + "Terminal-directory open shortcut prefixes must be consumed and armed" + ) + #else + XCTFail("debugHandleCustomShortcut is only available in DEBUG") + #endif + } + } + func testWindowPerformKeyEquivalentRoutesHorizontalArrowsToCommandPaletteFieldEditor() { guard let appDelegate = AppDelegate.shared else { XCTFail("Expected AppDelegate.shared") From 6a22307ba68105f541704654869617d0fa7602c9 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Mon, 18 May 2026 22:11:10 -0700 Subject: [PATCH 7/8] fix: unwrap terminal open launcher test error --- cmuxTests/CommandPaletteShortcutCustomizationTests.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/cmuxTests/CommandPaletteShortcutCustomizationTests.swift b/cmuxTests/CommandPaletteShortcutCustomizationTests.swift index 8c9443fda73c..123b4ae321e0 100644 --- a/cmuxTests/CommandPaletteShortcutCustomizationTests.swift +++ b/cmuxTests/CommandPaletteShortcutCustomizationTests.swift @@ -119,6 +119,7 @@ final class CommandPaletteShortcutCustomizationTests: XCTestCase { in: .vscode, tabManager: nil, onOpenFailure: { error in + guard let error else { return } reportedErrors.append(error as NSError) }, applicationURLProvider: { target in From bf486da99cf1adfe59e6614b81bffaecca2bf834 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Mon, 18 May 2026 22:13:38 -0700 Subject: [PATCH 8/8] fix: localize open with shortcut category --- web/messages/ar.json | 1 + web/messages/bs.json | 1 + web/messages/da.json | 1 + web/messages/de.json | 1 + web/messages/es.json | 1 + web/messages/fr.json | 1 + web/messages/it.json | 1 + web/messages/km.json | 1 + web/messages/ko.json | 1 + web/messages/no.json | 1 + web/messages/pl.json | 1 + web/messages/pt-BR.json | 1 + web/messages/ru.json | 1 + web/messages/th.json | 1 + web/messages/tr.json | 1 + web/messages/uk.json | 1 + web/messages/zh-CN.json | 1 + web/messages/zh-TW.json | 1 + 18 files changed, 18 insertions(+) diff --git a/web/messages/ar.json b/web/messages/ar.json index 017f82ce4c08..925b339eb775 100644 --- a/web/messages/ar.json +++ b/web/messages/ar.json @@ -421,6 +421,7 @@ "notifications": "الإشعارات", "find": "البحث", "terminal": "الطرفية", + "openWith": "فتح باستخدام", "window": "النافذة" }, "sc": { diff --git a/web/messages/bs.json b/web/messages/bs.json index fe4da2a2a5cd..799f5474876f 100644 --- a/web/messages/bs.json +++ b/web/messages/bs.json @@ -421,6 +421,7 @@ "notifications": "Notifikacije", "find": "Pretraga", "terminal": "Terminal", + "openWith": "Otvori pomoću", "window": "Prozor" }, "sc": { diff --git a/web/messages/da.json b/web/messages/da.json index f5a3f5bacbd1..661cf530707e 100644 --- a/web/messages/da.json +++ b/web/messages/da.json @@ -421,6 +421,7 @@ "notifications": "Notifikationer", "find": "Find", "terminal": "Terminal", + "openWith": "Åbn med", "window": "Vindue" }, "sc": { diff --git a/web/messages/de.json b/web/messages/de.json index c7cbb0ecd082..ddc0f7759f0d 100644 --- a/web/messages/de.json +++ b/web/messages/de.json @@ -421,6 +421,7 @@ "notifications": "Benachrichtigungen", "find": "Suche", "terminal": "Terminal", + "openWith": "Öffnen mit", "window": "Fenster" }, "sc": { diff --git a/web/messages/es.json b/web/messages/es.json index 0eb6f7561744..ed2e399d890c 100644 --- a/web/messages/es.json +++ b/web/messages/es.json @@ -421,6 +421,7 @@ "notifications": "Notificaciones", "find": "Búsqueda", "terminal": "Terminal", + "openWith": "Abrir con", "window": "Ventana" }, "sc": { diff --git a/web/messages/fr.json b/web/messages/fr.json index 3aea36b212c0..09ff0094884a 100644 --- a/web/messages/fr.json +++ b/web/messages/fr.json @@ -421,6 +421,7 @@ "notifications": "Notifications", "find": "Recherche", "terminal": "Terminal", + "openWith": "Ouvrir avec", "window": "Fenêtre" }, "sc": { diff --git a/web/messages/it.json b/web/messages/it.json index e8230417f71c..944f52f9810f 100644 --- a/web/messages/it.json +++ b/web/messages/it.json @@ -421,6 +421,7 @@ "notifications": "Notifiche", "find": "Ricerca", "terminal": "Terminale", + "openWith": "Apri con", "window": "Finestra" }, "sc": { diff --git a/web/messages/km.json b/web/messages/km.json index 5f3d7112b673..b3d541be7878 100644 --- a/web/messages/km.json +++ b/web/messages/km.json @@ -421,6 +421,7 @@ "notifications": "ជូនដំណឹង", "find": "ស្វែងរក", "terminal": "ទែមីណល", + "openWith": "បើកជាមួយ", "window": "បង្អួច" }, "sc": { diff --git a/web/messages/ko.json b/web/messages/ko.json index 9e8d94170a12..c360549213f2 100644 --- a/web/messages/ko.json +++ b/web/messages/ko.json @@ -421,6 +421,7 @@ "notifications": "알림", "find": "찾기", "terminal": "터미널", + "openWith": "다음으로 열기", "window": "창" }, "sc": { diff --git a/web/messages/no.json b/web/messages/no.json index 2c1f853cc6c1..b267a91a7202 100644 --- a/web/messages/no.json +++ b/web/messages/no.json @@ -421,6 +421,7 @@ "notifications": "Varsler", "find": "Finn", "terminal": "Terminal", + "openWith": "Åpne med", "window": "Vindu" }, "sc": { diff --git a/web/messages/pl.json b/web/messages/pl.json index f82b7a128cda..244c31f76b87 100644 --- a/web/messages/pl.json +++ b/web/messages/pl.json @@ -421,6 +421,7 @@ "notifications": "Powiadomienia", "find": "Znajdź", "terminal": "Terminal", + "openWith": "Otwórz za pomocą", "window": "Okno" }, "sc": { diff --git a/web/messages/pt-BR.json b/web/messages/pt-BR.json index 62eaef3aad94..985c6f5a4abd 100644 --- a/web/messages/pt-BR.json +++ b/web/messages/pt-BR.json @@ -421,6 +421,7 @@ "notifications": "Notificações", "find": "Buscar", "terminal": "Terminal", + "openWith": "Abrir com", "window": "Janela" }, "sc": { diff --git a/web/messages/ru.json b/web/messages/ru.json index ce7aaa0dbbb1..1038712e7203 100644 --- a/web/messages/ru.json +++ b/web/messages/ru.json @@ -421,6 +421,7 @@ "notifications": "Уведомления", "find": "Поиск", "terminal": "Терминал", + "openWith": "Открыть с помощью", "window": "Окно" }, "sc": { diff --git a/web/messages/th.json b/web/messages/th.json index ee126e993ce8..587f3d44a4ec 100644 --- a/web/messages/th.json +++ b/web/messages/th.json @@ -421,6 +421,7 @@ "notifications": "การแจ้งเตือน", "find": "ค้นหา", "terminal": "เทอร์มินัล", + "openWith": "เปิดด้วย", "window": "หน้าต่าง" }, "sc": { diff --git a/web/messages/tr.json b/web/messages/tr.json index e64c2a0f4d47..6254a272cbb2 100644 --- a/web/messages/tr.json +++ b/web/messages/tr.json @@ -421,6 +421,7 @@ "notifications": "Bildirimler", "find": "Bul", "terminal": "Terminal", + "openWith": "Birlikte Aç", "window": "Pencere" }, "sc": { diff --git a/web/messages/uk.json b/web/messages/uk.json index efa0fd206cb1..af9f58be1e9b 100644 --- a/web/messages/uk.json +++ b/web/messages/uk.json @@ -422,6 +422,7 @@ "notifications": "Сповіщення", "find": "Пошук", "terminal": "Термінал", + "openWith": "Відкрити за допомогою", "window": "Вікно" }, "sc": { diff --git a/web/messages/zh-CN.json b/web/messages/zh-CN.json index 7898b77a285b..d7d144ce38e7 100644 --- a/web/messages/zh-CN.json +++ b/web/messages/zh-CN.json @@ -421,6 +421,7 @@ "notifications": "通知", "find": "查找", "terminal": "终端", + "openWith": "打开方式", "window": "窗口" }, "sc": { diff --git a/web/messages/zh-TW.json b/web/messages/zh-TW.json index f317d0611b73..5b812bebc3c0 100644 --- a/web/messages/zh-TW.json +++ b/web/messages/zh-TW.json @@ -421,6 +421,7 @@ "notifications": "通知", "find": "搜尋", "terminal": "終端機", + "openWith": "開啟方式", "window": "視窗" }, "sc": {