diff options
| author | Danilo M. <danix@danix.xyz> | 2026-10-21 10:11:12 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-10-21 10:11:12 +0200 |
| commit | cf63dfb7ffa306025ad2a196cb8991256aa091a8 (patch) | |
| tree | 7a35627871989508415a0a1e80030aae1a98da0b /desktop/modules/sbbuilder/Sbb.qml | |
| parent | 6e45e93e6e225015304716b934bea5dd1f3e1478 (diff) | |
| download | quickshell-cf63dfb7ffa306025ad2a196cb8991256aa091a8.tar.gz quickshell-cf63dfb7ffa306025ad2a196cb8991256aa091a8.zip | |
sbbuilder: the two config drafts
The desktop config is a watched local file; the VM one goes through the
helper's config-get and config-set over ssh, so the helper validates what
lands in root's file. Field text is typed after the key's default, the
same check sb-builder makes on load.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Diffstat (limited to 'desktop/modules/sbbuilder/Sbb.qml')
| -rw-r--r-- | desktop/modules/sbbuilder/Sbb.qml | 150 |
1 files changed, 150 insertions, 0 deletions
diff --git a/desktop/modules/sbbuilder/Sbb.qml b/desktop/modules/sbbuilder/Sbb.qml new file mode 100644 index 0000000..d623423 --- /dev/null +++ b/desktop/modules/sbbuilder/Sbb.qml @@ -0,0 +1,150 @@ +// Copyright (C) 2026 Danilo M. <danix@danix.xyz> +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +pragma Singleton + +import Quickshell +import Quickshell.Io +import QtQuick +import "Toml.js" as T + +// sb-builder's two configs. The desktop one is a local file, watched; the +// GUI watches it too, so a save applies there at once. The VM one is root's +// file on the build VM, read and written only through the helper's +// config-get and config-set, which validate it: the page never writes it +// over ssh by hand. +Singleton { + id: root + + readonly property string path: (Quickshell.env("XDG_CONFIG_HOME") || Quickshell.env("HOME") + "/.config") + + "/sb-builder/config.toml" + + // ponytail: mirrors DEFAULTS in sb-builder's sb_builder/config.py; a key + // added there needs adding here. + readonly property Store desktop: Store { + defaults: ({ + remote: { target: "buildsystem", ssh_options: [] }, + refresh: { dashboard: 30, jobs: 5 }, + notify: { enabled: true, events: ["finished", "failed", "killed", "lost", "vm-up"] }, + vm: { libvirt_uri: "qemu:///system", domain: "buildsystem", autostart: false }, + builds: { publish: true }, + hints: { nvchecker_keyfile: "~/.config/nvchecker/keys.toml" }, + repository: { path: "/repo" }, + }) + } + readonly property Store vm: Store {} + + // The VM side: reachable once a config-get answered; busy while one runs. + property bool reachable: false + property bool busy: false + property string vmPath: "" + + // From the desktop file as saved, not its draft: an unsaved target is + // not where the GUI connects. + function ssh(cmd) { + const r = T.get(root.desktop.text, "remote", "target") ?? root.desktop.defaults.remote.target; + const o = T.get(root.desktop.text, "remote", "ssh_options") ?? []; + return ["ssh", "-o", "BatchMode=yes", "-o", "ConnectTimeout=5", ...o, r, "sbb-remote", cmd]; + } + + function refresh() { + fv.reload(); + root.fetch(false); + } + + function saveDesktop() { + const d = root.desktop; + if (d.text !== d.base) { d.error = "changed on disk: Discard to load it, then redo"; return; } + if (d.draft === "") return; // FileView.setText("") writes nothing (AGENTS.md) + d.error = ""; + d.text = d.draft; + d.base = d.draft; + fv.setText(d.draft); + } + + // A save first re-reads the VM file: one changed since the draft began + // is refused rather than written over. + function saveVm() { + root.vm.error = ""; + root.fetch(true); + } + + function fetch(thenSave) { + get.thenSave = thenSave; + root.busy = true; + get.running = false; + get.running = true; + } + + FileView { + id: fv + path: root.path + atomicWrites: true + watchChanges: true + printErrors: false + onFileChanged: reload() + onLoaded: root.desktop.loaded(text()) + onLoadFailed: root.desktop.loaded("") + onSaveFailed: err => root.desktop.error = "cannot write: " + FileViewError.toString(err) + } + + Process { + id: get + property bool thenSave: false + command: root.ssh("config-get") + stdout: StdioCollector { id: getOut } + stderr: StdioCollector { id: getErr } + onExited: code => { + let d = null; + try { d = JSON.parse(getOut.text); } catch (e) {} + if (!d || d.error !== undefined) { + root.busy = false; + root.reachable = false; + root.vm.error = d ? d.error : (getErr.text.trim() || `ssh exited ${code}`); + return; + } + const defs = Object.assign({}, d.defaults); + const top = {}; + for (const k in defs) if (k !== "manifests") top[k] = defs[k]; + root.vm.defaults = { "": top, manifests: defs.manifests || {} }; + root.vmPath = d.path; + root.reachable = true; + if (!get.thenSave) { root.busy = false; root.vm.loaded(d.text); return; } + if (d.text !== root.vm.base) { + root.busy = false; + root.vm.text = d.text; + root.vm.error = "changed on the VM: Discard to load it, then redo"; + return; + } + put.running = false; + put.running = true; + } + } + + Process { + id: put + command: root.ssh("config-set") + stdinEnabled: true + stdout: StdioCollector { id: putOut } + stderr: StdioCollector { id: putErr } + onStarted: { put.write(root.vm.draft); put.stdinEnabled = false; } + onExited: code => { + root.busy = false; + put.stdinEnabled = true; + let d = null; + try { d = JSON.parse(putOut.text); } catch (e) {} + if (code === 0 && d && d.error === undefined) { + root.vm.text = root.vm.draft; + root.vm.base = root.vm.draft; + } else root.vm.error = d && d.error ? d.error : (putErr.text.trim() || `ssh exited ${code}`); + } + } +} |
