// Copyright (C) 2026 Danilo M. // // 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}`); } } }