mirror of
https://github.com/feschber/lan-mouse.git
synced 2026-05-15 06:06:07 -06:00
[PR #420] feat: cross-machine cursor sync + wall-press auto-release + host-lock cross-suppression #411
Labels
No labels
Xorg
documentation
enhancement
macos
pull-request
question
windows
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: github-starred/lan-mouse#411
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
📋 Pull Request Information
Original PR: https://github.com/feschber/lan-mouse/pull/420
Author: @jondkinney
Created: 5/6/2026
Status: 🔄 Open
Base:
main← Head:split/01-cursor-and-walls📝 Commits (10+)
31298f2feat: cross-platform wall-press auto-release fallbackbffbef0proto: add Bounds(width, height) event variant2219f45feat(emulation): display_bounds + warp_cursor for all backends5de5016feat(emulation): send Bounds + warp cursor on Enter68fab55feat(capture): cache peer bounds and use as wall-press upper clamp944e758ui: wrap window content in GtkScrolledWindow56f7730ui: rename Auto-Release group to scope it to outgoing capturecd63f5dfix: preserve cross-axis cursor position across machine transitions2571b47fix(capture/layer_shell): report screen-space cursor position on Entercbe2ec1fix(capture): warp host cursor to guest position on release📊 Changes
25 files changed (+1753 additions, -86 deletions)
View changed files
📝
input-capture/Cargo.toml(+1 -0)📝
input-capture/src/dummy.rs(+2 -2)📝
input-capture/src/layer_shell.rs(+67 -17)📝
input-capture/src/lib.rs(+579 -11)📝
input-capture/src/libei.rs(+5 -2)📝
input-capture/src/macos.rs(+152 -13)📝
input-capture/src/windows.rs(+1 -1)📝
input-capture/src/windows/event_thread.rs(+64 -12)📝
input-capture/src/x11.rs(+1 -1)📝
input-emulation/src/lib.rs(+36 -0)📝
input-emulation/src/libei.rs(+41 -2)📝
input-emulation/src/macos.rs(+34 -1)📝
input-emulation/src/windows.rs(+24 -1)📝
input-emulation/src/wlroots.rs(+190 -1)📝
input-emulation/src/x11.rs(+27 -0)📝
lan-mouse-gtk/resources/window.ui(+66 -5)📝
lan-mouse-gtk/src/lib.rs(+3 -0)📝
lan-mouse-gtk/src/window.rs(+26 -0)📝
lan-mouse-gtk/src/window/imp.rs(+77 -1)📝
lan-mouse-ipc/src/lib.rs(+5 -0)...and 5 more files
📄 Description
Summary
Three intertwined improvements to cross-machine cursor handling, plus the GUI control they expose. They share
peer_boundsstorage anddisplay_bounds/warp_cursorinfrastructure on theInputCapture/InputEmulationtraits — splitting them produces non-compiling intermediate states, so they're bundled here.Cross-machine cursor position sync
The host computes its cursor's position as a normalized fraction
(nx, ny) ∈ [0, 1]against its own bounds and sendsProtoEvent::CursorPos { pos, nx, ny }right after Enter. The receiver scales against its own live bounds and pins the on-axis dimension to the entry edge. Self-sufficient — works on the very first crossing, noBoundsround-trip needed.One architectural change to flag for review: the release path is now split.
release_capture(release-bind chord, backend auto-release) computes a host-side cursor warp from the cachedvirtual_cursor.release_capture_handover(used when the peer takes over viaReleaseNotifyorProtoEvent::Leave) skips the host warp so the peer's authoritativeCursorPosis the only signal seating our shared cursor.Wall-press auto-release
User pushes cursor past the host-adjacent edge of the guest and keeps pushing for N more pixels → capture releases. Threshold tunable in GUI; 0 disables.
The triggering scenario is two locked screens. When the peer's screen is locked, its lock screen owns the input pipeline before lan-mouse's capture barrier can fire — so the peer can't detect the user crossing back and never sends
Leaveto the host. The host stays in capture mode indefinitely: every keystroke goes to the (probably-locked) guest, and there's no way to unlock the host without first recovering the cursor via the release-bind chord (Ctrl+Shift+Meta+Alt). Wall-press auto-release breaks this deadlock by treating sustained motion past the host-adjacent edge as an explicit "I want out" signal — without protocol cooperation from the peer, which is the part that's broken.Same mechanism doubles as the host-lock escape hatch. When the host's screen is locked while the cursor is already on the peer, the host's input hook keeps running under the lock screen, so wall-press still accumulates and still fires. Without it, a user whose host locks mid-capture would have to fall back to the release-bind chord or wait for the peer to fire a
Leave. With it, "lock the Mac with cursor on Linux, push left, cursor returns" Just Works on every platform that has wall-press configured.The naive virtual-cursor accumulator runs away if the user holds against the wall, so the protocol-based fix adds
ProtoEvent::Bounds(width, height)(sent by emulation right after Enter), caches it per-position, and clampsvirtual_posto the peer's actual extent. Emulation also warps the cursor on Enter sovirtual_pos = 0lines up with the guest's actual cursor.display_boundsandwarp_cursorare added as trait methods onInputEmulationand implemented for every backend exceptxdg_desktop_portal(no protocol support — falls back to heuristic-only path).Peer-Leave deadline gate (
34605a7): wall-press used to fire the moment the threshold was crossed, which raced the peer-side layer-shell handover and only "worked correctly" because the network round-trip beat 200px of physical motion. With the gate, wall-press defers AutoRelease for ~150ms after threshold and cancels if a peerLeavearrives in that window. Result: in normal operation the layer-shell handover always wins (no spurious wall-press fires); in true fallback scenarios (peer's layer-shell suppressed, or host-lock-stuck-on-peer case) the deadline elapses and wall-press fires as designed.Bundled forward-compat fix (
3025422): we now log-and-skip undecodable peer datagrams instead of disconnecting. Necessary because peers running pre-Boundsbuilds can interop with this branch.Host-lock cross-suppression
When the host's screen is locked, prevent the cursor from leaving for the peer — the host's lock screen consumes keyboard events before any capture hook sees them, so a mouse-only-on-peer state is broken-by-design. This standardizes prevention across all platforms; previously Wayland enforced it (because the compositor revokes input on lock) but macOS and Windows didn't, so users on those hosts could silently end up in the half-broken state.
Two cases worth distinguishing:
virtual_cursormodel, and pushing against the host-adjacent edge of the peer fires the wall-press release as if everything were normal. Verified end-to-end on macOS host ↔ Linux peer: lock the Mac while cursor is on Linux, push left, cursor returns. Same mechanism works on Windows host (and on Wayland host where the compositor handles it natively). Release-bind chord and peer-sideLeaveare the alternative recovery paths.Wayland (
free) — the compositor revokes input on layer-shell surfaces when the screen locks, sowl_pointer.Enterstops firing AND the in-flightwl_pointer.Leavearrives, naturally tearing down whichever direction was in play. No code needed.macOS (
8cd86ba/9d6f427/76f3544) — initial attempt wasCFNotificationCenterAddObserverforcom.apple.screenIsLocked/Unlocked, but those callbacks never fired in practice because lan-mouse's main thread runs the GLib event loop, not a CFRunLoop, and the distnoted mach port attaches to the main thread regardless of which thread calledAddObserver. Replaced with a directCGSessionCopyCurrentDictionary["CGSSessionScreenIsLocked"]poll evaluated at the moment a barrier crossing is about to commit — ~10–50 µs of XPC to WindowServer, paid only on cross attempts (a few per minute), zero per-event overhead during in-flight capture. Mid-capture lock isn't separately handled and doesn't need to be: wall-press is the user-facing recovery path and it works under the lock screen because the CGEventTap keeps running.Windows (
8cd86ba) —WTSRegisterSessionNotificationon the existing message-only window;window_procflips aHOST_LOCKEDthread-local onWTS_SESSION_LOCK/WTS_SESSION_UNLOCKand gatescheck_client_activation. Convenience extra: also synthesizesCaptureEvent::AutoReleaseif a capture is already in flight when the lock fires — same hand-off into the wall-press release plumbing, so the cursor returns to the host the instant the lock screen comes up rather than waiting for the user to wall-press out. Possible because the WTS notification arrives unconditionally; on macOS the equivalent would require polling on every motion event during capture, and wall-press already covers it.UI controls (slider + group label)
3743edd): cursor over the release-threshold slider no longer eats main-window scroll events. Capture-phase handler suppressesGtkScale's own scroll-to-adjust handler (so the slider value stays put) AND forwards the scroll to the ancestorScrolledWindow(so the page scrolls naturally).GtkScrolledWindowso the preferences pane scrolls cleanly when the window is shorter than the natural content height.8b0a169) scopes the group label to what it actually controls. Description retightened (66df7e5) to frame it as a peer-locked-screen fallback rather than a positive-action feature, since the deadline gate makes that its actual role.Test plan
Verified locally: macOS host (2056×1329 logical) ↔ Hyprland guest (2400×1500 logical) over both wired and wifi LAN. Crossings at top / middle / bottom of source land proportionally on destination, both directions, including fast back-and-forth within the same second. Wall-press auto-release deadline gate behaves correctly: never fires on healthy crosses, fires after the deadline elapses on locked-peer scenarios. Host-lock suppression verified on macOS (
Cmd+Ctrl+Qlock; cursor refuses to cross until unlock; with cursor already on Linux, push-left wall-press cleanly returns the cursor to the locked Mac).Open items (not blocking — environment access):
Build hygiene:
cargo fmt --allclean,cargo clippy --workspace --all-targets --all-features -- -D warningsclean,cargo test --workspacepasses.Split out from #418, the umbrella PR collecting ~10 independent feature areas. This PR contains the cursor-positioning, wall-press auto-release, host-lock cross-suppression, and slider/UI subset. The other split PRs will stack on top of this one — see #418 for the full picture.
Stack overview
These PRs are split out from #418 and stack in this order:
Each PR's branch builds on the previous one, so until earlier PRs are merged the cumulative diff against
mainincludes all preceding work. Reviewing in order is easiest.🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.