feat: ccos emulator

This commit is contained in:
2026-01-28 18:08:11 +01:00
parent ee8d400ad7
commit 16bf766de9
9 changed files with 178 additions and 41 deletions

View File

@@ -46,15 +46,6 @@
element?.closest<HTMLElement>("[popover]")?.hidePopover();
}
async function connectCC0(event: MouseEvent) {
const { fetchCCOS } = await import("$lib/ccos/ccos");
closePopover();
const ccos = await fetchCCOS();
if (ccos) {
connect(ccos, !event.shiftKey);
}
}
async function connectDevice(event: MouseEvent) {
const port = await navigator.serial.requestPort({
filters: event.shiftKey ? [] : [...PORT_FILTERS.values()],

View File

@@ -116,6 +116,25 @@
{/if}
</div>
<ul>
<li>
<a
href={import.meta.env.VITE_DISCORD_URL}
rel="noreferrer"
target="_blank"
>
<svg
class="discord-icon"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 126.64 96"
>
<path
fill="currentColor"
d="m81 0-3 7Q63 4 49 7l-4-7-26 8Q-4 45 1 80q14 10 32 16l6-11-10-5 2-2q33 13 64 0l3 2-11 5 7 11q17-5 32-16 4-40-19-72-12-5-26-8M42 65q-10-1-11-12 0-15 11-13c11 2 12 6 12 13q-1 11-12 12m42 0q-10-1-11-12 0-15 11-13c11 2 12 6 12 13q-1 11-12 12"
/></svg
>
Discord</a
>
</li>
<li>
<a href={import.meta.env.VITE_BUGS_URL} rel="noreferrer" target="_blank"
><span class="icon">bug_report</span> Bugs</a
@@ -168,6 +187,11 @@
$sync-border-radius: 16px;
.discord-icon {
margin: 5px;
inline-size: 14px;
}
.sync-box {
display: flex;
position: relative;

View File

@@ -17,6 +17,11 @@
: []),
],
[
{
href: "/editor/",
icon: "playground_2",
title: "Emulator",
},
{
href: import.meta.env.VITE_LEARN_URL,
icon: "school",

View File

@@ -5,18 +5,17 @@
import TrackChords from "$lib/charrecorder/TrackChords.svelte";
import TrackRollingWpm from "$lib/charrecorder/TrackRollingWpm.svelte";
import { fade } from "svelte/transition";
import { initSerial, serialPort } from "$lib/serial/connection";
import { tick } from "svelte";
import { ccosKeyInterceptor } from "$lib/ccos/attachment";
let recorder: ReplayRecorder = $state(new ReplayRecorder());
let replay: Replay | undefined = $state();
let wpm = $state(0);
let cc0Loading = $state(false);
let chords: InferredChord[] = $state([]);
function handleRawKey(event: KeyboardEvent) {
event.preventDefault();
keyEvent(event);
}
function keyEvent(event: KeyboardEvent) {
if (event.key === "Tab") {
clear();
@@ -47,15 +46,60 @@
a.download = "replay.json";
a.click();
}
async function connectCC0(event: MouseEvent) {
cc0Loading = true;
try {
await tick();
if ($serialPort) {
$serialPort?.close();
$serialPort = undefined;
}
const { fetchCCOS } = await import("$lib/ccos/ccos");
const ccos = await fetchCCOS();
if (ccos) {
try {
await initSerial(ccos, !event.shiftKey);
} catch (error) {
console.error(error);
}
}
} finally {
cc0Loading = false;
}
}
</script>
<svelte:head>
<title>Editor</title>
</svelte:head>
<svelte:window onkeydown={handleRawKey} onkeyup={handleRawKey} />
<section>
<h2>Editor</h2>
<h2>
CCOS Emulator
{#if $serialPort?.chipset === "WASM"}
<small>(Emulator Active)</small>
{:else}
<button class="primary" disabled={cc0Loading} onclick={connectCC0}>
<span class="icon">play_arrow</span>
Boot CCOS Emulator</button
>
{/if}
</h2>
<p style:max-width="600px">
Try a (limited) demo of CCOS running directly in your browser.<br /><span
style:color="var(--md-sys-color-primary)"
>Chording requires an <b>NKRO Keyboard</b> to work properly.</span
>
<br />Browsers usually report key timings with limited accuracy to revent
fingerprinting, which can impact chording.
<br /><i>Results may vary.</i>
<br />
Use sidebar tabs to configure <a href="/config/chords/">Chords</a>,
<a href="/config/layout/">Layout</a>
and <a href="/config/settings/">Settings</a>.
</p>
{#if replay}
<div class="replay" transition:fade={{ duration: 100 }}>
@@ -66,7 +110,9 @@
{#key recorder}
<div
class="editor"
tabindex="-1"
out:fade={{ duration: 100 }}
{@attach ccosKeyInterceptor($serialPort, recorder)}
style:opacity={replay ? 0 : undefined}
>
<CharRecorder replay={recorder.player} cursor={true} keys={true}>
@@ -95,15 +141,38 @@
width: 100%;
}
a {
display: inline;
padding: 0;
color: var(--md-sys-color-primary);
}
small {
display: inline-flex;
align-items: center;
gap: 4px;
color: var(--md-sys-color-primary);
font-weight: 500;
font-size: 0.6em;
}
button.primary {
display: inline-flex;
background: none;
color: var(--md-sys-color-primary);
}
.replay,
.editor {
position: absolute;
top: 3em;
left: 0;
transition: opacity 0.1s;
margin: 4px;
outline: 1px solid var(--md-sys-color-outline);
padding: 16px;
padding-bottom: 5em;
padding-left: 0;
&:focus-within {
outline: 2px solid var(--md-sys-color-primary);
}
}
.toolbar {