Skip to content
Merged
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
1 change: 0 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions public/devices/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,3 +201,10 @@ package:
MCHOSE publishes only `A7V3Pro_*` renders and the four V3 models share a
shell, so one image covers the family. **Needs a maintainer upload**, and
carries the same unresolved licensing question.

- `dareu-a950-pro-mg.png` — **needs a maintainer upload.** The mapping covers
the A950 PRO Mg mouse (`0x260d:0x1117`) and dedicated receiver
(`0x260d:0x1114`). Suggested source: the contributor-supplied transparent
render from Dareu's driver panel
(`https://driver.dareu.com/allinone/products/1117/TM271F.png?v=1.3.24`). It
is vendor product art; request licensing review before publishing it in R2.
17 changes: 13 additions & 4 deletions src/app/OverviewPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ import {
PulsarProCard,
SignalCard,
SleepCard,
TeevolutionDpiLightingCard,
DpiLightingCard,
} from "./cards/AdvancedCards";
import { cardAvailability } from "./cards/availability";
import { TeevolutionProfileCard } from "./cards/teevolution/ProfileCard";
Expand Down Expand Up @@ -317,10 +317,18 @@ function OverviewEmpty({ snapshot, compact = false }: { snapshot: ControlSnapsho
function OverviewContent({ snapshot }: { snapshot: ControlSnapshot }): ReactNode {
const status = snapshot.status;
if (!status) return <OverviewEmpty snapshot={snapshot} />;
const has = cardAvailability(snapshot);
const powerOverview = status.ui?.powerOverview === true;
return (
<>
<DeviceShowcase snapshot={snapshot} />
<DeviceInfoGrid snapshot={snapshot} />
{powerOverview && (has.teevolutionDpiLighting || has.sleep) ? (
<section id="power-overview-settings" className="settings-grid device-data" aria-label="Power settings">
{has.teevolutionDpiLighting ? <DpiLightingCard snapshot={snapshot} /> : null}
{has.sleep ? <SleepCard snapshot={snapshot} /> : null}
</section>
) : null}
</>
);
}
Expand All @@ -338,6 +346,7 @@ export function Workspace({
const locale = snapshot.preferences.locale;
const has = cardAvailability(snapshot);
const show = (available: boolean, tabs: readonly WorkspaceTab[]): boolean => available && on(tab, tabs);
const powerOverview = status.ui?.powerOverview === true;

const performance = [
show(has.dpi, ["performance"]) ? <DpiCard key="dpi" snapshot={snapshot} /> : null,
Expand All @@ -349,7 +358,7 @@ export function Workspace({
const advanced = [
show(has.signal, ["advanced"]) ? <SignalCard key="signal" snapshot={snapshot} /> : null,
show(has.debounce, ["buttons"]) ? <DebounceCard key="debounce" snapshot={snapshot} /> : null,
show(has.sleep, ["advanced"]) ? <SleepCard key="sleep" snapshot={snapshot} /> : null,
!powerOverview && show(has.sleep, ["advanced"]) ? <SleepCard key="sleep" snapshot={snapshot} /> : null,
show(has.lightingAdvanced, ["advanced"])
? <LightingCard key="lighting" snapshot={snapshot} variant="advanced" /> : null,
show(has.ninjutsoSensor, ["performance"])
Expand Down Expand Up @@ -383,8 +392,8 @@ export function Workspace({
const lighting = [
show(has.lighting, ["lighting"])
? <LightingCard key="lighting-tab" snapshot={snapshot} variant="tab" zones={lightingZones} /> : null,
show(has.teevolutionDpiLighting, ["lighting"])
? <TeevolutionDpiLightingCard key="teevo" snapshot={snapshot} /> : null,
!powerOverview && show(has.teevolutionDpiLighting, ["lighting"])
? <DpiLightingCard key="dpi-indicator" snapshot={snapshot} /> : null,
].filter((node) => node !== null);

const showProfiles = show(has.profiles, ["profiles"]);
Expand Down
28 changes: 23 additions & 5 deletions src/app/cards/AdvancedCards.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -434,7 +434,7 @@ export function ProcessingCard({ snapshot }: { snapshot: ControlSnapshot }): Rea
);
}

export function TeevolutionDpiLightingCard({ snapshot }: { snapshot: ControlSnapshot }): ReactNode {
export function DpiLightingCard({ snapshot }: { snapshot: ControlSnapshot }): ReactNode {
const status = snapshot.status;
const profile = snapshot.capabilities?.teevolutionProfile;
const hint = status?.ui?.dpiLighting;
Expand All @@ -446,14 +446,15 @@ export function TeevolutionDpiLightingCard({ snapshot }: { snapshot: ControlSnap
const speedMax = hint?.speed.at(-1) ?? profile!.dpiLighting.speed.max;
const locale = snapshot.preferences.locale;
const lightMode = status.dpiLedMode ?? 0;
const staged = snapshot.pending.keys.some((key) => key.startsWith("teevolution-dpi-light-"));
const powerOverview = status.ui?.powerOverview === true;
const staged = snapshot.pending.keys.some((key) => key.startsWith("teevolution-dpi-light-") || key === "dpi-light-sleep");
return (
<article
id="teevolution-dpi-lighting"
className={`setting-card${staged ? " is-staged" : ""}`}
data-pending-key="teevolution-dpi-light-mode teevolution-dpi-light-brightness teevolution-dpi-light-speed"
data-pending-key="teevolution-dpi-light-mode teevolution-dpi-light-brightness teevolution-dpi-light-speed dpi-light-sleep"
>
<div className="setting-heading compact"><div><p>LIGHTING</p><h2>{t(locale, "adv.dpiIndicator")}</h2></div></div>
<div className="setting-heading compact"><div><p>{powerOverview ? "POWER" : "LIGHTING"}</p><h2>{t(locale, "adv.dpiIndicator")}</h2></div></div>
<label className="field-label">
{t(locale, "adv.effect")}
<select
Expand All @@ -480,7 +481,7 @@ export function TeevolutionDpiLightingCard({ snapshot }: { snapshot: ControlSnap
max={brightnessMax}
step={1}
value={status.dpiLedBrightness ?? brightnessMin}
disabled={lightMode !== 1 || status.dpiLedBrightness == null}
disabled={lightMode === 0 || status.dpiLedBrightness == null}
style={{
"--fill": `${((status.dpiLedBrightness ?? brightnessMin) - brightnessMin)
/ Math.max(1, brightnessMax - brightnessMin) * 100}%`,
Expand Down Expand Up @@ -512,11 +513,28 @@ export function TeevolutionDpiLightingCard({ snapshot }: { snapshot: ControlSnap
</span>
</label>
</div>
{hint?.sleepTimeouts?.length && status.dpiLedSleepTimeout != null ? (
<label className="field-label spaced">
{t(locale, "adv.autoSleep")}
<select
id="dpi-light-sleep-timeout"
value={status.dpiLedSleepTimeout}
onChange={(event) => control.applyDpiLightingSleepTimeout(Number(event.currentTarget.value))}
>
{hint.sleepTimeouts.map((seconds) => (
<option key={seconds} value={seconds}>{sleepLabel(seconds, locale)}</option>
))}
</select>
</label>
) : null}
<small className="setting-note">{t(locale, "adv.dpiStageNote")}</small>
</article>
);
}

/** @deprecated Kept as an import alias for integrations built before generic DPI lighting. */
export const TeevolutionDpiLightingCard = DpiLightingCard;

export function NinjutsoSensorCard({ snapshot }: { snapshot: ControlSnapshot }): ReactNode {
const status = snapshot.status;
if (!status) return null;
Expand Down
20 changes: 20 additions & 0 deletions src/app/cards/availability.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,26 @@ test("ATK exposes its processing and DPI-lighting cards from reported controls",
assert.equal(has.teevolutionDpiLighting, true);
});

test("Dareu exposes its verified mouse and DPI-indicator sleep controls", () => {
const has = cardAvailability(snapshot({
status: {
brand: "Dareu",
ui: {
family: "dareu",
showAdvancedSection: true,
dpiLighting: {
modes: [0, 1, 2], brightness: [1, 2], speed: [1, 2], sleepTimeouts: [0, 60],
},
},
sleepTimeout: 180,
dpiLedSleepTimeout: 180,
},
}));
assert.equal(has.advancedHost, true);
assert.equal(has.sleep, true);
assert.equal(has.teevolutionDpiLighting, true);
});

test("angle tuning belongs to processing rather than creating a second power-mode card", () => {
const angleOnly = cardAvailability(snapshot({
status: { brand: "VXE", ui: { family: "atk" }, angleTuning: 0 },
Expand Down
38 changes: 31 additions & 7 deletions src/device/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ import {
import { SUPPORTED_HID_FILTERS } from "@openmouse/protocol/drivers/vendors";
import { WLMouseHidClient } from "@openmouse/protocol/drivers/wlmouse/hid";
import { MicrosoftHidClient } from "@openmouse/protocol/drivers/microsoft/hid";
import { DareuHidClient } from "@openmouse/protocol/drivers/dareu/hid";
import { IncottHidClient } from "@openmouse/protocol/drivers/incott/hid";
import { parsePreviewMode, previewsEnabled, type PreviewMode } from "../preview-modes";
import { sleepLabel } from "./options";
Expand Down Expand Up @@ -227,7 +228,7 @@ function activeAs<T>(...classes: ClientClass<T>[]): T | null {

const DM_CLASSES = [WLMouseHidClient, LamzuHidClient, LamzuAtlantisHidClient, AtkHidClient, AtkBitmouseHidClient, NinjutsoHidClient] as const;
const RAZER_CLASSES = [RazerHidClient, RazerViperMiniHidClient, RazerViperHidClient, RazerCobraHidClient] as const;
const NEEDS_OPEN = [LamzuAtlantisHidClient, TeevolutionHidClient, VgnF2HidClient, KeychronNapeHidClient, KeychronM6HidClient, ModdoHidClient, ZaunkoenigHidClient, CorsairHidClient, FantechHidClient, WallhackMouseHidClient, WallhackKeyboardHidClient, GloriousHidClient, GloriousClassicHidClient, MchoseHidClient, MchoseDockHidClient, MchoseA5ProMaxHidClient, MchoseV3HidClient, MicrosoftHidClient, IncottHidClient] as const;
const NEEDS_OPEN = [LamzuAtlantisHidClient, TeevolutionHidClient, VgnF2HidClient, KeychronNapeHidClient, KeychronM6HidClient, ModdoHidClient, ZaunkoenigHidClient, CorsairHidClient, FantechHidClient, WallhackMouseHidClient, WallhackKeyboardHidClient, GloriousHidClient, GloriousClassicHidClient, MchoseHidClient, MchoseDockHidClient, MchoseA5ProMaxHidClient, MchoseV3HidClient, MicrosoftHidClient, DareuHidClient, IncottHidClient] as const;
const PULSAR_CLASSES = [PulsarHidClient, PulsarProHidClient, PulsarXs1HidClient] as const;

const logitechClient = (): LogitechHidppClient | null => activeAs(LogitechHidppClient);
Expand All @@ -241,8 +242,8 @@ const razerClient = (): RazerHidClient | RazerViperMiniHidClient | RazerViperHid
const viperClient = (): RazerViperV4ProHidClient | null => activeAs(RazerViperV4ProHidClient);

const teevolutionClient = (): TeevolutionHidClient | null => activeAs(TeevolutionHidClient);
const dpiLightingClient = (): TeevolutionHidClient | AtkHidClient | null =>
activeAs<TeevolutionHidClient | AtkHidClient>(TeevolutionHidClient, AtkHidClient);
const dpiLightingClient = (): TeevolutionHidClient | AtkHidClient | DareuHidClient | null =>
activeAs<TeevolutionHidClient | AtkHidClient | DareuHidClient>(TeevolutionHidClient, AtkHidClient, DareuHidClient);
const finalmouseClient = (): FinalmouseHidClient | null => activeAs(FinalmouseHidClient);
const orbitalClient = (): OrbitalHidClient | null => activeAs(OrbitalHidClient);
const vgnClient = (): VgnF2HidClient | null => activeAs(VgnF2HidClient);
Expand Down Expand Up @@ -1753,7 +1754,6 @@ async function activateClientNow(client: SupportedClient): Promise<void> {
lastSleepSeconds = status.sleepTimeout ?? keychron.getSleepOptions()[0] ?? 60;
}
deviceStatuses.set(client.device, status);
capabilities = readCapabilities();
applyStatus(status);
await readButtons();
await loadNapeKeymap(status.napeLayer ?? editedNapeLayer ?? 1);
Expand Down Expand Up @@ -3333,9 +3333,12 @@ export function applyPulsarToggle(setting: PulsarToggleSetting, enabled: boolean
}

export function applyPulsarValue(setting: "debounce" | "sleep", value: number): void {
if (!(pulsarClient() ?? dmClient() ?? orbitalClient() ?? razerClient()
?? viperClient() ?? teevolutionClient() ?? vgnClient() ?? keychronNapeClient() ?? keychronM6Client() ?? wallhackMouseClient()
?? incottClient())) return;
const client = setting === "sleep"
? activeSettingsClient()
: pulsarClient() ?? dmClient() ?? orbitalClient() ?? razerClient()
?? viperClient() ?? teevolutionClient() ?? vgnClient() ?? keychronNapeClient() ?? keychronM6Client() ?? wallhackMouseClient()
?? incottClient();
if (!client || (setting === "sleep" && !("setSleepTimeout" in client))) return;
const asleep = value !== WLMOUSE_SLEEP_NEVER;
stageChange({
key: setting,
Expand Down Expand Up @@ -3602,6 +3605,9 @@ async function writeStagedTeevolutionDpiLighting(): Promise<void> {

export function applyTeevolutionDpiLighting(setting: "mode" | "brightness" | "speed", value: number): void {
if (!dpiLightingClient()) return;
const hint = latestDeviceStatus?.ui?.dpiLighting;
const allowed = setting === "mode" ? hint?.modes : setting === "brightness" ? hint?.brightness : hint?.speed;
if (allowed && !allowed.includes(value)) return;
const names = { mode: "effect", brightness: "brightness", speed: "speed" } as const;
const display = setting === "mode" ? (["Off", "Steady", "Breathing"][value] ?? `${value}`) : `${value}`;
stageChange({
Expand All @@ -3619,6 +3625,24 @@ export function applyTeevolutionDpiLighting(setting: "mode" | "brightness" | "sp
});
}

export function applyDpiLightingSleepTimeout(seconds: number): void {
const allowed = latestDeviceStatus?.ui?.dpiLighting?.sleepTimeouts;
if (!allowed?.includes(seconds)) return;
stageChange({
key: "dpi-light-sleep",
label: `DPI indicator sleep ${sleepLabel(seconds, interfacePreferences.locale)}`,
command: `Set DPI indicator sleep to ${sleepLabel(seconds, interfacePreferences.locale)}`,
progress: "Setting DPI indicator sleep…",
preview: (status) => { status.dpiLedSleepTimeout = seconds; },
apply: async () => {
const client = requireClientMethod("setDpiLedSleepTimeout", "DPI indicator sleep") as unknown as {
setDpiLedSleepTimeout(value: number): Promise<unknown>;
};
await client.setDpiLedSleepTimeout(seconds);
},
});
}

export function applyEggFilter(setting: "slamclick" | "motionJitter", enabled: boolean): void {
if (!eggClient()) return;
const label = setting === "slamclick" ? "slamclick filter" : "motion-jitter filter";
Expand Down
1 change: 1 addition & 0 deletions src/device/traits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ const BY_FAMILY: Readonly<Record<string, Partial<DriverTraits>>> = {
// "mchose-v3" is deliberately absent: that driver reads and cannot write, so
// every card these flags open would render controls with nothing behind
// them. Give it the same flags as `mchose` once it grows setters.
dareu: { advancedSection: true, sleep: true },
// Incott supports a 0-30 ms debounce and a 1-900 s sleep timer over its own
// vendor protocol, not the CompX direct-mode transport, so it takes the
// plain flags rather than DIRECT_MODE (its advancedSection already comes
Expand Down
7 changes: 7 additions & 0 deletions src/ui/device-images.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,13 @@ test("Attack Shark R5 Ultra wired and wireless share the same artwork", () => {
assert.equal(deviceImage(null, "Attack Shark R5 Ultra"), CDN + "attackshark-r5-ultra.png");
});

test("Dareu A950 PRO Mg wired and receiver paths share the requested artwork", () => {
const dareuArt = CDN + "dareu-a950-pro-mg.png";
assert.equal(deviceImage(dev(0x260d, 0x1117)), dareuArt);
assert.equal(deviceImage(dev(0x260d, 0x1114)), dareuArt);
assert.equal(deviceImage(null, "Dareu A950 PRO Mg"), dareuArt);
});

test("Attack Shark R2 resolves by name (PID 0x402D is shared with the M5 Pro)", () => {
assert.equal(deviceImage(null, "Attack Shark R2"), CDN + "attackshark-r2.png");
// The shared receiver PID must NOT resolve to the R2 render.
Expand Down
4 changes: 4 additions & 0 deletions src/ui/device-images.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,9 @@ const DEVICE_IMAGES: ReadonlyMap<string, string> = new Map([
// K-snake X11 wired / 2.4 GHz dongle share the same shell.
["a8a4:2255", "ksnake-x11.png"],
["a8a5:2255", "ksnake-x11.png"],
// A950 PRO Mg wired mouse and its dedicated 2.4 GHz receiver.
["260d:1117", "dareu-a950-pro-mg.png"],
["260d:1114", "dareu-a950-pro-mg.png"],
// Microsoft Intellimouse
["045e:0823", "microsoft-classic-intellimouse.png"],
["045e:082a", "microsoft-pro-intellimouse.png"],
Expand Down Expand Up @@ -332,6 +335,7 @@ function resolveDeviceImageFilename(device: HIDDevice | null | undefined, displa
// an "A7 V3" name is not swallowed by a looser A7 match later.
if (/\ba7\s*v3\b/i.test(displayName)) return "mchose-a7-v3.png";
if (/\ba7\s*v2\b/i.test(displayName)) return "mchose-a7-v2.png";
if (/\ba950\s*pro\s*mg\b/i.test(displayName)) return "dareu-a950-pro-mg.png";
if (/\b(finalmouse|starlight|ulx)\b/i.test(displayName)) return "finalmouse-ulx.png";
if (/\borbital\b/i.test(displayName)) return "unknown-device.png";
if (/\bmoddo/i.test(displayName)) return "unknown-device.png";
Expand Down