Skip to content

Commit 33dcc60

Browse files
austinywangclaude
andauthored
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>
1 parent 661a4e8 commit 33dcc60

7 files changed

Lines changed: 272 additions & 59 deletions

File tree

Resources/Localizable.xcstrings

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57395,13 +57395,13 @@
5739557395
"en": {
5739657396
"stringUnit": {
5739757397
"state": "translated",
57398-
"value": "Show Cmd/Ctrl-Hold Shortcut Hints"
57398+
"value": "Show Shortcut Hints While Holding Modifiers"
5739957399
}
5740057400
},
5740157401
"ja": {
5740257402
"stringUnit": {
5740357403
"state": "translated",
57404-
"value": "Cmd/Ctrl長押しのショートカットヒントを表示"
57404+
"value": "修飾キー長押しでショートカットヒントを表示"
5740557405
}
5740657406
},
5740757407
"zh-Hans": {
@@ -57508,13 +57508,13 @@
5750857508
"en": {
5750957509
"stringUnit": {
5751057510
"state": "translated",
57511-
"value": "Holding Cmd or Ctrl keeps shortcut hint pills hidden."
57511+
"value": "Holding shortcut modifiers keeps shortcut hint pills hidden."
5751257512
}
5751357513
},
5751457514
"ja": {
5751557515
"stringUnit": {
5751657516
"state": "translated",
57517-
"value": "CmdまたはCtrlを長押ししてもショートカットヒントピルは非表示のままです。"
57517+
"value": "ショートカットの修飾キーを長押ししてもショートカットヒントピルは非表示のままです。"
5751857518
}
5751957519
},
5752057520
"zh-Hans": {
@@ -57621,13 +57621,13 @@
5762157621
"en": {
5762257622
"stringUnit": {
5762357623
"state": "translated",
57624-
"value": "Holding Cmd (sidebar/titlebar) or Ctrl/Cmd (pane tabs) shows shortcut hint pills."
57624+
"value": "Holding the configured shortcut modifiers shows shortcut hint pills."
5762557625
}
5762657626
},
5762757627
"ja": {
5762857628
"stringUnit": {
5762957629
"state": "translated",
57630-
"value": "Cmd(サイドバー/タイトルバー)またはCtrl/Cmd(ペインタブ)を長押しするとショートカットヒントピルが表示されます。"
57630+
"value": "設定されたショートカットの修飾キーを長押しするとショートカットヒントピルが表示されます。"
5763157631
}
5763257632
},
5763357633
"zh-Hans": {
@@ -61457,6 +61457,40 @@
6145761457
}
6145861458
}
6145961459
},
61460+
"shortcut.selectSurfaceByNumber.label": {
61461+
"extractionState": "manual",
61462+
"localizations": {
61463+
"en": {
61464+
"stringUnit": {
61465+
"state": "translated",
61466+
"value": "Select Surface 1…9"
61467+
}
61468+
},
61469+
"ja": {
61470+
"stringUnit": {
61471+
"state": "translated",
61472+
"value": "サーフェス 1…9 を選択"
61473+
}
61474+
}
61475+
}
61476+
},
61477+
"shortcut.selectWorkspaceByNumber.label": {
61478+
"extractionState": "manual",
61479+
"localizations": {
61480+
"en": {
61481+
"stringUnit": {
61482+
"state": "translated",
61483+
"value": "Select Workspace 1…9"
61484+
}
61485+
},
61486+
"ja": {
61487+
"stringUnit": {
61488+
"state": "translated",
61489+
"value": "ワークスペース 1…9 を選択"
61490+
}
61491+
}
61492+
}
61493+
},
6146061494
"shortcut.showBrowserJSConsole.label": {
6146161495
"extractionState": "manual",
6146261496
"localizations": {

Sources/AppDelegate.swift

Lines changed: 80 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1114,9 +1114,9 @@ final class ServeWebOutputCollector {
11141114
}
11151115

11161116
enum WorkspaceShortcutMapper {
1117-
/// Maps Cmd+digit workspace shortcuts to a zero-based workspace index.
1118-
/// Cmd+1...Cmd+8 target fixed indices; Cmd+9 always targets the last workspace.
1119-
static func workspaceIndex(forCommandDigit digit: Int, workspaceCount: Int) -> Int? {
1117+
/// Maps numbered workspace shortcuts to a zero-based workspace index.
1118+
/// 1...8 target fixed indices; 9 always targets the last workspace.
1119+
static func workspaceIndex(forDigit digit: Int, workspaceCount: Int) -> Int? {
11201120
guard workspaceCount > 0 else { return nil }
11211121
guard (1...9).contains(digit) else { return nil }
11221122

@@ -1128,12 +1128,12 @@ enum WorkspaceShortcutMapper {
11281128
return index < workspaceCount ? index : nil
11291129
}
11301130

1131-
/// Returns the primary Cmd+digit badge to display for a workspace row.
1131+
/// Returns the primary digit badge to display for a workspace row.
11321132
/// Picks the lowest digit that maps to that row index.
1133-
static func commandDigitForWorkspace(at index: Int, workspaceCount: Int) -> Int? {
1133+
static func digitForWorkspace(at index: Int, workspaceCount: Int) -> Int? {
11341134
guard index >= 0 && index < workspaceCount else { return nil }
11351135
for digit in 1...9 {
1136-
if workspaceIndex(forCommandDigit: digit, workspaceCount: workspaceCount) == index {
1136+
if workspaceIndex(forDigit: digit, workspaceCount: workspaceCount) == index {
11371137
return digit
11381138
}
11391139
}
@@ -9474,30 +9474,33 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
94749474
return true
94759475
}
94769476

9477-
// Numeric shortcuts for specific sidebar tabs: Cmd+1-9 (9 = last workspace)
9478-
if flags == [.command],
9479-
let manager = tabManager,
9480-
let num = Int(chars),
9481-
let targetIndex = WorkspaceShortcutMapper.workspaceIndex(forCommandDigit: num, workspaceCount: manager.tabs.count) {
9477+
// Numeric shortcuts for specific workspaces (9 = last workspace)
9478+
if let manager = tabManager,
9479+
let digit = numberedShortcutDigit(
9480+
event: event,
9481+
shortcut: KeyboardShortcutSettings.shortcut(for: .selectWorkspaceByNumber)
9482+
),
9483+
let targetIndex = WorkspaceShortcutMapper.workspaceIndex(forDigit: digit, workspaceCount: manager.tabs.count) {
94829484
#if DEBUG
94839485
dlog(
9484-
"shortcut.action name=workspaceDigit digit=\(num) targetIndex=\(targetIndex) manager=\(debugManagerToken(manager)) \(debugShortcutRouteSnapshot(event: event))"
9486+
"shortcut.action name=workspaceDigit digit=\(digit) targetIndex=\(targetIndex) manager=\(debugManagerToken(manager)) \(debugShortcutRouteSnapshot(event: event))"
94859487
)
94869488
#endif
94879489
manager.selectTab(at: targetIndex)
94889490
return true
94899491
}
94909492

9491-
// Numeric shortcuts for surfaces within pane: Ctrl+1-9 (9 = last)
9492-
if flags == [.control] {
9493-
if let num = Int(chars), num >= 1 && num <= 9 {
9494-
if num == 9 {
9495-
tabManager?.selectLastSurface()
9496-
} else {
9497-
tabManager?.selectSurface(at: num - 1)
9498-
}
9499-
return true
9493+
// Numeric shortcuts for surfaces within the focused pane (9 = last)
9494+
if let digit = numberedShortcutDigit(
9495+
event: event,
9496+
shortcut: KeyboardShortcutSettings.shortcut(for: .selectSurfaceByNumber)
9497+
) {
9498+
if digit == 9 {
9499+
tabManager?.selectLastSurface()
9500+
} else {
9501+
tabManager?.selectSurface(at: digit - 1)
95009502
}
9503+
return true
95019504
}
95029505

95039506
// 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
1052410527
return false
1052510528
}
1052610529

10530+
private func numberedShortcutDigit(event: NSEvent, shortcut: StoredShortcut) -> Int? {
10531+
let flags = event.modifierFlags.intersection(.deviceIndependentFlagsMask)
10532+
.subtracting([.numericPad, .function, .capsLock])
10533+
guard flags == shortcut.modifierFlags else { return nil }
10534+
10535+
if let digit = numberedShortcutDigit(
10536+
eventCharacter: event.charactersIgnoringModifiers,
10537+
applyShiftSymbolNormalization: flags.contains(.shift),
10538+
eventKeyCode: event.keyCode
10539+
) {
10540+
return digit
10541+
}
10542+
10543+
let layoutCharacter = shortcutLayoutCharacterProvider(event.keyCode, event.modifierFlags)
10544+
if let digit = numberedShortcutDigit(
10545+
eventCharacter: layoutCharacter,
10546+
applyShiftSymbolNormalization: false,
10547+
eventKeyCode: event.keyCode
10548+
) {
10549+
return digit
10550+
}
10551+
10552+
return digitForNumberKeyCode(event.keyCode)
10553+
}
10554+
10555+
private func numberedShortcutDigit(
10556+
eventCharacter: String?,
10557+
applyShiftSymbolNormalization: Bool,
10558+
eventKeyCode: UInt16
10559+
) -> Int? {
10560+
guard let eventCharacter, !eventCharacter.isEmpty else { return nil }
10561+
let normalized = normalizedShortcutEventCharacter(
10562+
eventCharacter,
10563+
applyShiftSymbolNormalization: applyShiftSymbolNormalization,
10564+
eventKeyCode: eventKeyCode
10565+
)
10566+
guard let digit = Int(normalized), (1...9).contains(digit) else { return nil }
10567+
return digit
10568+
}
10569+
1052710570
private func shouldRequireCharacterMatchForCommandShortcut(shortcutKey: String) -> Bool {
1052810571
guard shortcutKey.count == 1, let scalar = shortcutKey.unicodeScalars.first else {
1052910572
return false
@@ -10643,6 +10686,22 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
1064310686
}
1064410687
}
1064510688

10689+
private func digitForNumberKeyCode(_ keyCode: UInt16) -> Int? {
10690+
switch keyCode {
10691+
case 18: return 1 // kVK_ANSI_1
10692+
case 19: return 2 // kVK_ANSI_2
10693+
case 20: return 3 // kVK_ANSI_3
10694+
case 21: return 4 // kVK_ANSI_4
10695+
case 23: return 5 // kVK_ANSI_5
10696+
case 22: return 6 // kVK_ANSI_6
10697+
case 26: return 7 // kVK_ANSI_7
10698+
case 28: return 8 // kVK_ANSI_8
10699+
case 25: return 9 // kVK_ANSI_9
10700+
default:
10701+
return nil
10702+
}
10703+
}
10704+
1064610705
/// Match arrow key shortcuts using keyCode
1064710706
/// Arrow keys include .numericPad and .function in their modifierFlags, so strip those before comparing.
1064810707
private func matchArrowShortcut(event: NSEvent, shortcut: StoredShortcut, keyCode: UInt16) -> Bool {

Sources/ContentView.swift

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8187,6 +8187,8 @@ struct VerticalTabsSidebar: View {
81878187
private var sidebarShowNotificationMessage = SidebarWorkspaceDetailSettings.defaultShowNotificationMessage
81888188
@AppStorage(WorkspacePresentationModeSettings.modeKey)
81898189
private var workspacePresentationMode = WorkspacePresentationModeSettings.defaultMode.rawValue
8190+
@AppStorage(KeyboardShortcutSettings.Action.selectWorkspaceByNumber.defaultsKey)
8191+
private var selectWorkspaceByNumberShortcutData = Data()
81908192

81918193
/// Space at top of sidebar for traffic light buttons
81928194
private let trafficLightPadding: CGFloat = 28
@@ -8204,9 +8206,25 @@ struct VerticalTabsSidebar: View {
82048206
)
82058207
}
82068208

8209+
private var workspaceNumberShortcut: StoredShortcut {
8210+
decodeShortcut(
8211+
from: selectWorkspaceByNumberShortcutData,
8212+
fallback: KeyboardShortcutSettings.Action.selectWorkspaceByNumber.defaultShortcut
8213+
)
8214+
}
8215+
8216+
private func decodeShortcut(from data: Data, fallback: StoredShortcut) -> StoredShortcut {
8217+
guard !data.isEmpty,
8218+
let shortcut = try? JSONDecoder().decode(StoredShortcut.self, from: data) else {
8219+
return fallback
8220+
}
8221+
return shortcut
8222+
}
8223+
82078224
var body: some View {
82088225
let workspaceCount = tabManager.tabs.count
82098226
let canCloseWorkspace = workspaceCount > 1
8227+
let workspaceNumberShortcut = self.workspaceNumberShortcut
82108228

82118229
VStack(spacing: 0) {
82128230
GeometryReader { proxy in
@@ -8231,10 +8249,11 @@ struct VerticalTabsSidebar: View {
82318249
tab: tab,
82328250
index: index,
82338251
isActive: tabManager.selectedTabId == tab.id,
8234-
workspaceShortcutDigit: WorkspaceShortcutMapper.commandDigitForWorkspace(
8252+
workspaceShortcutDigit: WorkspaceShortcutMapper.digitForWorkspace(
82358253
at: index,
82368254
workspaceCount: workspaceCount
82378255
),
8256+
workspaceShortcutModifierSymbol: workspaceNumberShortcut.modifierDisplayString,
82388257
canCloseWorkspace: canCloseWorkspace,
82398258
accessibilityWorkspaceCount: workspaceCount,
82408259
unreadCount: notificationStore.unreadCount(forTabId: tab.id),
@@ -8378,7 +8397,8 @@ enum ShortcutHintModifierPolicy {
83788397
defaults: UserDefaults = .standard
83798398
) -> Bool {
83808399
let normalized = modifierFlags.intersection(.deviceIndependentFlagsMask)
8381-
guard normalized == [.command] else {
8400+
.subtracting([.numericPad, .function, .capsLock])
8401+
guard normalized == KeyboardShortcutSettings.shortcut(for: .selectWorkspaceByNumber).modifierFlags else {
83828402
return false
83838403
}
83848404
return ShortcutHintDebugSettings.showHintsOnCommandHoldEnabled(defaults: defaults)
@@ -10637,6 +10657,7 @@ private struct TabItemView: View, Equatable {
1063710657
lhs.index == rhs.index &&
1063810658
lhs.isActive == rhs.isActive &&
1063910659
lhs.workspaceShortcutDigit == rhs.workspaceShortcutDigit &&
10660+
lhs.workspaceShortcutModifierSymbol == rhs.workspaceShortcutModifierSymbol &&
1064010661
lhs.canCloseWorkspace == rhs.canCloseWorkspace &&
1064110662
lhs.accessibilityWorkspaceCount == rhs.accessibilityWorkspaceCount &&
1064210663
lhs.unreadCount == rhs.unreadCount &&
@@ -10658,6 +10679,7 @@ private struct TabItemView: View, Equatable {
1065810679
let index: Int
1065910680
let isActive: Bool
1066010681
let workspaceShortcutDigit: Int?
10682+
let workspaceShortcutModifierSymbol: String
1066110683
let canCloseWorkspace: Bool
1066210684
let accessibilityWorkspaceCount: Int
1066310685
let unreadCount: Int
@@ -10772,7 +10794,7 @@ private struct TabItemView: View, Equatable {
1077210794

1077310795
private var workspaceShortcutLabel: String? {
1077410796
guard let workspaceShortcutDigit else { return nil }
10775-
return "\(workspaceShortcutDigit)"
10797+
return "\(workspaceShortcutModifierSymbol)\(workspaceShortcutDigit)"
1077610798
}
1077710799

1077810800
private var showsWorkspaceShortcutHint: Bool {

0 commit comments

Comments
 (0)