From 74abfb2ff5e3275d5562284ea55567df57bb14a1 Mon Sep 17 00:00:00 2001 From: user_1781515504933 Date: Fri, 2 Oct 2026 15:50:38 +0700 Subject: [PATCH] feat: add Noir Gear NOIR E1 support --- src/app/OverviewPage.tsx | 68 ++++++++++++++++-------- src/app/cards/AdvancedCards.tsx | 11 ++-- src/app/cards/DpiCard.tsx | 14 +++-- src/app/cards/PerformanceCards.tsx | 2 + src/app/cards/availability.test.ts | 28 ++++++++++ src/app/cards/availability.ts | 10 ++-- src/control.css | 47 ++++++++++++++++ src/device/controller.ts | 51 +++++++++++++----- src/device/m2nex-profiles.test.ts | 22 ++++++++ src/device/m2nex-profiles.ts | 38 +++++++++---- src/device/noir.test.ts | 25 +++++++++ src/device/noir.ts | 31 +++++++++++ src/supported-devices-crosscheck.test.ts | 11 ++++ src/supported-devices-crosscheck.ts | 11 ++-- src/supported-devices-data.ts | 1 + src/ui/device-images.test.ts | 1 + src/ui/device-images.ts | 1 + 17 files changed, 309 insertions(+), 63 deletions(-) create mode 100644 src/device/noir.test.ts create mode 100644 src/device/noir.ts diff --git a/src/app/OverviewPage.tsx b/src/app/OverviewPage.tsx index 326df2f6..02214db9 100644 --- a/src/app/OverviewPage.tsx +++ b/src/app/OverviewPage.tsx @@ -15,6 +15,7 @@ import { } from "lucide-react"; import { useEffect, useRef, useState, type KeyboardEvent, type ReactNode } from "react"; import * as control from "../device/controller"; +import { isNoirE1Status, isNoirKsnakeStatus, noirBrandLabel } from "../device/noir.ts"; import type { ControlSnapshot, WorkspaceTab } from "../device/types"; import { t, tp, connectionText, type I18nKey } from "../i18n"; import { Diagnostics, LogitechDetails } from "./Diagnostics"; @@ -94,10 +95,22 @@ const M2_NEX_BUTTON_MARKER_POSITIONS: Readonly> = { + left: { x: 0.28, y: 0.2 }, + right: { x: 0.72, y: 0.2 }, + middle: { x: 0.5, y: 0.18 }, + forward: { x: 0.08, y: 0.32 }, + backward: { x: 0.08, y: 0.5 }, +}; + function m2NexButtonMarkerPosition(button: string): ButtonMarkerPosition | null { return M2_NEX_BUTTON_MARKER_POSITIONS[button.trim().toLowerCase()] ?? null; } +function noirE1ButtonMarkerPosition(button: string): ButtonMarkerPosition | null { + return NOIR_E1_BUTTON_MARKER_POSITIONS[button.trim().toLowerCase()] ?? null; +} + // Device artwork requests go straight to the documented GitHub issue form // (public/devices/README.md, .github/ISSUE_TEMPLATE/device-artwork.yml). const ARTWORK_ISSUE_URL = "https://github.com/OpenMouse-Project/openmouse/issues/new?template=device-artwork.yml"; @@ -219,7 +232,7 @@ function DeviceShowcase({ snapshot }: { function M2NexProfileOverview({ snapshot }: { snapshot: ControlSnapshot }): ReactNode { const status = snapshot.status; const locale = snapshot.preferences.locale; - if (status?.brand !== "Noir Gear" || status.name !== "M2-NEX" || snapshot.m2nexProfiles === null) return null; + if (!status || !isNoirKsnakeStatus(status) || snapshot.m2nexProfiles === null) return null; const profile = snapshot.m2nexProfiles[snapshot.activeM2NexProfile]; if (!profile) return null; const applyDisabled = snapshot.settingInProgress || snapshot.pending.count > 0; @@ -227,16 +240,16 @@ function M2NexProfileOverview({ snapshot }: { snapshot: ControlSnapshot }): Reac
{t(locale, "m2nex.overline")} - {t(locale, "m2nex.label")} + {status.name}