Added proper code editor to the commands list

This commit is contained in:
Jono Targett 2026-03-16 23:56:35 +10:30
parent 300b1de7de
commit d8cdbcd0cc
4 changed files with 358 additions and 10 deletions

View File

@ -8,9 +8,16 @@
"name": "primevue-vite-quickstart",
"version": "0.0.0",
"dependencies": {
"@codemirror/autocomplete": "^6.20.1",
"@codemirror/commands": "^6.10.3",
"@codemirror/lang-json": "^6.0.2",
"@codemirror/state": "^6.6.0",
"@codemirror/theme-one-dark": "^6.1.3",
"@codemirror/view": "^6.40.0",
"@primeuix/themes": "^1.0.0",
"@primevue/core": "^4.2.5",
"chart.js": "^4.4.7",
"codemirror": "^6.0.2",
"mqtt": "^5.15.0",
"primeicons": "^7.0.0",
"primevue": "^4.2.5",
@ -78,6 +85,109 @@
"node": ">=6.9.0"
}
},
"node_modules/@codemirror/autocomplete": {
"version": "6.20.1",
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.1.tgz",
"integrity": "sha512-1cvg3Vz1dSSToCNlJfRA2WSI4ht3K+WplO0UMOgmUYPivCyy2oueZY6Lx7M9wThm7SDUBViRmuT+OG/i8+ON9A==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.17.0",
"@lezer/common": "^1.0.0"
}
},
"node_modules/@codemirror/commands": {
"version": "6.10.3",
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.3.tgz",
"integrity": "sha512-JFRiqhKu+bvSkDLI+rUhJwSxQxYb759W5GBezE8Uc8mHLqC9aV/9aTC7yJSqCtB3F00pylrLCwnyS91Ap5ej4Q==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.6.0",
"@codemirror/view": "^6.27.0",
"@lezer/common": "^1.1.0"
}
},
"node_modules/@codemirror/lang-json": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.2.tgz",
"integrity": "sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@lezer/json": "^1.0.0"
}
},
"node_modules/@codemirror/language": {
"version": "6.12.2",
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.2.tgz",
"integrity": "sha512-jEPmz2nGGDxhRTg3lTpzmIyGKxz3Gp3SJES4b0nAuE5SWQoKdT5GoQ69cwMmFd+wvFUhYirtDTr0/DRHpQAyWg==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.23.0",
"@lezer/common": "^1.5.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0",
"style-mod": "^4.0.0"
}
},
"node_modules/@codemirror/lint": {
"version": "6.9.5",
"resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.5.tgz",
"integrity": "sha512-GElsbU9G7QT9xXhpUg1zWGmftA/7jamh+7+ydKRuT0ORpWS3wOSP0yT1FOlIZa7mIJjpVPipErsyvVqB9cfTFA==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.35.0",
"crelt": "^1.0.5"
}
},
"node_modules/@codemirror/search": {
"version": "6.6.0",
"resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.6.0.tgz",
"integrity": "sha512-koFuNXcDvyyotWcgOnZGmY7LZqEOXZaaxD/j6n18TCLx2/9HieZJ5H6hs1g8FiRxBD0DNfs0nXn17g872RmYdw==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.37.0",
"crelt": "^1.0.5"
}
},
"node_modules/@codemirror/state": {
"version": "6.6.0",
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.6.0.tgz",
"integrity": "sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ==",
"license": "MIT",
"dependencies": {
"@marijn/find-cluster-break": "^1.0.0"
}
},
"node_modules/@codemirror/theme-one-dark": {
"version": "6.1.3",
"resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.3.tgz",
"integrity": "sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"@lezer/highlight": "^1.0.0"
}
},
"node_modules/@codemirror/view": {
"version": "6.40.0",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.40.0.tgz",
"integrity": "sha512-WA0zdU7xfF10+5I3HhUUq3kqOx3KjqmtQ9lqZjfK7jtYk4G72YW9rezcSywpaUMCWOMlq+6E0pO1IWg1TNIhtg==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.6.0",
"crelt": "^1.0.6",
"style-mod": "^4.1.0",
"w3c-keyname": "^2.2.4"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
@ -524,6 +634,47 @@
"integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
"license": "MIT"
},
"node_modules/@lezer/common": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.1.tgz",
"integrity": "sha512-6YRVG9vBkaY7p1IVxL4s44n5nUnaNnGM2/AckNgYOnxTG2kWh1vR8BMxPseWPjRNpb5VtXnMpeYAEAADoRV1Iw==",
"license": "MIT"
},
"node_modules/@lezer/highlight": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz",
"integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.3.0"
}
},
"node_modules/@lezer/json": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.3.tgz",
"integrity": "sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0"
}
},
"node_modules/@lezer/lr": {
"version": "1.4.8",
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.8.tgz",
"integrity": "sha512-bPWa0Pgx69ylNlMlPvBPryqeLYQjyJjqPx+Aupm5zydLIF3NE+6MMLT8Yi23Bd9cif9VS00aUebn+6fDIGBcDA==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.0.0"
}
},
"node_modules/@marijn/find-cluster-break": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz",
"integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==",
"license": "MIT"
},
"node_modules/@primeuix/styled": {
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/@primeuix/styled/-/styled-0.7.4.tgz",
@ -1289,6 +1440,21 @@
"fsevents": "~2.3.2"
}
},
"node_modules/codemirror": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz",
"integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/commands": "^6.0.0",
"@codemirror/language": "^6.0.0",
"@codemirror/lint": "^6.0.0",
"@codemirror/search": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0"
}
},
"node_modules/commist": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/commist/-/commist-3.2.0.tgz",
@ -1331,6 +1497,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/crelt": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
"license": "MIT"
},
"node_modules/csstype": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
@ -2019,6 +2191,12 @@
"safe-buffer": "~5.2.0"
}
},
"node_modules/style-mod": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz",
"integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==",
"license": "MIT"
},
"node_modules/tinyglobby": {
"version": "0.2.15",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
@ -2287,6 +2465,12 @@
}
}
},
"node_modules/w3c-keyname": {
"version": "2.2.8",
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
"license": "MIT"
},
"node_modules/webpack-virtual-modules": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz",

