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, modifierFlags: NSEvent.ModifierFlags,
isInsideTmux: Bool, isInsideTmux: Bool,
userConfigDefinesShiftEnterBinding: Bool, userConfigDefinesShiftEnterBinding: Bool,
ghosttyHasBinding: Bool,
hasMarkedText: Bool hasMarkedText: Bool
) -> Bool { ) -> Bool {
guard isInsideTmux else { return false } guard isInsideTmux else { return false }
guard !userConfigDefinesShiftEnterBinding else { return false } guard !userConfigDefinesShiftEnterBinding else { return false }
guard !ghosttyHasBinding else { return false }
guard !hasMarkedText else { return false } guard !hasMarkedText else { return false }
let normalizedModifiers = terminalKeyboardCopyModeNormalizedModifiers(modifierFlags) let normalizedModifiers = terminalKeyboardCopyModeNormalizedModifiers(modifierFlags)
@ -6236,27 +6234,44 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations {
event: NSEvent, event: NSEvent,
surface: ghostty_surface_t surface: ghostty_surface_t
) -> Bool { ) -> Bool {
guard !GhosttyApp.shared.userConfigDefinesShiftEnterBinding else { return false } let userConfigDefinesShiftEnterBinding = GhosttyApp.shared.userConfigDefinesShiftEnterBinding
guard !userConfigDefinesShiftEnterBinding else { return false }
let normalizedModifiers = terminalKeyboardCopyModeNormalizedModifiers(event.modifierFlags) let normalizedModifiers = terminalKeyboardCopyModeNormalizedModifiers(event.modifierFlags)
guard normalizedModifiers == [.shift] else { return false } guard normalizedModifiers == [.shift] else { return false }
guard event.keyCode == 36 || event.keyCode == 76 else { return false } guard event.keyCode == 36 || event.keyCode == 76 else { return false }
guard let terminalSurface else { return false } guard let terminalSurface else { return false }
let tabId = terminalSurface.tabId let tabId = terminalSurface.tabId
let panelId = terminalSurface.id let panelId = terminalSurface.id
let isInsideTmux = AppDelegate.shared? guard let tab = AppDelegate.shared?
.tabManagerFor(tabId: tabId)? .tabManagerFor(tabId: tabId)?
.tabs .tabs
.first(where: { $0.id == tabId })? .first(where: { $0.id == tabId }) else {
.panelIsInsideTmux(panelId: panelId) ?? false return false
let ghosttyHasBinding = ghosttyBindingFlags(for: event, surface: surface) != nil }
return GhosttyApp.shouldRemapShiftEnterForTmux( 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, keyCode: event.keyCode,
modifierFlags: event.modifierFlags, modifierFlags: event.modifierFlags,
isInsideTmux: isInsideTmux, isInsideTmux: isInsideTmux,
userConfigDefinesShiftEnterBinding: false, userConfigDefinesShiftEnterBinding: userConfigDefinesShiftEnterBinding,
ghosttyHasBinding: ghosttyHasBinding,
hasMarkedText: hasMarkedText() hasMarkedText: hasMarkedText()
) )
return shouldRemap
} }
#if DEBUG #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( static func detectForTesting(
ttyName: String, ttyName: String,
processes: [ProcessSnapshot], processes: [ProcessSnapshot],
@ -474,8 +492,16 @@ enum TerminalSSHSessionDetector {
} }
private static func isForegroundSSHProcess(_ process: ProcessSnapshot, ttyName: String) -> Bool { 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) && normalizeTTYName(process.tty) == normalizeTTYName(ttyName) &&
process.executableName == "ssh" && process.executableName == executableName &&
process.pgid > 0 && process.pgid > 0 &&
process.tpgid > 0 && process.tpgid > 0 &&
process.pgid == process.tpgid process.pgid == process.tpgid

View file

@ -2559,7 +2559,6 @@ final class GhosttyMouseFocusTests: XCTestCase {
modifierFlags: [.shift], modifierFlags: [.shift],
isInsideTmux: true, isInsideTmux: true,
userConfigDefinesShiftEnterBinding: false, userConfigDefinesShiftEnterBinding: false,
ghosttyHasBinding: false,
hasMarkedText: false hasMarkedText: false
) )
) )
@ -2570,7 +2569,6 @@ final class GhosttyMouseFocusTests: XCTestCase {
modifierFlags: [.shift], modifierFlags: [.shift],
isInsideTmux: false, isInsideTmux: false,
userConfigDefinesShiftEnterBinding: false, userConfigDefinesShiftEnterBinding: false,
ghosttyHasBinding: false,
hasMarkedText: false hasMarkedText: false
) )
) )
@ -2581,18 +2579,6 @@ final class GhosttyMouseFocusTests: XCTestCase {
modifierFlags: [.shift], modifierFlags: [.shift],
isInsideTmux: true, isInsideTmux: true,
userConfigDefinesShiftEnterBinding: true, userConfigDefinesShiftEnterBinding: true,
ghosttyHasBinding: false,
hasMarkedText: false
)
)
XCTAssertFalse(
GhosttyApp.shouldRemapShiftEnterForTmux(
keyCode: 36,
modifierFlags: [.shift],
isInsideTmux: true,
userConfigDefinesShiftEnterBinding: false,
ghosttyHasBinding: true,
hasMarkedText: false hasMarkedText: false
) )
) )
@ -2603,12 +2589,56 @@ final class GhosttyMouseFocusTests: XCTestCase {
modifierFlags: [.shift, .command], modifierFlags: [.shift, .command],
isInsideTmux: true, isInsideTmux: true,
userConfigDefinesShiftEnterBinding: false, userConfigDefinesShiftEnterBinding: false,
ghosttyHasBinding: false,
hasMarkedText: 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 { func testLoadedCJKScanPathsSkipsReleaseAppSupportWhenTaggedConfigExists() throws {
let appSupport = FileManager.default.temporaryDirectory let appSupport = FileManager.default.temporaryDirectory
.appendingPathComponent("cmux-test-cjk-app-support-\(UUID().uuidString)") .appendingPathComponent("cmux-test-cjk-app-support-\(UUID().uuidString)")