Skip to content
Merged
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
44 changes: 44 additions & 0 deletions Sources/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1176,6 +1176,33 @@ func shouldRouteTerminalFontZoomShortcutToGhostty(
) != nil
}

func shouldRouteTerminalCommandShortcutToGhostty(
flags: NSEvent.ModifierFlags,
chars: String,
keyCode: UInt16,
terminalHasSelection: Bool
) -> Bool {
let normalizedFlags = flags
.intersection(.deviceIndependentFlagsMask)
.subtracting([.numericPad, .function, .capsLock])
guard normalizedFlags.contains(.command) else { return false }

let normalizedChars = chars.lowercased()
if normalizedFlags == [.command] {
// Keep Preferences (Cmd+,) menu-routed even when a terminal is focused.
if normalizedChars == "," || keyCode == 43 {
return false
}

// Preserve standard copy behavior when text is selected in the terminal.
if (normalizedChars == "c" || keyCode == 8), terminalHasSelection {
return false
}
}

return true
}

func cmuxOwningGhosttyView(for responder: NSResponder?) -> GhosttyNSView? {
guard let responder else { return nil }
if let ghosttyView = responder as? GhosttyNSView {
Expand Down Expand Up @@ -8043,6 +8070,23 @@ private extension NSWindow {
return true
}

// Support custom tmux prefixes (for example Cmd+C): when the terminal is focused
// and no app-level shortcut matched, prefer forwarding Command-key input to the
// terminal rather than consuming it as a menu key equivalent.
if let ghosttyView = firstResponderGhosttyView,
shouldRouteTerminalCommandShortcutToGhostty(
flags: event.modifierFlags,
chars: event.charactersIgnoringModifiers ?? "",
keyCode: event.keyCode,
terminalHasSelection: ghosttyView.terminalSurface?.hasSelection() ?? false
) {
ghosttyView.keyDown(with: event)
#if DEBUG
dlog(" → ghostty command passthrough")
#endif
return true
}

// When the terminal is focused, skip the full NSWindow.performKeyEquivalent
// (which walks the SwiftUI content view hierarchy) and dispatch Command-key
// events directly to the main menu. This avoids the broken SwiftUI focus path.
Expand Down
65 changes: 65 additions & 0 deletions cmuxTests/CmuxWebViewKeyEquivalentTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2228,6 +2228,71 @@ final class BrowserZoomShortcutRoutingPolicyTests: XCTestCase {
}
}

final class TerminalCommandShortcutRoutingPolicyTests: XCTestCase {
func testRoutesCommandCToTerminalWhenNoSelection() {
XCTAssertTrue(
shouldRouteTerminalCommandShortcutToGhostty(
flags: [.command],
chars: "c",
keyCode: 8, // kVK_ANSI_C
terminalHasSelection: false
)
)
}

func testKeepsCommandCCopyMenuRoutedWhenSelectionExists() {
XCTAssertFalse(
shouldRouteTerminalCommandShortcutToGhostty(
flags: [.command],
chars: "c",
keyCode: 8, // kVK_ANSI_C
terminalHasSelection: true
)
)
}

func testKeepsCommandCommaMenuRoutedForPreferences() {
XCTAssertFalse(
shouldRouteTerminalCommandShortcutToGhostty(
flags: [.command],
chars: ",",
keyCode: 43, // kVK_ANSI_Comma
terminalHasSelection: false
)
)
}

func testRequiresCommandModifier() {
XCTAssertFalse(
shouldRouteTerminalCommandShortcutToGhostty(
flags: [.control],
chars: "c",
keyCode: 8,
terminalHasSelection: false
)
)
}

func testRoutesOtherCommandShortcutsToTerminal() {
XCTAssertTrue(
shouldRouteTerminalCommandShortcutToGhostty(
flags: [.command, .option],
chars: "c",
keyCode: 8,
terminalHasSelection: false
)
)
XCTAssertTrue(
shouldRouteTerminalCommandShortcutToGhostty(
flags: [.command],
chars: "v",
keyCode: 9, // kVK_ANSI_V
terminalHasSelection: false
)
)
}
}

final class GhosttyResponderResolutionTests: XCTestCase {
private final class FocusProbeView: NSView {
override var acceptsFirstResponder: Bool { true }
Expand Down