Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
27 changes: 5 additions & 22 deletions Sources/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13306,10 +13306,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
return true
}

if shouldBypassPrintableOptionTextForShortcutRouting(event: event) {
return false
}

let canvasSurfaceDigitShortcutIsActive =
shortcutEventFocusContext(event).shortcutContext.bool(ShortcutContextKnownKey.workspaceCanvasLayout.rawValue) &&
shortcutWhenClauseAllows(action: .selectSurfaceByNumber, event: event) &&
Expand Down Expand Up @@ -15232,22 +15228,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
return nil
}

fileprivate func shouldBypassPrintableOptionTextForShortcutRouting(event: NSEvent) -> Bool {
guard shortcutRoutingShouldBypassForPrintableOptionText(event: event) else {
return false
}

if routableNumberedConfiguredShortcutDigit(event: event, action: .selectWorkspaceByNumber) != nil {
return false
}

if routableNumberedConfiguredShortcutDigit(event: event, action: .selectSurfaceByNumber) != nil {
return false
}

return true
}

private func tabManagerForNumberedShortcut(event: NSEvent) -> TabManager? {
preferredMainWindowContextForShortcutRouting(event: event)?.tabManager ?? tabManager
}
Expand Down Expand Up @@ -17075,13 +17055,16 @@ private extension NSWindow {
return true
}
let browserWebKitKeyDownReentry = firstResponderWebView != nil && cmuxBrowserWebKitKeyDownDispatchIsActive()
if AppDelegate.shared?.shouldBypassPrintableOptionTextForShortcutRouting(event: event) == true {
if shortcutRoutingShouldBypassForPrintableOptionText(event: event) {
if browserWebKitKeyDownReentry { return false }
if AppDelegate.shared?.handleConfiguredShortcutKeyEquivalent(event) == true {
return true
}
let textInputTarget: NSResponder? = firstResponderGhosttyView
?? firstResponderWebView
?? self.firstResponder
if let textInputTarget, textInputTarget !== self {
if cmuxForceDispatchKeyDownOnce(event, to: textInputTarget, reason: "printable Option text") {
if cmuxForceDispatchKeyDownOnce(event, to: textInputTarget, reason: "unmatched Option input") {
return true
}
// Same event already in flight on this stack (WebKit replay /
Expand Down
3 changes: 0 additions & 3 deletions Sources/KeyboardShortcutSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1663,9 +1663,6 @@ struct ShortcutStroke: Equatable, Hashable {
}

guard event.type == .keyDown else { return false }
if shortcutRoutingShouldBypassForPrintableOptionText(event: event) {
return false
}

return matches(
keyCode: Self.recordableKey(from: event)?.keyCode ?? event.keyCode,
Expand Down
144 changes: 142 additions & 2 deletions cmuxTests/AppDelegateOptionDigitShortcutRoutingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,145 @@ struct AppDelegateOptionDigitShortcutRoutingTests {
}
}

@Test
func focusHistoryRebindingRoutesNewShortcutsAndDropsDefaults() throws {
try withIsolatedShortcutRoutingState {
let appDelegate = try #require(AppDelegate.shared)
let windowId = appDelegate.createMainWindow()
defer { closeWindow(withId: windowId) }

let testWindow = try #require(self.window(withId: windowId))
let manager = try #require(appDelegate.tabManagerFor(windowId: windowId))
let firstWorkspace = try #require(manager.selectedWorkspace)
let secondWorkspace = manager.addTab(select: true)

let reboundBack = StoredShortcut(
key: "y",
command: false,
shift: false,
option: true,
control: false
)
let reboundForward = StoredShortcut(
key: "u",
command: false,
shift: true,
option: true,
control: false
)

try withTemporaryShortcut(action: .focusHistoryBack, shortcut: reboundBack) {
try withTemporaryShortcut(action: .focusHistoryForward, shortcut: reboundForward) {
let reboundBackEvent = try #require(makeKeyEvent(
modifierFlags: [.option],
characters: "¥",
charactersIgnoringModifiers: "y",
keyCode: 16,
windowNumber: testWindow.windowNumber
))
let defaultBackEvent = try #require(makeKeyEvent(
modifierFlags: [.command],
characters: "[",
charactersIgnoringModifiers: "[",
keyCode: 33,
windowNumber: testWindow.windowNumber
))

#expect(appDelegate.debugMatchesConfiguredShortcut(
event: reboundBackEvent,
action: .focusHistoryBack
))
#expect(appDelegate.debugHandleCustomShortcut(event: reboundBackEvent))
#expect(manager.selectedTabId == firstWorkspace.id)
#expect(!appDelegate.debugMatchesConfiguredShortcut(
event: defaultBackEvent,
action: .focusHistoryBack
))
#expect(!appDelegate.debugHandleCustomShortcut(event: defaultBackEvent))

