diff --git a/README.md b/README.md index 31578c73..e2c10ae0 100644 --- a/README.md +++ b/README.md @@ -114,6 +114,7 @@ Everything is scriptable through the CLI and socket API — create workspaces/ta | ⌃ ⌘ ] | Next workspace | | ⌃ ⌘ [ | Previous workspace | | ⌘ ⇧ W | Close workspace | +| ⌘ ⇧ R | Rename workspace | | ⌘ B | Toggle sidebar | ### Surfaces diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index 23197bba..870bfaf8 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -1673,6 +1673,36 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent return true } + func promptRenameSelectedWorkspace() -> Bool { + guard let tabManager, + let tabId = tabManager.selectedTabId, + let tab = tabManager.tabs.first(where: { $0.id == tabId }) else { + NSSound.beep() + return false + } + + let alert = NSAlert() + alert.messageText = "Rename Workspace" + alert.informativeText = "Enter a custom name for this workspace." + let input = NSTextField(string: tab.customTitle ?? tab.title) + input.placeholderString = "Workspace name" + input.frame = NSRect(x: 0, y: 0, width: 240, height: 22) + alert.accessoryView = input + alert.addButton(withTitle: "Rename") + alert.addButton(withTitle: "Cancel") + let alertWindow = alert.window + alertWindow.initialFirstResponder = input + DispatchQueue.main.async { + alertWindow.makeFirstResponder(input) + input.selectText(nil) + } + + let response = alert.runModal() + guard response == .alertFirstButtonReturn else { return true } + tabManager.setCustomTitle(tabId: tab.id, title: input.stringValue) + return true + } + private func handleCustomShortcut(event: NSEvent) -> Bool { // `charactersIgnoringModifiers` can be nil for some synthetic NSEvents and certain special keys. // Most shortcuts below use keyCode fallbacks, so treat nil as "" rather than bailing out. @@ -1877,6 +1907,11 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent return true } + if matchShortcut(event: event, shortcut: KeyboardShortcutSettings.shortcut(for: .renameWorkspace)) { + _ = promptRenameSelectedWorkspace() + return true + } + // Numeric shortcuts for specific sidebar tabs: Cmd+1-9 (9 = last workspace) if flags == [.command], let manager = tabManager, diff --git a/Sources/KeyboardShortcutSettings.swift b/Sources/KeyboardShortcutSettings.swift index 8b2b8d14..6d1d0978 100644 --- a/Sources/KeyboardShortcutSettings.swift +++ b/Sources/KeyboardShortcutSettings.swift @@ -17,6 +17,7 @@ enum KeyboardShortcutSettings { case prevSurface case nextSidebarTab case prevSidebarTab + case renameWorkspace case newSurface // Panes / splits @@ -48,6 +49,7 @@ enum KeyboardShortcutSettings { case .prevSurface: return "Previous Surface" case .nextSidebarTab: return "Next Workspace" case .prevSidebarTab: return "Previous Workspace" + case .renameWorkspace: return "Rename Workspace" case .newSurface: return "New Surface" case .focusLeft: return "Focus Pane Left" case .focusRight: return "Focus Pane Right" @@ -73,6 +75,7 @@ enum KeyboardShortcutSettings { case .triggerFlash: return "shortcut.triggerFlash" case .nextSidebarTab: return "shortcut.nextSidebarTab" case .prevSidebarTab: return "shortcut.prevSidebarTab" + case .renameWorkspace: return "shortcut.renameWorkspace" case .focusLeft: return "shortcut.focusLeft" case .focusRight: return "shortcut.focusRight" case .focusUp: return "shortcut.focusUp" @@ -108,6 +111,8 @@ enum KeyboardShortcutSettings { return StoredShortcut(key: "]", command: true, shift: false, option: false, control: true) case .prevSidebarTab: return StoredShortcut(key: "[", command: true, shift: false, option: false, control: true) + case .renameWorkspace: + return StoredShortcut(key: "r", command: true, shift: true, option: false, control: false) case .focusLeft: return StoredShortcut(key: "←", command: true, shift: false, option: true, control: false) case .focusRight: @@ -190,6 +195,7 @@ enum KeyboardShortcutSettings { static func nextSidebarTabShortcut() -> StoredShortcut { shortcut(for: .nextSidebarTab) } static func prevSidebarTabShortcut() -> StoredShortcut { shortcut(for: .prevSidebarTab) } + static func renameWorkspaceShortcut() -> StoredShortcut { shortcut(for: .renameWorkspace) } static func focusLeftShortcut() -> StoredShortcut { shortcut(for: .focusLeft) } static func focusRightShortcut() -> StoredShortcut { shortcut(for: .focusRight) } diff --git a/Sources/cmuxApp.swift b/Sources/cmuxApp.swift index 09b18c59..2f74980c 100644 --- a/Sources/cmuxApp.swift +++ b/Sources/cmuxApp.swift @@ -478,6 +478,10 @@ struct cmuxApp: App { (AppDelegate.shared?.tabManager ?? tabManager).selectPreviousTab() } + Button("Rename Workspace…") { + _ = AppDelegate.shared?.promptRenameSelectedWorkspace() + } + Divider() splitCommandButton(title: "Split Right", shortcut: splitRightMenuShortcut) { diff --git a/cmuxTests/CmuxWebViewKeyEquivalentTests.swift b/cmuxTests/CmuxWebViewKeyEquivalentTests.swift index cc8f5395..5ff9b633 100644 --- a/cmuxTests/CmuxWebViewKeyEquivalentTests.swift +++ b/cmuxTests/CmuxWebViewKeyEquivalentTests.swift @@ -318,6 +318,25 @@ final class BrowserDeveloperToolsShortcutDefaultsTests: XCTestCase { } } +final class WorkspaceRenameShortcutDefaultsTests: XCTestCase { + func testRenameWorkspaceShortcutDefaultsAndMetadata() { + XCTAssertEqual(KeyboardShortcutSettings.Action.renameWorkspace.label, "Rename Workspace") + XCTAssertEqual(KeyboardShortcutSettings.Action.renameWorkspace.defaultsKey, "shortcut.renameWorkspace") + + let shortcut = KeyboardShortcutSettings.Action.renameWorkspace.defaultShortcut + XCTAssertEqual(shortcut.key, "r") + XCTAssertTrue(shortcut.command) + XCTAssertTrue(shortcut.shift) + XCTAssertFalse(shortcut.option) + XCTAssertFalse(shortcut.control) + } + + func testShortcutDefaultsKeysRemainUnique() { + let keys = KeyboardShortcutSettings.Action.allCases.map(\.defaultsKey) + XCTAssertEqual(Set(keys).count, keys.count) + } +} + @MainActor final class BrowserDeveloperToolsConfigurationTests: XCTestCase { func testBrowserPanelEnablesInspectableWebViewAndDeveloperExtras() { diff --git a/web/app/keyboard-shortcuts.tsx b/web/app/keyboard-shortcuts.tsx index f4c483c0..9aa81ddf 100644 --- a/web/app/keyboard-shortcuts.tsx +++ b/web/app/keyboard-shortcuts.tsx @@ -38,6 +38,11 @@ const CATEGORIES: ShortcutCategory[] = [ combos: [["⌘", "⇧", "W"]], description: "Close workspace", }, + { + id: "ws-rename", + combos: [["⌘", "⇧", "R"]], + description: "Rename workspace", + }, ], }, {