From 8336aae8657a8f90471e92a603127c97af5da319 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Mon, 30 Mar 2026 04:15:23 -0700 Subject: [PATCH] Use pane TTY fallback for tmux Shift+Enter --- Sources/GhosttyTerminalView.swift | 35 ++++++++++---- Sources/TerminalSSHSessionDetector.swift | 28 ++++++++++- cmuxTests/GhosttyConfigTests.swift | 60 ++++++++++++++++++------ 3 files changed, 97 insertions(+), 26 deletions(-) diff --git a/Sources/GhosttyTerminalView.swift b/Sources/GhosttyTerminalView.swift index 368e09b4..2f3a4628 100644 --- a/Sources/GhosttyTerminalView.swift +++ b/Sources/GhosttyTerminalView.swift @@ -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 diff --git a/Sources/TerminalSSHSessionDetector.swift b/Sources/TerminalSSHSessionDetector.swift index ea111a35..69d9fde7 100644 --- a/Sources/TerminalSSHSessionDetector.swift +++ b/Sources/TerminalSSHSessionDetector.swift @@ -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 diff --git a/cmuxTests/GhosttyConfigTests.swift b/cmuxTests/GhosttyConfigTests.swift index ec79ad9a..5f52dd43 100644 --- a/cmuxTests/GhosttyConfigTests.swift +++ b/cmuxTests/GhosttyConfigTests.swift @@ -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)")