Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 36 additions & 1 deletion src/app/cards/AdvancedCards.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import { teevolutionSensorModeUi } from "@openmouse/protocol/teevolution";
import * as control from "../../device/controller";
import { PULSAR_SLEEP_OPTIONS } from "../../device/controller";
import { isPulsarProProtocol } from "../../device/traits";
import { selectableValues, sleepLabel, sleepParts, sleepTotalSeconds, KEYCHRON_SLEEP_MAX_HOURS, KEYCHRON_SLEEP_MAX_SECONDS, KEYCHRON_SLEEP_MIN_SECONDS } from "../../device/options";
import type { ControlSnapshot } from "../../device/types";
import { Collapsible, Segmented, SwitchRow } from "../ui";
Expand Down Expand Up @@ -264,6 +265,11 @@
? "Competitive mode"
: status.brand === "Teevolution" ? "Highest performance" : "Performance mode";

// WLMouse calls the same sensor setting High-speed mode in its own tool.
const hyperLabel = status.brand === "WLMouse" ? "High-speed mode" : "Hyper mode";
// Pulsar Pro shows the angle with the rest of its Pro-only settings.
const angleTuning = isPulsarProProtocol(status) ? null : status.angleTuning;

return (
<article id="processing-settings" className="setting-card">
<div className="setting-heading compact"><div><p>SENSOR</p><h2>Processing</h2></div></div>
Expand Down Expand Up @@ -322,11 +328,40 @@
/>
<SwitchRow
id="hyper-mode-toggle"
label="Hyper mode"
label={hyperLabel}
value={status.hyperMode}
hidden={status.hyperMode == null}
onChange={(next) => control.applyPulsarToggle("hyperMode", next)}
/>
<SwitchRow
id="turbo-mode-toggle"
label="Turbo mode"
value={status.turboMode}

Check failure on line 339 in src/app/cards/AdvancedCards.tsx

View workflow job for this annotation

GitHub Actions / check

Property 'turboMode' does not exist on type 'MouseStatus'.
hidden={status.turboMode == null}

Check failure on line 340 in src/app/cards/AdvancedCards.tsx

View workflow job for this annotation

GitHub Actions / check

Property 'turboMode' does not exist on type 'MouseStatus'.
disabled={status.hyperMode === false}
onChange={(next) => control.applyPulsarToggle("turboMode", next)}
/>
<SwitchRow
id="button-combination-toggle"
label="Button combinations"
value={status.buttonCombination}

Check failure on line 347 in src/app/cards/AdvancedCards.tsx

View workflow job for this annotation

GitHub Actions / check

Property 'buttonCombination' does not exist on type 'MouseStatus'.
hidden={status.buttonCombination == null}

Check failure on line 348 in src/app/cards/AdvancedCards.tsx

View workflow job for this annotation

GitHub Actions / check

Property 'buttonCombination' does not exist on type 'MouseStatus'.
onChange={(next) => control.applyPulsarToggle("buttonCombination", next)}
/>
{angleTuning != null ? (
<label className="field-label spaced">
Angle tune
<select
id="angle-tune-select"
value={angleTuning}
onChange={(event) => control.applyAngleTuning(Number(event.currentTarget.value))}
>
{Array.from({ length: 61 }, (_, index) => index - 30).map((angle) => (
<option key={angle} value={angle}>{angle}°</option>
))}
</select>
</label>
) : null}

{traits.teevolution && teevolutionProfile ? (
<label id="teevolution-performance-duration-row" className="field-label spaced">
Expand Down
3 changes: 3 additions & 0 deletions src/app/cards/availability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@
|| (status.rippleControl != null && ui?.hideRippleControl !== true)
|| (status.performanceMode != null && !traits.eggFamily && !traits.finalmouse)
|| status.hyperMode != null
|| status.turboMode != null

Check failure on line 85 in src/app/cards/availability.ts

View workflow job for this annotation

GitHub Actions / check

Property 'turboMode' does not exist on type 'MouseStatus'.
|| status.buttonCombination != null

Check failure on line 86 in src/app/cards/availability.ts

View workflow job for this annotation

GitHub Actions / check

Property 'buttonCombination' does not exist on type 'MouseStatus'.
|| status.angleTuning != null
|| status.sensorMode != null || status.performanceDuration != null
);

Expand Down
24 changes: 24 additions & 0 deletions src/device/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2965,6 +2965,8 @@
rippleControl: "ripple control",
performanceMode: teevolutionClient() ? "highest performance" : "performance mode",
hyperMode: "Hyper mode",
turboMode: "turbo mode",
buttonCombination: "button combinations",
} as const)[setting];
}

Expand All @@ -2974,6 +2976,8 @@
rippleControl: "setRippleControl",
performanceMode: "setPerformanceMode",
hyperMode: "setHyperMode",
turboMode: "setTurboMode",
buttonCombination: "setButtonCombination",
};

