mirror of
https://github.com/CharaChorder/DeviceManager.git
synced 2026-01-19 08:22:53 +00:00
feat: code sandbox
[deploy]
This commit is contained in:
@@ -58,6 +58,11 @@ const de = {
|
||||
TITLE: "Einstellungen",
|
||||
},
|
||||
},
|
||||
plugin: {
|
||||
editor: {
|
||||
RUN: "Ausführen",
|
||||
},
|
||||
},
|
||||
} satisfies Translation
|
||||
|
||||
export default de
|
||||
|
||||
@@ -56,6 +56,11 @@ const en = {
|
||||
TITLE: "Settings",
|
||||
},
|
||||
},
|
||||
plugin: {
|
||||
editor: {
|
||||
RUN: "Run",
|
||||
},
|
||||
},
|
||||
} satisfies BaseTranslation
|
||||
|
||||
export default en
|
||||
|
||||
186
src/routes/plugin/+page.svelte
Normal file
186
src/routes/plugin/+page.svelte
Normal file
@@ -0,0 +1,186 @@
|
||||
<script lang="ts">
|
||||
import {KEYMAP_CODES} from "$lib/serial/keymap-codes"
|
||||
import {onMount} from "svelte"
|
||||
import {basicSetup, EditorView} from "codemirror"
|
||||
import {javascript, javascriptLanguage} from "@codemirror/lang-javascript"
|
||||
import {defaultKeymap} from "@codemirror/commands"
|
||||
import {keymap} from "@codemirror/view"
|
||||
import {HighlightStyle, syntaxHighlighting} from "@codemirror/language"
|
||||
import {tags} from "@lezer/highlight"
|
||||
import LL from "../../i18n/i18n-svelte"
|
||||
import type {CompletionContext} from "@codemirror/autocomplete"
|
||||
import {serialPort} from "$lib/serial/connection"
|
||||
import type {CharaDevice} from "$lib/serial/device"
|
||||
import examplePlugin from "./example-plugin.js?raw"
|
||||
|
||||
let theme = EditorView.baseTheme({
|
||||
".cm-editor .cm-content": {
|
||||
fontFamily: '"Noto Sans Mono", monospace',
|
||||
},
|
||||
".cm-FoldPlaceholder": {
|
||||
backgroundColor: "var(--md-sys-color-surface-variant)",
|
||||
color: "var(--md-sys-color-on-surface-variant)",
|
||||
},
|
||||
".cm-gutters": {
|
||||
backgroundColor: "var(--md-sys-color-surface-variant)",
|
||||
color: "var(--md-sys-color-on-surface-variant)",
|
||||
borderColor: "var(--md-sys-color-outline)",
|
||||
},
|
||||
".cm-activeLineGutter": {
|
||||
backgroundColor: "var(--md-sys-color-tertiary)",
|
||||
color: "var(--md-sys-color-on-tertiary)",
|
||||
},
|
||||
".cm-activeLine": {
|
||||
backgroundColor: "transparent",
|
||||
},
|
||||
".cm-cursor": {
|
||||
borderColor: "var(--md-sys-color-on-background)",
|
||||
},
|
||||
".cm-selectionBackground": {
|
||||
background: "transparent !important",
|
||||
backdropFilter: "invert(0.3)",
|
||||
},
|
||||
})
|
||||
const highlightStyle = HighlightStyle.define(
|
||||
[
|
||||
{tag: tags.keyword, color: "var(--md-sys-color-primary)"},
|
||||
{tag: tags.number, color: "var(--md-sys-color-secondary)"},
|
||||
{tag: tags.string, color: "var(--md-sys-color-tertiary)"},
|
||||
{tag: tags.comment, color: "var(--md-sys-color-on-background)", opacity: 0.6},
|
||||
],
|
||||
{
|
||||
all: {fontFamily: '"Noto Sans Mono", monospace', fontSize: "14px"},
|
||||
},
|
||||
)
|
||||
const completion = javascriptLanguage.data.of({
|
||||
autocomplete: function completeGlobals(context: CompletionContext) {
|
||||
if (context.matchBefore(/Chara\./)) {
|
||||
// TODO
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
editorView = new EditorView({
|
||||
extensions: [
|
||||
basicSetup,
|
||||
javascript(),
|
||||
keymap.of(defaultKeymap),
|
||||
theme,
|
||||
syntaxHighlighting(highlightStyle),
|
||||
completion,
|
||||
],
|
||||
parent: editor,
|
||||
doc: examplePlugin,
|
||||
})
|
||||
})
|
||||
|
||||
const charaMethods = [
|
||||
"reboot",
|
||||
"bootloader",
|
||||
"getRamBytesAvailable",
|
||||
"getSetting",
|
||||
"setSetting",
|
||||
"getLayoutKey",
|
||||
"setLayoutKey",
|
||||
"deleteChord",
|
||||
"setChord",
|
||||
"getChordPhrase",
|
||||
"getChordCount",
|
||||
"getChord",
|
||||
"send",
|
||||
] satisfies Array<keyof CharaDevice>
|
||||
$: channels = $serialPort
|
||||
? ({
|
||||
getVersion: async (..._args: unknown[]) => $serialPort.version,
|
||||
getDevice: async (..._args: unknown[]) => $serialPort.device,
|
||||
commit: async (..._args: unknown[]) => {
|
||||
if (
|
||||
confirm(
|
||||
"Perform a commit? Settings are already applied until the next reboot.\n\n" +
|
||||
"Excessive commits can lead to premature breakdowns, as the settings storage is only rated for 10,000-25,000 commits.\n\n" +
|
||||
"Click OK to perform the commit anyways.",
|
||||
)
|
||||
) {
|
||||
return $serialPort.commit()
|
||||
}
|
||||
},
|
||||
...Object.fromEntries(charaMethods.map(it => [it, $serialPort[it].bind($serialPort)] as const)),
|
||||
} satisfies Record<string, Function>)
|
||||
: ({} as any)
|
||||
|
||||
async function onMessage(event: MessageEvent) {
|
||||
if (event.origin !== "null" || event.source !== frame.contentWindow) return
|
||||
|
||||
const [channel, params] = event.data
|
||||
const response = channels[channel as keyof typeof channels](...params)
|
||||
frame.contentWindow!.postMessage({response: await response}, "*")
|
||||
}
|
||||
|
||||
function runPlugin() {
|
||||
frame.contentWindow?.postMessage(
|
||||
{
|
||||
actionCodes: KEYMAP_CODES,
|
||||
script: editorView.state.doc.toString(),
|
||||
charaChannels: Object.keys(channels),
|
||||
},
|
||||
"*",
|
||||
)
|
||||
}
|
||||
|
||||
let frame: HTMLIFrameElement
|
||||
let editor: HTMLDivElement
|
||||
let editorView: EditorView
|
||||
</script>
|
||||
|
||||
<svelte:window on:message={onMessage} />
|
||||
<section>
|
||||
<button on:click={runPlugin}><span class="icon">play_arrow</span>{$LL.plugin.editor.RUN()}</button>
|
||||
<div class="editor-root" bind:this={editor} />
|
||||
</section>
|
||||
|
||||
<iframe
|
||||
aria-hidden="true"
|
||||
title="code sandbox"
|
||||
bind:this={frame}
|
||||
src="/sandbox.html"
|
||||
sandbox="allow-scripts"
|
||||
/>
|
||||
|
||||
<style lang="scss">
|
||||
section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
iframe {
|
||||
display: none;
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: min-content;
|
||||
padding-inline-start: 0;
|
||||
padding-inline-end: 8px;
|
||||
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: var(--md-sys-color-on-primary);
|
||||
|
||||
background: var(--md-sys-color-primary);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.editor-root {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
31
src/routes/plugin/example-plugin.js
Normal file
31
src/routes/plugin/example-plugin.js
Normal file
@@ -0,0 +1,31 @@
|
||||
/*******************************
|
||||
* HOLD UP AND READ THIS FIRST *
|
||||
*******************************
|
||||
*
|
||||
* Chara devices have a LIMITED number of commits.
|
||||
* calling `Chara.commit()` can be a dangerous operation, which is why a confirmation dialog will be shown.
|
||||
* Devices are only rated for 10,000-25,000 commits, exceeding that limit may result in premature breakdowns.
|
||||
* `Chara.setSetting` or `Chara.setLayoutKey` is not affected by this, they last however only until the next boot.
|
||||
*
|
||||
* Chord writing is more forgiving, but keep in mind that excessive large-scale writing can still damage the device.
|
||||
*
|
||||
*/
|
||||
|
||||
const count = await Chara.getChordCount() // => 499
|
||||
const chord = await Chara.getChord(2) // => {actions: [1, 2, 3], phrase: [4, 5, 6]}
|
||||
|
||||
const setting = await Chara.getSetting(5) // => 0
|
||||
|
||||
// This, for example, would return all chords
|
||||
const chords = []
|
||||
for (let i = 0; i < count; i++) {
|
||||
chords.push(await Chara.getChord(i))
|
||||
}
|
||||
|
||||
// You can also print values to the browser console (F12)
|
||||
console.log("Chords:", chords)
|
||||
|
||||
// You can access the actions by ID!
|
||||
Actions.SPACE // => {id: "SPACE", code: 32, icon: "space_bar", description: ...}
|
||||
Actions[32] // This also works
|
||||
Actions[0x20] // Or this!
|
||||
Reference in New Issue
Block a user