Architecture
OpenMouse is a mouse-only control panel at its core: the shared device
model, MouseStatus, has no concept of a keyboard or an analog key.
Non-mouse devices (like keyboards) are supported by shaping their status to
look like a read-only mouse — see Non-mouse devices
below.
Repos
The workspace has three sibling repos:
openmouse— the Preact/Vite web control panel. This is the app.mouse-protocol(@openmouse/protocol) — device codecs and WebHID drivers. Published as a git dependency;openmouseinstalls it from GitHub, not from the local checkout (see Version skew below).OpenMouse-Bridge— a Rust local companion for devices WebHID can't reach directly.
The device model: MouseStatus
The shared status type lives at mouse-protocol's src/drivers/mouse-types.ts.
It covers DPI, polling rate, lift-off distance, scroll, buttons, and battery.
Every driver class implements the same client contract, which the app calls unconditionally on every connected device:
interface SupportedClient {
readonly device: HIDDevice;
open(): Promise<void>;
close(): Promise<void>;
readStatus(): Promise<MouseStatus>;
getDpiOptions(): DpiOption[]; // or Promise<DpiOption[]>
// ...device-specific setters
}
getDpiOptions() in particular is called on connect for every client
(openmouse/src/device/controller.ts) — a read-only or non-mouse device must
still implement it and can just return [].
brand is a fixed string union in mouse-types.ts; adding a new
manufacturer means extending that union.
Non-mouse devices
A device that isn't a mouse (e.g. a keyboard) is exposed by returning a
MouseStatus with ui.settingsReady: false, which hides the settings grid
in the UI, and by omitting setters.
Plugging in a driver
A new driver needs entries in both repos:
In mouse-protocol
- Add the driver class under
src/drivers/<vendor>/. - Register it in
src/drivers/registry.ts: import the class, add it to theSupportedClientunion, and add it toDEVICE_DRIVERS.createSupportedClientmatches withDEVICE_DRIVERS.find(...)— first match wins, not highest score — so array order matters if filters could overlap. - Add a discovery filter in
src/drivers/vendors.ts(SUPPORTED_HID_FILTERS), which the app'srequestDevice()call uses to populate the browser's device picker. - If the driver is exposed as a subpath (e.g.
@openmouse/protocol/wooting), add the entry to bothpackage.jsonexportsandtsconfig.jsonpaths— missing either one breaks the build in a way that's easy to miss locally. registry.test.tshas a probe matrix that will fail the build unless the new driver's HID usage page is added toUSAGE_PAGESand its vendor ID is added toVENDOR_ID.
In openmouse
- Connect routing (the sharp edge):
openmouse/src/device/controller.tstreats the Pulsar client as the fallback — any connected client that isn't in aDEDICATEDgroup gets routed throughpulsarClient(), which callsclient.describeCollections(). A new driver must be added to aDEDICATEDgroup — the simplest way is appending it toNEEDS_OPEN(which is spread intoDEDICATEDand also pre-opens the device on connect) — or the app crashes on connect withclient.describeCollections is not a function. - Optionally, map the device to a product photo in
ui/device-images.ts.
So: driver + registry in mouse-protocol, plus NEEDS_OPEN /
import in openmouse. Forgetting the second half is the most common way a
new driver connects but then immediately crashes.
Version skew
openmouse depends on @openmouse/protocol via a git URL, and that
remote copy can be ahead of whatever you have checked out locally in
./mouse-protocol. Concretely, the installed lib may already contain
drivers or fields that don't exist in your local source tree.
Practical implication: you generally can't just point openmouse at
file:./mouse-protocol and expect it to compile — the app may reference
exports that only exist upstream. Develop and verify your driver against
mouse-protocol's own test suite (npm run check), then push it and update
the mouse-protocol dependency in openmouse to pick it up. If you need to
smoke-test end-to-end before pushing, overlay your built dist/ into
openmouse/node_modules/@openmouse/protocol — but treat that as throwaway,
since any npm install wipes it.
Sandboxed installs
@openmouse/protocol is a git dependency with a prepare build step. In
restricted/sandboxed environments where install scripts are blocked, npm install can leave a stale or inconsistent dist/ (e.g. a subpath module
landing in the wrong place while package.json still points elsewhere). A
normal, unsandboxed npm install runs prepare and won't hit this — it's
mainly a CI/agent-sandbox gotcha, not something end users or most
contributors will see.