diff --git a/clients/apple/README.md b/clients/apple/README.md index b1a5e2d..cade77d 100644 --- a/clients/apple/README.md +++ b/clients/apple/README.md @@ -145,8 +145,11 @@ signing, bundle id `io.unom.punktfunk`. Notes: nothing sticks down host-side. While the stream has focus the LOCAL cursor is hidden and frozen mid-view (`CursorCapture` in StreamView.swift — the host renders its own cursor; the local one diverges from it and a stray click would focus another app); - Cmd+Tab frees it, ⌘D disconnects. Local shortcuts (⌘-anything) still also reach the - host; a capture toggle is a small follow-up. One live capture per process (the GC + Cmd+Tab frees it, ⌘D disconnects. While captured, key NSEvents are swallowed by a + local event monitor (GC reads HID directly; without it every keystroke bubbles up the + responder chain unhandled and NSWindow beeps) — except ⌘-combos, which still reach + the local app (⌘D/⌘Q) in addition to the host; a capture toggle is a small + follow-up. One live capture per process (the GC mouse/keyboard singletons have a single handler slot — ownership is tracked so a stale capture's stop() can't clobber a newer one). 9. **iOS**: same package (`BUILD_IOS=1` for the xcframework slice); `StreamView` needs the diff --git a/clients/apple/Sources/PunktfunkKit/InputCapture.swift b/clients/apple/Sources/PunktfunkKit/InputCapture.swift index 54deb90..e255add 100644 --- a/clients/apple/Sources/PunktfunkKit/InputCapture.swift +++ b/clients/apple/Sources/PunktfunkKit/InputCapture.swift @@ -31,6 +31,7 @@ public final class InputCapture { private var observers: [NSObjectProtocol] = [] private var mice: [GCMouse] = [] private var keyboards: [GCKeyboard] = [] + private var keyEventMonitor: Any? // Main-queue-only state (see header comment). private var residualX: Float = 0 @@ -66,12 +67,26 @@ public final class InputCapture { ) { [weak self] _ in self?.releaseAll() }) + // GC reads the HID state directly — the NSEvents still travel the responder + // chain, where every unhandled keyDown makes NSWindow beep ("invalid input"). + // Swallow key events while captured, EXCEPT ⌘-combos: those stay local (the + // HUD's ⌘D disconnect, ⌘Q, …) in addition to reaching the host via GC. + keyEventMonitor = NSEvent.addLocalMonitorForEvents( + matching: [.keyDown, .keyUp] + ) { event in + event.modifierFlags.intersection(.deviceIndependentFlagsMask).contains(.command) + ? event : nil + } } public func stop() { releaseAll() observers.forEach(NotificationCenter.default.removeObserver(_:)) observers.removeAll() + if let monitor = keyEventMonitor { + NSEvent.removeMonitor(monitor) + keyEventMonitor = nil + } // Don't clobber the handlers if a newer capture has taken the global devices. if Self.activeCapture === self || Self.activeCapture == nil { for mouse in mice {