Customizable number shortcuts (#1951)
* Allow customizing numbered workspace and surface shortcuts * Update bonsplit submodule to squashed main commit Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
661a4e8c7f
commit
33dcc606bf
7 changed files with 272 additions and 59 deletions
|
|
@ -57395,13 +57395,13 @@
|
|||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Show Cmd/Ctrl-Hold Shortcut Hints"
|
||||
"value": "Show Shortcut Hints While Holding Modifiers"
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Cmd/Ctrl長押しのショートカットヒントを表示"
|
||||
"value": "修飾キー長押しでショートカットヒントを表示"
|
||||
}
|
||||
},
|
||||
"zh-Hans": {
|
||||
|
|
@ -57508,13 +57508,13 @@
|
|||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Holding Cmd or Ctrl keeps shortcut hint pills hidden."
|
||||
"value": "Holding shortcut modifiers keeps shortcut hint pills hidden."
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "CmdまたはCtrlを長押ししてもショートカットヒントピルは非表示のままです。"
|
||||
"value": "ショートカットの修飾キーを長押ししてもショートカットヒントピルは非表示のままです。"
|
||||
}
|
||||
},
|
||||
"zh-Hans": {
|
||||
|
|
@ -57621,13 +57621,13 @@
|
|||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Holding Cmd (sidebar/titlebar) or Ctrl/Cmd (pane tabs) shows shortcut hint pills."
|
||||
"value": "Holding the configured shortcut modifiers shows shortcut hint pills."
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Cmd(サイドバー/タイトルバー)またはCtrl/Cmd(ペインタブ)を長押しするとショートカットヒントピルが表示されます。"
|
||||
"value": "設定されたショートカットの修飾キーを長押しするとショートカットヒントピルが表示されます。"
|
||||
}
|
||||
},
|
||||
"zh-Hans": {
|
||||
|
|
@ -61457,6 +61457,40 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"shortcut.selectSurfaceByNumber.label": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Select Surface 1…9"
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "サーフェス 1…9 を選択"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"shortcut.selectWorkspaceByNumber.label": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Select Workspace 1…9"
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "ワークスペース 1…9 を選択"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"shortcut.showBrowserJSConsole.label": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
|
|
|
|||
|
|
@ -1114,9 +1114,9 @@ final class ServeWebOutputCollector {
|
|||
}
|
||||
|
||||
enum WorkspaceShortcutMapper {
|
||||
/// Maps Cmd+digit workspace shortcuts to a zero-based workspace index.
|
||||
/// Cmd+1...Cmd+8 target fixed indices; Cmd+9 always targets the last workspace.
|
||||
static func workspaceIndex(forCommandDigit digit: Int, workspaceCount: Int) -> Int? {
|
||||
/// Maps numbered workspace shortcuts to a zero-based workspace index.
|
||||
/// 1...8 target fixed indices; 9 always targets the last workspace.
|
||||
static func workspaceIndex(forDigit digit: Int, workspaceCount: Int) -> Int? {
|
||||
guard workspaceCount > 0 else { return nil }
|
||||
guard (1...9).contains(digit) else { return nil }
|
||||
|
||||
|
|
@ -1128,12 +1128,12 @@ enum WorkspaceShortcutMapper {
|
|||
return index < workspaceCount ? index : nil
|
||||
}
|
||||
|
||||
/// Returns the primary Cmd+digit badge to display for a workspace row.
|
||||
/// Returns the primary digit badge to display for a workspace row.
|
||||
/// Picks the lowest digit that maps to that row index.
|
||||
static func commandDigitForWorkspace(at index: Int, workspaceCount: Int) -> Int? {
|
||||
static func digitForWorkspace(at index: Int, workspaceCount: Int) -> Int? {
|
||||
guard index >= 0 && index < workspaceCount else { return nil }
|
||||
for digit in 1...9 {
|
||||
if workspaceIndex(forCommandDigit: digit, workspaceCount: workspaceCount) == index {
|
||||
if workspaceIndex(forDigit: digit, workspaceCount: workspaceCount) == index {
|
||||
return digit
|
||||
}
|
||||
}
|
||||
|
|
@ -9474,30 +9474,33 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
|
|||
return true
|
||||
}
|
||||
|
||||
// Numeric shortcuts for specific sidebar tabs: Cmd+1-9 (9 = last workspace)
|
||||
if flags == [.command],
|
||||
let manager = tabManager,
|
||||
let num = Int(chars),
|
||||
let targetIndex = WorkspaceShortcutMapper.workspaceIndex(forCommandDigit: num, workspaceCount: manager.tabs.count) {
|
||||
// Numeric shortcuts for specific workspaces (9 = last workspace)
|
||||
if let manager = tabManager,
|
||||
let digit = numberedShortcutDigit(
|
||||
event: event,
|
||||
shortcut: KeyboardShortcutSettings.shortcut(for: .selectWorkspaceByNumber)
|
||||
),
|
||||
let targetIndex = WorkspaceShortcutMapper.workspaceIndex(forDigit: digit, workspaceCount: manager.tabs.count) {
|
||||
#if DEBUG
|
||||
dlog(
|
||||
"shortcut.action name=workspaceDigit digit=\(num) targetIndex=\(targetIndex) manager=\(debugManagerToken(manager)) \(debugShortcutRouteSnapshot(event: event))"
|
||||
"shortcut.action name=workspaceDigit digit=\(digit) targetIndex=\(targetIndex) manager=\(debugManagerToken(manager)) \(debugShortcutRouteSnapshot(event: event))"
|
||||
)
|
||||
#endif
|
||||
manager.selectTab(at: targetIndex)
|
||||
return true
|
||||
}
|
||||
|
||||
// Numeric shortcuts for surfaces within pane: Ctrl+1-9 (9 = last)
|
||||
if flags == [.control] {
|
||||
if let num = Int(chars), num >= 1 && num <= 9 {
|
||||
if num == 9 {
|
||||
tabManager?.selectLastSurface()
|
||||
} else {
|
||||
tabManager?.selectSurface(at: num - 1)
|
||||
}
|
||||
return true
|
||||
// Numeric shortcuts for surfaces within the focused pane (9 = last)
|
||||
if let digit = numberedShortcutDigit(
|
||||
event: event,
|
||||
shortcut: KeyboardShortcutSettings.shortcut(for: .selectSurfaceByNumber)
|
||||
) {
|
||||
if digit == 9 {
|
||||
tabManager?.selectLastSurface()
|
||||
} else {
|
||||
tabManager?.selectSurface(at: digit - 1)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Pane focus navigation (defaults to Cmd+Option+Arrow, but can be customized to letter/number keys).
|
||||
|
|
@ -10524,6 +10527,46 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
|
|||
return false
|
||||
}
|
||||
|
||||
private func numberedShortcutDigit(event: NSEvent, shortcut: StoredShortcut) -> Int? {
|
||||
let flags = event.modifierFlags.intersection(.deviceIndependentFlagsMask)
|
||||
.subtracting([.numericPad, .function, .capsLock])
|
||||
guard flags == shortcut.modifierFlags else { return nil }
|
||||
|
||||
if let digit = numberedShortcutDigit(
|
||||
eventCharacter: event.charactersIgnoringModifiers,
|
||||
applyShiftSymbolNormalization: flags.contains(.shift),
|
||||
eventKeyCode: event.keyCode
|
||||
) {
|
||||
return digit
|
||||
}
|
||||
|
||||
let layoutCharacter = shortcutLayoutCharacterProvider(event.keyCode, event.modifierFlags)
|
||||
if let digit = numberedShortcutDigit(
|
||||
eventCharacter: layoutCharacter,
|
||||
applyShiftSymbolNormalization: false,
|
||||
eventKeyCode: event.keyCode
|
||||
) {
|
||||
return digit
|
||||
}
|
||||
|
||||
return digitForNumberKeyCode(event.keyCode)
|
||||
}
|
||||
|
||||
private func numberedShortcutDigit(
|
||||
eventCharacter: String?,
|
||||
applyShiftSymbolNormalization: Bool,
|
||||
eventKeyCode: UInt16
|
||||
) -> Int? {
|
||||
guard let eventCharacter, !eventCharacter.isEmpty else { return nil }
|
||||
let normalized = normalizedShortcutEventCharacter(
|
||||
eventCharacter,
|
||||
applyShiftSymbolNormalization: applyShiftSymbolNormalization,
|
||||
eventKeyCode: eventKeyCode
|
||||
)
|
||||
guard let digit = Int(normalized), (1...9).contains(digit) else { return nil }
|
||||
return digit
|
||||
}
|
||||
|
||||
private func shouldRequireCharacterMatchForCommandShortcut(shortcutKey: String) -> Bool {
|
||||
guard shortcutKey.count == 1, let scalar = shortcutKey.unicodeScalars.first else {
|
||||
return false
|
||||
|
|
@ -10643,6 +10686,22 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
|
|||
}
|
||||
}
|
||||
|
||||
private func digitForNumberKeyCode(_ keyCode: UInt16) -> Int? {
|
||||
switch keyCode {
|
||||
case 18: return 1 // kVK_ANSI_1
|
||||
case 19: return 2 // kVK_ANSI_2
|
||||
case 20: return 3 // kVK_ANSI_3
|
||||
case 21: return 4 // kVK_ANSI_4
|
||||
case 23: return 5 // kVK_ANSI_5
|
||||
case 22: return 6 // kVK_ANSI_6
|
||||
case 26: return 7 // kVK_ANSI_7
|
||||
case 28: return 8 // kVK_ANSI_8
|
||||
case 25: return 9 // kVK_ANSI_9
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/// Match arrow key shortcuts using keyCode
|
||||
/// Arrow keys include .numericPad and .function in their modifierFlags, so strip those before comparing.
|
||||
private func matchArrowShortcut(event: NSEvent, shortcut: StoredShortcut, keyCode: UInt16) -> Bool {
|
||||
|
|
|
|||
|
|
@ -8187,6 +8187,8 @@ struct VerticalTabsSidebar: View {
|
|||
private var sidebarShowNotificationMessage = SidebarWorkspaceDetailSettings.defaultShowNotificationMessage
|
||||
@AppStorage(WorkspacePresentationModeSettings.modeKey)
|
||||
private var workspacePresentationMode = WorkspacePresentationModeSettings.defaultMode.rawValue
|
||||
@AppStorage(KeyboardShortcutSettings.Action.selectWorkspaceByNumber.defaultsKey)
|
||||
private var selectWorkspaceByNumberShortcutData = Data()
|
||||
|
||||
/// Space at top of sidebar for traffic light buttons
|
||||
private let trafficLightPadding: CGFloat = 28
|
||||
|
|
@ -8204,9 +8206,25 @@ struct VerticalTabsSidebar: View {
|
|||
)
|
||||
}
|
||||
|
||||
private var workspaceNumberShortcut: StoredShortcut {
|
||||
decodeShortcut(
|
||||
from: selectWorkspaceByNumberShortcutData,
|
||||
fallback: KeyboardShortcutSettings.Action.selectWorkspaceByNumber.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
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
let workspaceCount = tabManager.tabs.count
|
||||
let canCloseWorkspace = workspaceCount > 1
|
||||
let workspaceNumberShortcut = self.workspaceNumberShortcut
|
||||
|
||||
VStack(spacing: 0) {
|
||||
GeometryReader { proxy in
|
||||
|
|
@ -8231,10 +8249,11 @@ struct VerticalTabsSidebar: View {
|
|||
tab: tab,
|
||||
index: index,
|
||||
isActive: tabManager.selectedTabId == tab.id,
|
||||
workspaceShortcutDigit: WorkspaceShortcutMapper.commandDigitForWorkspace(
|
||||
workspaceShortcutDigit: WorkspaceShortcutMapper.digitForWorkspace(
|
||||
at: index,
|
||||
workspaceCount: workspaceCount
|
||||
),
|
||||
workspaceShortcutModifierSymbol: workspaceNumberShortcut.modifierDisplayString,
|
||||
canCloseWorkspace: canCloseWorkspace,
|
||||
accessibilityWorkspaceCount: workspaceCount,
|
||||
unreadCount: notificationStore.unreadCount(forTabId: tab.id),
|
||||
|
|
@ -8378,7 +8397,8 @@ enum ShortcutHintModifierPolicy {
|
|||
defaults: UserDefaults = .standard
|
||||
) -> Bool {
|
||||
let normalized = modifierFlags.intersection(.deviceIndependentFlagsMask)
|
||||
guard normalized == [.command] else {
|
||||
.subtracting([.numericPad, .function, .capsLock])
|
||||
guard normalized == KeyboardShortcutSettings.shortcut(for: .selectWorkspaceByNumber).modifierFlags else {
|
||||
return false
|
||||
}
|
||||
return ShortcutHintDebugSettings.showHintsOnCommandHoldEnabled(defaults: defaults)
|
||||
|
|
@ -10637,6 +10657,7 @@ private struct TabItemView: View, Equatable {
|
|||
lhs.index == rhs.index &&
|
||||
lhs.isActive == rhs.isActive &&
|
||||
lhs.workspaceShortcutDigit == rhs.workspaceShortcutDigit &&
|
||||
lhs.workspaceShortcutModifierSymbol == rhs.workspaceShortcutModifierSymbol &&
|
||||
lhs.canCloseWorkspace == rhs.canCloseWorkspace &&
|
||||
lhs.accessibilityWorkspaceCount == rhs.accessibilityWorkspaceCount &&
|
||||
lhs.unreadCount == rhs.unreadCount &&
|
||||
|
|
@ -10658,6 +10679,7 @@ private struct TabItemView: View, Equatable {
|
|||
let index: Int
|
||||
let isActive: Bool
|
||||
let workspaceShortcutDigit: Int?
|
||||
let workspaceShortcutModifierSymbol: String
|
||||
let canCloseWorkspace: Bool
|
||||
let accessibilityWorkspaceCount: Int
|
||||
let unreadCount: Int
|
||||
|
|
@ -10772,7 +10794,7 @@ private struct TabItemView: View, Equatable {
|
|||
|
||||
private var workspaceShortcutLabel: String? {
|
||||
guard let workspaceShortcutDigit else { return nil }
|
||||
return "⌘\(workspaceShortcutDigit)"
|
||||
return "\(workspaceShortcutModifierSymbol)\(workspaceShortcutDigit)"
|
||||
}
|
||||
|
||||
private var showsWorkspaceShortcutHint: Bool {
|
||||
|
|
|
|||
|
|
@ -21,8 +21,10 @@ enum KeyboardShortcutSettings {
|
|||
// Navigation
|
||||
case nextSurface
|
||||
case prevSurface
|
||||
case selectSurfaceByNumber
|
||||
case nextSidebarTab
|
||||
case prevSidebarTab
|
||||
case selectWorkspaceByNumber
|
||||
case renameTab
|
||||
case renameWorkspace
|
||||
case closeWorkspace
|
||||
|
|
@ -60,8 +62,10 @@ enum KeyboardShortcutSettings {
|
|||
case .triggerFlash: return String(localized: "shortcut.flashFocusedPanel.label", defaultValue: "Flash Focused Panel")
|
||||
case .nextSurface: return String(localized: "shortcut.nextSurface.label", defaultValue: "Next Surface")
|
||||
case .prevSurface: return String(localized: "shortcut.previousSurface.label", defaultValue: "Previous Surface")
|
||||
case .selectSurfaceByNumber: return String(localized: "shortcut.selectSurfaceByNumber.label", defaultValue: "Select Surface 1…9")
|
||||
case .nextSidebarTab: return String(localized: "shortcut.nextWorkspace.label", defaultValue: "Next Workspace")
|
||||
case .prevSidebarTab: return String(localized: "shortcut.previousWorkspace.label", defaultValue: "Previous Workspace")
|
||||
case .selectWorkspaceByNumber: return String(localized: "shortcut.selectWorkspaceByNumber.label", defaultValue: "Select Workspace 1…9")
|
||||
case .renameTab: return String(localized: "shortcut.renameTab.label", defaultValue: "Rename Tab")
|
||||
case .renameWorkspace: return String(localized: "shortcut.renameWorkspace.label", defaultValue: "Rename Workspace")
|
||||
case .closeWorkspace: return String(localized: "shortcut.closeWorkspace.label", defaultValue: "Close Workspace")
|
||||
|
|
@ -93,6 +97,7 @@ enum KeyboardShortcutSettings {
|
|||
case .showNotifications: return "shortcut.showNotifications"
|
||||
case .jumpToUnread: return "shortcut.jumpToUnread"
|
||||
case .triggerFlash: return "shortcut.triggerFlash"
|
||||
case .selectWorkspaceByNumber: return "shortcut.selectWorkspaceByNumber"
|
||||
case .nextSidebarTab: return "shortcut.nextSidebarTab"
|
||||
case .prevSidebarTab: return "shortcut.prevSidebarTab"
|
||||
case .renameTab: return "shortcut.renameTab"
|
||||
|
|
@ -109,6 +114,7 @@ enum KeyboardShortcutSettings {
|
|||
case .splitBrowserDown: return "shortcut.splitBrowserDown"
|
||||
case .nextSurface: return "shortcut.nextSurface"
|
||||
case .prevSurface: return "shortcut.prevSurface"
|
||||
case .selectSurfaceByNumber: return "shortcut.selectSurfaceByNumber"
|
||||
case .newSurface: return "shortcut.newSurface"
|
||||
case .toggleTerminalCopyMode: return "shortcut.toggleTerminalCopyMode"
|
||||
case .openBrowser: return "shortcut.openBrowser"
|
||||
|
|
@ -169,10 +175,14 @@ enum KeyboardShortcutSettings {
|
|||
return StoredShortcut(key: "]", command: true, shift: true, option: false, control: false)
|
||||
case .prevSurface:
|
||||
return StoredShortcut(key: "[", command: true, shift: true, option: false, control: false)
|
||||
case .selectSurfaceByNumber:
|
||||
return StoredShortcut(key: "1", command: false, shift: false, option: false, control: true)
|
||||
case .newSurface:
|
||||
return StoredShortcut(key: "t", command: true, shift: false, option: false, control: false)
|
||||
case .toggleTerminalCopyMode:
|
||||
return StoredShortcut(key: "m", command: true, shift: true, option: false, control: false)
|
||||
case .selectWorkspaceByNumber:
|
||||
return StoredShortcut(key: "1", command: true, shift: false, option: false, control: false)
|
||||
case .openBrowser:
|
||||
return StoredShortcut(key: "l", command: true, shift: true, option: false, control: false)
|
||||
case .toggleBrowserDeveloperTools:
|
||||
|
|
@ -185,7 +195,37 @@ enum KeyboardShortcutSettings {
|
|||
}
|
||||
|
||||
func tooltip(_ base: String) -> String {
|
||||
"\(base) (\(KeyboardShortcutSettings.shortcut(for: self).displayString))"
|
||||
"\(base) (\(displayedShortcutString(for: KeyboardShortcutSettings.shortcut(for: self))))"
|
||||
}
|
||||
|
||||
var usesNumberedDigitMatching: Bool {
|
||||
switch self {
|
||||
case .selectSurfaceByNumber, .selectWorkspaceByNumber:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func displayedShortcutString(for shortcut: StoredShortcut) -> String {
|
||||
if usesNumberedDigitMatching {
|
||||
return shortcut.modifierDisplayString + "1…9"
|
||||
}
|
||||
return shortcut.displayString
|
||||
}
|
||||
|
||||
func normalizedRecordedShortcut(_ shortcut: StoredShortcut) -> StoredShortcut? {
|
||||
guard usesNumberedDigitMatching else { return shortcut }
|
||||
guard let digit = Int(shortcut.key), (1...9).contains(digit) else {
|
||||
return nil
|
||||
}
|
||||
return StoredShortcut(
|
||||
key: "1",
|
||||
command: shortcut.command,
|
||||
shift: shortcut.shift,
|
||||
option: shortcut.option,
|
||||
control: shortcut.control
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -198,7 +238,16 @@ enum KeyboardShortcutSettings {
|
|||
}
|
||||
|
||||
static func setShortcut(_ shortcut: StoredShortcut, for action: Action) {
|
||||
if let data = try? JSONEncoder().encode(shortcut) {
|
||||
let storedShortcut: StoredShortcut
|
||||
if let normalizedShortcut = action.normalizedRecordedShortcut(shortcut) {
|
||||
storedShortcut = normalizedShortcut
|
||||
} else if action.usesNumberedDigitMatching {
|
||||
return
|
||||
} else {
|
||||
storedShortcut = shortcut
|
||||
}
|
||||
|
||||
if let data = try? JSONEncoder().encode(storedShortcut) {
|
||||
UserDefaults.standard.set(data, forKey: action.defaultsKey)
|
||||
}
|
||||
postDidChangeNotification(action: action)
|
||||
|
|
@ -267,7 +316,9 @@ enum KeyboardShortcutSettings {
|
|||
|
||||
static func nextSurfaceShortcut() -> StoredShortcut { shortcut(for: .nextSurface) }
|
||||
static func prevSurfaceShortcut() -> StoredShortcut { shortcut(for: .prevSurface) }
|
||||
static func selectSurfaceByNumberShortcut() -> StoredShortcut { shortcut(for: .selectSurfaceByNumber) }
|
||||
static func newSurfaceShortcut() -> StoredShortcut { shortcut(for: .newSurface) }
|
||||
static func selectWorkspaceByNumberShortcut() -> StoredShortcut { shortcut(for: .selectWorkspaceByNumber) }
|
||||
|
||||
static func openBrowserShortcut() -> StoredShortcut { shortcut(for: .openBrowser) }
|
||||
static func toggleBrowserDeveloperToolsShortcut() -> StoredShortcut { shortcut(for: .toggleBrowserDeveloperTools) }
|
||||
|
|
@ -283,22 +334,27 @@ struct StoredShortcut: Codable, Equatable {
|
|||
var control: Bool
|
||||
|
||||
var displayString: String {
|
||||
modifierDisplayString + keyDisplayString
|
||||
}
|
||||
|
||||
var modifierDisplayString: String {
|
||||
var parts: [String] = []
|
||||
if control { parts.append("⌃") }
|
||||
if option { parts.append("⌥") }
|
||||
if shift { parts.append("⇧") }
|
||||
if command { parts.append("⌘") }
|
||||
let keyText: String
|
||||
return parts.joined()
|
||||
}
|
||||
|
||||
var keyDisplayString: String {
|
||||
switch key {
|
||||
case "\t":
|
||||
keyText = "TAB"
|
||||
return "TAB"
|
||||
case "\r":
|
||||
keyText = "↩"
|
||||
return "↩"
|
||||
default:
|
||||
keyText = key.uppercased()
|
||||
return key.uppercased()
|
||||
}
|
||||
parts.append(keyText)
|
||||
return parts.joined()
|
||||
}
|
||||
|
||||
var modifierFlags: NSEvent.ModifierFlags {
|
||||
|
|
@ -436,6 +492,8 @@ struct StoredShortcut: Codable, Equatable {
|
|||
struct KeyboardShortcutRecorder: View {
|
||||
let label: String
|
||||
@Binding var shortcut: StoredShortcut
|
||||
var displayString: (StoredShortcut) -> String = { $0.displayString }
|
||||
var transformRecordedShortcut: (StoredShortcut) -> StoredShortcut? = { $0 }
|
||||
@State private var isRecording = false
|
||||
|
||||
var body: some View {
|
||||
|
|
@ -444,7 +502,12 @@ struct KeyboardShortcutRecorder: View {
|
|||
|
||||
Spacer()
|
||||
|
||||
ShortcutRecorderButton(shortcut: $shortcut, isRecording: $isRecording)
|
||||
ShortcutRecorderButton(
|
||||
shortcut: $shortcut,
|
||||
isRecording: $isRecording,
|
||||
displayString: displayString,
|
||||
transformRecordedShortcut: transformRecordedShortcut
|
||||
)
|
||||
.frame(width: 120)
|
||||
}
|
||||
}
|
||||
|
|
@ -453,10 +516,14 @@ struct KeyboardShortcutRecorder: View {
|
|||
private struct ShortcutRecorderButton: NSViewRepresentable {
|
||||
@Binding var shortcut: StoredShortcut
|
||||
@Binding var isRecording: Bool
|
||||
let displayString: (StoredShortcut) -> String
|
||||
let transformRecordedShortcut: (StoredShortcut) -> StoredShortcut?
|
||||
|
||||
func makeNSView(context: Context) -> ShortcutRecorderNSButton {
|
||||
let button = ShortcutRecorderNSButton()
|
||||
button.shortcut = shortcut
|
||||
button.displayString = displayString
|
||||
button.transformRecordedShortcut = transformRecordedShortcut
|
||||
button.onShortcutRecorded = { newShortcut in
|
||||
shortcut = newShortcut
|
||||
isRecording = false
|
||||
|
|
@ -469,12 +536,16 @@ private struct ShortcutRecorderButton: NSViewRepresentable {
|
|||
|
||||
func updateNSView(_ nsView: ShortcutRecorderNSButton, context: Context) {
|
||||
nsView.shortcut = shortcut
|
||||
nsView.displayString = displayString
|
||||
nsView.transformRecordedShortcut = transformRecordedShortcut
|
||||
nsView.updateTitle()
|
||||
}
|
||||
}
|
||||
|
||||
private class ShortcutRecorderNSButton: NSButton {
|
||||
var shortcut: StoredShortcut = KeyboardShortcutSettings.showNotificationsDefault
|
||||
var displayString: (StoredShortcut) -> String = { $0.displayString }
|
||||
var transformRecordedShortcut: (StoredShortcut) -> StoredShortcut? = { $0 }
|
||||
var onShortcutRecorded: ((StoredShortcut) -> Void)?
|
||||
var onRecordingChanged: ((Bool) -> Void)?
|
||||
private var isRecording = false
|
||||
|
|
@ -502,7 +573,7 @@ private class ShortcutRecorderNSButton: NSButton {
|
|||
if isRecording {
|
||||
title = String(localized: "shortcut.pressShortcut.prompt", defaultValue: "Press shortcut…")
|
||||
} else {
|
||||
title = shortcut.displayString
|
||||
title = displayString(shortcut)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -528,8 +599,12 @@ private class ShortcutRecorderNSButton: NSButton {
|
|||
}
|
||||
|
||||
if let newShortcut = StoredShortcut.from(event: event) {
|
||||
self.shortcut = newShortcut
|
||||
self.onShortcutRecorded?(newShortcut)
|
||||
guard let transformedShortcut = self.transformRecordedShortcut(newShortcut) else {
|
||||
NSSound.beep()
|
||||
return nil
|
||||
}
|
||||
self.shortcut = transformedShortcut
|
||||
self.onShortcutRecorded?(transformedShortcut)
|
||||
self.stopRecording()
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11020,26 +11020,30 @@ class TerminalController {
|
|||
let name = parts[0].lowercased()
|
||||
let combo = parts[1].trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
let defaultsKey: String?
|
||||
let action: KeyboardShortcutSettings.Action?
|
||||
switch name {
|
||||
case "focus_left", "focusleft":
|
||||
defaultsKey = KeyboardShortcutSettings.focusLeftKey
|
||||
action = .focusLeft
|
||||
case "focus_right", "focusright":
|
||||
defaultsKey = KeyboardShortcutSettings.focusRightKey
|
||||
action = .focusRight
|
||||
case "focus_up", "focusup":
|
||||
defaultsKey = KeyboardShortcutSettings.focusUpKey
|
||||
action = .focusUp
|
||||
case "focus_down", "focusdown":
|
||||
defaultsKey = KeyboardShortcutSettings.focusDownKey
|
||||
action = .focusDown
|
||||
case "workspace_digits", "workspace_number", "select_workspace_by_number":
|
||||
action = .selectWorkspaceByNumber
|
||||
case "surface_digits", "surface_number", "select_surface_by_number":
|
||||
action = .selectSurfaceByNumber
|
||||
default:
|
||||
defaultsKey = nil
|
||||
action = nil
|
||||
}
|
||||
|
||||
guard let defaultsKey else {
|
||||
return "ERROR: Unknown shortcut name. Supported: focus_left, focus_right, focus_up, focus_down"
|
||||
guard let action else {
|
||||
return "ERROR: Unknown shortcut name. Supported: focus_left, focus_right, focus_up, focus_down, workspace_digits, surface_digits"
|
||||
}
|
||||
|
||||
if combo.lowercased() == "clear" || combo.lowercased() == "default" || combo.lowercased() == "reset" {
|
||||
UserDefaults.standard.removeObject(forKey: defaultsKey)
|
||||
KeyboardShortcutSettings.resetShortcut(for: action)
|
||||
return "OK"
|
||||
}
|
||||
|
||||
|
|
@ -11054,10 +11058,13 @@ class TerminalController {
|
|||
option: parsed.modifierFlags.contains(.option),
|
||||
control: parsed.modifierFlags.contains(.control)
|
||||
)
|
||||
guard let data = try? JSONEncoder().encode(shortcut) else {
|
||||
return "ERROR: Failed to encode shortcut"
|
||||
if action.usesNumberedDigitMatching,
|
||||
action.normalizedRecordedShortcut(shortcut) == nil {
|
||||
return "ERROR: Numbered shortcuts must use a digit key (1-9). Example: ctrl+1"
|
||||
}
|
||||
UserDefaults.standard.set(data, forKey: defaultsKey)
|
||||
|
||||
let storedShortcut = action.normalizedRecordedShortcut(shortcut) ?? shortcut
|
||||
KeyboardShortcutSettings.setShortcut(storedShortcut, for: action)
|
||||
return "OK"
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -155,6 +155,7 @@ struct cmuxApp: App {
|
|||
@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.selectWorkspaceByNumber.defaultsKey) private var selectWorkspaceByNumberShortcutData = Data()
|
||||
@AppStorage(KeyboardShortcutSettings.Action.splitRight.defaultsKey) private var splitRightShortcutData = Data()
|
||||
@AppStorage(KeyboardShortcutSettings.Action.splitDown.defaultsKey) private var splitDownShortcutData = Data()
|
||||
@AppStorage(BrowserToolbarAccessorySpacingDebugSettings.key) private var browserToolbarAccessorySpacingRaw = BrowserToolbarAccessorySpacingDebugSettings.defaultSpacing
|
||||
|
|
@ -799,15 +800,18 @@ struct cmuxApp: App {
|
|||
|
||||
Divider()
|
||||
|
||||
// Cmd+1 through Cmd+9 for workspace selection (9 = last workspace)
|
||||
// Numbered workspace selection (9 = last workspace)
|
||||
ForEach(1...9, id: \.self) { number in
|
||||
Button(String(localized: "menu.view.workspace", defaultValue: "Workspace \(number)")) {
|
||||
let manager = activeTabManager
|
||||
if let targetIndex = WorkspaceShortcutMapper.workspaceIndex(forCommandDigit: number, workspaceCount: manager.tabs.count) {
|
||||
if let targetIndex = WorkspaceShortcutMapper.workspaceIndex(forDigit: number, workspaceCount: manager.tabs.count) {
|
||||
manager.selectTab(at: targetIndex)
|
||||
}
|
||||
}
|
||||
.keyboardShortcut(KeyEquivalent(Character("\(number)")), modifiers: .command)
|
||||
.keyboardShortcut(
|
||||
KeyEquivalent(Character("\(number)")),
|
||||
modifiers: selectWorkspaceByNumberMenuShortcut.eventModifiers
|
||||
)
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
|
@ -921,6 +925,13 @@ struct cmuxApp: App {
|
|||
)
|
||||
}
|
||||
|
||||
private var selectWorkspaceByNumberMenuShortcut: StoredShortcut {
|
||||
decodeShortcut(
|
||||
from: selectWorkspaceByNumberShortcutData,
|
||||
fallback: KeyboardShortcutSettings.Action.selectWorkspaceByNumber.defaultShortcut
|
||||
)
|
||||
}
|
||||
|
||||
private var splitDownMenuShortcut: StoredShortcut {
|
||||
decodeShortcut(from: splitDownShortcutData, fallback: KeyboardShortcutSettings.Action.splitDown.defaultShortcut)
|
||||
}
|
||||
|
|
@ -6129,7 +6140,12 @@ private struct ShortcutSettingRow: View {
|
|||
}
|
||||
|
||||
var body: some View {
|
||||
KeyboardShortcutRecorder(label: action.label, shortcut: $shortcut)
|
||||
KeyboardShortcutRecorder(
|
||||
label: action.label,
|
||||
shortcut: $shortcut,
|
||||
displayString: { action.displayedShortcutString(for: $0) },
|
||||
transformRecordedShortcut: { action.normalizedRecordedShortcut($0) }
|
||||
)
|
||||
.onChange(of: shortcut) { newValue in
|
||||
KeyboardShortcutSettings.setShortcut(newValue, for: action)
|
||||
}
|
||||
|
|
|
|||
2
vendor/bonsplit
vendored
2
vendor/bonsplit
vendored
|
|
@ -1 +1 @@
|
|||
Subproject commit efa23f4c3c7d00688d8448dc7e4d08b4d847548d
|
||||
Subproject commit 1610b457bc44bb1d50dd246792f8724ce21a7c81
|
||||
Loading…
Add table
Add a link
Reference in a new issue