Use pane TTY fallback for tmux Shift+Enter

This commit is contained in:
austinpower1258 2026-03-30 04:15:23 -07:00
parent f4c99d34f3
commit 8336aae865
3 changed files with 97 additions and 26 deletions

View file

@ -1930,12 +1930,10 @@ class GhosttyApp {
modifierFlags: NSEvent.ModifierFlags,
isInsideTmux: Bool,
userConfigDefinesShiftEnterBinding: Bool,
ghosttyHasBinding: Bool,
hasMarkedText: Bool
) -> Bool {
guard isInsideTmux else { return false }
guard !userConfigDefinesShiftEnterBinding else { return false }
guard !ghosttyHasBinding else { return false }
guard !hasMarkedText else { return false }
let normalizedModifiers = terminalKeyboardCopyModeNormalizedModifiers(modifierFlags)
@ -6236,27 +6234,44 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations {
event: NSEvent,
surface: ghostty_surface_t
) -> Bool {
guard !GhosttyApp.shared.userConfigDefinesShiftEnterBinding else { return false }
let userConfigDefinesShiftEnterBinding = GhosttyApp.shared.userConfigDefinesShiftEnterBinding
guard !userConfigDefinesShiftEnterBinding else { return false }
let normalizedModifiers = terminalKeyboardCopyModeNormalizedModifiers(event.modifierFlags)
guard normalizedModifiers == [.shift] else { return false }
guard event.keyCode == 36 || event.keyCode == 76 else { return false }
guard let terminalSurface else { return false }
let tabId = terminalSurface.tabId
let panelId = terminalSurface.id
let isInsideTmux = AppDelegate.shared?
guard let tab = AppDelegate.shared?
.tabManagerFor(tabId: tabId)?
.tabs
.first(where: { $0.id == tabId })?
.panelIsInsideTmux(panelId: panelId) ?? false
let ghosttyHasBinding = ghosttyBindingFlags(for: event, surface: surface) != nil
return GhosttyApp.shouldRemapShiftEnterForTmux(
.first(where: { $0.id == tabId }) else {
return false
}
let reportedInsideTmux = tab.panelIsInsideTmux(panelId: panelId)
// Shell-side tmux telemetry can lag behind pane focus changes, so fall back to
// the current foreground process on the pane TTY before deciding whether to remap.
let detectedInsideTmux = tab.surfaceTTYNames[panelId].map {
TerminalSSHSessionDetector.isInsideTmux(forTTY: $0)
} ?? false
let isInsideTmux = reportedInsideTmux || detectedInsideTmux
if detectedInsideTmux != reportedInsideTmux {
AppDelegate.shared?
.tabManagerFor(tabId: tabId)?
.updateSurfaceTmuxState(
tabId: tabId,
surfaceId: panelId,
isInsideTmux: detectedInsideTmux
)
}
let shouldRemap = GhosttyApp.shouldRemapShiftEnterForTmux(
keyCode: event.keyCode,
modifierFlags: event.modifierFlags,
isInsideTmux: isInsideTmux,
userConfigDefinesShiftEnterBinding: false,
ghosttyHasBinding: ghosttyHasBinding,
userConfigDefinesShiftEnterBinding: userConfigDefinesShiftEnterBinding,
hasMarkedText: hasMarkedText()
)
return shouldRemap
}
#if DEBUG

View file

@ -420,6 +420,24 @@ enum TerminalSSHSessionDetector {
)
}
static func isInsideTmux(forTTY ttyName: String) -> Bool {
let normalizedTTY = normalizeTTYName(ttyName)
guard !normalizedTTY.isEmpty else { return false }
return isInsideTmuxForTesting(
ttyName: normalizedTTY,
processes: processSnapshots(forTTY: normalizedTTY)
)
}
static func isInsideTmuxForTesting(
ttyName: String,
processes: [ProcessSnapshot]
) -> Bool {
let normalizedTTY = normalizeTTYName(ttyName)
guard !normalizedTTY.isEmpty else { return false }
return processes.contains { isForegroundProcess($0, ttyName: normalizedTTY, executableName: "tmux") }
}
static func detectForTesting(
ttyName: String,
processes: [ProcessSnapshot],
@ -474,8 +492,16 @@ enum TerminalSSHSessionDetector {
}
private static func isForegroundSSHProcess(_ process: ProcessSnapshot, ttyName: String) -> Bool {
isForegroundProcess(process, ttyName: ttyName, executableName: "ssh")
}
private static func isForegroundProcess(
_ process: ProcessSnapshot,
ttyName: String,
executableName: String
) -> Bool {
normalizeTTYName(process.tty) == normalizeTTYName(ttyName) &&
process.executableName == "ssh" &&
process.executableName == executableName &&
process.pgid > 0 &&
process.tpgid > 0 &&
process.pgid == process.tpgid

View file

@ -2559,7 +2559,6 @@ final class GhosttyMouseFocusTests: XCTestCase {
modifierFlags: [.shift],
isInsideTmux: true,
userConfigDefinesShiftEnterBinding: false,
ghosttyHasBinding: false,
hasMarkedText: false
)
)
@ -2570,7 +2569,6 @@ final class GhosttyMouseFocusTests: XCTestCase {
modifierFlags: [.shift],
isInsideTmux: false,
userConfigDefinesShiftEnterBinding: false,
ghosttyHasBinding: false,
hasMarkedText: false
)
)
@ -2581,18 +2579,6 @@ final class GhosttyMouseFocusTests: XCTestCase {
modifierFlags: [.shift],
isInsideTmux: true,
userConfigDefinesShiftEnterBinding: true,
ghosttyHasBinding: false,
hasMarkedText: false
)
)
XCTAssertFalse(
GhosttyApp.shouldRemapShiftEnterForTmux(
keyCode: 36,
modifierFlags: [.shift],
isInsideTmux: true,
userConfigDefinesShiftEnterBinding: false,
ghosttyHasBinding: true,
hasMarkedText: false
)
)
@ -2603,12 +2589,56 @@ final class GhosttyMouseFocusTests: XCTestCase {
modifierFlags: [.shift, .command],
isInsideTmux: true,
userConfigDefinesShiftEnterBinding: false,
ghosttyHasBinding: false,
hasMarkedText: false
)
)
}
func testForegroundTmuxProcessOnTTYIsDetected() {
let processes = [
TerminalSSHSessionDetector.ProcessSnapshot(
pid: 47486,
pgid: 47486,
tpgid: 48365,
tty: "ttys089",
executableName: "login"
),
TerminalSSHSessionDetector.ProcessSnapshot(
pid: 47487,
pgid: 47487,
tpgid: 48365,
tty: "ttys089",
executableName: "zsh"
),
TerminalSSHSessionDetector.ProcessSnapshot(
pid: 48365,
pgid: 48365,
tpgid: 48365,
tty: "ttys089",
executableName: "tmux"
),
]
XCTAssertTrue(
TerminalSSHSessionDetector.isInsideTmuxForTesting(
ttyName: "ttys089",
processes: processes
)
)
XCTAssertFalse(
TerminalSSHSessionDetector.isInsideTmuxForTesting(
ttyName: "ttys090",
processes: processes
)
)
XCTAssertFalse(
TerminalSSHSessionDetector.isInsideTmuxForTesting(
ttyName: "ttys089",
processes: processes.filter { $0.executableName != "tmux" }
)
)
}
func testLoadedCJKScanPathsSkipsReleaseAppSupportWhenTaggedConfigExists() throws {
let appSupport = FileManager.default.temporaryDirectory
.appendingPathComponent("cmux-test-cjk-app-support-\(UUID().uuidString)")