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
8 changes: 4 additions & 4 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"preview": "vite preview"
},
"dependencies": {
"@openmouse/keyboard-protocol": "1.0.0",
"@openmouse/keyboard-protocol": "1.1.0",
"@openmouse/protocol": "0.33.0",
"lucide-react": "^1.43.0",
"preact": "^10.29.8"
Expand Down
8 changes: 5 additions & 3 deletions src/app/OverviewPage.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { WORKSPACE_TAB_ORDER } from "../device/types";
import {
ArrowLeft,
BarChart3,
Expand Down Expand Up @@ -66,8 +65,9 @@ import { cardAvailability } from "./cards/availability";
import { TeevolutionProfileCard } from "./cards/teevolution/ProfileCard";
import { deviceImage, deviceImageFilename, isUnknownDevice, showcaseDeviceImageUrls } from "../ui/device-images";
import { BatteryIcon } from "./ui";
import { availableWorkspaceTab, availableWorkspaceTabs } from "./workspace-tabs";
import { availableKeyboardWorkspaceTabs, availableWorkspaceTab, availableWorkspaceTabs } from "./workspace-tabs";

/** True when the workspace tab is present in the list. */
function on(tab: WorkspaceTab, tabs: readonly WorkspaceTab[]): boolean {
return tabs.includes(tab);
}
Expand Down Expand Up @@ -760,6 +760,7 @@ export function Workspace({
);
}

/** The authorized-device list shown when no device is active. */
function DeviceListView({ snapshot }: { snapshot: ControlSnapshot }): ReactNode {
const devices = snapshot.devices;
const locale = snapshot.preferences.locale;
Expand Down Expand Up @@ -865,6 +866,7 @@ function DeviceListView({ snapshot }: { snapshot: ControlSnapshot }): ReactNode
);
}

/** The device overview: status, workspace tabs and the active device's cards. */
export function OverviewPage({
snapshot,
onOpenCapture,
Expand All @@ -879,7 +881,7 @@ export function OverviewPage({
const { preferences } = snapshot;
const locale = preferences.locale;

const tabs = snapshot.keyboard ? WORKSPACE_TAB_ORDER : availableWorkspaceTabs(status !== null, cardAvailability(snapshot), {
const tabs = snapshot.keyboard ? availableKeyboardWorkspaceTabs(snapshot.keyboard.family) : availableWorkspaceTabs(status !== null, cardAvailability(snapshot), {
hideLighting: isNoirKsnakeStatus(status),
});
const workspaceTab = availableWorkspaceTab(snapshot.workspaceTab, tabs);
Expand Down
2 changes: 2 additions & 0 deletions src/app/cards/KeyboardPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { KochiKeyboardPanel } from "./KochiKeyboardPanel";
import { KeyboardWorkspaceCards } from "./KeyboardWorkspaceCards";
import type { ReactNode } from "react";
import * as control from "../../device/controller";
Expand All @@ -22,6 +23,7 @@ function millimetres(value: number): string {
export function KeyboardPanel({ snapshot }: { snapshot: ControlSnapshot }): ReactNode {
const keyboard = snapshot.keyboard;
if (!keyboard) return null;
if (keyboard.family === "kochi") return <KochiKeyboardPanel snapshot={snapshot} />;
const locale = snapshot.preferences.locale;
const { identity, polling, trigger } = keyboard;
const busy = trigger.busy;
Expand Down
30 changes: 30 additions & 0 deletions src/app/cards/KochiKeyboardPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { ReactNode } from "react";
import type { ControlSnapshot } from "../../device/types";
import * as control from "../../device/controller";
import { t } from "../../i18n";
import { Segmented } from "../ui";

/** Reuse the keyboard workspace's system-mode control, without Wuque settings. */
export function KochiKeyboardPanel({ snapshot }: { snapshot: ControlSnapshot }): ReactNode {
const keyboard = snapshot.keyboard;
if (!keyboard) return null;
const locale = snapshot.preferences.locale;
const mode = keyboard.workspace.systemMode;
const busy = keyboard.trigger.busy;
return (
<section className="keyboard-panel" data-workspace-host role="tabpanel" aria-labelledby="workspace-tab-overview">
{keyboard.trigger.statusText ? <output className="keyboard-status" role="status">{keyboard.trigger.statusText}</output> : null}
<article className="setting-card keyboard-identity-card keyboard-workspace-card">
<div className="setting-heading"><div><div className="title-row"><h2>{keyboard.name}</h2><p>{t(locale, "kb.keyboardLabel")}</p></div>
<small className="setting-note">{t(locale, "kb.kochiModeNote")}</small>
</div></div>
<fieldset disabled={busy || mode == null} className="keyboard-fields">
<Segmented id="keyboard-system" ariaLabel={t(locale, "kb.system")} options={[{ value: 0, label: "WIN" }, { value: 1, label: "MAC" }]}
value={mode === "win" ? 0 : mode === "mac" ? 1 : null}
onChange={value => control.applyKeyboardCommand({ kind: "system", value })} />
</fieldset>
<button type="button" disabled={busy} onClick={() => control.applyKeyboardCommand({ kind: "refresh" })}>{t(locale, "kb.refresh")}</button>
</article>
</section>
);
}
10 changes: 9 additions & 1 deletion src/app/workspace-tabs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,16 @@ import assert from "node:assert/strict";
import test from "node:test";

import type { CardAvailability } from "./cards/availability.ts";
import { availableWorkspaceTab, availableWorkspaceTabs } from "./workspace-tabs.ts";
import { availableKeyboardWorkspaceTabs, availableWorkspaceTab, availableWorkspaceTabs } from "./workspace-tabs.ts";

test("HB68 exposes only system mode; existing keyboard tabs remain available", () => {
assert.deepEqual(availableKeyboardWorkspaceTabs("kochi"), ["overview"]);
assert.equal(availableWorkspaceTab("performance", availableKeyboardWorkspaceTabs("kochi")), "overview");
assert.ok(availableKeyboardWorkspaceTabs("wuque").includes("performance"));
assert.ok(availableKeyboardWorkspaceTabs().includes("lighting"));
});

/** A CardAvailability fixture where every unset field reads as false. */
function availability(enabled: Partial<CardAvailability>): CardAvailability {
return new Proxy(enabled, { get: (target, property) => Reflect.get(target, property) ?? false }) as CardAvailability;
}
Expand Down
7 changes: 7 additions & 0 deletions src/app/workspace-tabs.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { WORKSPACE_TAB_ORDER, type WorkspaceTab } from "../device/types.ts";
import type { CardAvailability } from "./cards/availability.ts";

/** Tabs a connected mouse may show, narrowed by what its cards support. */
export function availableWorkspaceTabs(
connected: boolean,
has: CardAvailability,
Expand All @@ -21,9 +22,15 @@ export function availableWorkspaceTabs(
});
}

/** Fall back to the overview when the requested tab is not available. */
export function availableWorkspaceTab(
current: WorkspaceTab,
tabs: readonly WorkspaceTab[],
): WorkspaceTab {
return tabs.includes(current) ? current : "overview";
}

/** The HB68 currently implements only the overview system-mode control. */
export function availableKeyboardWorkspaceTabs(family?: string): readonly WorkspaceTab[] {
return family === "kochi" ? ["overview"] : WORKSPACE_TAB_ORDER;
}
12 changes: 10 additions & 2 deletions src/device-clients.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import {
type SupportedClient,
} from "@openmouse/protocol/drivers/registry";
import { RAZER_PRODUCTS } from "@openmouse/protocol/razer-devices";
import { KochiKeyboardClient } from "./device/kochi-keyboard.ts";
import type { KeyboardClient } from "./device/keyboard-client.ts";
import { WuqueKeyboardClient } from "./device/wuque-keyboard.ts";
export { describeHidDevice } from "./hid-diagnostics.ts";
export { deviceBrand, type PulsarClient, type SupportedClient };
Expand All @@ -46,19 +48,25 @@ function bridgeRazerClient(device: HIDDevice): SupportedClient | null {
}


/** The mouse client for a device, including Bridge-exposed Razer boards. */
export function createSupportedClient(device: HIDDevice): SupportedClient | null {
return registryCreateSupportedClient(device) ?? bridgeRazerClient(device);
}

/** A keyboard client, when the device is a Wuque Studio board rather than a mouse. */
export type KeyboardClient = WuqueKeyboardClient;
export type { KeyboardClient };

/** True when the client drives a keyboard rather than a mouse. */
export function isKeyboardClient(client: SupportedClient | KeyboardClient): client is KeyboardClient {
return client instanceof WuqueKeyboardClient || client instanceof KochiKeyboardClient;
}

/**
* Map an authorized HID device to a keyboard client. Keyboards are never
* `SupportedClient`s: the mouse registry does not know them, and faking a
* `MouseStatus` would surface mouse cards for a device that has none.
*/
export function createKeyboardClient(device: HIDDevice): KeyboardClient | null {
if (KochiKeyboardClient.isSupported(device)) return new KochiKeyboardClient(device);
return WuqueKeyboardClient.isSupported(device) ? new WuqueKeyboardClient(device) : null;
}

Expand Down
Loading
Loading