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
4 changes: 4 additions & 0 deletions public/devices/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -32,6 +32,10 @@ before including it in a public release package.
URL for the OP1 8K. Confirm redistribution terms before including it in a
public release package.

`keychron-nape-pro.png` was supplied from Keychron's sysmgr cover CDN for the
Nape Pro. Confirm redistribution terms before including it in a public release
package.

`teevolution-terra-pro.png` was supplied from Teevolution's Terra PRO Shopify
CDN product render. Confirm redistribution terms before including it in a
public release package.
Expand Down
Binary file addedpublic/devices/keychron-nape-pro.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 11 additions & 7 deletions src/control.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -1987,13 +1987,17 @@ async function activateClient(client: SupportedClient): Promise<void> {
dpiOptions = client.getDpiOptions();
configureDpiControl(status.dpi);
showStatus(status);
} else if (client instanceof KeychronHidClient || client instanceof ModdoHidClient) {
if (client instanceof ModdoHidClient) {
activeModdoClient = client;
await client.open();
} else {
activeKeychronClient = client;
}
} else if (client instanceof KeychronHidClient) {
activeKeychronClient = client;
await client.open();
const status = await client.readStatus();
deviceStatuses.set(client.device, status);
dpiOptions = client.getDpiOptions();
configureDpiControl(status.dpi);
showStatus(status);
} else if (client instanceof ModdoHidClient) {
activeModdoClient = client;
await client.open();
const status = await client.readStatus();
deviceStatuses.set(client.device, status);
dpiOptions = client.getDpiOptions();
Expand Down
27 changes: 26 additions & 1 deletion src/preview-fixtures.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -335,7 +335,7 @@ const FINALMOUSE: MouseStatus = {
batteryState: "Discharging",
dpi: 1600,
pollingRateHz: 8000,
supportedPollingRates: [500, 1000, 2000, 4000, 8000],
supportedPollingRates: [125, 500, 1000],
activeProfile: null,
connectionType: "Wireless",
connectionDetail: "2.4 GHz receiver · -48 dBm",
Expand DownExpand Up@@ -398,6 +398,30 @@ const NINJUTSO: MouseStatus = {
firmware: ["Mouse 1611", "Receiver 0116"],
};

const KEYCHRON: MouseStatus = {
brand: "Keychron",
name: "Nape Pro",
ui: {
family: "keychron-nape",
defaultDisplayName: "Nape Pro",
hideUnsupportedPollingRates: true,
hideProcessingCard: true,
forceShowBattery: true,
pollingNote: "Nape Pro exposes polling through Keychron's misc HID commands when the firmware allows it.",
},
batteryPercent: 76,
batteryState: "Discharging",
dpi: 800,
pollingRateHz: 1000,
supportedPollingRates: [500, 1000, 2000, 4000, 8000],
activeProfile: null,
connectionType: "Wired",
connectionDetail: "Wired USB · 90° orientation · DPI stage 2/5",
liftOffDistance: null,
supportedLiftOffDistances: [],
firmware: ["v1.0.4"],
};

const LOGITECH_LEGACY: MouseStatus = {
brand: "Logitech",
name: "G402 Hyperion Fury",
Expand DownExpand Up@@ -437,5 +461,6 @@ export const PREVIEW_FIXTURES: Record<FixturePreviewMode, PreviewFixture> = {
vgn: { label: "VGN F2 Master Plus", status: VGN },
finalmouse: { label: "Finalmouse UltralightX", status: FINALMOUSE },
ninjutso: { label: "Ninjutso Sora V3", status: NINJUTSO },
keychron: { label: "Keychron Nape Pro", status: KEYCHRON },
"logitech-legacy": { label: "Logitech G402 (legacy DPI)", status: LOGITECH_LEGACY },
};
2 changes: 2 additions & 0 deletions src/preview-modes.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,6 +24,7 @@ export const PREVIEW_KEYS = [
"vgn",
"finalmouse",
"ninjutso",
"keychron",
"logitech-legacy",
] as const;

Expand DownExpand Up@@ -52,6 +53,7 @@ export function parsePreviewMode(value: string | null): PreviewMode | null {
case "vgn": return "vgn";
case "finalmouse": return "finalmouse";
case "ninjutso": return "ninjutso";
case "keychron": return "keychron";
case "logitech-legacy": return "logitech-legacy";
default: return null;
}
Expand Down
5 changes: 5 additions & 0 deletions src/ui/device-images.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -34,6 +34,10 @@ const DEVICE_IMAGES: ReadonlyMap<string, string> = new Map([
["093a:e020", "/devices/ninjutso-ten.png"],
["093a:ea01", "/devices/ninjutso-ten.png"],
["093a:eb01", "/devices/ninjutso-ten.png"],
// Nape Pro wired / Link-KM receivers share the same shell artwork.
["3434:0440", "/devices/keychron-nape-pro.png"],
["3434:d026", "/devices/keychron-nape-pro.png"],
["3434:d029", "/devices/keychron-nape-pro.png"],
// Teevolution Terra Pro wired / receiver Compx transports.
["3554:f520", "/devices/teevolution-terra-pro.png"],
["3554:f522", "/devices/teevolution-terra-pro.png"],
Expand All@@ -51,5 +55,6 @@ export function deviceImage(device: HIDDevice | null | undefined, displayName =
if (mapped) return mapped;
if (/superlight/i.test(displayName)) return "/devices/logitech-pro-x-superlight-2c.png";
if (/\bop1\s*8k\b/i.test(displayName)) return "/devices/endgame-gear-op1-8k.png";
if (/\bnape\s*pro\b/i.test(displayName)) return "/devices/keychron-nape-pro.png";
return "/devices/unknown-device.png";
}