Skip to content

fix(control): hold the pointer, drop stale moves, settle before clicking - #62

Merged
mrprecioustech merged 1 commit into
masterfrom
fix/pointer-capture-and-input-latency
Aug 11, 2026
Merged

fix(control): hold the pointer, drop stale moves, settle before clicking#62
mrprecioustech merged 1 commit into
masterfrom
fix/pointer-capture-and-input-latency

Conversation

@mrprecioustech

Copy link
Copy Markdown
Contributor

Diagnosed from the Mac guest console and the KDE/Wayland host's pairux-input.log. Three reported symptoms — "the mouse leaves the window", "I can't click anything", "it's super slow" — turned out to have three separate causes, all visible in the logs.

1. The pointer escapes the video

"when I'm scrolling to the edge the mouse leaves the window and goes out into my own window… the mouse should be locked in and then I can release it with a keyboard command"

Taking control never captured the pointer. The guest's real cursor walked out of the picture the moment they pushed toward an edge, so reaching a menu bar, a corner, or a window's close button — most of what taking control is for — was impossible.

Pointer lock did exist, but only on the desktop viewer, only inside fullscreen, behind a hover button in a corner. The web viewer had none at all.

Now: click the picture to take the pointer, Esc to give it back. No fullscreen involved, with Click to control · Esc to release on screen while control is granted but not yet taken. Ported to the web viewer, which had nothing.

Two details that matter:

  • The click that acquires control is not forwarded to the host — the guest is reaching for the window, not for whatever sits under the pointer.
  • If the browser refuses the lock, it falls back to absolute coordinates rather than leaving the guest unable to act at all.

2. Clicks landing nowhere

The host log shows the two commands issued back to back:

commands: [ 'mousemove --absolute 1775 172', 'click 64' ]

Those are two separate ydotool invocations writing to uinput, and the compositor processes that stream asynchronously — so the press can be delivered while the pointer is still where the host left it. The guest's cursor sits over a button, they click, nothing happens.

The nut.js backend has waited a frame between positioning and pressing since the same bug was found on macOS. This path never got it.

3. Everything lagging

Every injection is serialized through one promise chain, and on Wayland each one costs a process spawn (~10ms). A viewer streams pointer movement at their display's refresh rate — far faster than that drains — so the queue grew without bound. The pointer trailed further and further behind, and a click sat behind hundreds of stale positions before it was even attempted.

Moves that a newer position has already replaced are now dropped before they can queue. Nothing is lost: a move says where the pointer is, not how it got there. Never dropped: clicks, scrolls, keystrokes, and any move during a drag, where the path is the whole point. stats.coalesced is in diagnostics, so a host that feels slow with nothing coalesced points somewhere else.

Also in the log, not fixed here

The host's [RemoteInput] Capture bounds primary display line lands before [RemoteInput] Screen size: 1920x1080 — the capture source is resolved before the injector has initialised, so the multi-monitor mapping from #59 silently falls back to the primary display. It does not affect this single-monitor host, but it will affect any host sharing a second screen. Worth its own fix.

lint 0 errors, typecheck 9/9. 185 remote-input tests, 585 desktop, 648 web — +8 covering the coalescing rules, the settle ordering, and the new capture flow.

Not run on real hardware; the three causes are each confirmed from the logs and covered by tests.

🤖 Generated with Claude Code

Three symptoms from a real Mac-to-KDE session — "the mouse leaves the window",
"I can't click anything", "it's super slow" — with three separate causes. The
host's debug log has all of them.

**The pointer escapes the video.** Taking control did not capture the pointer,
so the guest's real cursor walked out of the picture the moment they pushed
toward an edge. Reaching a menu bar, a corner, or a window's close button —
most of what taking control is for — was impossible. Pointer lock existed but
only on the desktop viewer, only inside fullscreen, behind a hover button in a
corner; the web viewer had none at all. Now clicking the picture takes the
pointer and Escape gives it back, no fullscreen involved, with the two words
of instruction on screen while control is granted but not yet taken. The click
that acquires control is not forwarded — the guest is reaching for the window,
not for whatever sits under the pointer. If the browser refuses the lock, it
falls back to absolute coordinates rather than leaving the guest unable to act.

**Clicks landing nowhere.** On Wayland, positioning the pointer and pressing
the button are two separate ydotool invocations writing to uinput, and the
compositor processes that stream asynchronously — so the press could be
delivered while the pointer was still where the host left it. The guest's
cursor sits over a button, they click, nothing happens. The nut.js backend has
waited a frame here since the same bug was found on macOS; this path never got
it. The host log shows the two commands issued back to back with nothing
between them.

**Everything lagging.** Every injection is serialized, and on Wayland each one
costs a process spawn. A viewer streams pointer movement at their display's
refresh rate, which arrives faster than that drains, so the queue grew without
bound: the pointer trailed further and further behind and a click sat behind
hundreds of stale positions before it was even attempted. Moves that a newer
position has already replaced are now dropped before they can queue. Nothing is
lost — a move says where the pointer *is*, not how it got there — and clicks,
scrolls and keystrokes are never dropped, nor is any move during a drag, where
the path is the whole point. The count is reported in diagnostics, so a host
that feels slow with nothing coalesced points somewhere else.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

