diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index 70d3e4af..15a63335 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -4559,6 +4559,14 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent return true } + // Alias Cmd+Shift+R to the same rename-tab command palette flow as Cmd+R. + if normalizedFlags == [.command, .shift], (chars == "r" || event.keyCode == 15) { + return handleRenameTabCommandPaletteShortcut( + event: event, + commandPaletteTargetWindow: commandPaletteTargetWindow + ) + } + if matchShortcut(event: event, shortcut: KeyboardShortcutSettings.shortcut(for: .renameWorkspace)) { _ = promptRenameSelectedWorkspace() return true @@ -4629,13 +4637,10 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent } if matchShortcut(event: event, shortcut: KeyboardShortcutSettings.shortcut(for: .renameTab)) { - // Keep Cmd+R browser reload behavior when a browser panel is focused. - if tabManager?.focusedBrowserPanel != nil { - return false - } - let targetWindow = commandPaletteTargetWindow ?? event.window ?? NSApp.keyWindow ?? NSApp.mainWindow - NotificationCenter.default.post(name: .commandPaletteRenameTabRequested, object: targetWindow) - return true + return handleRenameTabCommandPaletteShortcut( + event: event, + commandPaletteTargetWindow: commandPaletteTargetWindow + ) } // Numeric shortcuts for specific sidebar tabs: Cmd+1-9 (9 = last workspace) @@ -4864,6 +4869,19 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent return false } + private func handleRenameTabCommandPaletteShortcut( + event: NSEvent, + commandPaletteTargetWindow: NSWindow? + ) -> Bool { + // Keep Cmd+R browser reload behavior when a browser panel is focused. + if tabManager?.focusedBrowserPanel != nil { + return false + } + let targetWindow = commandPaletteTargetWindow ?? event.window ?? NSApp.keyWindow ?? NSApp.mainWindow + NotificationCenter.default.post(name: .commandPaletteRenameTabRequested, object: targetWindow) + return true + } + private func shouldSuppressSplitShortcutForTransientTerminalFocusState(direction: SplitDirection) -> Bool { guard let tabManager, let workspace = tabManager.selectedWorkspace, diff --git a/Sources/KeyboardShortcutSettings.swift b/Sources/KeyboardShortcutSettings.swift index 61d7b799..9d99781e 100644 --- a/Sources/KeyboardShortcutSettings.swift +++ b/Sources/KeyboardShortcutSettings.swift @@ -125,7 +125,7 @@ enum KeyboardShortcutSettings { case .renameTab: return StoredShortcut(key: "r", command: true, shift: false, option: false, control: false) case .renameWorkspace: - return StoredShortcut(key: "r", command: true, shift: true, option: false, control: false) + return StoredShortcut(key: "r", command: true, shift: false, option: false, control: true) case .closeWorkspace: return StoredShortcut(key: "w", command: true, shift: true, option: false, control: false) case .focusLeft: diff --git a/cmuxTests/AppDelegateShortcutRoutingTests.swift b/cmuxTests/AppDelegateShortcutRoutingTests.swift index c5a9435a..77a09c18 100644 --- a/cmuxTests/AppDelegateShortcutRoutingTests.swift +++ b/cmuxTests/AppDelegateShortcutRoutingTests.swift @@ -311,6 +311,60 @@ final class AppDelegateShortcutRoutingTests: XCTestCase { XCTAssertTrue(appDelegate.tabManager === secondManager, "Shortcut routing should retarget active manager to event window") } + func testCmdShiftRAliasesToRenameTabCommandPaletteRequest() { + guard let appDelegate = AppDelegate.shared else { + XCTFail("Expected AppDelegate.shared") + return + } + + let windowId = appDelegate.createMainWindow() + defer { + closeWindow(withId: windowId) + KeyboardShortcutSettings.resetShortcut(for: .renameWorkspace) + } + + guard let window = window(withId: windowId) else { + XCTFail("Expected test window") + return + } + + KeyboardShortcutSettings.setShortcut( + StoredShortcut(key: "r", command: true, shift: false, option: true, control: false), + for: .renameWorkspace + ) + + let expectation = expectation(description: "Expected rename tab command palette notification") + var observedWindow: NSWindow? + let token = NotificationCenter.default.addObserver( + forName: .commandPaletteRenameTabRequested, + object: nil, + queue: nil + ) { notification in + observedWindow = notification.object as? NSWindow + expectation.fulfill() + } + defer { NotificationCenter.default.removeObserver(token) } + + guard let event = makeKeyDownEvent( + key: "r", + modifiers: [.command, .shift], + keyCode: 15, // kVK_ANSI_R + windowNumber: window.windowNumber + ) else { + XCTFail("Failed to construct Cmd+Shift+R event") + return + } + +#if DEBUG + XCTAssertTrue(appDelegate.debugHandleCustomShortcut(event: event)) +#else + XCTFail("debugHandleCustomShortcut is only available in DEBUG") +#endif + + wait(for: [expectation], timeout: 1.0) + XCTAssertEqual(observedWindow?.windowNumber, window.windowNumber) + } + func testCmdDigitDoesNotFallbackToOtherWindowWhenEventWindowContextIsMissing() { guard let appDelegate = AppDelegate.shared else { XCTFail("Expected AppDelegate.shared") diff --git a/cmuxTests/CmuxWebViewKeyEquivalentTests.swift b/cmuxTests/CmuxWebViewKeyEquivalentTests.swift index bcc96c61..5d4e7863 100644 --- a/cmuxTests/CmuxWebViewKeyEquivalentTests.swift +++ b/cmuxTests/CmuxWebViewKeyEquivalentTests.swift @@ -966,18 +966,18 @@ final class WorkspaceRenameShortcutDefaultsTests: XCTestCase { let shortcut = KeyboardShortcutSettings.Action.renameWorkspace.defaultShortcut XCTAssertEqual(shortcut.key, "r") XCTAssertTrue(shortcut.command) - XCTAssertTrue(shortcut.shift) + XCTAssertFalse(shortcut.shift) XCTAssertFalse(shortcut.option) - XCTAssertFalse(shortcut.control) + XCTAssertTrue(shortcut.control) } func testRenameWorkspaceShortcutConvertsToMenuShortcut() { let shortcut = KeyboardShortcutSettings.Action.renameWorkspace.defaultShortcut XCTAssertNotNil(shortcut.keyEquivalent) XCTAssertTrue(shortcut.eventModifiers.contains(.command)) - XCTAssertTrue(shortcut.eventModifiers.contains(.shift)) + XCTAssertFalse(shortcut.eventModifiers.contains(.shift)) XCTAssertFalse(shortcut.eventModifiers.contains(.option)) - XCTAssertFalse(shortcut.eventModifiers.contains(.control)) + XCTAssertTrue(shortcut.eventModifiers.contains(.control)) } func testCloseWorkspaceShortcutDefaultsAndMetadata() {