Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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 .focusPreviousPane: return nil
case .focusNextPane: 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 @@ -93,6 +93,10 @@ extension ShortcutAction {
case .focusRight: return "Focus Pane Right"
case .focusUp: return "Focus Pane Up"
case .focusDown: return "Focus Pane Down"
case .focusPreviousPane:
return String(localized: "shortcut.focusPreviousPane.label", defaultValue: "Focus Previous Pane")
case .focusNextPane:
return String(localized: "shortcut.focusNextPane.label", defaultValue: "Focus Next Pane")
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,8 @@ extension ShortcutAction {
.cycleTextBoxSubmitAction, .attachTextBoxFile, .sendCtrlFToTerminal,
.clearScreenKeepScrollback:
return .navigation
case .focusLeft, .focusRight, .focusUp, .focusDown, .splitRight, .splitDown,
case .focusLeft, .focusRight, .focusUp, .focusDown,
.focusPreviousPane, .focusNextPane, .splitRight, .splitDown,
.toggleSplitZoom, .increaseWorkspaceTerminalFontSize,
.decreaseWorkspaceTerminalFontSize, .resetWorkspaceTerminalFontSize,
.equalizeSplits, .splitBrowserRight, .splitBrowserDown,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ public enum ShortcutAction: String, CaseIterable, Sendable, Hashable, SettingCod
case focusRight
case focusUp
case focusDown
case focusPreviousPane
case focusNextPane
case splitRight
case splitDown
case toggleSplitZoom
Expand Down
238 changes: 238 additions & 0 deletions Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -202977,6 +202977,125 @@
}
}
},
"shortcut.focusNextPane.label": {
"extractionState": "manual",
"localizations": {
"ar": {
"stringUnit": {
"state": "translated",
"value": "التركيز على اللوحة التالية"
}
},
"bs": {
"stringUnit": {
"state": "translated",
"value": "Fokusiraj sljedeći panel"
}
},
"da": {
"stringUnit": {
"state": "translated",
"value": "Fokuser næste panel"
}
},
"de": {
"stringUnit": {
"state": "translated",
"value": "Nächsten Bereich fokussieren"
}
},
"en": {
"stringUnit": {
"state": "translated",
"value": "Focus Next Pane"
}
},
"es": {
"stringUnit": {
"state": "translated",
"value": "Enfocar panel siguiente"
}
},
"fr": {
"stringUnit": {
"state": "translated",
"value": "Activer le panneau suivant"
}
},
"it": {
"stringUnit": {
"state": "translated",
"value": "Sposta focus pannello successivo"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "次のペインにフォーカス"
}
},
"ko": {
"stringUnit": {
"state": "translated",
"value": "다음 패널로 포커스"
}
},
"nb": {
"stringUnit": {
"state": "translated",
"value": "Fokuser neste panel"
}
},
"pl": {
"stringUnit": {
"state": "translated",
"value": "Fokus na następny panel"
}
},
"pt-BR": {
"stringUnit": {
"state": "translated",
"value": "Focar Próximo Painel"
}
},
"ru": {
"stringUnit": {
"state": "translated",
"value": "Фокус на следующую панель"
}
},
"th": {
"stringUnit": {
"state": "translated",
"value": "โฟกัสบานหน้าต่างถัดไป"
}
},
"tr": {
"stringUnit": {
"state": "translated",
"value": "Sonraki Bölmeye Odaklan"
}
},
"uk": {
"stringUnit": {
"state": "translated",
"value": "Фокус на наступну панель"
}
},
"zh-Hans": {
"stringUnit": {
"state": "translated",
"value": "聚焦下一个面板"
}
},
"zh-Hant": {
"stringUnit": {
"state": "translated",
"value": "聚焦下一個面板"
}
}
}
},
Comment on lines +202980 to +203098

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Add Khmer translations for both new shortcut labels.

Both entries omit the supported km locale. Add a reviewed Khmer translation to each entry.

  • Resources/Localizable.xcstrings#L202980-L203098: Add localizations.km.stringUnit for shortcut.focusNextPane.label.
  • Resources/Localizable.xcstrings#L203575-L203693: Add localizations.km.stringUnit for shortcut.focusPreviousPane.label.

Based on learnings and path instructions, new catalog keys must cover every locale supported by Resources/Localizable.xcstrings, including km.

