Merge pull request #321 from manaflow-ai/pr-316-head
Follow-up: sync customizable workspace shortcuts across UI
This commit is contained in:
commit
1bc3edf75f
8 changed files with 397 additions and 80 deletions
|
|
@ -1967,6 +1967,11 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
|
|||
return true
|
||||
}
|
||||
|
||||
if matchShortcut(event: event, shortcut: KeyboardShortcutSettings.shortcut(for: .closeWorkspace)) {
|
||||
tabManager?.closeCurrentWorkspaceWithConfirmation()
|
||||
return true
|
||||
}
|
||||
|
||||
// Numeric shortcuts for specific sidebar tabs: Cmd+1-9 (9 = last workspace)
|
||||
if flags == [.command],
|
||||
let manager = tabManager,
|
||||
|
|
@ -3305,6 +3310,9 @@ final class MenuBarExtraController: NSObject, NSMenuDelegate {
|
|||
|
||||
stateHintItem.title = snapshot.stateHintTitle
|
||||
|
||||
applyShortcut(KeyboardShortcutSettings.shortcut(for: .showNotifications), to: showNotificationsItem)
|
||||
applyShortcut(KeyboardShortcutSettings.shortcut(for: .jumpToUnread), to: jumpToUnreadItem)
|
||||
|
||||
jumpToUnreadItem.isEnabled = snapshot.hasUnreadNotifications
|
||||
markAllReadItem.isEnabled = snapshot.hasUnreadNotifications
|
||||
clearAllItem.isEnabled = snapshot.hasNotifications
|
||||
|
|
@ -3319,6 +3327,16 @@ final class MenuBarExtraController: NSObject, NSMenuDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
private func applyShortcut(_ shortcut: StoredShortcut, to item: NSMenuItem) {
|
||||
guard let keyEquivalent = shortcut.menuItemKeyEquivalent else {
|
||||
item.keyEquivalent = ""
|
||||
item.keyEquivalentModifierMask = []
|
||||
return
|
||||
}
|
||||
item.keyEquivalent = keyEquivalent
|
||||
item.keyEquivalentModifierMask = shortcut.modifierFlags
|
||||
}
|
||||
|
||||
private func rebuildInlineNotificationItems(recentNotifications: [TerminalNotification]) {
|
||||
for item in notificationItems {
|
||||
menu.removeItem(item)
|
||||
|
|
|
|||
|
|
@ -2544,7 +2544,7 @@ private struct TabItemView: View {
|
|||
.foregroundColor(isActive ? .white.opacity(0.7) : .secondary)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.help("Close Workspace (\(StoredShortcut(key: "w", command: true, shift: true, option: false, control: false).displayString))")
|
||||
.help(KeyboardShortcutSettings.Action.closeWorkspace.tooltip("Close Workspace"))
|
||||
.frame(width: 16, height: 16, alignment: .center)
|
||||
.opacity(showCloseButton && !showsWorkspaceShortcutHint ? 1 : 0)
|
||||
.allowsHitTesting(showCloseButton && !showsWorkspaceShortcutHint)
|
||||
|
|
@ -2772,6 +2772,8 @@ private struct TabItemView: View {
|
|||
let closeLabel = targetIds.count > 1 ? "Close Workspaces" : "Close Workspace"
|
||||
let markReadLabel = targetIds.count > 1 ? "Mark Workspaces as Read" : "Mark Workspace as Read"
|
||||
let markUnreadLabel = targetIds.count > 1 ? "Mark Workspaces as Unread" : "Mark Workspace as Unread"
|
||||
let renameWorkspaceShortcut = KeyboardShortcutSettings.shortcut(for: .renameWorkspace)
|
||||
let closeWorkspaceShortcut = KeyboardShortcutSettings.shortcut(for: .closeWorkspace)
|
||||
Button(pinLabel) {
|
||||
for id in targetIds {
|
||||
if let tab = tabManager.tabs.first(where: { $0.id == id }) {
|
||||
|
|
@ -2781,8 +2783,15 @@ private struct TabItemView: View {
|
|||
syncSelectionAfterMutation()
|
||||
}
|
||||
|
||||
Button("Rename Workspace…") {
|
||||
promptRename()
|
||||
if let key = renameWorkspaceShortcut.keyEquivalent {
|
||||
Button("Rename Workspace…") {
|
||||
promptRename()
|
||||
}
|
||||
.keyboardShortcut(key, modifiers: renameWorkspaceShortcut.eventModifiers)
|
||||
} else {
|
||||
Button("Rename Workspace…") {
|
||||
promptRename()
|
||||
}
|
||||
}
|
||||
|
||||
if tab.hasCustomTitle {
|
||||
|
|
@ -2811,10 +2820,18 @@ private struct TabItemView: View {
|
|||
|
||||
Divider()
|
||||
|
||||
Button(closeLabel) {
|
||||
closeTabs(targetIds, allowPinned: true)
|
||||
if let key = closeWorkspaceShortcut.keyEquivalent {
|
||||
Button(closeLabel) {
|
||||
closeTabs(targetIds, allowPinned: true)
|
||||
}
|
||||
.keyboardShortcut(key, modifiers: closeWorkspaceShortcut.eventModifiers)
|
||||
.disabled(targetIds.isEmpty)
|
||||
} else {
|
||||
Button(closeLabel) {
|
||||
closeTabs(targetIds, allowPinned: true)
|
||||
}
|
||||
.disabled(targetIds.isEmpty)
|
||||
}
|
||||
.disabled(targetIds.isEmpty)
|
||||
|
||||
Button("Close Other Workspaces") {
|
||||
closeOtherTabs(targetIds)
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ enum KeyboardShortcutSettings {
|
|||
case nextSidebarTab
|
||||
case prevSidebarTab
|
||||
case renameWorkspace
|
||||
case closeWorkspace
|
||||
case newSurface
|
||||
|
||||
// Panes / splits
|
||||
|
|
@ -50,6 +51,7 @@ enum KeyboardShortcutSettings {
|
|||
case .nextSidebarTab: return "Next Workspace"
|
||||
case .prevSidebarTab: return "Previous Workspace"
|
||||
case .renameWorkspace: return "Rename Workspace"
|
||||
case .closeWorkspace: return "Close Workspace"
|
||||
case .newSurface: return "New Surface"
|
||||
case .focusLeft: return "Focus Pane Left"
|
||||
case .focusRight: return "Focus Pane Right"
|
||||
|
|
@ -76,6 +78,7 @@ enum KeyboardShortcutSettings {
|
|||
case .nextSidebarTab: return "shortcut.nextSidebarTab"
|
||||
case .prevSidebarTab: return "shortcut.prevSidebarTab"
|
||||
case .renameWorkspace: return "shortcut.renameWorkspace"
|
||||
case .closeWorkspace: return "shortcut.closeWorkspace"
|
||||
case .focusLeft: return "shortcut.focusLeft"
|
||||
case .focusRight: return "shortcut.focusRight"
|
||||
case .focusUp: return "shortcut.focusUp"
|
||||
|
|
@ -113,6 +116,8 @@ enum KeyboardShortcutSettings {
|
|||
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 .closeWorkspace:
|
||||
return StoredShortcut(key: "w", command: true, shift: true, option: false, control: false)
|
||||
case .focusLeft:
|
||||
return StoredShortcut(key: "←", command: true, shift: false, option: true, control: false)
|
||||
case .focusRight:
|
||||
|
|
@ -196,6 +201,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 closeWorkspaceShortcut() -> StoredShortcut { shortcut(for: .closeWorkspace) }
|
||||
|
||||
static func focusLeftShortcut() -> StoredShortcut { shortcut(for: .focusLeft) }
|
||||
static func focusRightShortcut() -> StoredShortcut { shortcut(for: .focusRight) }
|
||||
|
|
@ -250,6 +256,65 @@ struct StoredShortcut: Codable, Equatable {
|
|||
return flags
|
||||
}
|
||||
|
||||
var keyEquivalent: KeyEquivalent? {
|
||||
switch key {
|
||||
case "←":
|
||||
return .leftArrow
|
||||
case "→":
|
||||
return .rightArrow
|
||||
case "↑":
|
||||
return .upArrow
|
||||
case "↓":
|
||||
return .downArrow
|
||||
case "\t":
|
||||
return .tab
|
||||
default:
|
||||
let lowered = key.lowercased()
|
||||
guard lowered.count == 1, let character = lowered.first else { return nil }
|
||||
return KeyEquivalent(character)
|
||||
}
|
||||
}
|
||||
|
||||
var eventModifiers: EventModifiers {
|
||||
var modifiers: EventModifiers = []
|
||||
if command {
|
||||
modifiers.insert(.command)
|
||||
}
|
||||
if shift {
|
||||
modifiers.insert(.shift)
|
||||
}
|
||||
if option {
|
||||
modifiers.insert(.option)
|
||||
}
|
||||
if control {
|
||||
modifiers.insert(.control)
|
||||
}
|
||||
return modifiers
|
||||
}
|
||||
|
||||
var menuItemKeyEquivalent: String? {
|
||||
switch key {
|
||||
case "←":
|
||||
guard let scalar = UnicodeScalar(NSLeftArrowFunctionKey) else { return nil }
|
||||
return String(Character(scalar))
|
||||
case "→":
|
||||
guard let scalar = UnicodeScalar(NSRightArrowFunctionKey) else { return nil }
|
||||
return String(Character(scalar))
|
||||
case "↑":
|
||||
guard let scalar = UnicodeScalar(NSUpArrowFunctionKey) else { return nil }
|
||||
return String(Character(scalar))
|
||||
case "↓":
|
||||
guard let scalar = UnicodeScalar(NSDownArrowFunctionKey) else { return nil }
|
||||
return String(Character(scalar))
|
||||
case "\t":
|
||||
return "\t"
|
||||
default:
|
||||
let lowered = key.lowercased()
|
||||
guard lowered.count == 1 else { return nil }
|
||||
return lowered
|
||||
}
|
||||
}
|
||||
|
||||
static func from(event: NSEvent) -> StoredShortcut? {
|
||||
guard let key = storedKey(from: event) else { return nil }
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ struct NotificationsPage: View {
|
|||
@EnvironmentObject var tabManager: TabManager
|
||||
@Binding var selection: SidebarSelection
|
||||
@FocusState private var focusedNotificationId: UUID?
|
||||
@AppStorage(KeyboardShortcutSettings.Action.jumpToUnread.defaultsKey) private var jumpToUnreadShortcutData = Data()
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
|
|
@ -73,6 +74,8 @@ struct NotificationsPage: View {
|
|||
Spacer()
|
||||
|
||||
if !notificationStore.notifications.isEmpty {
|
||||
jumpToUnreadButton
|
||||
|
||||
Button("Clear All") {
|
||||
notificationStore.clearAll()
|
||||
}
|
||||
|
|
@ -97,11 +100,76 @@ struct NotificationsPage: View {
|
|||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var jumpToUnreadButton: some View {
|
||||
if let key = jumpToUnreadShortcut.keyEquivalent {
|
||||
Button(action: {
|
||||
AppDelegate.shared?.jumpToLatestUnread()
|
||||
}) {
|
||||
HStack(spacing: 6) {
|
||||
Text("Jump to Latest Unread")
|
||||
ShortcutAnnotation(text: jumpToUnreadShortcut.displayString)
|
||||
}
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.keyboardShortcut(key, modifiers: jumpToUnreadShortcut.eventModifiers)
|
||||
.help(KeyboardShortcutSettings.Action.jumpToUnread.tooltip("Jump to Latest Unread"))
|
||||
.disabled(!hasUnreadNotifications)
|
||||
} else {
|
||||
Button(action: {
|
||||
AppDelegate.shared?.jumpToLatestUnread()
|
||||
}) {
|
||||
HStack(spacing: 6) {
|
||||
Text("Jump to Latest Unread")
|
||||
ShortcutAnnotation(text: jumpToUnreadShortcut.displayString)
|
||||
}
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.help(KeyboardShortcutSettings.Action.jumpToUnread.tooltip("Jump to Latest Unread"))
|
||||
.disabled(!hasUnreadNotifications)
|
||||
}
|
||||
}
|
||||
|
||||
private var jumpToUnreadShortcut: StoredShortcut {
|
||||
decodeShortcut(
|
||||
from: jumpToUnreadShortcutData,
|
||||
fallback: KeyboardShortcutSettings.Action.jumpToUnread.defaultShortcut
|
||||
)
|
||||
}
|
||||
|
||||
private var hasUnreadNotifications: Bool {
|
||||
notificationStore.notifications.contains(where: { !$0.isRead })
|
||||
}
|
||||
|
||||
private func decodeShortcut(from data: Data, fallback: StoredShortcut) -> StoredShortcut {
|
||||
guard !data.isEmpty,
|
||||
let shortcut = try? JSONDecoder().decode(StoredShortcut.self, from: data) else {
|
||||
return fallback
|
||||
}
|
||||
return shortcut
|
||||
}
|
||||
|
||||
private func tabTitle(for tabId: UUID) -> String? {
|
||||
AppDelegate.shared?.tabTitle(for: tabId) ?? tabManager.tabs.first(where: { $0.id == tabId })?.title
|
||||
}
|
||||
}
|
||||
|
||||
private struct ShortcutAnnotation: View {
|
||||
let text: String
|
||||
|
||||
var body: some View {
|
||||
Text(text)
|
||||
.font(.system(size: 10, weight: .semibold, design: .rounded))
|
||||
.foregroundStyle(.primary)
|
||||
.padding(.horizontal, 6)
|
||||
.padding(.vertical, 2)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 5)
|
||||
.fill(Color(nsColor: .controlBackgroundColor))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private struct NotificationRow: View {
|
||||
let notification: TerminalNotification
|
||||
let tabTitle: String?
|
||||
|
|
|
|||
|
|
@ -500,7 +500,7 @@ struct BrowserPanelView: View {
|
|||
}
|
||||
.buttonStyle(OmnibarAddressButtonStyle())
|
||||
.frame(width: addressBarButtonSize, height: addressBarButtonSize, alignment: .center)
|
||||
.help("Toggle Developer Tools")
|
||||
.help(KeyboardShortcutSettings.Action.toggleBrowserDeveloperTools.tooltip("Toggle Developer Tools"))
|
||||
.accessibilityIdentifier("BrowserToggleDevToolsButton")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -177,6 +177,8 @@ extension WorkspaceContentView {
|
|||
struct EmptyPanelView: View {
|
||||
@ObservedObject var workspace: Workspace
|
||||
let paneId: PaneID
|
||||
@AppStorage(KeyboardShortcutSettings.Action.newSurface.defaultsKey) private var newSurfaceShortcutData = Data()
|
||||
@AppStorage(KeyboardShortcutSettings.Action.openBrowser.defaultsKey) private var openBrowserShortcutData = Data()
|
||||
|
||||
private struct ShortcutHint: View {
|
||||
let text: String
|
||||
|
|
@ -211,6 +213,49 @@ struct EmptyPanelView: View {
|
|||
_ = workspace.newBrowserSurface(inPane: paneId)
|
||||
}
|
||||
|
||||
private var newSurfaceShortcut: StoredShortcut {
|
||||
decodeShortcut(from: newSurfaceShortcutData, fallback: KeyboardShortcutSettings.Action.newSurface.defaultShortcut)
|
||||
}
|
||||
|
||||
private var openBrowserShortcut: StoredShortcut {
|
||||
decodeShortcut(from: openBrowserShortcutData, fallback: KeyboardShortcutSettings.Action.openBrowser.defaultShortcut)
|
||||
}
|
||||
|
||||
private func decodeShortcut(from data: Data, fallback: StoredShortcut) -> StoredShortcut {
|
||||
guard !data.isEmpty,
|
||||
let shortcut = try? JSONDecoder().decode(StoredShortcut.self, from: data) else {
|
||||
return fallback
|
||||
}
|
||||
return shortcut
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func emptyPaneActionButton(
|
||||
title: String,
|
||||
systemImage: String,
|
||||
shortcut: StoredShortcut,
|
||||
action: @escaping () -> Void
|
||||
) -> some View {
|
||||
if let key = shortcut.keyEquivalent {
|
||||
Button(action: action) {
|
||||
HStack(spacing: 10) {
|
||||
Label(title, systemImage: systemImage)
|
||||
ShortcutHint(text: shortcut.displayString)
|
||||
}
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.keyboardShortcut(key, modifiers: shortcut.eventModifiers)
|
||||
} else {
|
||||
Button(action: action) {
|
||||
HStack(spacing: 10) {
|
||||
Label(title, systemImage: systemImage)
|
||||
ShortcutHint(text: shortcut.displayString)
|
||||
}
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 16) {
|
||||
Image(systemName: "terminal.fill")
|
||||
|
|
@ -222,27 +267,19 @@ struct EmptyPanelView: View {
|
|||
.foregroundStyle(.secondary)
|
||||
|
||||
HStack(spacing: 12) {
|
||||
Button {
|
||||
createTerminal()
|
||||
} label: {
|
||||
HStack(spacing: 10) {
|
||||
Label("Terminal", systemImage: "terminal.fill")
|
||||
ShortcutHint(text: "⌘T")
|
||||
}
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.keyboardShortcut("t", modifiers: [.command])
|
||||
emptyPaneActionButton(
|
||||
title: "Terminal",
|
||||
systemImage: "terminal.fill",
|
||||
shortcut: newSurfaceShortcut,
|
||||
action: createTerminal
|
||||
)
|
||||
|
||||
Button {
|
||||
createBrowser()
|
||||
} label: {
|
||||
HStack(spacing: 10) {
|
||||
Label("Browser", systemImage: "globe")
|
||||
ShortcutHint(text: "⌘⇧L")
|
||||
}
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.keyboardShortcut("l", modifiers: [.command, .shift])
|
||||
emptyPaneActionButton(
|
||||
title: "Browser",
|
||||
systemImage: "globe",
|
||||
shortcut: openBrowserShortcut,
|
||||
action: createBrowser
|
||||
)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
|
|
|
|||
|
|
@ -13,6 +13,15 @@ struct cmuxApp: App {
|
|||
@AppStorage("titlebarControlsStyle") private var titlebarControlsStyle = TitlebarControlsStyle.classic.rawValue
|
||||
@AppStorage(ShortcutHintDebugSettings.alwaysShowHintsKey) private var alwaysShowShortcutHints = ShortcutHintDebugSettings.defaultAlwaysShowHints
|
||||
@AppStorage(SocketControlSettings.appStorageKey) private var socketControlMode = SocketControlSettings.defaultMode.rawValue
|
||||
@AppStorage(KeyboardShortcutSettings.Action.toggleSidebar.defaultsKey) private var toggleSidebarShortcutData = Data()
|
||||
@AppStorage(KeyboardShortcutSettings.Action.newTab.defaultsKey) private var newWorkspaceShortcutData = Data()
|
||||
@AppStorage(KeyboardShortcutSettings.Action.newWindow.defaultsKey) private var newWindowShortcutData = Data()
|
||||
@AppStorage(KeyboardShortcutSettings.Action.showNotifications.defaultsKey) private var showNotificationsShortcutData = Data()
|
||||
@AppStorage(KeyboardShortcutSettings.Action.jumpToUnread.defaultsKey) private var jumpToUnreadShortcutData = Data()
|
||||
@AppStorage(KeyboardShortcutSettings.Action.nextSurface.defaultsKey) private var nextSurfaceShortcutData = Data()
|
||||
@AppStorage(KeyboardShortcutSettings.Action.prevSurface.defaultsKey) private var prevSurfaceShortcutData = Data()
|
||||
@AppStorage(KeyboardShortcutSettings.Action.nextSidebarTab.defaultsKey) private var nextWorkspaceShortcutData = Data()
|
||||
@AppStorage(KeyboardShortcutSettings.Action.prevSidebarTab.defaultsKey) private var prevWorkspaceShortcutData = Data()
|
||||
@AppStorage(KeyboardShortcutSettings.Action.splitRight.defaultsKey) private var splitRightShortcutData = Data()
|
||||
@AppStorage(KeyboardShortcutSettings.Action.splitDown.defaultsKey) private var splitDownShortcutData = Data()
|
||||
@AppStorage(KeyboardShortcutSettings.Action.toggleBrowserDeveloperTools.defaultsKey)
|
||||
|
|
@ -21,6 +30,8 @@ struct cmuxApp: App {
|
|||
private var showBrowserJavaScriptConsoleShortcutData = Data()
|
||||
@AppStorage(KeyboardShortcutSettings.Action.splitBrowserRight.defaultsKey) private var splitBrowserRightShortcutData = Data()
|
||||
@AppStorage(KeyboardShortcutSettings.Action.splitBrowserDown.defaultsKey) private var splitBrowserDownShortcutData = Data()
|
||||
@AppStorage(KeyboardShortcutSettings.Action.renameWorkspace.defaultsKey) private var renameWorkspaceShortcutData = Data()
|
||||
@AppStorage(KeyboardShortcutSettings.Action.closeWorkspace.defaultsKey) private var closeWorkspaceShortcutData = Data()
|
||||
@NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
|
||||
|
||||
init() {
|
||||
|
|
@ -257,11 +268,11 @@ struct cmuxApp: App {
|
|||
Divider()
|
||||
}
|
||||
|
||||
Button("Show Notifications") {
|
||||
splitCommandButton(title: "Show Notifications", shortcut: showNotificationsMenuShortcut) {
|
||||
showNotificationsPopover()
|
||||
}
|
||||
|
||||
Button("Jump to Latest Unread") {
|
||||
splitCommandButton(title: "Jump to Latest Unread", shortcut: jumpToUnreadMenuShortcut) {
|
||||
appDelegate.jumpToLatestUnread()
|
||||
}
|
||||
.disabled(!snapshot.hasUnreadNotifications)
|
||||
|
|
@ -337,12 +348,11 @@ struct cmuxApp: App {
|
|||
|
||||
// New tab commands
|
||||
CommandGroup(replacing: .newItem) {
|
||||
Button("New Window") {
|
||||
splitCommandButton(title: "New Window", shortcut: newWindowMenuShortcut) {
|
||||
appDelegate.openNewMainWindow(nil)
|
||||
}
|
||||
.keyboardShortcut("n", modifiers: [.command, .shift])
|
||||
|
||||
Button("New Workspace") {
|
||||
splitCommandButton(title: "New Workspace", shortcut: newWorkspaceMenuShortcut) {
|
||||
(AppDelegate.shared?.tabManager ?? tabManager).addTab()
|
||||
}
|
||||
}
|
||||
|
|
@ -359,10 +369,9 @@ struct cmuxApp: App {
|
|||
|
||||
// Cmd+Shift+W closes the current workspace (with confirmation if needed). If this
|
||||
// is the last workspace, it closes the window.
|
||||
Button("Close Workspace") {
|
||||
splitCommandButton(title: "Close Workspace", shortcut: closeWorkspaceMenuShortcut) {
|
||||
closeTabOrWindow()
|
||||
}
|
||||
.keyboardShortcut("w", modifiers: [.command, .shift])
|
||||
|
||||
Button("Reopen Closed Browser Panel") {
|
||||
_ = (AppDelegate.shared?.tabManager ?? tabManager).reopenMostRecentlyClosedBrowserPanel()
|
||||
|
|
@ -408,17 +417,17 @@ struct cmuxApp: App {
|
|||
|
||||
// Tab navigation
|
||||
CommandGroup(after: .toolbar) {
|
||||
Button("Toggle Sidebar") {
|
||||
splitCommandButton(title: "Toggle Sidebar", shortcut: toggleSidebarMenuShortcut) {
|
||||
sidebarState.toggle()
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
Button("Next Surface") {
|
||||
splitCommandButton(title: "Next Surface", shortcut: nextSurfaceMenuShortcut) {
|
||||
(AppDelegate.shared?.tabManager ?? tabManager).selectNextSurface()
|
||||
}
|
||||
|
||||
Button("Previous Surface") {
|
||||
splitCommandButton(title: "Previous Surface", shortcut: prevSurfaceMenuShortcut) {
|
||||
(AppDelegate.shared?.tabManager ?? tabManager).selectPreviousSurface()
|
||||
}
|
||||
|
||||
|
|
@ -470,15 +479,15 @@ struct cmuxApp: App {
|
|||
BrowserHistoryStore.shared.clearHistory()
|
||||
}
|
||||
|
||||
Button("Next Workspace") {
|
||||
splitCommandButton(title: "Next Workspace", shortcut: nextWorkspaceMenuShortcut) {
|
||||
(AppDelegate.shared?.tabManager ?? tabManager).selectNextTab()
|
||||
}
|
||||
|
||||
Button("Previous Workspace") {
|
||||
splitCommandButton(title: "Previous Workspace", shortcut: prevWorkspaceMenuShortcut) {
|
||||
(AppDelegate.shared?.tabManager ?? tabManager).selectPreviousTab()
|
||||
}
|
||||
|
||||
Button("Rename Workspace…") {
|
||||
splitCommandButton(title: "Rename Workspace…", shortcut: renameWorkspaceMenuShortcut) {
|
||||
_ = AppDelegate.shared?.promptRenameSelectedWorkspace()
|
||||
}
|
||||
|
||||
|
|
@ -515,11 +524,11 @@ struct cmuxApp: App {
|
|||
|
||||
Divider()
|
||||
|
||||
Button("Jump to Latest Unread") {
|
||||
splitCommandButton(title: "Jump to Latest Unread", shortcut: jumpToUnreadMenuShortcut) {
|
||||
AppDelegate.shared?.jumpToLatestUnread()
|
||||
}
|
||||
|
||||
Button("Show Notifications") {
|
||||
splitCommandButton(title: "Show Notifications", shortcut: showNotificationsMenuShortcut) {
|
||||
showNotificationsPopover()
|
||||
}
|
||||
}
|
||||
|
|
@ -578,6 +587,54 @@ struct cmuxApp: App {
|
|||
decodeShortcut(from: splitRightShortcutData, fallback: KeyboardShortcutSettings.Action.splitRight.defaultShortcut)
|
||||
}
|
||||
|
||||
private var toggleSidebarMenuShortcut: StoredShortcut {
|
||||
decodeShortcut(from: toggleSidebarShortcutData, fallback: KeyboardShortcutSettings.Action.toggleSidebar.defaultShortcut)
|
||||
}
|
||||
|
||||
private var newWorkspaceMenuShortcut: StoredShortcut {
|
||||
decodeShortcut(from: newWorkspaceShortcutData, fallback: KeyboardShortcutSettings.Action.newTab.defaultShortcut)
|
||||
}
|
||||
|
||||
private var newWindowMenuShortcut: StoredShortcut {
|
||||
decodeShortcut(from: newWindowShortcutData, fallback: KeyboardShortcutSettings.Action.newWindow.defaultShortcut)
|
||||
}
|
||||
|
||||
private var showNotificationsMenuShortcut: StoredShortcut {
|
||||
decodeShortcut(
|
||||
from: showNotificationsShortcutData,
|
||||
fallback: KeyboardShortcutSettings.Action.showNotifications.defaultShortcut
|
||||
)
|
||||
}
|
||||
|
||||
private var jumpToUnreadMenuShortcut: StoredShortcut {
|
||||
decodeShortcut(
|
||||
from: jumpToUnreadShortcutData,
|
||||
fallback: KeyboardShortcutSettings.Action.jumpToUnread.defaultShortcut
|
||||
)
|
||||
}
|
||||
|
||||
private var nextSurfaceMenuShortcut: StoredShortcut {
|
||||
decodeShortcut(from: nextSurfaceShortcutData, fallback: KeyboardShortcutSettings.Action.nextSurface.defaultShortcut)
|
||||
}
|
||||
|
||||
private var prevSurfaceMenuShortcut: StoredShortcut {
|
||||
decodeShortcut(from: prevSurfaceShortcutData, fallback: KeyboardShortcutSettings.Action.prevSurface.defaultShortcut)
|
||||
}
|
||||
|
||||
private var nextWorkspaceMenuShortcut: StoredShortcut {
|
||||
decodeShortcut(
|
||||
from: nextWorkspaceShortcutData,
|
||||
fallback: KeyboardShortcutSettings.Action.nextSidebarTab.defaultShortcut
|
||||
)
|
||||
}
|
||||
|
||||
private var prevWorkspaceMenuShortcut: StoredShortcut {
|
||||
decodeShortcut(
|
||||
from: prevWorkspaceShortcutData,
|
||||
fallback: KeyboardShortcutSettings.Action.prevSidebarTab.defaultShortcut
|
||||
)
|
||||
}
|
||||
|
||||
private var splitDownMenuShortcut: StoredShortcut {
|
||||
decodeShortcut(from: splitDownShortcutData, fallback: KeyboardShortcutSettings.Action.splitDown.defaultShortcut)
|
||||
}
|
||||
|
|
@ -610,6 +667,20 @@ struct cmuxApp: App {
|
|||
)
|
||||
}
|
||||
|
||||
private var renameWorkspaceMenuShortcut: StoredShortcut {
|
||||
decodeShortcut(
|
||||
from: renameWorkspaceShortcutData,
|
||||
fallback: KeyboardShortcutSettings.Action.renameWorkspace.defaultShortcut
|
||||
)
|
||||
}
|
||||
|
||||
private var closeWorkspaceMenuShortcut: StoredShortcut {
|
||||
decodeShortcut(
|
||||
from: closeWorkspaceShortcutData,
|
||||
fallback: KeyboardShortcutSettings.Action.closeWorkspace.defaultShortcut
|
||||
)
|
||||
}
|
||||
|
||||
private var notificationMenuSnapshot: NotificationMenuSnapshot {
|
||||
NotificationMenuSnapshotBuilder.make(notifications: notificationStore.notifications)
|
||||
}
|
||||
|
|
@ -651,50 +722,14 @@ struct cmuxApp: App {
|
|||
|
||||
@ViewBuilder
|
||||
private func splitCommandButton(title: String, shortcut: StoredShortcut, action: @escaping () -> Void) -> some View {
|
||||
if let key = keyEquivalent(for: shortcut) {
|
||||
if let key = shortcut.keyEquivalent {
|
||||
Button(title, action: action)
|
||||
.keyboardShortcut(key, modifiers: eventModifiers(for: shortcut))
|
||||
.keyboardShortcut(key, modifiers: shortcut.eventModifiers)
|
||||
} else {
|
||||
Button(title, action: action)
|
||||
}
|
||||
}
|
||||
|
||||
private func keyEquivalent(for shortcut: StoredShortcut) -> KeyEquivalent? {
|
||||
switch shortcut.key {
|
||||
case "←":
|
||||
return .leftArrow
|
||||
case "→":
|
||||
return .rightArrow
|
||||
case "↑":
|
||||
return .upArrow
|
||||
case "↓":
|
||||
return .downArrow
|
||||
case "\t":
|
||||
return .tab
|
||||
default:
|
||||
let lowered = shortcut.key.lowercased()
|
||||
guard lowered.count == 1, let character = lowered.first else { return nil }
|
||||
return KeyEquivalent(character)
|
||||
}
|
||||
}
|
||||
|
||||
private func eventModifiers(for shortcut: StoredShortcut) -> EventModifiers {
|
||||
var modifiers: EventModifiers = []
|
||||
if shortcut.command {
|
||||
modifiers.insert(.command)
|
||||
}
|
||||
if shortcut.shift {
|
||||
modifiers.insert(.shift)
|
||||
}
|
||||
if shortcut.option {
|
||||
modifiers.insert(.option)
|
||||
}
|
||||
if shortcut.control {
|
||||
modifiers.insert(.control)
|
||||
}
|
||||
return modifiers
|
||||
}
|
||||
|
||||
private func closePanelOrWindow() {
|
||||
if let window = NSApp.keyWindow,
|
||||
window.identifier?.rawValue == "cmux.settings" {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import XCTest
|
||||
import AppKit
|
||||
import WebKit
|
||||
import SwiftUI
|
||||
import ObjectiveC.runtime
|
||||
|
||||
#if canImport(cmux_DEV)
|
||||
|
|
@ -331,6 +332,82 @@ final class WorkspaceRenameShortcutDefaultsTests: XCTestCase {
|
|||
XCTAssertFalse(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(.option))
|
||||
XCTAssertFalse(shortcut.eventModifiers.contains(.control))
|
||||
}
|
||||
|
||||
func testCloseWorkspaceShortcutDefaultsAndMetadata() {
|
||||
XCTAssertEqual(KeyboardShortcutSettings.Action.closeWorkspace.label, "Close Workspace")
|
||||
XCTAssertEqual(KeyboardShortcutSettings.Action.closeWorkspace.defaultsKey, "shortcut.closeWorkspace")
|
||||
|
||||
let shortcut = KeyboardShortcutSettings.Action.closeWorkspace.defaultShortcut
|
||||
XCTAssertEqual(shortcut.key, "w")
|
||||
XCTAssertTrue(shortcut.command)
|
||||
XCTAssertTrue(shortcut.shift)
|
||||
XCTAssertFalse(shortcut.option)
|
||||
XCTAssertFalse(shortcut.control)
|
||||
}
|
||||
|
||||
func testCloseWorkspaceShortcutConvertsToMenuShortcut() {
|
||||
let shortcut = KeyboardShortcutSettings.Action.closeWorkspace.defaultShortcut
|
||||
XCTAssertNotNil(shortcut.keyEquivalent)
|
||||
XCTAssertTrue(shortcut.eventModifiers.contains(.command))
|
||||
XCTAssertTrue(shortcut.eventModifiers.contains(.shift))
|
||||
XCTAssertFalse(shortcut.eventModifiers.contains(.option))
|
||||
XCTAssertFalse(shortcut.eventModifiers.contains(.control))
|
||||
}
|
||||
|
||||
func testNextPreviousWorkspaceShortcutDefaultsAndMetadata() {
|
||||
XCTAssertEqual(KeyboardShortcutSettings.Action.nextSidebarTab.label, "Next Workspace")
|
||||
XCTAssertEqual(KeyboardShortcutSettings.Action.prevSidebarTab.label, "Previous Workspace")
|
||||
XCTAssertEqual(KeyboardShortcutSettings.Action.nextSidebarTab.defaultsKey, "shortcut.nextSidebarTab")
|
||||
XCTAssertEqual(KeyboardShortcutSettings.Action.prevSidebarTab.defaultsKey, "shortcut.prevSidebarTab")
|
||||
|
||||
let nextShortcut = KeyboardShortcutSettings.Action.nextSidebarTab.defaultShortcut
|
||||
XCTAssertEqual(nextShortcut.key, "]")
|
||||
XCTAssertTrue(nextShortcut.command)
|
||||
XCTAssertFalse(nextShortcut.shift)
|
||||
XCTAssertFalse(nextShortcut.option)
|
||||
XCTAssertTrue(nextShortcut.control)
|
||||
|
||||
let prevShortcut = KeyboardShortcutSettings.Action.prevSidebarTab.defaultShortcut
|
||||
XCTAssertEqual(prevShortcut.key, "[")
|
||||
XCTAssertTrue(prevShortcut.command)
|
||||
XCTAssertFalse(prevShortcut.shift)
|
||||
XCTAssertFalse(prevShortcut.option)
|
||||
XCTAssertTrue(prevShortcut.control)
|
||||
}
|
||||
|
||||
func testNextPreviousWorkspaceShortcutsConvertToMenuShortcut() {
|
||||
let nextShortcut = KeyboardShortcutSettings.Action.nextSidebarTab.defaultShortcut
|
||||
XCTAssertNotNil(nextShortcut.keyEquivalent)
|
||||
XCTAssertEqual(nextShortcut.menuItemKeyEquivalent, "]")
|
||||
XCTAssertTrue(nextShortcut.eventModifiers.contains(.command))
|
||||
XCTAssertTrue(nextShortcut.eventModifiers.contains(.control))
|
||||
|
||||
let prevShortcut = KeyboardShortcutSettings.Action.prevSidebarTab.defaultShortcut
|
||||
XCTAssertNotNil(prevShortcut.keyEquivalent)
|
||||
XCTAssertEqual(prevShortcut.menuItemKeyEquivalent, "[")
|
||||
XCTAssertTrue(prevShortcut.eventModifiers.contains(.command))
|
||||
XCTAssertTrue(prevShortcut.eventModifiers.contains(.control))
|
||||
}
|
||||
|
||||
func testMenuItemKeyEquivalentHandlesArrowAndTabKeys() {
|
||||
XCTAssertNotNil(StoredShortcut(key: "←", command: true, shift: false, option: false, control: false).menuItemKeyEquivalent)
|
||||
XCTAssertNotNil(StoredShortcut(key: "→", command: true, shift: false, option: false, control: false).menuItemKeyEquivalent)
|
||||
XCTAssertNotNil(StoredShortcut(key: "↑", command: true, shift: false, option: false, control: false).menuItemKeyEquivalent)
|
||||
XCTAssertNotNil(StoredShortcut(key: "↓", command: true, shift: false, option: false, control: false).menuItemKeyEquivalent)
|
||||
XCTAssertEqual(
|
||||
StoredShortcut(key: "\t", command: true, shift: false, option: false, control: false).menuItemKeyEquivalent,
|
||||
"\t"
|
||||
)
|
||||
}
|
||||
|
||||
func testShortcutDefaultsKeysRemainUnique() {
|
||||
let keys = KeyboardShortcutSettings.Action.allCases.map(\.defaultsKey)
|
||||
XCTAssertEqual(Set(keys).count, keys.count)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue