7 Commits

Author SHA1 Message Date
b7c8ebfb3c feat: adjust chunking 2026-01-30 18:13:29 +01:00
632297d266 feat: change timing 2026-01-30 18:06:43 +01:00
0ee7e02c53 feat: test timeout 2026-01-30 17:43:16 +01:00
f618ffbada feat: wait ready 2026-01-30 17:38:46 +01:00
afa0d9ffd7 feat: goto terminal 2026-01-30 17:31:35 +01:00
cda2a527d9 feat: change update chunking 2026-01-30 17:26:31 +01:00
1ca2a70bc1 feat: changes 2026-01-30 17:04:36 +01:00
9 changed files with 152 additions and 73 deletions

View File

@@ -10,19 +10,15 @@ import Action from "$lib/components/Action.svelte";
import type { Range } from "@codemirror/state"; import type { Range } from "@codemirror/state";
import { parsedChordsField } from "./parsed-chords-plugin"; import { parsedChordsField } from "./parsed-chords-plugin";
import { iterActions } from "./parse-meta"; import { iterActions } from "./parse-meta";
import type { KeyInfo } from "$lib/serial/keymap-codes";
export class ActionWidget extends WidgetType { export class ActionWidget extends WidgetType {
component?: {}; component?: {};
constructor(readonly id: string | number) { constructor(readonly info: KeyInfo) {
super(); super();
this.id = id;
} }
/*override eq(other: ActionWidget) {
return this.id == other.id;
}*/
toDOM() { toDOM() {
if (this.component) { if (this.component) {
unmount(this.component); unmount(this.component);
@@ -32,15 +28,16 @@ export class ActionWidget extends WidgetType {
this.component = mount(Action, { this.component = mount(Action, {
target: element, target: element,
props: { action: this.id, display: "keys", inText: true }, props: {
action: this.info,
display: "keys",
inText: true,
withPopover: false,
},
}); });
return element; return element;
} }
override ignoreEvent() {
return true;
}
override destroy() { override destroy() {
if (this.component) { if (this.component) {
unmount(this.component); unmount(this.component);
@@ -63,7 +60,7 @@ function actionWidgets(view: EditorView) {
} }
if (action.info && action.explicit) { if (action.info && action.explicit) {
const deco = Decoration.replace({ const deco = Decoration.replace({
widget: new ActionWidget(action.code), widget: new ActionWidget(action.info),
}); });
widgets.push(deco.range(action.range[0], action.range[1])); widgets.push(deco.range(action.range[0], action.range[1]));
} }

View File

@@ -0,0 +1,41 @@
import { hoverTooltip } from "@codemirror/view";
import { parsedChordsField } from "./parsed-chords-plugin";
import { type ActionMeta, iterActions } from "./parse-meta";
import { mount, unmount } from "svelte";
import ActionTooltip from "$lib/components/action/ActionTooltip.svelte";
function inRange(pos: number, side: 1 | -1, range: [number, number]) {
if (side < 0) {
return pos > range[0] && pos <= range[1];
} else {
return pos >= range[0] && pos < range[1];
}
}
export const actionHover = hoverTooltip((view, pos, side) => {
const chord = view.state
.field(parsedChordsField)
.chords.find((chord) => inRange(pos, side, chord.range));
if (!chord) return null;
let action = iterActions<ActionMeta>(chord, (action) =>
inRange(pos, side, action.range) ? action : undefined,
);
if (!action?.info) return null;
return {
pos: action.range[0],
end: action.range[1],
create() {
const dom = document.createElement("div");
const element = mount(ActionTooltip, {
target: dom,
props: { info: action.info, valid: true },
});
return {
dom,
destroy() {
unmount(element);
},
};
},
};
});

View File

@@ -152,25 +152,35 @@ export function mapParseResult(
}; };
} }
export function iterActions( export function iterActions<T = void>(
chord: ChordMeta, chord: ChordMeta,
callback: (action: ActionMeta) => void, callback: (action: ActionMeta) => T | void,
) { ): T | undefined {
if (chord.input) { if (chord.input) {
for (const action of chord.input.actions) { for (const action of chord.input.actions) {
callback(action); const result = callback(action);
if (result !== undefined) {
return result;
}
} }
} }
if (chord.compounds) { if (chord.compounds) {
for (const compound of chord.compounds) { for (const compound of chord.compounds) {
for (const action of compound.actions) { for (const action of compound.actions) {
callback(action); const result = callback(action);
if (result !== undefined) {
return result;
}
} }
} }
} }
if (chord.phrase) { if (chord.phrase) {
for (const action of chord.phrase.actions) { for (const action of chord.phrase.actions) {
callback(action); const result = callback(action);
if (result !== undefined) {
return result;
}
} }
} }
return undefined;
} }

View File

@@ -29,6 +29,7 @@ import { actionMetaPlugin } from "./action-meta-plugin";
import { parsedChordsField } from "./parsed-chords-plugin"; import { parsedChordsField } from "./parsed-chords-plugin";
import { changesPanel } from "./changes-panel.svelte"; import { changesPanel } from "./changes-panel.svelte";
import { searchKeymap } from "@codemirror/search"; import { searchKeymap } from "@codemirror/search";
import { actionHover } from "./action-tooltip";
const serializedFields = { const serializedFields = {
history: historyField, history: historyField,
@@ -47,6 +48,7 @@ export function createConfig(params: EditorConfig) {
actionMetaPlugin.plugin, actionMetaPlugin.plugin,
deviceChordField, deviceChordField,
parsedChordsField, parsedChordsField,
actionHover,
changesPanel(), changesPanel(),
lintGutter(), lintGutter(),
params.rawCode ? [lineNumbers()] : [delimPlugin, actionPlugin], params.rawCode ? [lineNumbers()] : [delimPlugin, actionPlugin],

View File

@@ -4,17 +4,20 @@
import { osLayout } from "$lib/os-layout"; import { osLayout } from "$lib/os-layout";
import { isVerbose } from "./verbose-action"; import { isVerbose } from "./verbose-action";
import { actionTooltip } from "$lib/title"; import { actionTooltip } from "$lib/title";
import ActionTooltip from "./action/ActionTooltip.svelte";
let { let {
action, action,
display, display,
ignoreIcon = false, ignoreIcon = false,
inText = false, inText = false,
withPopover = true,
}: { }: {
action: string | number | KeyInfo; action: string | number | KeyInfo;
display: "inline-keys" | "keys" | "verbose"; display: "inline-keys" | "keys" | "verbose";
ignoreIcon?: boolean; ignoreIcon?: boolean;
inText?: boolean; inText?: boolean;
withPopover?: boolean;
} = $props(); } = $props();
let retrievedInfo = $derived( let retrievedInfo = $derived(
@@ -35,39 +38,13 @@
let icon = $derived(ignoreIcon ? undefined : info.icon); let icon = $derived(ignoreIcon ? undefined : info.icon);
let dynamicMapping = $derived(info.keyCode && $osLayout.get(info.keyCode)); let dynamicMapping = $derived(info.keyCode && $osLayout.get(info.keyCode));
let hasPopover = $derived( let hasPopover = $derived(
!retrievedInfo || !info.id || info.title || info.description, withPopover &&
(!retrievedInfo || !info.id || info.title || info.description),
); );
</script> </script>
{#snippet popover()} {#snippet popover()}
{#if retrievedInfo} <ActionTooltip valid={!!retrievedInfo} {info} />
{#if info.icon || info.display || !info.id}
&lt;<b>{info.id ?? `0x${info.code.toString(16)}`}</b>&gt;
{/if}
{#if info.title}
{info.title}
{/if}
{#if info.variant === "left"}
(Left)
{:else if info.variant === "right"}
(Right)
{/if}
{#if info.description}
<br />
<small>{info.description}</small>
{/if}
{#if info.breaking}
<br />&nbsp;<i>Prevents prepended autospaces</i>
{/if}
{#if info.separator || info.breaking}
<br />&nbsp;<i>Stops autocorrect</i>
{/if}
{:else}
<b>Unknown Action</b><br />
{#if info.code > 1023}
This action cannot be translated and will be ingored.
{/if}
{/if}
{/snippet} {/snippet}
{#snippet kbdText()} {#snippet kbdText()}

View File

@@ -0,0 +1,34 @@
<script lang="ts">
import type { KeyInfo } from "$lib/serial/keymap-codes";
let { valid, info }: { valid: boolean; info: KeyInfo } = $props();
</script>
{#if valid}
{#if info.icon || info.display || !info.id}
&lt;<b>{info.id ?? `0x${info.code.toString(16)}`}</b>&gt;
{/if}
{#if info.title}
{info.title}
{/if}
{#if info.variant === "left"}
(Left)
{:else if info.variant === "right"}
(Right)
{/if}
{#if info.description}
<br />
<small>{info.description}</small>
{/if}
{#if info.breaking}
<br />&nbsp;<i>Prevents prepended autospaces</i>
{/if}
{#if info.separator || info.breaking}
<br />&nbsp;<i>Stops autocorrect</i>
{/if}
{:else}
<b>Unknown Action</b><br />
{#if info.code > 1023}
This action cannot be translated and will be ingored.
{/if}
{/if}

View File

@@ -1,5 +1,5 @@
import { LineBreakTransformer } from "$lib/serial/line-break-transformer"; import { LineBreakTransformer } from "$lib/serial/line-break-transformer";
import { serialLog } from "$lib/serial/connection"; import { serialLog, type SerialLogEntry } from "$lib/serial/connection";
import type { Chord } from "$lib/serial/chord"; import type { Chord } from "$lib/serial/chord";
import { import {
parseChordActions, parseChordActions,
@@ -158,7 +158,7 @@ export class CharaDevice {
constructor( constructor(
readonly port: SerialPortLike, readonly port: SerialPortLike,
public baudRate = 115200, public baudRate = navigator.userAgent.includes("Mac") ? 38400 : 115200,
) {} ) {}
async init() { async init() {
@@ -564,37 +564,48 @@ export class CharaDevice {
const writer = this.port.writable!.getWriter(); const writer = this.port.writable!.getWriter();
try { try {
await writer.write(new TextEncoder().encode(`RST OTA\r\n`)); const start = performance.now();
serialLog.update((it) => { writer.write(new TextEncoder().encode(`RST OTA\r\n`));
it.push({
type: "input",
value: "RST OTA",
});
return it;
});
// Wait for the device to be ready // Wait for the device to be ready
const signal = await this.reader.read(); const signal = await this.reader.read();
serialLog.update((it) => { const signalTime = performance.now();
it.push({
type: "output",
value: signal.value!.trim(),
});
return it;
});
const chunkSize = 128; const chunkSize = 128;
const chunks: Promise<void>[] = [];
for (let i = 0; i < file.byteLength; i += chunkSize) { for (let i = 0; i < file.byteLength; i += chunkSize) {
const chunk = file.slice(i, i + chunkSize); const size = Math.min(chunkSize, file.byteLength - i);
await writer.write(new Uint8Array(chunk)); chunks.push(
progress(i + chunk.byteLength, file.byteLength); writer
.write(new Uint8Array(file, i, size))
.then(() => progress(i + size, file.byteLength)),
);
} }
await Promise.all(chunks);
serialLog.update((it) => { serialLog.update((it) => {
it.push({ it.push(
type: "input", {
value: `...${file.byteLength} bytes`, type: "input",
}); value: "RST OTA",
},
{
type: "system",
value: `+${(signalTime - start).toFixed(0)} ms`,
},
{
type: "output",
value: signal.value!.trim(),
},
{
type: "system",
value: `+${(performance.now() - signalTime).toFixed(0)} ms`,
},
{
type: "input",
value: `...${file.byteLength} bytes`,
},
);
return it; return it;
}); });
@@ -621,9 +632,8 @@ export class CharaDevice {
}); });
} finally { } finally {
writer.releaseLock(); writer.releaseLock();
await this.suspend();
} }
await this.suspend();
} finally { } finally {
delete this.lock; delete this.lock;
resolveLock!(true); resolveLock!(true);

View File

@@ -10,6 +10,7 @@
import { showConnectionFailedDialog } from "$lib/dialogs/connection-failed-dialog"; import { showConnectionFailedDialog } from "$lib/dialogs/connection-failed-dialog";
import { onMount } from "svelte"; import { onMount } from "svelte";
import { persistentWritable } from "$lib/storage"; import { persistentWritable } from "$lib/storage";
import { goto } from "$app/navigation";
let ports = $state<SerialPort[]>([]); let ports = $state<SerialPort[]>([]);
let element: HTMLDivElement | undefined = $state(); let element: HTMLDivElement | undefined = $state();
@@ -47,6 +48,10 @@
} }
async function connectDevice(event: MouseEvent) { async function connectDevice(event: MouseEvent) {
if (event.altKey) {
goto("/terminal/");
return;
}
const port = await navigator.serial.requestPort({ const port = await navigator.serial.requestPort({
filters: event.shiftKey ? [] : [...PORT_FILTERS.values()], filters: event.shiftKey ? [] : [...PORT_FILTERS.values()],
}); });

View File

@@ -15,6 +15,7 @@
} from "$lib/serial/connection"; } from "$lib/serial/connection";
import { fade, slide } from "svelte/transition"; import { fade, slide } from "svelte/transition";
import ConnectPopup from "./ConnectPopup.svelte"; import ConnectPopup from "./ConnectPopup.svelte";
import { goto } from "$app/navigation";
let locale = $state( let locale = $state(
(browser && (localStorage.getItem("locale") as Locales)) || detectLocale(), (browser && (localStorage.getItem("locale") as Locales)) || detectLocale(),
@@ -49,6 +50,8 @@
function disconnect(event: MouseEvent) { function disconnect(event: MouseEvent) {
if (event.shiftKey) { if (event.shiftKey) {
sync(); sync();
} else if (event.altKey) {
goto("/terminal/");
} else { } else {
$serialPort?.close(); $serialPort?.close();
$serialPort = undefined; $serialPort = undefined;