const backend = new WaylandYdotoolInputBackend(
run,
{ hasBinary: true, hasSocket: true, socketPath: '/tmp/.ydotool_socket' },
@github-actions

Copy link
Copy Markdown

ThreatCrush Security Scan

170 finding(s)

HIGH/CRITICAL: 16 | MEDIUM: 48 | LOW: 106

Severity Rule Location
HIGH sh-eval-expansion .githooks/pre-commit:33
HIGH sh-remote-script-execution apps/installer/scripts/install.sh:691
HIGH sh-unquoted-expansion-destructive apps/installer/scripts/install.sh:715
HIGH sh-remote-script-execution apps/installer/scripts/install.sh:813
HIGH sh-remote-script-execution apps/installer/scripts/install.sh:815
HIGH sh-remote-script-execution apps/installer/scripts/install.sh:894
HIGH sh-unquoted-expansion-destructive apps/installer/scripts/install.sh:910
HIGH sh-remote-script-execution apps/installer/scripts/install.sh:1064
HIGH sh-remote-script-execution apps/installer/scripts/install.sh:1066
HIGH sh-remote-script-execution apps/installer/scripts/install.sh:1128
HIGH secret-generic-credential apps/livekit/fly.toml:12
HIGH sh-remote-script-execution apps/livekit/setup-livekit-server.sh:93
HIGH sh-remote-script-execution apps/turn/deploy-droplet.sh:62
HIGH secret-generic-credential apps/turn/fly.toml:11
HIGH secret-generic-credential docs/API.md:648
HIGH secret-generic-credential docs/API.md:654
MEDIUM insecure-temp-file .githooks/commit-msg:19
MEDIUM insecure-temp-file .githooks/post-commit:22
MEDIUM insecure-temp-file apps/desktop/src/main/streaming/index.test.ts:64
MEDIUM insecure-temp-file apps/installer/scripts/install.sh:370
MEDIUM insecure-temp-file apps/installer/scripts/install.sh:393
MEDIUM insecure-temp-file apps/installer/scripts/install.sh:402
MEDIUM insecure-temp-file apps/installer/scripts/install.sh:438
MEDIUM js-unescaped-html-sink apps/web/src/app/blog/[slug]/page.tsx:48
MEDIUM js-unescaped-html-sink apps/web/src/app/blog/[slug]/page.tsx:73
MEDIUM js-unescaped-html-sink apps/web/src/app/c/[handle]/page.tsx:191
MEDIUM js-unescaped-html-sink apps/web/src/app/l/[joinCode]/page.tsx:165
MEDIUM js-unescaped-html-sink apps/web/src/app/layout.tsx:128
MEDIUM js-unescaped-html-sink apps/web/src/app/live/page.tsx:145
MEDIUM js-unescaped-html-sink apps/web/src/app/page.tsx:122
MEDIUM js-unescaped-html-sink apps/web/src/app/pricing/page.tsx:284
MEDIUM js-open-redirect apps/web/src/app/pricing/UpgradeButton.tsx:50
MEDIUM js-unescaped-html-sink apps/web/src/app/u/[username]/page.tsx:282
MEDIUM manifest-install-lifecycle-script package.json:54
MEDIUM sql-template-interpolation packages/ai-core/src/prompts.ts:36
MEDIUM insecure-temp-file packages/remote-input/src/backends/waylandYdotool.test.ts:81
MEDIUM insecure-temp-file packages/remote-input/src/backends/waylandYdotool.test.ts:96
MEDIUM insecure-temp-file packages/remote-input/src/backends/waylandYdotool.test.ts:112
MEDIUM insecure-temp-file packages/remote-input/src/backends/waylandYdotool.test.ts:133
MEDIUM insecure-temp-file packages/remote-input/src/backends/waylandYdotool.test.ts:160
MEDIUM insecure-temp-file packages/remote-input/src/backends/waylandYdotool.test.ts:189
MEDIUM insecure-temp-file packages/remote-input/src/backends/waylandYdotool.test.ts:214
MEDIUM insecure-temp-file packages/remote-input/src/backends/waylandYdotool.test.ts:237
MEDIUM insecure-temp-file packages/remote-input/src/backends/waylandYdotool.test.ts:273
MEDIUM insecure-temp-file packages/remote-input/src/backends/waylandYdotool.test.ts:298
MEDIUM insecure-temp-file packages/remote-input/src/backends/waylandYdotool.test.ts:324
MEDIUM insecure-temp-file packages/remote-input/src/backends/waylandYdotool.test.ts:346
MEDIUM insecure-temp-file packages/remote-input/src/backends/waylandYdotool.test.ts:383
MEDIUM js-shell-exec-interpolation scripts/lib/package-managers/apt.ts:154
MEDIUM js-shell-exec-interpolation scripts/lib/package-managers/apt.ts:160

…and 120 more. Full results in the Security tab.

Snippets are redacted; ThreatCrush never prints matched credential material.

@mrprecioustech
mrprecioustech merged commit 73016af into master Aug 11, 2026
13 checks passed
@mrprecioustech
mrprecioustech deleted the fix/pointer-capture-and-input-latency branch August 11, 2026 09:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants