Skip to content

fix(control): cross-platform clicks, keyboard shortcuts, and scroll direction - #47

Merged
mrprecioustech merged 4 commits into
masterfrom
fix/cross-platform-input
Aug 10, 2026
Merged

fix(control): cross-platform clicks, keyboard shortcuts, and scroll direction#47
mrprecioustech merged 4 commits into
masterfrom
fix/cross-platform-input

Conversation

@mrprecioustech

Copy link
Copy Markdown
Contributor

The merge of #46 went in at the wrong head — these four commits were pushed
after the PR was created and didn't make it into master.

What's here

Remote clicks mapped with the host's screen, not the stream's resolution.
The renderer was feeding the capture track's pixel dimensions as the injection
screen size. Those are in different units: on a Retina Mac, nut-js positions
in logical points (1440x900) while the track reports physical pixels
(2880x1800), so normalized 0.5 landed at the bottom-right corner and clicking
did nothing at all. Wrong on every platform, just less visibly — the quality
setting calls applyConstraints({ width: { ideal: 1920 } }), so the track
reports the encoded resolution and every click landed at 75% of its offset.
The backend already reads the host's own geometry from the OS in that API's
units, so the renderer no longer sends a screen size.

Keyboard shortcuts survive crossing operating systems. Modifiers went
over the wire exactly as the viewer's OS reported them — a Mac viewer's Cmd+C
arrived at a Linux host as Super+C (copies nothing), and a Linux viewer's
Ctrl+C arrived at a macOS host as Control+C (also copies nothing). Copy,
paste, save, undo, quit — every shortcut broke in both directions. Viewers
now send accel for "my platform's shortcut modifier", host maps to its own.

Scroll direction corrected. Both backends treated positive deltaY as
scroll up; DOM convention is positive = scroll down. Every remote scroll went
the wrong way on every platform.

Validation: lint, typecheck, 1447 tests, 34 script tests, format, build — all green.

The host told the injection backend that its screen was however many pixels the
capture stream happened to be encoding. Those are different numbers in
different units, so remote clicks landed nowhere near where the guest aimed.

Worst on a Retina Mac. nut-js positions the pointer in logical points (1440x900
on a 2880x1800 panel) while the track reports physical pixels, so normalized
0.5 became (1440, 900) — the bottom-right corner — and everything past halfway
mapped clean off the display. Clicking did nothing at all.

It was wrong everywhere else too, just less visibly: the quality setting calls
applyConstraints({ width: { ideal: 1920 } }), so the track reports the *encoded*
resolution. A 2560x1440 Linux host was treated as 1920x1080, putting every
click at 75% of its intended offset and getting worse toward the bottom right.

Normalized coordinates need nothing but the host's own screen geometry, and
each backend already reads that from its own OS API in that API's units —
nut-js from screen.width(), the Wayland backend from the compositor. So the
renderer now sends no screen size at all, and the test that asserted it did is
inverted to keep it that way.
Modifiers went over the wire exactly as the viewer's OS reported them, which
does not travel. "The shortcut key" is Cmd on macOS and Ctrl everywhere else,
so both directions of a mixed session were broken:

- a Mac viewer's Cmd+C arrived at a Linux host as Super+C, which copies nothing
- a Linux or Windows viewer's Ctrl+C arrived at a macOS host as Control+C,
  which also copies nothing

Copy, paste, save, undo, quit — every shortcut, in both directions.

Viewers now send `accel` for "my platform's shortcut modifier was held", and
the host maps that to whichever modifier means shortcut locally. The literal
`ctrl` and `meta` still travel for the cases that really do mean Control
(macOS Control+click) or Super (Linux window manager bindings), so
Control+Cmd+F still arrives intact on a Mac host.

The field is optional: an older viewer that omits it keeps today's literal
pass-through rather than losing modifiers altogether.

Split across packages because remote-input is deliberately standalone and
Node-only: `modifiersFromDomEvent` (viewer, DOM-facing) lives in shared-types,
`resolveModifiers` (host) in remote-input. Both are pure and tested, including
the two cases above.

Platform detection is its own tested unit per app rather than inline in the
hook — the desktop reads it from preload, the web app from the user agent
because a browser has nothing better.
Both backends treated a positive deltaY as a scroll up. In the DOM, positive
deltaY is a scroll *down*, so every remote scroll went the wrong way — on every
platform, since nut-js and ydotool had the same inversion. Horizontal was
already correct.

Two tests asserted the inverted behaviour and have been corrected rather than
deleted; the convention is now written down on MouseScrollEvent.deltaY, which is
where the ambiguity that caused this belonged in the first place.
const backend = new WaylandYdotoolInputBackend(run, {
hasBinary: true,
hasSocket: true,
socketPath: '/tmp/.ydotool_socket',
@github-actions

Copy link
Copy Markdown

ThreatCrush Security Scan

160 finding(s)

HIGH/CRITICAL: 5 | MEDIUM: 49 | LOW: 106

Severity Rule Location
HIGH secret-generic-credential apps/livekit/fly.toml:12
HIGH manifest-typosquat apps/mobile/package.json:43
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 apps/desktop/src/main/streaming/index.test.ts:64
MEDIUM sql-template-interpolation apps/desktop/src/renderer/components/capture/CameraBubble.tsx:129
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:132
MEDIUM insecure-temp-file packages/remote-input/src/backends/waylandYdotool.test.ts:152
MEDIUM insecure-temp-file packages/remote-input/src/backends/waylandYdotool.test.ts:178
MEDIUM insecure-temp-file packages/remote-input/src/backends/waylandYdotool.test.ts:200
MEDIUM sql-template-interpolation scripts/lib/package-managers/apt.ts:55
MEDIUM js-shell-exec-interpolation scripts/lib/package-managers/apt.ts:154
MEDIUM js-shell-exec-interpolation scripts/lib/package-managers/apt.ts:160
MEDIUM js-shell-exec-interpolation scripts/lib/package-managers/apt.ts:208
MEDIUM js-shell-exec-interpolation scripts/lib/package-managers/apt.ts:313
MEDIUM js-shell-exec-interpolation scripts/lib/package-managers/aur.ts:310
MEDIUM sql-template-interpolation scripts/lib/package-managers/aur.ts:341
MEDIUM js-shell-exec-interpolation scripts/lib/package-managers/aur.ts:341
MEDIUM js-shell-exec-interpolation scripts/lib/package-managers/chocolatey.ts:264
MEDIUM js-shell-exec-interpolation scripts/lib/package-managers/chocolatey.ts:288
MEDIUM sql-template-interpolation scripts/lib/package-managers/gentoo.ts:274
MEDIUM sql-template-interpolation scripts/lib/package-managers/homebrew.ts:152
MEDIUM sql-template-interpolation scripts/lib/package-managers/nix.ts:225
MEDIUM sql-template-interpolation scripts/lib/package-managers/rpm.ts:55
MEDIUM js-shell-exec-interpolation scripts/lib/package-managers/rpm.ts:201
MEDIUM js-shell-exec-interpolation scripts/lib/package-managers/rpm.ts:261
MEDIUM sql-template-interpolation scripts/lib/package-managers/scoop.ts:145
MEDIUM js-shell-exec-interpolation scripts/release.mjs:145
MEDIUM js-shell-exec-interpolation scripts/release.mjs:146
MEDIUM js-shell-exec-interpolation scripts/release.mjs:147

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

Snippets are redacted; ThreatCrush never prints matched credential material.

@mrprecioustech
mrprecioustech merged commit d8750ff into master Aug 10, 2026
13 checks passed
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