View File

@ -8,9 +8,16 @@
"preview": "vite preview"
},
"dependencies": {
"@codemirror/autocomplete": "^6.20.1",
"@codemirror/commands": "^6.10.3",
"@codemirror/lang-json": "^6.0.2",
"@codemirror/state": "^6.6.0",
"@codemirror/theme-one-dark": "^6.1.3",
"@codemirror/view": "^6.40.0",
"@primeuix/themes": "^1.0.0",
"@primevue/core": "^4.2.5",
"chart.js": "^4.4.7",
"codemirror": "^6.0.2",
"mqtt": "^5.15.0",
"primeicons": "^7.0.0",
"primevue": "^4.2.5",

View File

@ -7,6 +7,22 @@ import AccordionPanel from 'primevue/accordionpanel';
import AccordionHeader from 'primevue/accordionheader';
import AccordionContent from 'primevue/accordioncontent';
import { nextTick } from 'vue'
import {basicSetup} from "codemirror"
import { oneDark } from "@codemirror/theme-one-dark"
import { EditorView } from "@codemirror/view"
import { EditorState, StateEffect } from "@codemirror/state"
const lightTheme = EditorView.theme({}, { dark: false })
const darkTheme = oneDark //EditorView.theme({}, { dark: true })
import { json } from "@codemirror/lang-json"
const editorContainers = reactive({})
const editors = reactive({})
const openTabs = ref([])
const props = defineProps({
deviceId: String
})
@ -45,6 +61,91 @@ function sendCommand(cmd) {
mqtt2.publish(topic, cmd.input || '{}')
}
function setEditorRef(name, el) {
if (el) {
editorContainers[name] = el
}
}
function onAccordionChange(value) {
const opened = Array.isArray(value) ? value : [value]
opened.forEach(name => {
if (editors[name]) return
const cmd = commands.value.find(c => c.name === name)
if (!cmd) return
const el = editorContainers[name]
if (!el) return
nextTick(() => createEditor(cmd, el))
})
}
function createEditor(cmd, el) {
if (editors[cmd.name]) return
const initialDoc =
cmd.input ||
cmd.schema ||
''
const state = EditorState.create({
doc: initialDoc,
extensions: baseExtensions(currentThemeExtension)
})
const view = new EditorView({
state,
parent: el
})
editors[cmd.name] = view
}
function baseExtensions(theme) {
return [
basicSetup,
json(),
theme,
EditorView.updateListener.of(update => {
if (update.docChanged) {
const name = update.view.dom.dataset.command
if (name && commandMap[name]) {
commandMap[name].input = update.state.doc.toString()
}
}
})
]
}
function updateEditorTheme() {
const dark = isDarkMode()
currentThemeExtension = dark ? darkTheme : lightTheme
Object.values(editors).forEach(view => {
view.dispatch({
effects: StateEffect.reconfigure.of(baseExtensions(currentThemeExtension))
})
})
}
function isDarkMode() {
return document.documentElement.classList.contains('p-dark')
}
let currentThemeExtension = isDarkMode() ? darkTheme : lightTheme
watch(() => props.deviceId, () => {
Object.keys(commandMap).forEach(k => delete commandMap[k])
commands.value = []
@ -54,8 +155,6 @@ onMounted(() => {
mqtt2.subscribe('device/+/command/#', (payload, topic) => {
console.log(topic)
const parts = topic.split('/')
const device = parts[1]
@ -68,11 +167,21 @@ onMounted(() => {
updateCommand(device, command, 'description', payload)
}
if (field === 'schema') {
updateCommand(device, command, 'schema', payload)
const pretty = JSON.stringify(JSON.parse(payload), null, 2)
updateCommand(device, command, 'schema', pretty)
}
})
const observer = new MutationObserver(() => {
updateEditorTheme()
})
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['class']
})
})
</script>
@ -90,7 +199,7 @@ onMounted(() => {
</template>
<template #empty> No commands available.</template>
<template #list="slotProps">
<Accordion>
<Accordion v-model:value="openTabs" @update:value="onAccordionChange">
<AccordionPanel
v-for="cmd in slotProps.items"
@ -107,11 +216,9 @@ onMounted(() => {
<AccordionContent>
<div class="command-body">
<Textarea
v-model="cmd.input"
rows="4"
style="width:100%"
:placeholder="cmd.schema || 'JSON arguments...'"
<div
class="json-editor"
:ref="el => setEditorRef(cmd.name, el)"
/>
<Button
@ -153,4 +260,53 @@ onMounted(() => {
padding-top: 0.5rem;
}
.cm-editor {
height: auto;
}
.json-editor {
border: 1px solid var(--surface-border);
border-radius: var(--border-radius);
background: var(--surface-card);
}
/* editor root */
.json-editor .cm-editor {
font-family: var(--font-family);
font-size: 0.9rem;
}
/* content area */
.json-editor .cm-content {
padding: 0.75rem;
caret-color: var(--text-color);
}
/* background */
.json-editor .cm-scroller {
background: var(--surface-card);
color: var(--text-color);
}
/* cursor */
.json-editor .cm-cursor {
border-left: 2px solid var(--primary-color);
}
/* selection */
.json-editor .cm-selectionBackground {
background: var(--highlight-bg);
}
/* gutters if enabled later */
.json-editor .cm-gutters {
background: var(--surface-ground);
border-right: 1px solid var(--surface-border);
}
.json-editor .cm-editor.cm-focused {
outline: 1px solid var(--primary-color);
outline-offset: 0;
}
</style>

View File

@ -3,6 +3,7 @@ import asyncio
import inspect
import paho
import signal
import json
from dataclasses import dataclass
from command import (
@ -63,7 +64,7 @@ class MQTTHandler:
for name, command in self.get_available_commands().items():
await self.mqtt_client.publish(
f"{self.command_topic}/{command.name}/schema",
str(command.schema),
json.dumps(command.schema),
qos=1,
retain=True,
)