// 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 // The manual steps unified-desktop-theme cannot run itself: root copies into // /boot and /usr/share/sddm, the finish on the homepage host, and the last // install.sh log. Each is a file in ~/.cache/udt/steps/, written by // udt-steps, which then calls this shell's `steps` IPC verb. Nothing is // watched: the poke is the signal, and a shell started later finds the files // on its first reload. Singleton { id: root readonly property string dir: `${Quickshell.env("HOME")}/.cache/udt/steps` // [{ name, label, blocks: [{ cmd, text }] }]: the install.sh recap first, // then one tab per consumer by name. A file with no label here shows // under its own name, so a new producer needs no edit. property var tabs: [] readonly property var labels: ({ install: "General recap", grub: "GRUB", sddm: "SDDM", homepage: "Homepage", }) signal loaded // A line indented two spaces is a command, and a run of them is one block // with its own copy button. Blank lines split prose into paragraphs. The // format is what the producers already print to a terminal. function parse(text) { const out = []; const lines = text.split("\n"); for (let i = 0; i < lines.length; i++) { const line = lines[i]; const cmd = line.startsWith(" "); const last = out.length ? out[out.length - 1] : null; if (!cmd && line.trim() === "") { if (last) last.closed = true; continue; } const body = cmd ? line.slice(2) : line; if (last && last.cmd === cmd && !last.closed) last.lines.push(body); else out.push({ cmd: cmd, lines: [body], closed: false }); } return out.map(b => ({ cmd: b.cmd, text: b.lines.join("\n") })); } function commands(tab) { return tab.blocks.filter(b => b.cmd).map(b => b.text).join("\n"); } function copy(text) { Quickshell.execDetached(["wl-copy", "--", text]); } function refresh() { listProc.running = false; listProc.running = true; } function dismiss(name) { rmProc.command = ["rm", "-f", `${root.dir}/${name}`]; rmProc.running = true; } // One pass over every file, each prefixed by an ASCII record separator // and its name. The names are udt-steps' [a-z0-9-], and its temporary // files are dotfiles, which the glob skips. Process { id: listProc command: ["sh", "-c", `cd ${JSON.stringify(root.dir)} 2>/dev/null || exit 0; ` + `for f in *; do [ -f "$f" ] && printf '\\036%s\\n' "$f" && cat "$f"; done`] stdout: StdioCollector { onStreamFinished: { root.tabs = text.split("\x1e").filter(s => s.length).map(rec => { const nl = rec.indexOf("\n"); const name = rec.slice(0, nl); return { name: name, label: root.labels[name] ?? name, blocks: root.parse(rec.slice(nl + 1)) }; }).sort((a, b) => (b.name === "install") - (a.name === "install") || a.label.localeCompare(b.label)); root.loaded(); } } } Process { id: rmProc onExited: root.refresh() } function selftest(): string { const b = root.parse("Intro line\nstill intro\n cmd one\n cmd two\n\nNext para\n cmd three\n"); if (b.length !== 4 || b[0].cmd || b[0].text !== "Intro line\nstill intro" || !b[1].cmd || b[1].text !== "cmd one\ncmd two" || b[2].text !== "Next para" || b[3].text !== "cmd three") return `SELFTEST Steps FAIL: ${JSON.stringify(b)}`; if (root.commands({ blocks: b }) !== "cmd one\ncmd two\ncmd three") return "SELFTEST Steps FAIL: commands"; return "SELFTEST Steps PASS"; } }