Files
minesweeper/sidecar/input.ts
T
pastilhasandClaude Opus 5 a51c75cc6c a control for when the input state goes wrong
`POST /reset-input` and a "clicks acting wrong?" button in the panel's status
line. It releases every modifier and mouse button the display thinks is held.

Named for the SYMPTOM rather than the mechanism, because nobody experiencing
this knows a modifier is stuck — they know their clicks stopped doing the right
thing.

── both sides, and that ordering matters ──

Releasing on the server alone leaves the panel's own button mask stale and the
next mouse move re-asserts it, so the fix would appear not to work. Local state
is cleared first, a pointer event with an empty mask says so, and only then does
the server let go.

── a fixed list, not a query ──

`xinput --query-state` would report exactly what is held, which is better. It is
also a seventh host binary to declare and to require, for a repair. Releasing a
key that was not pressed is a no-op, so the fixed list costs nothing. Modifiers
and buttons only: a stuck letter types, which is obvious and harmless; a stuck
modifier silently changes what every click means.

── not guarded like the others ──

Every other command refuses while a dialog is open, because its clicks would
fall through onto the board. This one clicks nothing, and refusing a repair in
the state that most needs it would be the wrong way round.

Verified by stranding `Shift_L` on the display with `xdotool keydown`, hitting
the route, and watching `xinput --query-state` go from one key down to none.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: de3ed4e6-f88a-429c-94e6-a760336ba83b
2026-08-24 17:10:14 +00:00

54 lines
2.2 KiB
TypeScript

import { run } from './run';
/**
* Let go of everything the display thinks is held.
*
* ── Why this needs to exist at all ──
*
* Synthesised input is state, and this side owns it. A press without a matching release stays pressed on
* the X server forever, and there is nothing to notice: the panel looks connected, the picture updates,
* the game responds. It just responds WRONGLY. A stranded `Shift_L` turned every left click into a
* shift-click, which Minesweeper reads as a right click, so the board flagged instead of opening.
*
* The panel now releases what it holds on blur and on unmount, and no longer sends modifiers at all — so
* this should never be needed. "Should never be needed" is exactly the class of thing that wants a button,
* because when it IS needed the symptom names nothing and the only other cure is restarting the session
* and losing the game in progress.
*
* ── Why a fixed list rather than asking what is down ──
*
* `xinput --query-state` would report exactly what is held, which is better. It is also a seventh host
* binary this plugin would have to declare and a machine would have to have, for a repair. Releasing a key
* that was not pressed is a no-op, so the fixed list costs nothing and needs nothing.
*
* Modifiers and buttons only, deliberately. They are what DISTORTS input — a stuck letter types, which is
* obvious and harmless; a stuck modifier silently changes what every click means.
*/
const MODIFIERS = [
'Shift_L',
'Shift_R',
'Control_L',
'Control_R',
'Alt_L',
'Alt_R',
'Super_L',
'Super_R',
'Meta_L',
'Meta_R',
'ISO_Level3_Shift',
];
const BUTTONS = ['1', '2', '3', '4', '5'];
export type ResetResult = { ok: boolean; released: { modifiers: number; buttons: number } };
export async function resetInput(display: string): Promise<ResetResult> {
const keys = await run(['xdotool', 'keyup', ...MODIFIERS], { DISPLAY: display }, 5_000);
let buttons = 0;
for (const button of BUTTONS) {
const up = await run(['xdotool', 'mouseup', button], { DISPLAY: display }, 5_000);
if (up.ok) buttons++;
}
return { ok: keys.ok, released: { modifiers: keys.ok ? MODIFIERS.length : 0, buttons } };
}