From 7d59e550bc81b8da692bd596253a3cee485efd0a Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Mon, 23 Feb 2026 23:57:13 -0800 Subject: [PATCH] Fix browser Return/Enter routing and add enter trace logs --- Sources/AppDelegate.swift | 65 +++++++++- Sources/Panels/CmuxWebView.swift | 111 +++++++++++++++++- cmuxTests/CmuxWebViewKeyEquivalentTests.swift | 36 ++++++ 3 files changed, 206 insertions(+), 6 deletions(-) diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index befa7e43..20c3a27a 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -2660,6 +2660,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent guard let self else { return event } if event.type == .keyDown { #if DEBUG + let isEnterKey = event.keyCode == 36 || event.keyCode == 76 if (ProcessInfo.processInfo.environment["CMUX_KEY_LATENCY_PROBE"] == "1" || UserDefaults.standard.bool(forKey: "cmuxKeyLatencyProbe")), event.timestamp > 0 { @@ -2671,18 +2672,36 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent dlog( "monitor.keyDown: \(NSWindow.keyDescription(event)) fr=\(frType) addrBarId=\(self.browserAddressBarFocusedPanelId?.uuidString.prefix(8) ?? "nil") \(self.debugShortcutRouteSnapshot(event: event))" ) + if isEnterKey { + dlog( + "enter.trace stage=app.monitor.pre event=\(NSWindow.keyDescription(event)) " + + "fr=\(frType) addrBarId=\(self.browserAddressBarFocusedPanelId?.uuidString.prefix(8) ?? "nil")" + ) + } if let probeKind = self.developerToolsShortcutProbeKind(event: event) { self.logDeveloperToolsShortcutSnapshot(phase: "monitor.pre.\(probeKind)", event: event) } #endif if self.handleCustomShortcut(event: event) { #if DEBUG + if isEnterKey { + dlog( + "enter.trace stage=app.monitor.consume event=\(NSWindow.keyDescription(event)) " + + "reason=handleCustomShortcut" + ) + } dlog(" → consumed by handleCustomShortcut") DebugEventLog.shared.dump() #endif return nil // Consume the event } #if DEBUG + if isEnterKey { + dlog( + "enter.trace stage=app.monitor.pass event=\(NSWindow.keyDescription(event)) " + + "reason=handleCustomShortcutReturnedFalse" + ) + } DebugEventLog.shared.dump() #endif return event // Pass through @@ -3802,7 +3821,18 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent /// through the same app-level shortcut handler used by the local key monitor. @discardableResult func handleBrowserSurfaceKeyEquivalent(_ event: NSEvent) -> Bool { - handleCustomShortcut(event: event) + let consumed = handleCustomShortcut(event: event) +#if DEBUG + if event.keyCode == 36 || event.keyCode == 76 { + let frType = NSApp.keyWindow?.firstResponder.map { String(describing: type(of: $0)) } ?? "nil" + dlog( + "enter.trace stage=app.browserSurfaceKeyEquivalent event=\(NSWindow.keyDescription(event)) " + + "consumed=\(consumed ? 1 : 0) fr=\(frType) " + + "addrBarId=\(browserAddressBarFocusedPanelId?.uuidString.prefix(8) ?? "nil")" + ) + } +#endif + return consumed } #if DEBUG @@ -5220,9 +5250,19 @@ private extension NSWindow { } @objc func cmux_performKeyEquivalent(with event: NSEvent) -> Bool { + let isEnterKey = event.keyCode == 36 || event.keyCode == 76 #if DEBUG let frType = self.firstResponder.map { String(describing: type(of: $0)) } ?? "nil" dlog("performKeyEquiv: \(Self.keyDescription(event)) fr=\(frType)") + if isEnterKey { + let frGhostty = cmuxOwningGhosttyView(for: self.firstResponder) != nil + let frWeb = self.firstResponder.flatMap { Self.cmuxOwningWebView(for: $0) } != nil + dlog( + "enter.trace stage=window.performKeyEquivalent.start event=\(Self.keyDescription(event)) " + + "fr=\(frType) frGhostty=\(frGhostty ? 1 : 0) frWeb=\(frWeb ? 1 : 0) " + + "win=\(self.windowNumber)" + ) + } #endif // When the terminal surface is the first responder, prevent SwiftUI's @@ -5253,6 +5293,12 @@ private extension NSWindow { let result = ghosttyView.performKeyEquivalent(with: event) #if DEBUG dlog(" → ghostty direct: \(result)") + if isEnterKey { + dlog( + "enter.trace stage=window.performKeyEquivalent.ghosttyDirect " + + "event=\(Self.keyDescription(event)) consumed=\(result ? 1 : 0)" + ) + } #endif return result } @@ -5274,7 +5320,16 @@ private extension NSWindow { } } - if AppDelegate.shared?.handleBrowserSurfaceKeyEquivalent(event) == true { + let consumedByBrowserSurface = AppDelegate.shared?.handleBrowserSurfaceKeyEquivalent(event) == true +#if DEBUG + if isEnterKey { + dlog( + "enter.trace stage=window.performKeyEquivalent.browserSurface event=\(Self.keyDescription(event)) " + + "consumed=\(consumedByBrowserSurface ? 1 : 0)" + ) + } +#endif + if consumedByBrowserSurface { #if DEBUG dlog(" → consumed by handleBrowserSurfaceKeyEquivalent") #endif @@ -5313,6 +5368,12 @@ private extension NSWindow { let result = cmux_performKeyEquivalent(with: event) #if DEBUG if result { dlog(" → consumed by original performKeyEquivalent") } + if isEnterKey { + dlog( + "enter.trace stage=window.performKeyEquivalent.original event=\(Self.keyDescription(event)) " + + "consumed=\(result ? 1 : 0)" + ) + } #endif return result } diff --git a/Sources/Panels/CmuxWebView.swift b/Sources/Panels/CmuxWebView.swift index 68a13282..788e417e 100644 --- a/Sources/Panels/CmuxWebView.swift +++ b/Sources/Panels/CmuxWebView.swift @@ -63,6 +63,72 @@ final class CmuxWebView: WKWebView { } var debugPointerFocusAllowanceDepth: Int { pointerFocusAllowanceDepth } +#if DEBUG + private func debugKeyDescription(_ event: NSEvent) -> String { + var parts: [String] = [] + let flags = event.modifierFlags.intersection(.deviceIndependentFlagsMask) + if flags.contains(.command) { parts.append("Cmd") } + if flags.contains(.shift) { parts.append("Shift") } + if flags.contains(.option) { parts.append("Opt") } + if flags.contains(.control) { parts.append("Ctrl") } + let chars = event.charactersIgnoringModifiers ?? "?" + parts.append("'\(chars)'(\(event.keyCode))") + return parts.joined(separator: "+") + } + + private func debugEnterTrace( + stage: String, + event: NSEvent, + consumed: Bool? = nil, + note: String? = nil + ) { + let firstResponderType = window?.firstResponder.map { String(describing: type(of: $0)) } ?? "nil" + let host = url?.host ?? "nil" + var line = + "enter.trace stage=\(stage) web=\(ObjectIdentifier(self)) " + + "event=\(debugKeyDescription(event)) fr=\(firstResponderType) " + + "win=\(window?.windowNumber ?? -1) host=\(host)" + if let consumed { + line += " consumed=\(consumed ? 1 : 0)" + } + if let note { + line += " note=\(note)" + } + dlog(line) + } + + private func debugLogActiveElementForEnter(stage: String) { + let js = """ + (() => { + const el = document.activeElement; + if (!el) return 'none'; + const tag = (el.tagName || '').toLowerCase(); + const id = el.id || '-'; + const name = el.getAttribute('name') || '-'; + const type = el.getAttribute('type') || '-'; + return `${tag}|${id}|${name}|${type}`; + })(); + """ + evaluateJavaScript(js) { [weak self] result, error in + guard let self else { return } + let activeSummary: String + if let error { + activeSummary = "error=\(error.localizedDescription)" + } else if let text = result as? String { + activeSummary = text + } else if let result { + activeSummary = String(describing: result) + } else { + activeSummary = "nil" + } + dlog( + "enter.trace stage=\(stage).dom web=\(ObjectIdentifier(self)) " + + "active=\(activeSummary)" + ) + } + } +#endif + override func becomeFirstResponder() -> Bool { guard allowsFirstResponderAcquisitionEffective else { #if DEBUG @@ -113,13 +179,27 @@ final class CmuxWebView: WKWebView { } override func performKeyEquivalent(with event: NSEvent) -> Bool { - // Preserve Cmd+Return/Enter for web content (e.g. editors/forms). Do not - // route it through app/menu key equivalents, which can trigger unintended actions. - let flags = event.modifierFlags.intersection(.deviceIndependentFlagsMask) - if flags.contains(.command), event.keyCode == 36 || event.keyCode == 76 { + if event.keyCode == 36 || event.keyCode == 76 { + // Always bypass app/menu key-equivalent routing for Return/Enter so WebKit + // receives the keyDown path used by form submission handlers. +#if DEBUG + debugEnterTrace( + stage: "web.performKeyEquivalent.bypass", + event: event, + consumed: false, + note: "returnFalseForEnter" + ) +#endif return false } + let flags = event.modifierFlags.intersection(.deviceIndependentFlagsMask) + // Menu/app shortcut routing is only needed for Command equivalents + // (New Tab, Close Tab, tab switching, split commands, etc). + guard flags.contains(.command) else { + return super.performKeyEquivalent(with: event) + } + // Let the app menu handle key equivalents first (New Tab, Close Tab, tab switching, etc). if let menu = NSApp.mainMenu, menu.performKeyEquivalent(with: event) { return true @@ -135,14 +215,37 @@ final class CmuxWebView: WKWebView { } override func keyDown(with event: NSEvent) { +#if DEBUG + if event.keyCode == 36 || event.keyCode == 76 { + debugEnterTrace(stage: "web.keyDown.pre", event: event, note: "beforeSuper") + debugLogActiveElementForEnter(stage: "web.keyDown.pre") + } +#endif + // Some Cmd-based key paths in WebKit don't consistently invoke performKeyEquivalent. // Route them through the same app-level shortcut handler as a fallback. if event.modifierFlags.intersection(.deviceIndependentFlagsMask).contains(.command), AppDelegate.shared?.handleBrowserSurfaceKeyEquivalent(event) == true { +#if DEBUG + if event.keyCode == 36 || event.keyCode == 76 { + debugEnterTrace( + stage: "web.keyDown.commandConsumed", + event: event, + consumed: true, + note: "handleBrowserSurfaceKeyEquivalent" + ) + } +#endif return } super.keyDown(with: event) +#if DEBUG + if event.keyCode == 36 || event.keyCode == 76 { + debugEnterTrace(stage: "web.keyDown.post", event: event, note: "afterSuper") + debugLogActiveElementForEnter(stage: "web.keyDown.post") + } +#endif } // MARK: - Focus on click diff --git a/cmuxTests/CmuxWebViewKeyEquivalentTests.swift b/cmuxTests/CmuxWebViewKeyEquivalentTests.swift index 9ca9fb4d..a1ad944f 100644 --- a/cmuxTests/CmuxWebViewKeyEquivalentTests.swift +++ b/cmuxTests/CmuxWebViewKeyEquivalentTests.swift @@ -149,6 +149,42 @@ final class CmuxWebViewKeyEquivalentTests: XCTestCase { XCTAssertTrue(spy.invoked) } + func testReturnDoesNotRouteToMainMenuWhenWebViewIsFirstResponder() { + let spy = ActionSpy() + installMenu(spy: spy, key: "\r", modifiers: []) + + let webView = CmuxWebView(frame: .zero, configuration: WKWebViewConfiguration()) + let event = makeKeyDownEvent(key: "\r", modifiers: [], keyCode: 36) // kVK_Return + XCTAssertNotNil(event) + + XCTAssertFalse(webView.performKeyEquivalent(with: event!)) + XCTAssertFalse(spy.invoked) + } + + func testCmdReturnDoesNotRouteToMainMenuWhenWebViewIsFirstResponder() { + let spy = ActionSpy() + installMenu(spy: spy, key: "\r", modifiers: [.command]) + + let webView = CmuxWebView(frame: .zero, configuration: WKWebViewConfiguration()) + let event = makeKeyDownEvent(key: "\r", modifiers: [.command], keyCode: 36) // kVK_Return + XCTAssertNotNil(event) + + XCTAssertFalse(webView.performKeyEquivalent(with: event!)) + XCTAssertFalse(spy.invoked) + } + + func testKeypadEnterDoesNotRouteToMainMenuWhenWebViewIsFirstResponder() { + let spy = ActionSpy() + installMenu(spy: spy, key: "\r", modifiers: []) + + let webView = CmuxWebView(frame: .zero, configuration: WKWebViewConfiguration()) + let event = makeKeyDownEvent(key: "\r", modifiers: [], keyCode: 76) // kVK_ANSI_KeypadEnter + XCTAssertNotNil(event) + + XCTAssertFalse(webView.performKeyEquivalent(with: event!)) + XCTAssertFalse(spy.invoked) + } + @MainActor func testCanBlockFirstResponderAcquisitionWhenPaneIsUnfocused() { _ = NSApplication.shared