Add software-KVM: drive the receiver's native desktop from the sender's keyboard/mouse#85
Add software-KVM: drive the receiver's native desktop from the sender's keyboard/mouse#85DrDavidL wants to merge 7 commits into
Conversation
New toggle "Control receiver Mac (keyboard & mouse)" lets the sender's physical keyboard/mouse drive the receiver iMac's *native* macOS desktop over the existing session, then toggle back to the streamed display — Synergy-style, on the link-local connection. MVP scope (per design): relative mouse (receiver owns/clamps the cursor), keyboard + mouse only, and the toggle also hides the receiver window to expose its native desktop. Escape is the ⌃⌥⌘K hotkey (keyboard-only). Shared protocol (TBMonitorProtocol.swift / proto.h): five new JSON packet types — inputControl/inputMouseMove/inputMouseButton/inputScroll/inputKey (0x33–0x37), mirroring the existing length-prefixed framing. Sender (Swift): - TBKVMController: an active kCGSessionEventTap on a DEDICATED thread + CFRunLoop (never the main runloop — a slow callback there would freeze system-wide input). It consumes keyboard/mouse, coalesces mouse-move deltas at ~120 Hz, parks the local cursor (CGAssociateMouseAndMouseCursorPosition(false) + NSCursor.hide), and forwards events over the session's NWConnection (thread-safe send). - Escape hotkey ⌃⌥⌘K recognized inside the tap (swallowed, not forwarded). - Un-park failsafes: tap-disabled re-enable, app resign-active, terminate, connection drop (session.stop hook), and an atexit cursor re-associate for the crash case. - Service toggle `controlIMacKVM` (transient; targets the connected session) + a "Controlling receiver Mac — ⌃⌥⌘K to return" indicator. - Accessibility prompted via AXIsProcessTrustedWithOptions. Receiver (C): - input.c: re-synthesizes events via CGEventCreate*/CGEventPost to kCGHIDEventTap; owns a virtual cursor clamped to CGMainDisplayID bounds, drag-vs-move by button state, and resets modifiers on each KVM toggle so the escape chord can't leave keys stuck. AXIsProcessTrusted check logged. - main.c: handle the input packets; inputControl hides/restores the SDL window (minimize → resign active → reveal native desktop) and resets KVM on disconnect. - Makefile: build input.c, link ApplicationServices (AXIsProcessTrusted). Both apps build clean. Runtime verification (Accessibility/Input Monitoring grants on both machines + physical end-to-end) is pending — see the PR test plan. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Still working on this... |
The KVM build broke normal streaming on the receiver: the connection
dropped right after the display profile (sender saw NWError 54 reset by
peer; receiver read returned ENOTCONN), while the pre-KVM build connected
fine. Isolation showed the only thing the KVM changes added to the
non-KVM path was a startup tb_input_create() call — which touches the
CoreGraphics HID/Accessibility machinery (CGEventSourceCreate +
AXIsProcessTrusted) before the socket even listens.
Create tb_input lazily, the first time an inputControl{enabled:true}
packet arrives, so the normal streaming path never initializes any input
machinery and is identical to a build without KVM. tb_input_destroy stays
NULL-safe at teardown.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The KVM receiver build breaks normal streaming on the OCLP iMac (connect drops right after the display profile) while the pre-KVM build streams fine with the same sender — confirmed deterministic. With input init now lazy, no KVM code runs on the connect path, so the only remaining build-level difference is linking the ApplicationServices umbrella for the single AXIsProcessTrusted() diagnostic call. Drop ApplicationServices: use CoreGraphics (already linked) for all the CGEvent*/CGDisplay* injection APIs, and stop probing the Accessibility grant (injection simply no-ops until the user grants it). This makes the receiver link the exact same frameworks as a build without KVM, isolating whether the umbrella link was the cause. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
"⌃⌥⌘K" was misread (the ⌃ Control glyph looked like a caret to type). Show "Control-Option-Command-K" instead, in all three locales, so the return-from-KVM shortcut is unambiguous. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
While controlling the receiver, the sender's keyboard/mouse events are consumed by the tap and forwarded, so the local idle timer sees no activity and fires the screensaver/lock mid-session — which then trips the resign-active failsafe, drops KVM, and reverts the receiver to showing the stream. Worst while passively watching remote video (no local input for long stretches). Declare local user activity (IOPMAssertionDeclareUserActivity, kIOPMUserActiveLocal) on KVM activate and every 30s thereafter, cancelled on deactivate. The Mac stays awake for the duration of KVM control so the screensaver/display-sleep never interrupt it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CI (Xcode 16.4) failed with "the compiler is unable to type-check this expression in reasonable time" on TBKVMController's 14-term CGEventMask OR-chain. Build the mask with a loop over an explicit [CGEventType] array instead of one giant `|` expression — trivially type-checkable on every toolchain. No behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thanks a lot for working on this — the software-KVM direction is definitely valuable. I do not want to merge this directly onto So the right path here is to rebase / rework the useful parts against The main reason is to avoid regressions and protocol collisions:
So: thank you — this is important work, but the fixes need to be adapted on |
|
Small follow-up: I am actively working through this myself on |
|
Quick note to keep the integration path clear: any follow-up changes for this work should be opened against That branch already contains the current input-dockstation / receiver-master flow, protocol updates, permission handling, cursor/clipboard behavior, and the ongoing fixes needed to avoid regressions. Keeping the work there is the safest way to converge everything before merge. |
The main loop deliberately skipped its idle SDL_Delay while video was
active ("no extra millisecond of latency"), so during any stream it
spun a full CPU core at 100% — the iMac's fan ran constantly. Pre-existing
behavior, not KVM-related, but very visible during long KVM sessions.
Replace the busy-spin with poll() on the client socket (10ms timeout):
it returns the instant a packet is ready (same latency as the busy loop)
and otherwise sleeps, so idle CPU between frames drops to ~nothing while
the per-second housekeeping and quit handling still run.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Summary
Adds a software KVM to TargetBridge: a sender-side toggle, "Control receiver Mac (keyboard & mouse)", that re-routes the sender's physical keyboard/mouse to the receiver Mac's native macOS desktop over the existing session — then back to the streamed display. Synergy / Universal-Control-style, on the link-local connection you already have.
Verified working end-to-end on real hardware (MacBook Pro sender → OCLP iMac receiver): toggle on → the receiver window minimizes to expose its native desktop → the MacBook keyboard/mouse drive the iMac (move / click / type / ⌘-shortcuts) → escape chord returns control and streaming resumes.
MVP scope (agreed): relative mouse (receiver owns/clamps the cursor), mouse + keyboard only, the toggle also hides the receiver window to reveal its desktop, and a keyboard escape (Control-Option-Command-K) that always returns control.
How to run it
Receiver (the Mac being controlled):
TargetBridge-Receiver/scripts/build_tbreceiver_c_app.sh→ producesbuild/TargetBridge Receiver.app./Applicationsand launch it from Finder../tbreceiverfrom a terminal/VSCode — a bare-binary launch inherits the terminal's TCC/launch context and silently breaks the link-local data path (stream connects, sends the display profile, then dies withENOTCONN/NWError 54 reset by peer). This is unrelated to the KVM code — pristinemainfails identically from a terminal.Sender (the Mac you drive from):
How it works
Protocol (
TBMonitorProtocol.swift+ receiverproto.h): five new JSON packet typesinputControl / inputMouseMove / inputMouseButton / inputScroll / inputKey(0x33–0x37), reusing the existing length-prefixed framing.Sender (
TBKVMController.swift): an activekCGSessionEventTapon a dedicated thread + CFRunLoop (never the main runloop — a slow callback on an active tap would freeze system-wide input). It consumes keyboard/mouse, coalesces mouse-move deltas at ~120 Hz, parks the local cursor, and forwards over the session'sNWConnection. Escape chord recognized inside the tap. Un-park failsafes cover tap-disabled re-enable, resign-active, terminate, connection drop, and anatexitcursor re-associate for the crash case. While active it declares local user activity every 30 s so the controlling Mac doesn't idle into the screensaver/lock mid-session (even while passively watching remote video).Receiver (
input.c/main.c): re-synthesizes events viaCGEventCreate*+CGEventPost(kCGHIDEventTap, …), owning a virtual cursor clamped toCGMainDisplayIDbounds (drag-vs-move by button state).inputControlminimizes/restores the SDL window to reveal/hide the native desktop; modifiers are reset on each KVM toggle so the escape chord can't leave keys stuck. Disconnect auto-exits KVM. Input init is lazy (first KVM engage) so the normal streaming path is identical to a build without KVM.Commits
223d000— the feature (protocol + sender tap/UI + receiver injection/window-hide).5beadf6— lazy receiver input init (keep the non-KVM path identical to a no-KVM build).54b859e— drop theApplicationServiceslink /AXIsProcessTrustedprobe (use CoreGraphics only; receiver Accessibility is granted manually).75e747f— spell out the escape chord as Control-Option-Command-K in the indicator.20d54b5— keep the controlling Mac awake while KVM is active (screensaver/lock fix).9a512ed— fix a CI type-check timeout (Xcode 16.4) by building theCGEventMaskwith a loop instead of a 14-term OR-chain.39e19b1— receiver CPU fix: the main loop busy-spun a core during streaming (constant fan); nowpoll()s the socket with a short timeout — same latency, idle CPU drops to ~nothing. Pre-existing behavior, not KVM-specific, but rides along here.Known limitations / caveats
.app, not a bare binary (see above) — the one thing most likely to bite.Test plan
20d54b5.39e19b1): confirm the iMac no longer pegs a core / runs the fan during streaming (rebuild the receiver.app).Out of scope (v2)
Clipboard sharing, drag-and-drop fidelity, double-click timing, multi-display + edge-flip, absolute-coordinate mode, binary input packets, configurable hotkey, stable code-signing for persistent grants.
🤖 Generated with Claude Code