export function applyPulsarToggle(setting: PulsarToggleSetting, enabled: boolean): void {
Expand All @@ -2986,12 +2990,32 @@
command: `${enabled ? "Enable" : "Disable"} ${label}`,
progress: `${enabled ? "Enabling" : "Disabling"} ${label}…`,
preview: (status) => {
status[setting] = enabled;

Check failure on line 2993 in src/device/controller.ts

View workflow job for this annotation

GitHub Actions / check

Element implicitly has an 'any' type because expression of type 'PulsarToggleSetting' can't be used to index type 'MouseStatus'.
},
apply: () => callClientMethod(method, label, enabled),
});
}

/**
* Sensor angle in degrees. Pulsar Pro reaches the same setting through
* `applyProSetting`, which also carries settings only that protocol has.
*/
export function applyAngleTuning(degrees: number): void {
if (!hasActiveClient()) return;
stageChange({
key: "angle-tuning",
label: `Angle tune ${degrees}°`,
command: `Set the sensor angle to ${degrees}°`,
progress: `Setting the sensor angle to ${degrees}°…`,
preview: (status) => {
status.angleTuning = degrees;
},
apply: async () => {
await requireClientMethod("setAngleTuning", "angle tuning").setAngleTuning(degrees);
},
});
}

export function applyPulsarValue(setting: "debounce" | "sleep", value: number): void {
if (!(pulsarClient() ?? dmClient() ?? orbitalClient() ?? razerClient()
?? viperClient() ?? teevolutionClient() ?? vgnClient() ?? keychronNapeClient() ?? wallhackMouseClient())) return;
Expand Down
4 changes: 3 additions & 1 deletion src/device/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ export type PulsarToggleSetting =
| "angleSnapping"
| "rippleControl"
| "performanceMode"
| "hyperMode";
| "hyperMode"
| "turboMode"
| "buttonCombination";

export interface TeevolutionProfile {
sleepOptions: readonly number[];
Expand Down
13 changes: 12 additions & 1 deletion src/preview-fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,18 +136,29 @@
const WLMOUSE: MouseStatus = {
brand: "WLMouse",
name: "BEAST X",
ui: { family: "wlmouse", hideUnsupportedPollingRates: true, forceShowBattery: true },
ui: {
family: "wlmouse",
hideUnsupportedPollingRates: true,
forceShowBattery: true,
dpiStageEditor: { maxStages: 6, countEditable: true, minDpi: 50, maxDpi: 30000, stepDpi: 50 },
},
batteryPercent: 91,
batteryState: "Charging",
dpi: 800,
dpiY: 800,
dpiStages: [400, 800, 1600, 3200],
activeDpiStage: 1,
supportsSeparateDpiAxes: true,
pollingRateHz: 8000,
supportedPollingRates: [125, 250, 500, 1000, 2000, 4000, 8000],
activeProfile: 1,
liftOffDistance: "Low",
connectionType: "Wireless",
motionSync: true,
hyperMode: true,
turboMode: false,

Check failure on line 159 in src/preview-fixtures.ts

View workflow job for this annotation

GitHub Actions / check

Object literal may only specify known properties, and 'turboMode' does not exist in type 'MouseStatus'.
buttonCombination: true,
angleTuning: -8,
debounceMs: 2,
sleepTimeout: 60,
firmware: ["0.9.2"],
Expand Down
8 changes: 8 additions & 0 deletions src/ui/device-images.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,3 +200,11 @@ test("test-needed and unsupported models are not given new artwork", () => {
assert.equal(deviceImage(null, "VGN Dragonfly R1 Pro"), CDN + "unknown-device.png");
assert.equal(deviceImage(null, "Razer Viper 8KHz"), CDN + "unknown-device.png");
});

test("a mouse behind a shared WLMouse receiver resolves by name", () => {
// 0xa882 is the 1K receiver's own id; the model comes from the driver's name.
const receiver = { vendorId: 0x36a7, productId: 0xa882 } as HIDDevice;
assert.equal(deviceImage(receiver, "WLmouse Beast Max"), CDN + "wlmouse-beast-max.png");
assert.equal(deviceImage(receiver, "WLmouse Beast G"), CDN + "wlmouse-beast-g.png");
assert.equal(deviceImage(receiver, "WLmouse Beast Mini"), CDN + "unknown-device.png");
});
5 changes: 5 additions & 0 deletions src/ui/device-images.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,11 @@ function resolveDeviceImageFilename(device: HIDDevice | null | undefined, displa
if (/\bviper\s*v4\b/i.test(displayName)) return "razer-viper-v4-pro.png";
if (/\bxm2\s*8k\b/i.test(displayName)) return "endgame-gear-xm2-8k.png";
if (/\bxm2w\b/i.test(displayName)) return "endgame-gear-xm2w.png";
// WLMouse receivers are shared across models — the 1K dongle enumerates under
// one product id whatever it is paired with — so the model only arrives in the
// name the driver reads back from the mouse.
if (/\bbeast\s*max\b/i.test(displayName)) return "wlmouse-beast-max.png";
if (/\bbeast\s*g\b/i.test(displayName)) return "wlmouse-beast-g.png";
if (/\bbeast\s*x\s*pro\b/i.test(displayName)) return "unknown-device.png";
if (/\bbeast\s*mini\b/i.test(displayName)) return "unknown-device.png";
if (/\bbeast\s*x\b/i.test(displayName)) return "unknown-device.png";
Expand Down
Loading