Skip to content

feat: reach a mouse through OpenMouse Bridge where WebHID is missing - #127

Open
ydw1904 wants to merge 1 commit into
OpenMouse-Project:devfrom
ydw1904:feat/bridge-hid
Open

feat: reach a mouse through OpenMouse Bridge where WebHID is missing#127
ydw1904 wants to merge 1 commit into
OpenMouse-Project:devfrom
ydw1904:feat/bridge-hid

Conversation

@ydw1904

@ydw1904 ydw1904 commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Why

Firefox has no WebHID and Mozilla's standards position on it is negative, so the control panel is a dead end there — as it is in any browser without the API. Today browser-support.ts can only tell those users to switch browsers.

OpenMouse Bridge already runs on the user's machine. With OpenMouse-Project/OpenMouse-Bridge#3 it exposes native HID over its loopback socket, so this implements navigator.hid against it and installs the shim when the browser has none of its own.

Depends on OpenMouse-Project/OpenMouse-Bridge#3. Without a Bridge build carrying /v1/hid, installBridgeHid() finds nothing on loopback and everything behaves exactly as it does today.

What changes

src/bridge-hid.ts implements the WebHID surface the @openmouse/protocol drivers are written against, backed by the socket. control.tsx awaits installBridgeHid() before deciding the unsupported notice, and browser-support.ts now names Bridge in the non-Chromium copy.

Nothing else changes. The driver registry's auto-detection, every driver, and every card work unmodified and never learn they are not in Chrome — Bridge parses the report descriptor, so device.collections is real and isSupported() behaves as it does over WebHID. That is what lets this reuse the app wholesale instead of maintaining a parallel brand table the way Bridge's native-hid/ and the Desktop app have to.

installBridgeHid() also accepts force, for the case where WebHID exists but withholds a collection a device needs.

Where Bridge cannot match WebHID exactly

Both deliberate, both documented in the module:

  • getDevices() returns every supported mouse, not only ones granted through a picker. Bridge has no per-site grant and does not need one: the user installed a companion application for this site specifically, and Bridge only accepts sockets from its own origin allowlist. The practical effect is that a supported mouse is simply present on load, with nothing to click through.
  • Hot-plug comes from re-enumerating every two seconds, not a push. Enumeration is a few milliseconds and this keeps connect/disconnect handling in one place.

Verification

npm run build and npm test pass. Four new tests drive the shim through a fake transport: a full device round trip (list, open, sendReport, an input report arriving as a DataView, receiveFeatureReport), rejected requests surfacing Bridge's reason, re-enumeration preserving device identity while reporting hot-plug both ways, and a dropped socket closing every device and rejecting what was in flight.

Validated in Firefox against a Keychron M6 over Bridge: the device auto-detects, readStatus() reports 800 DPI / 500 Hz / 100% wired, and a DPI change flashes to the mouse and reads back.

CI note: the JS bundle-size check fails on this branch, and it already fails on dev at 659,822 / 650,000 bytes before this change. The shim adds ~6.2 kB. Happy to fold a budget bump in here or leave it to a separate fix — maintainer's call.

Out of scope

  • Safari. It does not treat loopback as a potentially trustworthy origin and blocks the socket as mixed content. Reaching Safari means Bridge serving the app itself over loopback, which would also cover offline use and is a separate change.
  • Making Bridge the preferred transport in Chrome. The force option exists, but nothing calls it; Chrome keeps using its own WebHID.

🤖 Generated with Claude Code

Firefox has no WebHID and Mozilla's position on it is negative, so the
control panel is currently a dead end there — as it is in any browser
without the API. Bridge already runs on the user's machine and now exposes
native HID over its loopback socket, so this implements `navigator.hid`
against it and installs the shim when the browser has none of its own.

Nothing else changes. The shim satisfies the same WebHID surface
`@openmouse/protocol`'s drivers are written against, so the registry's
auto-detection, every driver, and every card in the panel work unmodified
and never learn they are not in Chrome. `installBridgeHid()` also accepts
`force`, for the case where WebHID exists but withholds a collection a
device needs.

Two places where Bridge cannot match WebHID exactly, both deliberate:

- `getDevices()` returns every supported mouse rather than only ones the
  user granted through a picker. Bridge has no per-site grant and does not
  need one — the user installed a companion app for this site, and Bridge
  only accepts sockets from its own origin allowlist. A supported mouse is
  simply present on load, with nothing to click through.
- Hot-plug comes from re-enumerating every two seconds rather than a push,
  which keeps connect and disconnect handling in one place.

Safari is still out: it does not treat loopback as a trustworthy origin and
blocks the socket as mixed content. Reaching Safari means having Bridge
serve the app itself over loopback, which is a separate change.

Deciding whether the browser is supported now waits on the loopback probe,
which either answers in milliseconds or not at all.
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.

1 participant