📍 Affects 1 file
  • Resources/Localizable.xcstrings#L202980-L203098 (this comment)
  • Resources/Localizable.xcstrings#L203575-L203693
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Resources/Localizable.xcstrings` around lines 202980 - 203098, Add reviewed
Khmer (km) stringUnit translations to both shortcut.focusNextPane.label and
shortcut.focusPreviousPane.label entries in Resources/Localizable.xcstrings at
202980-203098 and 203575-203693, respectively, ensuring both new shortcut labels
cover every supported locale.

Sources: Path instructions, Learnings

"shortcut.focusPaneDown.label": {
"extractionState": "manual",
"localizations": {
Expand Down Expand Up @@ -203453,6 +203572,125 @@
}
}
},
"shortcut.focusPreviousPane.label": {
"extractionState": "manual",
"localizations": {
"ar": {
"stringUnit": {
"state": "translated",
"value": "التركيز على اللوحة السابقة"
}
},
"bs": {
"stringUnit": {
"state": "translated",
"value": "Fokusiraj prethodni panel"
}
},
"da": {
"stringUnit": {
"state": "translated",
"value": "Fokuser forrige panel"
}
},
"de": {
"stringUnit": {
"state": "translated",
"value": "Vorherigen Bereich fokussieren"
}
},
"en": {
"stringUnit": {
"state": "translated",
"value": "Focus Previous Pane"
}
},
"es": {
"stringUnit": {
"state": "translated",
"value": "Enfocar panel anterior"
}
},
"fr": {
"stringUnit": {
"state": "translated",
"value": "Activer le panneau précédent"
}
},
"it": {
"stringUnit": {
"state": "translated",
"value": "Sposta focus pannello precedente"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "前のペインにフォーカス"
}
},
"ko": {
"stringUnit": {
"state": "translated",
"value": "이전 패널로 포커스"
}
},
"nb": {
"stringUnit": {
"state": "translated",
"value": "Fokuser forrige panel"
}
},
"pl": {
"stringUnit": {
"state": "translated",
"value": "Fokus na poprzedni panel"
}
},
"pt-BR": {
"stringUnit": {
"state": "translated",
"value": "Focar Painel Anterior"
}
},
"ru": {
"stringUnit": {
"state": "translated",
"value": "Фокус на предыдущую панель"
}
},
"th": {
"stringUnit": {
"state": "translated",
"value": "โฟกัสบานหน้าต่างก่อนหน้า"
}
},
"tr": {
"stringUnit": {
"state": "translated",
"value": "Önceki Bölmeye Odaklan"
}
},
"uk": {
"stringUnit": {
"state": "translated",
"value": "Фокус на попередню панель"
}
},
"zh-Hans": {
"stringUnit": {
"state": "translated",
"value": "聚焦上一个面板"
}
},
"zh-Hant": {
"stringUnit": {
"state": "translated",
"value": "聚焦上一個面板"
}
}
}
},
"shortcut.focusRightSidebar.label": {
"extractionState": "manual",
"localizations": {
Expand Down
41 changes: 39 additions & 2 deletions Sources/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -783,6 +783,17 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
var ghosttyGotoSplitDownShortcut: StoredShortcut?
private var ghosttyGotoSplitPreviousShortcut: StoredShortcut?
private var ghosttyGotoSplitNextShortcut: StoredShortcut?

#if DEBUG
/// Test seam: unit tests can install the mirrored Ghostty
/// goto_split:previous/next triggers without loading a Ghostty config
/// (Ghostty's macOS defaults put them on ⌘[ / ⌘], colliding with the
/// focus-history defaults this dispatch must win).
func debugSetGhosttyGotoSplitCycleShortcuts(previous: StoredShortcut?, next: StoredShortcut?) {
ghosttyGotoSplitPreviousShortcut = previous
ghosttyGotoSplitNextShortcut = next
}
#endif
Comment on lines +786 to +796

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Remove the test-only seam from the production source.

cmuxTests/FocusHistoryBracketShortcutRoutingTests.swift calls debugSetGhosttyGotoSplitCycleShortcuts, and the method exists only under #if DEBUG. This adds a test-only debug... member to Sources/AppDelegate.swift. Move the override to a dedicated debug support file or inject the Ghostty shortcut source through the test harness.

As per path instructions, **/Sources/**/*.swift must not add test-only or debug-only seams in production Swift source; isolate an unavoidable debug facility in a dedicated debug file or folder.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/AppDelegate.swift` around lines 786 - 796, Remove
debugSetGhosttyGotoSplitCycleShortcuts from Sources/AppDelegate.swift and
relocate its test override behavior to a dedicated debug support file or
test-harness injection outside Sources/**/*.swift. Preserve the ability of
FocusHistoryBracketShortcutRoutingTests to configure the Ghostty previous/next
shortcuts without adding a test-only member to AppDelegate production source.

Source: Path instructions

private var browserAddressBarFocusedPanelId: UUID?
/// Owns the browser omnibar selection-repeat state machine, extracted into
/// `CmuxBrowser`. The app delegate is the composition root: it injects
Expand Down Expand Up @@ -14036,7 +14047,18 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
return true
}

