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
63 changes: 44 additions & 19 deletions src/app/OverviewPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
} from "lucide-react";
import { useEffect, useRef, useState, type KeyboardEvent, type ReactNode } from "react";
import * as control from "../device/controller";
import { isNoirKsnakeStatus, isNoirS1Status, noirBrandLabel } from "../device/noir.ts";
import { isNoirE1Status, isNoirKsnakeStatus, isNoirS1Status, 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";
Expand Down Expand Up @@ -104,6 +104,14 @@ const NOIR_S1_BUTTON_MARKER_POSITIONS: Readonly<Record<string, ButtonMarkerPosit
dpi: { x: 0.5, y: 0.42 },
};

const NOIR_E1_BUTTON_MARKER_POSITIONS: Readonly<Record<string, ButtonMarkerPosition>> = {
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;
}
Expand All @@ -112,6 +120,10 @@ function noirS1ButtonMarkerPosition(button: string): ButtonMarkerPosition | null
return NOIR_S1_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";
Expand Down Expand Up @@ -247,12 +259,12 @@ function M2NexProfileOverview({ snapshot }: { snapshot: ControlSnapshot }): Reac
>
<div className="m2nex-profile-overview-title">
<span>{t(locale, "m2nex.overline")}</span>
<strong>{t(locale, "m2nex.label")}</strong>
<strong>{status.name}</strong>
</div>
<label className="m2nex-profile-overview-picker" htmlFor="m2nex-profile-overview-select">
<select
id="m2nex-profile-overview-select"
aria-label={t(locale, "m2nex.select")}
aria-label={`${status.name} profile`}
value={snapshot.activeM2NexProfile}
disabled={snapshot.settingInProgress || snapshot.pending.busy}
onChange={(event) => control.selectM2NexProfile(Number(event.currentTarget.value))}
Expand Down Expand Up @@ -318,6 +330,7 @@ export function DeviceShowcaseSidebar({ snapshot }: { snapshot: ControlSnapshot
const locale = snapshot.preferences.locale;
const image = snapshot.deviceArtwork;
const isNoirS1 = isNoirS1Status(status);
const isNoirE1 = isNoirE1Status(status);
const showButtonMarkers = snapshot.workspaceTab === "buttons"
&& isNoirKsnakeStatus(status)
&& status.buttonMappings != null;
Expand All @@ -337,22 +350,34 @@ export function DeviceShowcaseSidebar({ snapshot }: { snapshot: ControlSnapshot
}}
/>
<div className="button-map-markers" aria-hidden="true">
{Object.keys(status.buttonMappings ?? {}).map((button, index) => {
const position = isNoirS1
? noirS1ButtonMarkerPosition(button)
: m2NexButtonMarkerPosition(button);
if (!position) return null;
return (
<span
key={button}
className="button-map-marker"
style={{ left: `${position.x * 100}%`, top: `${position.y * 100}%` }}
>
{String(index + 1).padStart(2, "0")}
</span>
);
})}
{Object.keys(status.buttonMappings ?? {})
.filter((button) => !isNoirE1 || button.trim().toLowerCase() !== "dpi")
.map((button, index) => {
const position = isNoirS1
? noirS1ButtonMarkerPosition(button)
: isNoirE1
? noirE1ButtonMarkerPosition(button)
: m2NexButtonMarkerPosition(button);
if (!position) return null;
return (
<span
key={button}
className="button-map-marker"
style={{ left: `${position.x * 100}%`, top: `${position.y * 100}%` }}
>
{String(index + 1).padStart(2, "0")}
</span>
);
})}
</div>
{isNoirE1 ? (
<div className="button-map-underside-callout" aria-hidden="true">
<span className="button-map-underside-badge">
<span className="button-map-underside-number">06</span>
<span>DPI <small>UNDERSIDE</small></span>
</span>
</div>
) : null}
</div>
) : (
<img
Expand Down Expand Up @@ -719,7 +744,7 @@ function DeviceListView({ snapshot }: { snapshot: ControlSnapshot }): ReactNode
const batteryText = liveStatus?.batteryPercent != null ? `${liveStatus.batteryPercent}%` : "–%";
const imageSrc = deviceImage({ vendorId: device.vendorId, productId: device.productId } as HIDDevice, device.name);
const imageFilename = deviceImageFilename(device.name);
const imageClass = imageFilename === "noir-m2-nex.png" || imageFilename === "noir-s1.png"
const imageClass = imageFilename === "noir-m2-nex.png" || imageFilename === "noir-s1.png" || imageFilename === "noir-e1.png"
? " device-tile-image-is-portrait"
: "";
return (
Expand Down
4 changes: 2 additions & 2 deletions src/app/cards/DpiCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
} from "@openmouse/protocol/drivers/logitech/onboard-profiles";
import * as control from "../../device/controller";
import type { ControlSnapshot, LiftOffLevel } from "../../device/types";
import { isNoirKsnakeStatus, isNoirS1Status, NOIR_DPI_STAGE_COLORS } from "../../device/noir.ts";
import { isNoirE1Status, isNoirKsnakeStatus, isNoirS1Status, NOIR_DPI_STAGE_COLORS } from "../../device/noir.ts";
import { closestDpiOption, dpiPresetValues } from "../../dpi-presets";
import { t, tp } from "../../i18n";
import { LiftOffDistance, hasLiftOff } from "./PerformanceCards";
Expand Down Expand Up @@ -471,7 +471,7 @@ export function DpiCard({ snapshot }: { snapshot: ControlSnapshot }): ReactNode
|| (Boolean(status?.ui?.dpiStageEditor)
&& Array.isArray(status?.dpiStages)
&& (status?.dpiStages?.length ?? 0) > 0);
const singleDpiAvailable = !isNoirS1Status(status);
const singleDpiAvailable = !isNoirS1Status(status) && !isNoirE1Status(status);

const label = (source: typeof status): string => `${source.dpi.toLocaleString()} DPI`;

Expand Down
28 changes: 28 additions & 0 deletions src/app/cards/availability.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,34 @@ test("the generic button card follows the driver\'s mappings, not the advanced s
assert.equal(has.processing, false, "the rest of the advanced section stays closed");
});

test("NOIR E1 gets the same core configurator controls even without a family hint", () => {
const has = cardAvailability(snapshot({
status: {
brand: "K-snake",
name: "NOIR E1",
ui: { showAdvancedSection: true },
buttonMappings: {
Left: "Left click",
Right: "Right click",
Middle: "Middle click",
Forward: "Forward",
Backward: "Backward",
DPI: "DPI loop",
},
buttonOptions: ["Left click", "Right click", "Scroll up", "DPI loop"],
dpiStages: [800, 1200, 1600, 3200, 5000, 12000],
scrollDirection: "Forward",
lighting: {} as never,
},
}));
assert.equal(has.dpi, true);
assert.equal(has.polling, true);
assert.equal(has.buttonMapping, true);
assert.equal(has.ksnakeScroll, true);
assert.equal(has.ksnakeMacros, true);
assert.equal(has.lightingAdvanced, false);
});

test("the G-Wolves XVI generation opens sleep, debounce and processing, but not signal", () => {
const cards = cardAvailability(snapshot({
status: {
Expand Down
2 changes: 1 addition & 1 deletion src/app/cards/availability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ export function cardAvailability(snapshot: ControlSnapshot): CardAvailability {
&& status.debounceMs !== null && status.debounceMs !== undefined,
sleep: host && (traits.sleep || capabilities?.sleepOptions != null || razerSleep)
&& ui?.hideSleepCard !== true,
ksnakeScroll: ui?.family === "ksnake" && status.scrollDirection != null,
ksnakeScroll: (ui?.family === "ksnake" || isNoirKsnake) && status.scrollDirection != null,
lowPower: host && razerLowPower,
processing: host && processing,
ninjutsoSensor: host && traits.ninjutso
Expand Down
47 changes: 47 additions & 0 deletions src/control.css
Original file line number Diff line number Diff line change
Expand Up @@ -1482,6 +1482,53 @@ nav { display: grid; gap: .3rem; margin-top: 1.4rem; }
line-height: 1;
transform: translate(-50%, -50%);
}
.button-map-underside-callout {
display: flex;
flex-direction: column;
align-items: center;
margin-top: .18rem;
line-height: 1;
}
.button-map-underside-callout::before {
width: 1px;
height: .72rem;
border-radius: 2px;
background: #727985;
content: "";
}
.button-map-underside-badge {
display: inline-flex;
align-items: center;
gap: .42rem;
padding: .2rem .58rem .2rem .22rem;
border: 1px solid #41464f;
border-radius: 999px;
background: #24272d;
box-shadow: 0 2px 8px rgb(0 0 0 / 32%);
color: #e0e3e8;
font: 650 .59rem var(--font-mono);
letter-spacing: .04em;
text-transform: uppercase;
}
.button-map-underside-badge small {
margin-left: .2rem;
color: #9ba2ac;
font: inherit;
font-size: .51rem;
letter-spacing: .06em;
}
.button-map-underside-number {
display: grid;
width: 1.42rem;
height: 1.42rem;
place-items: center;
border: 1px solid #fff;
border-radius: 50%;
background: #f2f3f5;
color: #202329;
font: 700 .57rem var(--font-mono);
letter-spacing: .02em;
}
.showcase-sidebar-info { width: 100%; min-width: 0; margin-top: 1rem; }
.showcase-sidebar-name {
margin: 0;
Expand Down
Loading
Loading