Skip to content

Commit 40658ae

Browse files
committed
feat(wayland): turn on KDE pointer restore by itself
Getting working pointer restore on KDE required knowing to set an environment variable, which no user would discover. Doing it in the installer would only cover launches from the desktop menu, not a terminal, and would bury the policy where nobody would find it again. Instead the helper enables itself when the session is KDE on Wayland — the only environment it targets — so it works however the app is launched, for every KDE user rather than whoever read the docs. PAIRUX_WAYLAND_CURSOR_RESTORE now means the opposite: 0 forces it off if a compositor misbehaves, 1 forces it on for a KDE session that does not advertise itself in XDG_CURRENT_DESKTOP. Safe to default on now that the rails are in place: the report rate is capped at 10/s regardless of pointer speed, the script exists only while a guest holds control, it self-disables after five consecutive DBus failures, and if it fails to report at all the viability probe drops two-cursor mode so clicks keep landing. A probe on KWin 6.6.5 confirmed cursorPosChanged is exposed to scripting. Ships as @profullstack/remote-input 0.2.4.
1 parent 34774a4 commit 40658ae

5 files changed

Lines changed: 85 additions & 13 deletions

File tree

docs/LINUX-SETUP.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,8 +151,11 @@ Wayland support requires PipeWire for screen capture. Input injection works via
151151

152152
### Pointer restoration on KDE (KWin)
153153

154-
Automatic — PairUX claims a DBus name and loads a small KWin script that reports
155-
`workspace.cursorPos`, then unloads it on quit. The only requirement is `gdbus`:
154+
Automatic on a KDE Wayland session — PairUX claims a DBus name and loads a small
155+
KWin script that reports `workspace.cursorPos`, then unloads it on quit. The
156+
report rate is capped, the script exists only while a guest holds control, and it
157+
self-disables after repeated failures. Set `PAIRUX_WAYLAND_CURSOR_RESTORE=0` to
158+
turn it off. The only requirement is `gdbus`:
156159

