Merge remote-tracking branch 'origin/main' into issue-151-ssh-remote-port-proxying

This commit is contained in:
Lawrence Chen 2026-03-11 18:21:39 -07:00
commit e6a293bd78
11 changed files with 1863 additions and 151 deletions

View file

@ -3074,16 +3074,7 @@ final class TerminalSurface: Identifiable, ObservableObject {
viewState = "NO_ATTACHED_VIEW hasSurface=\(hasSurface)"
}
#if DEBUG
let ts = ISO8601DateFormatter().string(from: Date())
let line = "[\(ts)] forceRefresh: \(id) reason=\(reason) \(viewState)\n"
let logPath = "/tmp/cmux-refresh-debug.log"
if let handle = FileHandle(forWritingAtPath: logPath) {
handle.seekToEndOfFile()
handle.write(line.data(using: .utf8)!)
handle.closeFile()
} else {
FileManager.default.createFile(atPath: logPath, contents: line.data(using: .utf8))
}
dlog("forceRefresh: \(id) reason=\(reason) \(viewState)")
#endif
guard let view = attachedView,
let surface,
@ -4346,10 +4337,7 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations {
#if DEBUG
private func recordKeyLatency(path: String, event: NSEvent) {
guard Self.keyLatencyProbeEnabled else { return }
guard event.timestamp > 0 else { return }
let delayMs = max(0, (CACurrentMediaTime() - event.timestamp) * 1000)
let delayText = String(format: "%.2f", delayMs)
dlog("key.latency path=\(path) ms=\(delayText) keyCode=\(event.keyCode) mods=\(event.modifierFlags.rawValue) repeat=\(event.isARepeat ? 1 : 0)")
CmuxTypingTiming.logEventDelay(path: path, event: event)
}
#endif
@ -4366,6 +4354,16 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations {
}
override func performKeyEquivalent(with event: NSEvent) -> Bool {
#if DEBUG
let typingTimingStart = CmuxTypingTiming.start()
defer {
CmuxTypingTiming.logDuration(
path: "terminal.performKeyEquivalent",
startedAt: typingTimingStart,
event: event
)
}
#endif
guard event.type == .keyDown else { return false }
guard let fr = window?.firstResponder as? NSView,
fr === self || fr.isDescendant(of: self) else { return false }
@ -4487,15 +4485,59 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations {
}
override func keyDown(with event: NSEvent) {
#if DEBUG
let typingTimingStart = CmuxTypingTiming.start()
let phaseTotalStart = ProcessInfo.processInfo.systemUptime
var ensureSurfaceMs: Double = 0
var dismissNotificationMs: Double = 0
var keyboardCopyModeMs: Double = 0
var interpretMs: Double = 0
var syncPreeditMs: Double = 0
var ghosttySendMs: Double = 0
var refreshMs: Double = 0
defer {
let totalMs = (ProcessInfo.processInfo.systemUptime - phaseTotalStart) * 1000.0
CmuxTypingTiming.logBreakdown(
path: "terminal.keyDown.phase",
totalMs: totalMs,
event: event,
thresholdMs: 1.0,
parts: [
("ensureSurfaceMs", ensureSurfaceMs),
("dismissNotificationMs", dismissNotificationMs),
("keyboardCopyModeMs", keyboardCopyModeMs),
("interpretMs", interpretMs),
("syncPreeditMs", syncPreeditMs),
("ghosttySendMs", ghosttySendMs),
("refreshMs", refreshMs),
],
extra: "marked=\(hasMarkedText() ? 1 : 0)"
)
CmuxTypingTiming.logDuration(path: "terminal.keyDown", startedAt: typingTimingStart, event: event)
}
let ensureSurfaceStart = ProcessInfo.processInfo.systemUptime
#endif
guard let surface = ensureSurfaceReadyForInput() else {
#if DEBUG
ensureSurfaceMs = (ProcessInfo.processInfo.systemUptime - ensureSurfaceStart) * 1000.0
#endif
super.keyDown(with: event)
return
}
#if DEBUG
ensureSurfaceMs = (ProcessInfo.processInfo.systemUptime - ensureSurfaceStart) * 1000.0
#endif
if let terminalSurface {
#if DEBUG
let dismissNotificationStart = ProcessInfo.processInfo.systemUptime
#endif
AppDelegate.shared?.tabManager?.dismissNotificationOnDirectInteraction(
tabId: terminalSurface.tabId,
surfaceId: terminalSurface.id
)
#if DEBUG
dismissNotificationMs = (ProcessInfo.processInfo.systemUptime - dismissNotificationStart) * 1000.0
#endif
}
if event.keyCode != 53 {
endFindEscapeSuppression()
@ -4503,10 +4545,19 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations {
if shouldConsumeSuppressedFindEscape(event) {
return
}
#if DEBUG
let keyboardCopyModeStart = ProcessInfo.processInfo.systemUptime
#endif
if handleKeyboardCopyModeIfNeeded(event, surface: surface) {
#if DEBUG
keyboardCopyModeMs = (ProcessInfo.processInfo.systemUptime - keyboardCopyModeStart) * 1000.0
#endif
keyboardCopyModeConsumedKeyUps.insert(event.keyCode)
return
}
#if DEBUG
keyboardCopyModeMs = (ProcessInfo.processInfo.systemUptime - keyboardCopyModeStart) * 1000.0
#endif
#if DEBUG
recordKeyLatency(path: "keyDown", event: event)
#endif
@ -4544,12 +4595,36 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations {
let handled: Bool
if text.isEmpty {
keyEvent.text = nil
#if DEBUG
let ghosttySendStart = ProcessInfo.processInfo.systemUptime
handled = sendTimedGhosttyKey(
surface,
keyEvent,
path: "terminal.keyDown.ctrlGhosttySend",
event: event
)
ghosttySendMs = (ProcessInfo.processInfo.systemUptime - ghosttySendStart) * 1000.0
#else
handled = ghostty_surface_key(surface, keyEvent)
#endif
} else {
#if DEBUG
let sendTimingStart = CmuxTypingTiming.start()
let ghosttySendStart = ProcessInfo.processInfo.systemUptime
#endif
handled = text.withCString { ptr in
keyEvent.text = ptr
return ghostty_surface_key(surface, keyEvent)
}
#if DEBUG
ghosttySendMs = (ProcessInfo.processInfo.systemUptime - ghosttySendStart) * 1000.0
CmuxTypingTiming.logDuration(
path: "terminal.keyDown.ctrlGhosttySend",
startedAt: sendTimingStart,
event: event,
extra: "handled=\(handled ? 1 : 0)"
)
#endif
}
#if DEBUG
dlog(
@ -4626,18 +4701,42 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations {
}
// Let the input system handle the event (for IME, dead keys, etc.)
#if DEBUG
let interpretTimingStart = CmuxTypingTiming.start()
let interpretPhaseStart = ProcessInfo.processInfo.systemUptime
#endif
interpretKeyEvents([translationEvent])
#if DEBUG
interpretMs = (ProcessInfo.processInfo.systemUptime - interpretPhaseStart) * 1000.0
CmuxTypingTiming.logDuration(
path: "terminal.keyDown.interpretKeyEvents",
startedAt: interpretTimingStart,
event: event
)
#endif
// If the keyboard layout changed, an input method grabbed the event.
// Sync preedit and return without sending the key to Ghostty.
if !markedTextBefore, let kbBefore = keyboardIdBefore, kbBefore != KeyboardLayout.id {
#if DEBUG
let syncPreeditStart = ProcessInfo.processInfo.systemUptime
#endif
syncPreedit(clearIfNeeded: markedTextBefore)
#if DEBUG
syncPreeditMs = (ProcessInfo.processInfo.systemUptime - syncPreeditStart) * 1000.0
#endif
return
}
// Sync the preedit state with Ghostty so it can render the IME
// composition overlay (e.g. for Korean, Japanese, Chinese input).
#if DEBUG
let syncPreeditStart = ProcessInfo.processInfo.systemUptime
#endif
syncPreedit(clearIfNeeded: markedTextBefore)
#if DEBUG
syncPreeditMs = (ProcessInfo.processInfo.systemUptime - syncPreeditStart) * 1000.0
#endif
// Build the key event
var keyEvent = ghostty_input_key_s()
@ -4667,13 +4766,37 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations {
for text in accumulatedText {
if shouldSendText(text) {
shouldRefreshAfterTextInput = true
#if DEBUG
let sendTimingStart = CmuxTypingTiming.start()
let ghosttySendStart = ProcessInfo.processInfo.systemUptime
#endif
text.withCString { ptr in
keyEvent.text = ptr
_ = ghostty_surface_key(surface, keyEvent)
}
#if DEBUG
ghosttySendMs += (ProcessInfo.processInfo.systemUptime - ghosttySendStart) * 1000.0
CmuxTypingTiming.logDuration(
path: "terminal.keyDown.accumulatedGhosttySend",
startedAt: sendTimingStart,
event: event,
extra: "textBytes=\(text.utf8.count)"
)
#endif
} else {
keyEvent.text = nil
#if DEBUG
let ghosttySendStart = ProcessInfo.processInfo.systemUptime
_ = sendTimedGhosttyKey(
surface,
keyEvent,
path: "terminal.keyDown.accumulatedGhosttySend",
event: event
)
ghosttySendMs += (ProcessInfo.processInfo.systemUptime - ghosttySendStart) * 1000.0
#else
_ = ghostty_surface_key(surface, keyEvent)
#endif
}
}
} else {
@ -4688,22 +4811,63 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations {
if let text = textForKeyEvent(translationEvent) {
if shouldSendText(text), !suppressShiftSpaceFallbackText {
shouldRefreshAfterTextInput = true
#if DEBUG
let sendTimingStart = CmuxTypingTiming.start()
let ghosttySendStart = ProcessInfo.processInfo.systemUptime
#endif
text.withCString { ptr in
keyEvent.text = ptr
_ = ghostty_surface_key(surface, keyEvent)
}
#if DEBUG
ghosttySendMs += (ProcessInfo.processInfo.systemUptime - ghosttySendStart) * 1000.0
CmuxTypingTiming.logDuration(
path: "terminal.keyDown.ghosttySend",
startedAt: sendTimingStart,
event: event,
extra: "textBytes=\(text.utf8.count)"
)
#endif
} else {
keyEvent.text = nil
#if DEBUG
let ghosttySendStart = ProcessInfo.processInfo.systemUptime
_ = sendTimedGhosttyKey(
surface,
keyEvent,
path: "terminal.keyDown.ghosttySend",
event: event
)
ghosttySendMs += (ProcessInfo.processInfo.systemUptime - ghosttySendStart) * 1000.0
#else
_ = ghostty_surface_key(surface, keyEvent)
#endif
}
} else {
keyEvent.text = nil
#if DEBUG
let ghosttySendStart = ProcessInfo.processInfo.systemUptime
_ = sendTimedGhosttyKey(
surface,
keyEvent,
path: "terminal.keyDown.ghosttySend",
event: event
)
ghosttySendMs += (ProcessInfo.processInfo.systemUptime - ghosttySendStart) * 1000.0
#else
_ = ghostty_surface_key(surface, keyEvent)
#endif
}
}
if shouldRefreshAfterTextInput {
#if DEBUG
let refreshStart = ProcessInfo.processInfo.systemUptime
#endif
terminalSurface?.forceRefresh(reason: "keyDown.textInput")
#if DEBUG
refreshMs = (ProcessInfo.processInfo.systemUptime - refreshStart) * 1000.0
#endif
}
// Rendering is driven by Ghostty's wakeups/renderer.
@ -4717,6 +4881,29 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations {
return ghostty_surface_key(surface, keyEvent)
}
#if DEBUG
@discardableResult
private func sendTimedGhosttyKey(
_ surface: ghostty_surface_t,
_ keyEvent: ghostty_input_key_s,
path: String,
event: NSEvent? = nil,
extra: String? = nil
) -> Bool {
let timingStart = CmuxTypingTiming.start()
let handled = sendGhosttyKey(surface, keyEvent)
let baseExtra = "handled=\(handled ? 1 : 0)"
let mergedExtra: String
if let extra, !extra.isEmpty {
mergedExtra = "\(baseExtra) \(extra)"
} else {
mergedExtra = baseExtra
}
CmuxTypingTiming.logDuration(path: path, startedAt: timingStart, event: event, extra: mergedExtra)
return handled
}
#endif
override func keyUp(with event: NSEvent) {
guard let surface = ensureSurfaceReadyForInput() else {
super.keyUp(with: event)
@ -7494,6 +7681,9 @@ final class GhosttySurfaceScrollView: NSView {
extension GhosttyNSView: NSTextInputClient {
fileprivate func sendTextToSurface(_ chars: String) {
guard let surface = surface else { return }
#if DEBUG
let typingTimingStart = CmuxTypingTiming.start()
#endif
#if DEBUG
cmuxWriteChildExitProbe(
[
@ -7513,6 +7703,13 @@ extension GhosttyNSView: NSTextInputClient {
keyEvent.composing = false
_ = ghostty_surface_key(surface, keyEvent)
}
#if DEBUG
CmuxTypingTiming.logDuration(
path: "terminal.sendTextToSurface",
startedAt: typingTimingStart,
extra: "textBytes=\(chars.utf8.count)"
)
#endif
}
func hasMarkedText() -> Bool {
@ -7529,6 +7726,16 @@ extension GhosttyNSView: NSTextInputClient {
}
func setMarkedText(_ string: Any, selectedRange: NSRange, replacementRange: NSRange) {
#if DEBUG
let typingTimingStart = CmuxTypingTiming.start()
defer {
CmuxTypingTiming.logDuration(
path: "terminal.setMarkedText",
startedAt: typingTimingStart,
extra: "markedLength=\(markedText.length)"
)
}
#endif
switch string {
case let v as NSAttributedString:
markedText = NSMutableAttributedString(attributedString: v)
@ -7547,6 +7754,17 @@ extension GhosttyNSView: NSTextInputClient {
}
func unmarkText() {
#if DEBUG
let hadMarkedText = markedText.length > 0
let typingTimingStart = CmuxTypingTiming.start()
defer {
CmuxTypingTiming.logDuration(
path: "terminal.unmarkText",
startedAt: typingTimingStart,
extra: "hadMarkedText=\(hadMarkedText ? 1 : 0)"
)
}
#endif
if markedText.length > 0 {
markedText.mutableString.setString("")
syncPreedit()
@ -7557,6 +7775,16 @@ extension GhosttyNSView: NSTextInputClient {
/// This tells Ghostty about IME composition text so it can render the
/// preedit overlay (e.g. for Korean, Japanese, Chinese input).
private func syncPreedit(clearIfNeeded: Bool = true) {
#if DEBUG
let typingTimingStart = CmuxTypingTiming.start()
defer {
CmuxTypingTiming.logDuration(
path: "terminal.syncPreedit",
startedAt: typingTimingStart,
extra: "markedLength=\(markedText.length) clearIfNeeded=\(clearIfNeeded ? 1 : 0)"
)
}
#endif
guard let surface = surface else { return }
if markedText.length > 0 {
@ -7624,6 +7852,17 @@ extension GhosttyNSView: NSTextInputClient {
}
func insertText(_ string: Any, replacementRange: NSRange) {
#if DEBUG
let typingTimingStart = CmuxTypingTiming.start()
defer {
CmuxTypingTiming.logDuration(
path: "terminal.insertText",
startedAt: typingTimingStart,
event: NSApp.currentEvent,
extra: "replacementLocation=\(replacementRange.location) replacementLength=\(replacementRange.length)"
)
}
#endif
// Get the string value
var chars = ""
switch string {