let reboundForwardEvent = try #require(makeKeyEvent(
modifierFlags: [.option, .shift],
characters: "¨",
charactersIgnoringModifiers: "U",
keyCode: 32,
windowNumber: testWindow.windowNumber
))
let defaultForwardEvent = try #require(makeKeyEvent(
modifierFlags: [.command],
characters: "]",
charactersIgnoringModifiers: "]",
keyCode: 30,
windowNumber: testWindow.windowNumber
))

#expect(appDelegate.debugMatchesConfiguredShortcut(
event: reboundForwardEvent,
action: .focusHistoryForward
))
#expect(appDelegate.debugHandleCustomShortcut(event: reboundForwardEvent))
#expect(manager.selectedTabId == secondWorkspace.id)
#expect(!appDelegate.debugMatchesConfiguredShortcut(
event: defaultForwardEvent,
action: .focusHistoryForward
))
#expect(!appDelegate.debugHandleCustomShortcut(event: defaultForwardEvent))
}
}
}
}

@Test
func focusHistoryRebindingMatchesCommandShiftOptionAndControlVariants() throws {
try withIsolatedShortcutRoutingState {
let appDelegate = try #require(AppDelegate.shared)
let windowId = appDelegate.createMainWindow()
defer { closeWindow(withId: windowId) }

let testWindow = try #require(self.window(withId: windowId))
let candidates: [(KeyboardShortcutSettings.Action, StoredShortcut, NSEvent.ModifierFlags, String, String, UInt16)] = [
(
.focusHistoryBack,
StoredShortcut(key: "y", command: true, shift: true, option: false, control: false),
[.command, .shift],
"Y",
"Y",
16
),
(
.focusHistoryForward,
StoredShortcut(key: "u", command: false, shift: false, option: false, control: true),
[.control],
"\u{15}",
"u",
32
),
(
.focusHistoryBack,
StoredShortcut(key: "y", command: false, shift: true, option: true, control: true),
[.shift, .option, .control],
"Y",
"Y",
16
),
]

for (action, shortcut, modifiers, characters, charactersIgnoringModifiers, keyCode) in candidates {
try withTemporaryShortcut(action: action, shortcut: shortcut) {
let event = try #require(makeKeyEvent(
modifierFlags: modifiers,
characters: characters,
charactersIgnoringModifiers: charactersIgnoringModifiers,
keyCode: keyCode,
windowNumber: testWindow.windowNumber
))

#expect(appDelegate.debugMatchesConfiguredShortcut(event: event, action: action))
#expect(appDelegate.debugHandleCustomShortcut(event: event))
}
}
}
}

@Test
func terminalKeyEquivalentRoutesActiveOptionDigitWorkspaceShortcut() throws {
try withIsolatedShortcutRoutingState {
Expand Down Expand Up @@ -245,14 +384,15 @@ struct AppDelegateOptionDigitShortcutRoutingTests {
modifierFlags: NSEvent.ModifierFlags,
characters: String,
charactersIgnoringModifiers: String,
keyCode: UInt16
keyCode: UInt16,
windowNumber: Int = 0
) -> NSEvent? {
NSEvent.keyEvent(
with: .keyDown,
location: .zero,
modifierFlags: modifierFlags,
timestamp: ProcessInfo.processInfo.systemUptime,
windowNumber: 0,
windowNumber: windowNumber,
context: nil,
characters: characters,
charactersIgnoringModifiers: charactersIgnoringModifiers,
Expand Down
10 changes: 5 additions & 5 deletions cmuxTests/AppDelegateShortcutRoutingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6186,7 +6186,7 @@ final class AppDelegateShortcutRoutingTests: XCTestCase {
#endif
}

func testPrintableOptionTextBypassesConfiguredShortcutRouting() throws {
func testConfiguredOptionShortcutWinsBeforeTextInputRouting() throws {
#if DEBUG
guard let appDelegate = AppDelegate.shared else {
XCTFail("Expected AppDelegate.shared")
Expand Down Expand Up @@ -6228,16 +6228,16 @@ final class AppDelegateShortcutRoutingTests: XCTestCase {
return
}

XCTAssertFalse(
XCTAssertTrue(
appDelegate.debugHandleCustomShortcut(event: event),
"Option+Q that produces @ on Turkish Q should pass through as text input"
"An exact Option+Q binding should remain routable on layouts where Option+Q produces text"
)
RunLoop.main.run(until: Date(timeIntervalSinceNow: 0.05))

XCTAssertEqual(
manager.tabs.count,
workspaceCountBefore,
"Printable Option text should not trigger the remapped New Workspace shortcut"
workspaceCountBefore + 1,
"The registered cmux shortcut should win before unmatched Option input reaches AppKit"
)
}
#else
Expand Down