157160
```bash
158161
sudo apt install libglib2.0-bin # usually already present

packages/remote-input/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,11 @@ method, then installs and loads a small script that pushes `workspace.cursorPos`
105105
to it — distance-throttled, since the signal fires on every motion event. This
106106
happens automatically; the user installs nothing.
107107

108+
Enabled automatically on a KDE session running Wayland — the only environment
109+
it targets. `PAIRUX_WAYLAND_CURSOR_RESTORE=0` forces it off if a compositor
110+
misbehaves; `=1` forces it on for a KDE session that does not advertise itself
111+
in `XDG_CURRENT_DESKTOP`.
112+
108113
Requires `gdbus` (`libglib2.0-bin`, present on essentially every desktop).
109114
Readings older than two seconds are discarded rather than used, so a
110115
half-working helper can never fling the pointer somewhere its owner never left

packages/remote-input/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@profullstack/remote-input",
3-
"version": "0.2.3",
3+
"version": "0.2.4",
44
"description": "Cross-platform OS input injection for remote control (macOS, Windows, Linux X11 and Wayland)",
55
"license": "MIT",
66
"type": "module",

packages/remote-input/src/wayland/kwinCursorProvider.test.ts

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1-
import { describe, it, expect } from 'vitest';
2-
import { KWinCursorProvider, buildKWinScript } from './kwinCursorProvider.js';
1+
import { describe, it, expect, afterEach } from 'vitest';
2+
import {
3+
KWinCursorProvider,
4+
buildKWinScript,
5+
isKWinCursorRestoreEnabled,
6+
} from './kwinCursorProvider.js';
37

48
const silent = { log: () => {}, warn: () => {} };
59

@@ -48,8 +52,8 @@ describe('KWinCursorProvider', () => {
4852
// Restoring the pointer is a comfort; the only way to do it puts our code in
4953
// the compositor's input path, where a mistake costs the user their desktop.
5054
// So it stays off until explicitly asked for.
51-
it('does nothing unless explicitly enabled', async () => {
52-
delete process.env.PAIRUX_WAYLAND_CURSOR_RESTORE;
55+
it('does nothing where the helper does not apply', async () => {
56+
process.env.PAIRUX_WAYLAND_CURSOR_RESTORE = '0';
5357
const provider = new KWinCursorProvider({ logger: silent });
5458
await expect(provider.start()).resolves.toBe(false);
5559
expect(provider.isAvailable).toBe(false);
@@ -76,3 +80,51 @@ describe('KWinCursorProvider', () => {
7680
expect(provider.getPosition()).toBeNull();
7781
});
7882
});
83+
84+
describe('isKWinCursorRestoreEnabled', () => {
85+
const saved = { ...process.env };
86+
87+
afterEach(() => {
88+
process.env = { ...saved };
89+
});
90+
91+
function env(vars: Record<string, string | undefined>): void {
92+
delete process.env.PAIRUX_WAYLAND_CURSOR_RESTORE;
93+
delete process.env.XDG_SESSION_TYPE;
94+
delete process.env.WAYLAND_DISPLAY;
95+
delete process.env.XDG_CURRENT_DESKTOP;
96+
for (const [k, v] of Object.entries(vars)) {
97+
if (v !== undefined) process.env[k] = v;
98+
}
99+
}
100+
101+
// The helper only targets KDE on Wayland, and a user should not have to
102+
// discover an env var to get working pointer restore there.
103+
it('is on for a KDE Wayland session', () => {
104+
env({ XDG_SESSION_TYPE: 'wayland', XDG_CURRENT_DESKTOP: 'KDE' });
105+
expect(isKWinCursorRestoreEnabled()).toBe(true);
106+
});
107+
108+
it('is off where the helper does not apply', () => {
109+
env({ XDG_SESSION_TYPE: 'x11', XDG_CURRENT_DESKTOP: 'KDE' });
110+
expect(isKWinCursorRestoreEnabled()).toBe(false);
111+
112+
env({ XDG_SESSION_TYPE: 'wayland', XDG_CURRENT_DESKTOP: 'GNOME' });
113+
expect(isKWinCursorRestoreEnabled()).toBe(false);
114+
});
115+
116+
// An escape hatch matters: this hooks the compositor's input path.
117+
it('can be forced off', () => {
118+
env({
119+
XDG_SESSION_TYPE: 'wayland',
120+
XDG_CURRENT_DESKTOP: 'KDE',
121+
PAIRUX_WAYLAND_CURSOR_RESTORE: '0',
122+
});
123+
expect(isKWinCursorRestoreEnabled()).toBe(false);
124+
});
125+
126+
it('can be forced on for a session that does not advertise itself', () => {
127+
env({ PAIRUX_WAYLAND_CURSOR_RESTORE: '1' });
128+
expect(isKWinCursorRestoreEnabled()).toBe(true);
129+
});
130+
});

packages/remote-input/src/wayland/kwinCursorProvider.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,15 +40,27 @@ const POSITION_MAX_AGE_MS = 2000;
4040
const REPORT_INTERVAL_MS = 100;
4141

4242
/**
43-
* Off unless explicitly enabled.
43+
* On automatically where it applies: a KDE session on Wayland.
4444
*
45-
* Restoring the pointer on Wayland is a comfort, not a requirement, and the
46-
* only way to do it puts our code in the compositor's input path — where a
47-
* mistake costs the user their whole desktop. Opt in with
48-
* PAIRUX_WAYLAND_CURSOR_RESTORE=1.
45+
* This is the only environment the helper targets, and enabling it by hand is
46+
* not something a user should have to discover. It puts a hook in the
47+
* compositor's input path, so the rails around it matter more than the switch:
48+
* the report rate is capped, it exists only while a guest holds control, and it
49+
* gives up after repeated failures.
50+
*
51+
* PAIRUX_WAYLAND_CURSOR_RESTORE=0 forces it off (if a compositor misbehaves),
52+
* =1 forces it on (e.g. a KDE session that does not advertise itself).
4953
*/
5054
export function isKWinCursorRestoreEnabled(): boolean {
51-
return process.env.PAIRUX_WAYLAND_CURSOR_RESTORE === '1';
55+
const override = process.env.PAIRUX_WAYLAND_CURSOR_RESTORE;
56+
if (override === '0') return false;
57+
if (override === '1') return true;
58+
59+
const wayland =
60+
process.env.XDG_SESSION_TYPE === 'wayland' || process.env.WAYLAND_DISPLAY !== undefined;
61+
const kde = (process.env.XDG_CURRENT_DESKTOP ?? '').toUpperCase().includes('KDE');
62+
63+
return wayland && kde;
5264
}
5365

5466
/**

0 commit comments

Comments
 (0)