Initial release of 8BitDo Control Center

Provide a Linux desktop controller configuration app with evdev input, uinput remapping, profiles, calibration, and reconnect recovery.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-21 12:18:10 -07:00
commit 6507f5f8b3
121 changed files with 15415 additions and 0 deletions

141
src/pages/InputTester.tsx Normal file
View File

@@ -0,0 +1,141 @@
import { clsx } from "clsx";
import { useState } from "react";
import { BUTTON_CAPABILITIES } from "@/constants/controls";
import { Badge } from "@/components/common/Badge";
import { Button } from "@/components/common/Button";
import { Panel } from "@/components/common/Panel";
import { ControllerDiagram, type ControllerView } from "@/components/controller/ControllerDiagram";
import { PageShell } from "@/components/layout/PageShell";
import { EventLog } from "@/components/visualizations/EventLog";
import { mockControllerBackend } from "@/services/mockControllerBackend";
import { useControllerStore } from "@/stores/controllerStore";
import { useProfileStore } from "@/stores/profileStore";
export function InputTester() {
const [view, setView] = useState<ControllerView>("front");
const activeController = useControllerStore((s) => s.activeController());
const liveState = useControllerStore((s) => s.liveState);
const eventLog = useControllerStore((s) => s.eventLog);
const pollRateHz = useControllerStore((s) => s.pollRateHz);
const latencyEstimateMs = useControllerStore((s) => s.latencyEstimateMs);
const clearEventLog = useControllerStore((s) => s.clearEventLog);
const activeProfile = useProfileStore((s) => s.activeProfile);
const buttons = liveState?.buttons ?? {};
const axes = liveState?.axes ?? {};
const hasGyro = activeController?.capabilities.hasGyro ?? false;
return (
<PageShell title="Input Tester" description="Live view of every raw and processed input event.">
<div className="grid grid-cols-3 gap-5">
<Panel title="Session info">
<dl className="grid grid-cols-2 gap-y-2 text-sm">
<dt className="text-fg-muted">Active profile</dt>
<dd className="text-right font-medium text-fg">{activeProfile?.name ?? "—"}</dd>
<dt className="text-fg-muted">Poll rate</dt>
<dd className="text-right font-mono text-fg">{pollRateHz ? `${pollRateHz} Hz` : "—"}</dd>
<dt className="text-fg-muted">Latency estimate</dt>
<dd className="text-right font-mono text-fg">{latencyEstimateMs ? `${latencyEstimateMs} ms` : "—"}</dd>
<dt className="text-fg-muted">Controller</dt>
<dd className="text-right text-fg">{activeController ? "Connected" : "None"}</dd>
</dl>
</Panel>
<Panel title="Analog sticks">
<dl className="grid grid-cols-2 gap-y-1.5 font-mono text-xs">
<dt className="text-fg-muted">left_stick_x</dt>
<dd className="text-right text-fg">{(axes.left_stick_x ?? 0).toFixed(3)}</dd>
<dt className="text-fg-muted">left_stick_y</dt>
<dd className="text-right text-fg">{(axes.left_stick_y ?? 0).toFixed(3)}</dd>
<dt className="text-fg-muted">right_stick_x</dt>
<dd className="text-right text-fg">{(axes.right_stick_x ?? 0).toFixed(3)}</dd>
<dt className="text-fg-muted">right_stick_y</dt>
<dd className="text-right text-fg">{(axes.right_stick_y ?? 0).toFixed(3)}</dd>
</dl>
</Panel>
<Panel title="Triggers & gyroscope">
<dl className="grid grid-cols-2 gap-y-1.5 font-mono text-xs">
<dt className="text-fg-muted">left_trigger</dt>
<dd className="text-right text-fg">{(axes.left_trigger ?? 0).toFixed(3)}</dd>
<dt className="text-fg-muted">right_trigger</dt>
<dd className="text-right text-fg">{(axes.right_trigger ?? 0).toFixed(3)}</dd>
{hasGyro ? (
<>
<dt className="text-fg-muted">gyro_x</dt>
<dd className="text-right text-fg">{(axes.gyro_x ?? 0).toFixed(3)}</dd>
<dt className="text-fg-muted">gyro_y</dt>
<dd className="text-right text-fg">{(axes.gyro_y ?? 0).toFixed(3)}</dd>
<dt className="text-fg-muted">gyro_z</dt>
<dd className="text-right text-fg">{(axes.gyro_z ?? 0).toFixed(3)}</dd>
</>
) : (
<dd className="col-span-2 text-fg-subtle">No gyroscope detected</dd>
)}
</dl>
</Panel>
<Panel
title="Controller diagram"
description={activeController?.isSimulated ? "Click a control to simulate a press." : undefined}
className="col-span-2"
actions={
<div className="flex gap-1.5">
<Button size="sm" variant={view === "front" ? "primary" : "secondary"} onClick={() => setView("front")}>
Front
</Button>
<Button size="sm" variant={view === "rear" ? "primary" : "secondary"} onClick={() => setView("rear")}>
Rear
</Button>
</div>
}
>
<div className="mx-auto aspect-[480/280] max-w-lg">
<ControllerDiagram
view={view}
buttons={buttons}
axes={axes}
onSelect={(id) => mockControllerBackend.pulseButton(id)}
/>
</div>
</Panel>
<Panel title="Buttons">
<div className="grid grid-cols-3 gap-2">
{BUTTON_CAPABILITIES.map((button) => {
const pressed = Boolean(buttons[button.id]);
return (
<button
key={button.id}
onClick={() => mockControllerBackend.pulseButton(button.id)}
className={clsx(
"cursor-pointer rounded-lg border px-2 py-1.5 text-center text-xs font-medium transition-colors duration-100",
pressed ? "border-accent bg-accent text-accent-fg" : "border-border bg-surface-2 text-fg-muted hover:border-border-strong",
)}
>
{button.label}
</button>
);
})}
</div>
</Panel>
<Panel
title="Event log"
className="col-span-3"
actions={
<Badge tone="neutral">
<Button size="sm" variant="ghost" onClick={clearEventLog}>
Clear
</Button>
</Badge>
}
>
<EventLog events={eventLog} />
</Panel>
</div>
</PageShell>
);
}