From 405f6ff56aae7b32baf226350e70681f3832e9a5 Mon Sep 17 00:00:00 2001
From: DEERSTUDIO101 <181780870+DEERSTUDIO101@users.noreply.github.com>
Date: Thu, 8 Oct 2026 22:58:51 +0200
Subject: [PATCH 1/2] fix(polling): keep unavailable rates unselected in the
control panel
---
src/app/cards/PerformanceCards.tsx | 3 ++-
src/app/ui.tsx | 13 +++----------
src/device/controller.ts | 5 +++--
src/preview-fixtures.ts | 30 ++++++++++++++++++++++++++++++
src/preview-modes.ts | 4 ++++
src/ui/polling-rate.test.ts | 21 +++++++++++++++++++++
src/ui/polling-rate.ts | 13 +++++++++++++
7 files changed, 76 insertions(+), 13 deletions(-)
create mode 100644 src/ui/polling-rate.test.ts
create mode 100644 src/ui/polling-rate.ts
diff --git a/src/app/cards/PerformanceCards.tsx b/src/app/cards/PerformanceCards.tsx
index f6163020..42a28eb3 100644
--- a/src/app/cards/PerformanceCards.tsx
+++ b/src/app/cards/PerformanceCards.tsx
@@ -7,6 +7,7 @@ import {
} from "@openmouse/protocol/drivers/logitech/onboard-profiles";
import * as control from "../../device/controller";
import { isNoirKsnakeStatus } from "../../device/noir.ts";
+import { pollingRateText } from "../../ui/polling-rate";
import { isNativeAttackSharkX11, RATE_STEPS_HZ } from "../../device/controller";
import type { ControlSnapshot, LiftOffLevel } from "../../device/types";
import { t, tp } from "../../i18n";
@@ -64,7 +65,7 @@ export function PollingCard({ snapshot }: { snapshot: ControlSnapshot }): ReactN
{note}
{!perProfile ? (
-
+
) : null}
diff --git a/src/app/ui.tsx b/src/app/ui.tsx
index 71ee026d..0a07dee6 100644
--- a/src/app/ui.tsx
+++ b/src/app/ui.tsx
@@ -2,6 +2,7 @@ import { useEffect, useId, useRef, useState, type ReactNode } from "react";
import { Check, ChevronDown, Minus, Plus } from "lucide-react";
import type { MouseStatus } from "@openmouse/protocol/drivers/mouse-types";
import { batteryFillWidth, batteryIconState, batteryLevel } from "../ui/battery-icon";
+import { selectedPollingStep } from "../ui/polling-rate";
import { t } from "../i18n";
import type { InterfaceLocale } from "../interface-preferences";
@@ -384,22 +385,14 @@ export function RateSlider({
bubble?: boolean;
}): ReactNode {
if (options.length === 0) return
{label ? (
{label}
-
+
) : null}
diff --git a/src/device/controller.ts b/src/device/controller.ts
index 1ae066c1..a7dca9b8 100644
--- a/src/device/controller.ts
+++ b/src/device/controller.ts
@@ -1,5 +1,6 @@
import { cachedBatterySamples, estimateBatteryTime, recordBatterySample, type BatteryMode } from "../battery-history";
import { applyBridgeNativeSettings } from "../bridge";
+import { pollingRateText } from "../ui/polling-rate";
import {
clientSupportScore,
createSupportedClient,
@@ -2323,10 +2324,10 @@ function applyStatusInner(deviceStatus: MouseStatus, statusKey?: string): void {
? st("ctl.connected")
: st("ctl.batteryPct", { n: deviceStatus.batteryPercent });
readStatus = status.ui?.valuesVerified
- ? [summary, `${deviceStatus.dpi.toLocaleString()} DPI`, `${deviceStatus.pollingRateHz.toLocaleString()} Hz`].join(" · ")
+ ? [summary, `${deviceStatus.dpi.toLocaleString()} DPI`, `${pollingRateText(deviceStatus.pollingRateHz)} Hz`].join(" · ")
: summary;
} else if (!hasPendingChanges()) {
- readStatus = st("ctl.currentLine", { dpi: deviceStatus.dpi.toLocaleString(), hz: deviceStatus.pollingRateHz.toLocaleString() });
+ readStatus = st("ctl.currentLine", { dpi: deviceStatus.dpi.toLocaleString(), hz: pollingRateText(deviceStatus.pollingRateHz) });
}
if (!customDpiEditing) customDpiText = `${status.dpi.toLocaleString()} DPI`;
diff --git a/src/preview-fixtures.ts b/src/preview-fixtures.ts
index 0a653a9b..f503f241 100644
--- a/src/preview-fixtures.ts
+++ b/src/preview-fixtures.ts
@@ -43,6 +43,34 @@ const PULSAR: MouseStatus = {
firmware: ["1.04.11"],
};
+const PULSAR_XS1_8K: MouseStatus = {
+ ...PULSAR,
+ name: "Pulsar 8K Dongle",
+ ui: {
+ family: "pulsar",
+ hideUnsupportedPollingRates: true,
+ pollingReadOnly: true,
+ pollingNote: "The XS-1 polling readout is not hardware-verified. Polling changes are unavailable; check the rate in Pulsar's software.",
+ hideSleepCard: true,
+ hideSignalCard: true,
+ forceShowBattery: true,
+ },
+ pollingRateHz: 8000,
+ supportedPollingRates: [125, 250, 500, 1000, 2000, 4000, 8000],
+ activeProfile: null,
+ dongleLedEnabled: undefined,
+ connectionDetail: "XS-1 feature-report interface",
+};
+
+const PULSAR_XS1_UNKNOWN: MouseStatus = {
+ ...PULSAR_XS1_8K,
+ pollingRateHz: 0,
+ ui: {
+ ...PULSAR_XS1_8K.ui,
+ pollingNote: "The XS-1 interface did not return a recognized polling rate. Polling changes are unavailable; check the rate in Pulsar's software.",
+ },
+};
+
const PULSAR_PRO: MouseStatus = {
...PULSAR,
name: "Pulsar X2 V2 Pro",
@@ -848,6 +876,8 @@ const RAWM_V4_GT: MouseStatus = {
export const PREVIEW_FIXTURES: Record = {
pulsar: { label: "Pulsar X2 V2", status: PULSAR },
"pulsar-pro": { label: "Pulsar X2 V2 Pro", status: PULSAR_PRO },
+ "pulsar-xs1-8k": { label: "Pulsar XS-1 8K dongle", status: PULSAR_XS1_8K },
+ "pulsar-xs1-unknown": { label: "Pulsar XS-1 unknown polling", status: PULSAR_XS1_UNKNOWN },
"egg-op1": { label: "Endgame Gear OP1 8K", status: EGG_OP1 },
"egg-we": { label: "Endgame Gear OP1we", status: EGG_WE },
"egg-xm2we": { label: "Endgame Gear XM2we", status: EGG_XM2WE },
diff --git a/src/preview-modes.ts b/src/preview-modes.ts
index a33c3c25..3c521b3b 100644
--- a/src/preview-modes.ts
+++ b/src/preview-modes.ts
@@ -10,6 +10,8 @@ export const PREVIEW_KEYS = [
"superstrike",
"pulsar",
"pulsar-pro",
+ "pulsar-xs1-8k",
+ "pulsar-xs1-unknown",
"egg-op1",
"egg-we",
"egg-xm2we",
@@ -55,6 +57,8 @@ export function parsePreviewMode(value: string | null): PreviewMode | null {
case "superstrike": return "superstrike";
case "pulsar": return "pulsar";
case "pulsar-pro": return "pulsar-pro";
+ case "pulsar-xs1-8k": return "pulsar-xs1-8k";
+ case "pulsar-xs1-unknown": return "pulsar-xs1-unknown";
case "egg-op1": return "egg-op1";
case "egg-we": return "egg-we";
case "egg-xm2we": return "egg-xm2we";
diff --git a/src/ui/polling-rate.test.ts b/src/ui/polling-rate.test.ts
new file mode 100644
index 00000000..4cd0dc53
--- /dev/null
+++ b/src/ui/polling-rate.test.ts
@@ -0,0 +1,21 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+import { pollingRateText, selectedPollingStep } from "./polling-rate.ts";
+
+const RATES = [125, 250, 500, 1000, 2000, 4000, 8000];
+
+test("unavailable driver rates display a dash and never select the 125 Hz button", () => {
+ for (const value of [0, -1, NaN, Infinity]) {
+ assert.equal(pollingRateText(value), "—");
+ assert.equal(selectedPollingStep(RATES, value), null);
+ }
+ assert.equal(selectedPollingStep(RATES, null), null);
+});
+
+test("8K polling can select its real step and known off-grid rates keep nearest-step behavior", () => {
+ assert.equal(selectedPollingStep(RATES, 8000), 6);
+ assert.equal(selectedPollingStep(RATES, 1000), 3);
+ assert.equal(selectedPollingStep(RATES, 750), 2);
+ assert.equal(selectedPollingStep([], 1000), null);
+ assert.notEqual(pollingRateText(8000), "—");
+});
diff --git a/src/ui/polling-rate.ts b/src/ui/polling-rate.ts
new file mode 100644
index 00000000..680f4681
--- /dev/null
+++ b/src/ui/polling-rate.ts
@@ -0,0 +1,13 @@
+/** Zero/non-finite rates represent a missing hardware read, not a setting. */
+export function pollingRateText(rate: number): string {
+ return Number.isFinite(rate) && rate > 0 ? rate.toLocaleString() : "—";
+}
+
+/** Preserve nearest-step selection for known rates; leave unknown ones blank. */
+export function selectedPollingStep(options: readonly number[], rate: number | null): number | null {
+ if (options.length === 0 || rate === null || !Number.isFinite(rate) || rate <= 0) return null;
+ const exact = options.indexOf(rate);
+ if (exact >= 0) return exact;
+ return options.reduce((best, candidate, index) =>
+ Math.abs(candidate - rate) < Math.abs(options[best]! - rate) ? index : best, 0);
+}
From e111fec1ac8cb2b8a7d43b283e21fcf3169dcf4b Mon Sep 17 00:00:00 2001
From: snekxs <26660858+snekxs@users.noreply.github.com>
Date: Thu, 8 Oct 2026 15:52:53 -0600
Subject: [PATCH 2/2] fix: hide per-charge readout when no polling rate is
selected
---
src/app/ui.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/app/ui.tsx b/src/app/ui.tsx
index 5238b892..10479cd6 100644
--- a/src/app/ui.tsx
+++ b/src/app/ui.tsx
@@ -416,7 +416,7 @@ export function RateSlider({
);
})}
- {hoursAt?.(options[selected] ?? 0) != null ? (
+ {selected !== null && hoursAt?.(options[selected] ?? 0) != null ? (
About {Math.round(hoursAt?.(options[selected] ?? 0) as number)} h per full charge at {options[selected]?.toLocaleString()} Hz