if matchesGhosttyGotoSplitPreviousShortcut(event) {
// Pane focus cycling. `focusPreviousPane` / `focusNextPane` are the
// cmux-owned rebindable entries (default unbound); Ghostty's
// goto_split:previous/next triggers (⌘[ / ⌘] in Ghostty's macOS
// defaults) stay honored as a fallback so terminal-config bindings
// keep working. The Ghostty mirror must yield to a bound Focus
// Back/Forward shortcut: both default to ⌘[ / ⌘], and without the
// yield the mirror consumed the key here, cycling panes within the
// workspace while the focus-history branch below became unreachable
// (the titlebar arrows navigated globally; the shortcut never did).
if matchConfiguredShortcut(event: event, action: .focusPreviousPane) ||
(matchesGhosttyGotoSplitPreviousShortcut(event) &&
!ghosttyGotoSplitCycleShortcutYieldsToFocusHistory(event)) {
let routedTabs = preferredMainWindowContextForShortcutRouting(event: event)?.tabManager ?? tabManager
cmuxRememberFindSelectionBeforePanelFocusMove(tabManager: routedTabs, window: shortcutRoutingKeyWindow)
let moved = routedTabs?.cyclePaneFocus(forward: false) ?? false
Expand All @@ -14052,7 +14074,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
return true
}

if matchesGhosttyGotoSplitNextShortcut(event) {
if matchConfiguredShortcut(event: event, action: .focusNextPane) ||
(matchesGhosttyGotoSplitNextShortcut(event) &&
!ghosttyGotoSplitCycleShortcutYieldsToFocusHistory(event)) {
let routedTabs = preferredMainWindowContextForShortcutRouting(event: event)?.tabManager ?? tabManager
cmuxRememberFindSelectionBeforePanelFocusMove(tabManager: routedTabs, window: shortcutRoutingKeyWindow)
let moved = routedTabs?.cyclePaneFocus(forward: true) ?? false
Expand Down Expand Up @@ -15815,10 +15839,23 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent

fileprivate func shouldRouteGhosttyGotoSplitCycleShortcutToTerminal(_ event: NSEvent) -> Bool {
guard event.type == .keyDown else { return false }
guard !ghosttyGotoSplitCycleShortcutYieldsToFocusHistory(event) else { return false }
return matchesGhosttyGotoSplitPreviousShortcut(event)
|| matchesGhosttyGotoSplitNextShortcut(event)
}

/// Whether an event that matches Ghostty's mirrored goto_split:previous/next
/// trigger belongs to a bound Focus Back/Forward shortcut instead. Ghostty's
/// macOS defaults put goto_split:previous/next on ⌘[ / ⌘], the same keys as
/// cmux's focus-history defaults, and the mirror branches run earlier in the
/// dispatch, so without this yield the configured focus-history shortcut is
/// unreachable. Unbinding Focus Back/Forward hands the keys back to the
/// pane-cycle mirror.
func ghosttyGotoSplitCycleShortcutYieldsToFocusHistory(_ event: NSEvent) -> Bool {
matchConfiguredShortcut(event: event, action: .focusHistoryBack)
|| matchConfiguredShortcut(event: event, action: .focusHistoryForward)
}

private func matchesGhosttyGotoSplitPreviousShortcut(_ event: NSEvent) -> Bool {
guard let ghosttyGotoSplitPreviousShortcut else { return false }
return matchShortcut(event: event, shortcut: ghosttyGotoSplitPreviousShortcut)
Expand Down
12 changes: 12 additions & 0 deletions Sources/KeyboardShortcutSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@ enum KeyboardShortcutSettings {
case focusRight
case focusUp
case focusDown
case focusPreviousPane
case focusNextPane
case splitRight
case splitDown, toggleSplitZoom
case increaseWorkspaceTerminalFontSize
Expand Down Expand Up @@ -281,6 +283,8 @@ enum KeyboardShortcutSettings {
case .focusRight: return String(localized: "shortcut.focusPaneRight.label", defaultValue: "Focus Pane Right")
case .focusUp: return String(localized: "shortcut.focusPaneUp.label", defaultValue: "Focus Pane Up")
case .focusDown: return String(localized: "shortcut.focusPaneDown.label", defaultValue: "Focus Pane Down")
case .focusPreviousPane: return String(localized: "shortcut.focusPreviousPane.label", defaultValue: "Focus Previous Pane")
case .focusNextPane: return String(localized: "shortcut.focusNextPane.label", defaultValue: "Focus Next Pane")
case .splitRight: return String(localized: "shortcut.splitRight.label", defaultValue: "Split Right")
case .splitDown: return String(localized: "shortcut.splitDown.label", defaultValue: "Split Down")
case .toggleSplitZoom: return String(localized: "shortcut.togglePaneZoom.label", defaultValue: "Toggle Pane Zoom")
Expand Down Expand Up @@ -485,6 +489,14 @@ enum KeyboardShortcutSettings {
return StoredShortcut(key: "↑", command: true, shift: false, option: true, control: false)
case .focusDown:
return StoredShortcut(key: "↓", command: true, shift: false, option: true, control: false)
// Unbound by default: Ghostty's goto_split:previous/next mirror still
// cycles panes on the terminal-config keys when Focus Back/Forward do
// not claim them; these entries exist so pane cycling stays rebindable
// now that ⌘[ / ⌘] reach global focus history.
case .focusPreviousPane:
return .unbound
case .focusNextPane:
return .unbound
case .splitRight:
return StoredShortcut(key: "d", command: true, shift: false, option: false, control: false)
case .splitDown: return StoredShortcut(key: "d", command: true, shift: true, option: false, control: false)
Expand Down
Loading