Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
0ceaef1
Add tmux split resize shortcuts
austinywang May 24, 2026
f953934
Address resize shortcut review feedback
austinywang May 24, 2026
82905df
Merge remote-tracking branch 'origin/main' into issue-4499-tmux-split…
austinywang May 26, 2026
332162a
merge: sync issue-4499-tmux-split-resize-shortcuts with main
austinywang Jun 4, 2026
0851bf8
fix: expose resize split shortcuts in settings catalog
austinywang Jun 4, 2026
bf40fe6
fix: snapshot keyboard shortcut settings rows
austinywang Jun 4, 2026
04575e7
fix: allow chord recording in shortcut settings
austinywang Jun 4, 2026
fc49797
Merge remote-tracking branch 'origin/main' into issue-4499-tmux-split…
austinywang Jun 14, 2026
12d8819
Merge remote-tracking branch 'origin/main' into issue-4499-tmux-split…
austinywang Jun 14, 2026
1ea4407
chore: refresh Swift file length budget
austinywang Jun 14, 2026
ee030a3
fix: serialize remote session process tests
austinywang Jun 14, 2026
b9912ee
Merge remote-tracking branch 'origin/main' into issue-4499-tmux-split…
austinywang Jun 14, 2026
5b28db9
fix: make split resize shortcuts opt-in
austinywang Jun 14, 2026
891ec52
Merge branch 'main' of https://github.com/manaflow-ai/cmux into issue…
austinywang Jul 24, 2026
7c310c6
test: cover shortcut chord mode controls
austinywang Jul 24, 2026
8db2700
fix: restore shortcut chord recording
austinywang Jul 24, 2026
23847e3
Merge remote-tracking branch 'origin/main' into autoreview-4688
austinywang Jul 24, 2026
96e1958
test: avoid production shortcut debug seam
austinywang Jul 24, 2026
0d09ecc
fix: import workspace shell activity state
austinywang Jul 24, 2026
181536a
fix: return resume approval policy
austinywang Jul 24, 2026
19e8bbd
fix: make session snapshot types explicit
austinywang Jul 24, 2026
dc10c56
test: cover shortcut review regressions
austinywang Jul 24, 2026
490b4cf
fix: close split shortcut review gaps
austinywang Jul 24, 2026
3f6b065
test: update Dock transfer fixture
austinywang Jul 24, 2026
adb5602
test: cover resize shortcut Ghostty precedence
austinywang Jul 24, 2026
fbd776d
fix: prioritize configured split resize shortcuts
austinywang Jul 24, 2026
63c79a4
Merge remote-tracking branch 'origin/main' into issue-4499-tmux-split…
austinywang Jul 24, 2026
2c15bae
fix: localize shortcut chord control
austinywang Jul 24, 2026
6a43d9d
test: cover mixed-metadata chord prefixes
austinywang Jul 24, 2026
5647bb6
fix: close shortcut review gaps
austinywang Jul 24, 2026
a9038ce
test: cover shortcut review regressions
austinywang Jul 24, 2026
c42a36d
fix: close final shortcut review gaps
austinywang Jul 24, 2026
1228d15
test: cover recorded arrow shortcut conflicts
austinywang Jul 24, 2026
bcfe7a3
fix: canonicalize recorded arrow shortcut conflicts
austinywang Jul 24, 2026
f491609
test: cover mixed arrow chord prefixes
austinywang Jul 24, 2026
cd24d9a
fix: canonicalize arrow chord prefix identity
austinywang Jul 24, 2026
af82680
Merge branch 'main' of https://github.com/manaflow-ai/cmux into issue…
austinywang Jul 25, 2026
d4ef41c
Merge origin/main into issue-4499-tmux-split-resize-shortcuts
austinywang Jul 27, 2026
ac77920
fix: disallow chords for global hotkey
austinywang Jul 28, 2026
78fd946
Merge remote-tracking branch 'origin/main' into issue-4499-tmux-split…
austinywang Jul 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ extension ShortcutAction {
case .focusRight: return ShortcutStroke(key: "→", command: true, option: true)
case .focusUp: return ShortcutStroke(key: "↑", command: true, option: true)
case .focusDown: return ShortcutStroke(key: "↓", command: true, option: true)
case .resizeSplitLeft, .resizeSplitRight, .resizeSplitUp, .resizeSplitDown:
return nil
case .splitRight: return ShortcutStroke(key: "d", command: true)
case .splitDown: return ShortcutStroke(key: "d", command: true, shift: true)
case .toggleSplitZoom: return ShortcutStroke(key: "\r", command: true, shift: true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,14 @@ extension ShortcutAction {
case .focusRight: return "Focus Pane Right"
case .focusUp: return "Focus Pane Up"
case .focusDown: return "Focus Pane Down"
case .resizeSplitLeft:
return String(localized: "shortcut.resizeSplitLeft.label", defaultValue: "Resize Split Left")
case .resizeSplitRight:
return String(localized: "shortcut.resizeSplitRight.label", defaultValue: "Resize Split Right")
case .resizeSplitUp:
return String(localized: "shortcut.resizeSplitUp.label", defaultValue: "Resize Split Up")
case .resizeSplitDown:
return String(localized: "shortcut.resizeSplitDown.label", defaultValue: "Resize Split Down")
case .splitRight: return "Split Right"
case .splitDown: return "Split Down"
case .toggleSplitZoom: return "Toggle Pane Zoom"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ extension ShortcutAction {
.cycleTextBoxSubmitAction, .attachTextBoxFile, .sendCtrlFToTerminal,
.clearScreenKeepScrollback:
return .navigation
case .focusLeft, .focusRight, .focusUp, .focusDown, .splitRight, .splitDown,
case .focusLeft, .focusRight, .focusUp, .focusDown,
.resizeSplitLeft, .resizeSplitRight, .resizeSplitUp, .resizeSplitDown,
.splitRight, .splitDown,
.toggleSplitZoom, .equalizeSplits, .splitBrowserRight, .splitBrowserDown,
.toggleRightSidebar, .fileExplorerOpenSelection, .fileExplorerOpenSelectionFinderAlias,
.toggleCanvasLayout, .canvasRevealFocusedPane, .canvasOverview,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@ public enum ShortcutAction: String, CaseIterable, Sendable, Hashable, SettingCod
case focusRight
case focusUp
case focusDown
case resizeSplitLeft
case resizeSplitRight
case resizeSplitUp
case resizeSplitDown
case splitRight
case splitDown
case toggleSplitZoom
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,26 @@ public struct ShortcutStroke: Sendable, Equatable, Hashable, Codable {
/// True when at least one of `cmd`, `shift`, `opt`, or `ctrl` is set.
public var hasAnyModifier: Bool { command || shift || option || control }

/// Canonical key token used when comparing stored shortcut strokes.
///
/// AppKit reports arrow-key events as private-use function-key scalars,
/// while parsed config and built-in shortcuts use visible arrow glyphs.
/// Both representations identify the same physical key.
public var canonicalKeyToken: String {
Self.canonicalKeyToken(for: key)
}

/// Returns the canonical token for a key string from either storage model.
public static func canonicalKeyToken(for key: String) -> String {
switch key {
case "\u{F702}": "←"
case "\u{F703}": "→"
case "\u{F700}": "↑"
case "\u{F701}": "↓"
default: key
}
}

/// Returns this stroke with its key normalized to cmux's persisted
/// physical-key representation when a recording-time key code is present.
public func canonicalized() -> ShortcutStroke {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,10 @@ struct ShortcutActionNumberedDigitTests {
}
}

@Test func fileExplorerOpenSelectionShortcutsAreSingleStrokeOnly() {
@Test func unsupportedChordActionsAreSingleStrokeOnly() {
#expect(!ShortcutAction.fileExplorerOpenSelection.allowsChordShortcut)
#expect(!ShortcutAction.fileExplorerOpenSelectionFinderAlias.allowsChordShortcut)
#expect(!ShortcutAction.cycleTextBoxSubmitAction.allowsChordShortcut)
#expect(!ShortcutAction.showHideAllWindows.allowsChordShortcut)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import Foundation
import Testing
@testable import CmuxSettings

@Suite("ShortcutAction")
struct ShortcutActionTests {
@Test func resizeSplitActionsExposePaneGroupNamesAndNoDefaults() {
let expected: [(ShortcutAction, String)] = [
(.resizeSplitLeft, "Resize Split Left"),
(.resizeSplitRight, "Resize Split Right"),
(.resizeSplitUp, "Resize Split Up"),
(.resizeSplitDown, "Resize Split Down"),
]

for (action, displayName) in expected {
#expect(ShortcutAction.allCases.contains(action))
#expect(action.group == .panes)
#expect(action.displayName == displayName)
#expect(action.defaultStroke == nil)
#expect(action.defaultShortcut == nil)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import CmuxSettings

extension ShortcutListModel {
/// Whether the row recorder should collect two strokes for `action`.
///
/// Existing chords stay in chord mode when Settings reopens. A user toggle
/// temporarily overrides that inferred state until the next assignment.
func chordsEnabled(for action: ShortcutAction) -> Bool {
guard action.allowsChordShortcut else { return false }
return chordModeOverrides[action.rawValue] ?? (effective(for: action)?.hasChord == true)
}

/// Toggles whether the action's recorder collects a two-stroke chord.
func toggleChordMode(for action: ShortcutAction) {
guard action.allowsChordShortcut else {
setChordModeOverride(nil, for: action)
return
}
setChordModeOverride(!chordsEnabled(for: action), for: action)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ final class ShortcutListModel {
var legacyBindings: [String: StoredShortcut]
private(set) var whenOverrideClauses: [String: ShortcutWhenClause] = [:]
private(set) var whenOverrideRawStrings: [String: String] = [:]
private(set) var chordModeActions: Set<String> = []
private(set) var chordModeOverrides: [String: Bool] = [:]
private(set) var restoreShortcuts: [String: StoredShortcut] = [:]
private(set) var bareKeyRejections: Set<String> = []
private(set) var primaryModifierRejections: Set<String> = []
Expand Down Expand Up @@ -93,6 +93,10 @@ final class ShortcutListModel {

var latestBindings: [String: StoredShortcut] { pendingBindings ?? bindings }

func setChordModeOverride(_ enabled: Bool?, for action: ShortcutAction) {
chordModeOverrides[action.rawValue] = enabled
}

private func ingestBindings(_ snapshot: ShortcutBindingsSnapshot) {
let dictionary = snapshot.bindings
let changedActionIds = Set(bindings.keys).union(dictionary.keys)
Expand Down Expand Up @@ -320,10 +324,12 @@ final class ShortcutListModel {
clearRejections(for: action)
conflictRejections[action.rawValue] = conflict
rejectedConflictShortcuts[action.rawValue] = proposed
chordModeOverrides.removeValue(forKey: action.rawValue)
return
}
var updated = latestBindings
updated[action.rawValue] = proposed
chordModeOverrides.removeValue(forKey: action.rawValue)
restoreShortcuts.removeValue(forKey: action.rawValue)
clearRejections(for: action)
await write(updated, clearingLegacyFor: action)
Expand All @@ -335,35 +341,35 @@ final class ShortcutListModel {
func assignChord(_ chord: StoredShortcut, to action: ShortcutAction) async {
let chord = chord.canonicalized()
guard action.allowsChordShortcut else {
chordModeActions.remove(action.rawValue)
chordModeOverrides.removeValue(forKey: action.rawValue)
return
}
guard action.allowsBareFirstStroke || chord.first.hasAnyModifier else {
markBareKeyRejected(action)
chordModeActions.remove(action.rawValue)
chordModeOverrides.removeValue(forKey: action.rawValue)
return
}
guard let proposed = normalizedNumberedShortcutIfNeeded(chord, for: action) else {
clearRejections(for: action)
numberedDigitRejections.insert(action.rawValue)
chordModeActions.remove(action.rawValue)
chordModeOverrides.removeValue(forKey: action.rawValue)
return
}
if action.rejectsSystemDefinedMediaKey(proposed) {
markSystemReservedRejected(action)
chordModeActions.remove(action.rawValue)
chordModeOverrides.removeValue(forKey: action.rawValue)
return
}
if let conflict = detectConflict(for: action, stroke: proposed) {
clearRejections(for: action)
conflictRejections[action.rawValue] = conflict
rejectedConflictShortcuts[action.rawValue] = proposed
chordModeActions.remove(action.rawValue)
chordModeOverrides.removeValue(forKey: action.rawValue)
return
}
var updated = latestBindings
updated[action.rawValue] = proposed
chordModeActions.remove(action.rawValue)
chordModeOverrides.removeValue(forKey: action.rawValue)
restoreShortcuts.removeValue(forKey: action.rawValue)
clearRejections(for: action)
await write(updated, clearingLegacyFor: action)
Expand All @@ -373,6 +379,7 @@ final class ShortcutListModel {
func clearBinding(for action: ShortcutAction) async {
var updated = latestBindings
updated[action.rawValue] = StoredShortcut.unbound
chordModeOverrides.removeValue(forKey: action.rawValue)
await write(updated, clearingLegacyFor: action)
}

Expand All @@ -392,6 +399,7 @@ final class ShortcutListModel {
/// Clears every override and all in-memory rejection/restore state — the
/// "Reset Defaults" action.
func resetAll() async {
chordModeOverrides.removeAll()
restoreShortcuts.removeAll()
bareKeyRejections.removeAll()
primaryModifierRejections.removeAll()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ struct ShortcutListRowActions {
let onStroke: (ShortcutStroke) -> Void
let onChord: (StoredShortcut) -> Void
let onBareKeyRejected: () -> Void
let onToggleChordMode: () -> Void
let onClearOrRestore: () -> Void
let onClearRejections: () -> Void
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ struct ShortcutListRowSnapshot: Equatable {
let title: String
let subtitle: String?
let placeholder: String
let showsChordModeButton: Bool
let chordsEnabled: Bool
let hasPendingRejection: Bool
let firstStrokeRequiresModifier: Bool
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,21 @@ struct ShortcutListRowView: View, Equatable {
.frame(width: 160)
.accessibilityIdentifier(snapshot.recorderAccessibilityIdentifier)

if snapshot.showsChordModeButton {
Button {
actions.onToggleChordMode()
} label: {
Image(systemName: "keyboard")
.imageScale(.medium)
.foregroundStyle(snapshot.chordsEnabled ? Color.accentColor : Color.secondary)
}
.buttonStyle(.borderless)
.help(String(localized: "settings.shortcuts.chords", defaultValue: "Shortcut Chords"))
.accessibilityLabel(String(localized: "settings.shortcuts.chords", defaultValue: "Shortcut Chords"))
.accessibilityAddTraits(snapshot.chordsEnabled ? .isSelected : [])
.accessibilityIdentifier("ShortcutRecorderChordModeButton.\(snapshot.action.rawValue)")
}

Button {
actions.onClearOrRestore()
} label: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ struct ShortcutListStableLazyView: View {
title: action.displayName,
subtitle: model.scopeCaption(for: action),
placeholder: model.formatPlaceholder(effective: effective, numbered: action.usesNumberedDigitMatching),
chordsEnabled: model.chordModeActions.contains(action.rawValue),
showsChordModeButton: action.allowsChordShortcut,
chordsEnabled: model.chordsEnabled(for: action),
hasPendingRejection: model.hasPendingRejection(for: action),
firstStrokeRequiresModifier: !action.allowsBareFirstStroke,
isUnbound: effective?.isUnbound ?? true,
Expand All @@ -36,6 +37,7 @@ struct ShortcutListStableLazyView: View {
onStroke: { stroke in Task { await model.assign(stroke: stroke, to: action) } },
onChord: { chord in Task { await model.assignChord(chord, to: action) } },
onBareKeyRejected: { model.markBareKeyRejected(action) },
onToggleChordMode: { model.toggleChordMode(for: action) },
onClearOrRestore: { Task { await model.clearOrRestore(for: action) } },
onClearRejections: { model.clearRejections(for: action) }
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ public struct GlobalHotkeySection: View {
title: String(localized: "settings.globalHotkey.shortcut", defaultValue: "Show/Hide All Windows"),
subtitle: nil,
placeholder: shortcutModel.formatPlaceholder(effective: effective, numbered: false),
showsChordModeButton: false,
chordsEnabled: false,
hasPendingRejection: shortcutModel.hasPendingRejection(for: hotkeyAction),
firstStrokeRequiresModifier: true,
Expand All @@ -102,6 +103,7 @@ public struct GlobalHotkeySection: View {
onStroke: { stroke in Task { await shortcutModel.assign(stroke: stroke, to: hotkeyAction) } },
onChord: { _ in },
onBareKeyRejected: { shortcutModel.markBareKeyRejected(hotkeyAction) },
onToggleChordMode: {},
onClearOrRestore: { Task { await shortcutModel.clearOrRestore(for: hotkeyAction) } },
onClearRejections: { shortcutModel.clearRejections(for: hotkeyAction) }
)
Expand Down
Loading
Loading