Skip to content

Game Mode: switch polling rate automatically when a game launches - #42

Closed
xBambooz wants to merge 9 commits into
OpenMouse-Project:devfrom
xBambooz:feat/game-mode-companion
Closed

xBambooz wants to merge 9 commits into
OpenMouse-Project:devfrom
xBambooz:feat/game-mode-companion

Conversation

@xBambooz

@xBambooz xBambooz commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Adds Game Mode: a small Windows tray app applies your idle and gaming polling rates as games start and stop, with the browser closed.

The page captures the HID bytes your mouse's own driver already sends for setPollingRate() and hands them over once, so no vendor protocol is reimplemented natively and adding a vendor stays a one-line change here.

  • Adds a Background Service sidebar page: install steps, game detection, manual game list, notifications, updates, and both rate sliders
  • Turns Game Mode on automatically for a supported mouse, clamped to rates that mouse advertises
  • Supports Razer, Razer Viper V4 Pro, Teevolution, Lamzu, WLMouse, ATK, VGN F2, Pulsar, Pulsar Pro, and Finalmouse
  • Guides the browser's local-network permission, and explains a refused connection instead of showing install steps
  • Hides the whole page off Windows, and nothing here needs the app installed to work
  • Adds tests for rate clamping, platform detection, permission state, and collection fingerprinting

Companion app: https://github.com/xBambooz/OpenMouseCompanion (MIT). Happy to hand the repo over or repoint the download at a release you publish, so you own the binary users install.

Why other vendors are excluded is documented in support.ts: Endgame Gear 8K and Orbital rewrite a whole settings blob, so replaying it would revert DPI and lift-off; OP1we spans two HID collections; Logitech resolves its feature index at runtime.

Three things for you to decide: the bundle budgets are raised (CSS 45 to 58 kB, JS 310 to 340 kB) with reasons in the comment, npm audit fix clears one high advisory in a dev dependency, and the installer is currently unsigned so Windows shows a SmartScreen warning.

Lets a supported mouse switch to a higher polling rate the instant a game
launches and back to an idle rate when it closes, even with the browser tab
closed. The browser can't see what process is in the foreground, so the
actual switch happens in a new companion app (OpenMouseCompanion) that pairs
with this page over a local WebSocket.

No vendor protocol logic is duplicated in the companion app: the site
records the exact HID transaction its own driver already performs for
setPollingRate() (src/game-mode/hid-recorder.ts) and hands that recipe over
once, so the companion just replays real bytes it never had to understand.

v1 covers vendors whose rate change is a self-contained HID transaction
(Razer, Teevolution, Lamzu, WLMouse, ATK, VGN, Pulsar) — see
src/game-mode/support.ts. Orbital, Endgame Gear, and Logitech read-modify-write
a whole settings blob or need live HID++ negotiation, so they're excluded
for now rather than risk reverting unrelated settings on every rate flip.
Drops the per-device Game Mode card in favor of a single home for the whole
feature: connection status, install steps, and now idle/gaming rate pickers
all live on the Background Service page reachable from the sidebar.

Idle defaults to 1000 Hz, gaming to 4000 Hz. Both use the same rate-slider
widget as the main Polling Rate card, extracted into src/ui/rate-slider.ts
so it's shared code rather than a second implementation.

Also drops the "NEW" sidebar badge.
Connecting to the companion needs the browser's loopback permission, and a
blocked page previously looked identical to one where the service was simply
not running.

Query loopback-network (falling back to Chromium's local-network-access
alias), keep the PermissionStatus so a change re-renders, and show at most one
of the two onboarding graphics: the Allow instructions while the decision is
pending, the site-settings recovery steps once it has been denied. Neither is
shown once connected.
Finalmouse was excluded from Game Mode without cause. Its setPollingRate
sends one output report whose payload is a pure function of the requested
rate ([len, 0x80|17, 2, rate_lo, rate_hi]), which is exactly the property the
replay design needs, so it now enrolls like the other nine vendors.

Rewrite the doc comment around the single rule that actually decides
eligibility, and record what each remaining vendor would need, so the list
does not read as arbitrary:

- Endgame Gear 8K and Orbital read the whole settings blob, patch one field
  and write it back, so a captured payload carries a snapshot of DPI,
  lift-off and button mapping and replaying it would revert them. They need a
  read-modify-write step that records changed offsets instead of the buffer.
- Endgame Gear OP1we writes only the rate, but across two vendor collections,
  and RecipeStep cannot say which collection a step belongs to.
- Logitech resolves its HID++ feature index at runtime, so a recorded index
  is only valid for the firmware it was captured on.
Game Mode is the only reason the Background Service exists, so leaving it off
until the user finds and clicks a toggle made the default state the useless
one. A supported mouse plus a connected service now enrolls itself, once per
device key, and a device the user has explicitly switched off stays off.

Because enrollment can now happen without anyone choosing rates, clamp both
defaults to rates the mouse actually advertises. Extract that as a pure
nearestRate() next to the slider it serves, with tests: ties break downward so
a default never lands on the faster rate on its own, and an empty rate list
returns null rather than enrolling undefined as a polling rate.

Also distinguish the two cases the status line used to conflate. "Connect a
supported mouse" was shown both when no mouse was connected and when a
connected mouse simply is not supported yet, which reads as a bug when your
mouse is plainly connected.
The CSS budget covers the Background Service page: its own sidebar route, the
install and permission onboarding steps, and the Game Mode card with two rate
sliders. The JS budget covers the companion WebSocket client, the HID
transcript recorder and collection fingerprinting.

Both budgets are set just above the current size rather than rounded up
generously, so the next unnoticed increase still trips the check.

npm audit fix clears one high advisory in a transitive dev dependency
(nanoid). No runtime dependencies were added; the tree is still empty.
@xBambooz
xBambooz marked this pull request as draft August 8, 2026 01:51
A denied connection proves the Background Service is installed and running, so
the page showing "Download and install" and "Allow access to apps on your
device" told the user to fix things that were not broken. It also had no way
back: ws-client sets stopped = true on pairDenied and never reconnects, on
purpose, so that a denial cannot re-trigger prompts on a timer. Nothing in the
UI could undo that, leaving the page dead until a manual reload.

Replace the onboarding in that state with what actually happened, naming the
origin that was turned away, and add a Try again button that calls connect().

Rename the badge from BLOCKED to REFUSED. "Blocked" reads as something the user
did, or as a browser permission problem, when it is the service declining an
origin. The status line was also still telling the user to approve the service
in a tray popup that no longer exists.
The service is a Windows executable, but its sidebar entry was unconditional.
WebHID works on macOS and Linux and the rest of OpenMouse is fully usable
there, so the only broken thing on the page was an installer those users
cannot run.

Hide the sidebar entry and skip opening the WebSocket entirely when the
platform is not Windows. Detection prefers navigator.userAgentData, the one
source of the three that is neither frozen nor deprecated, and deliberately
trusts it over the userAgent string, which Chromium still freezes a Windows
token into on some platforms. An unknown navigator is treated as not Windows,
so the failure mode is a hidden page rather than a broken one.
@xBambooz
xBambooz marked this pull request as ready for review August 8, 2026 03:16
@snekxs snekxs closed this Aug 8, 2026
@snekxs

snekxs commented Aug 8, 2026

Copy link
Copy Markdown
Member

This is in the roadmap.

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