aboutsummaryrefslogtreecommitdiffstats
path: root/appearance/Steps.qml
blob: 5bab15834a90a40e7705a1f21590903c3ef0039d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
// 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

// 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, blocks: [{ cmd, text }] }], sorted by name.
    property var tabs: []
    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");
                    return { name: rec.slice(0, nl), blocks: root.parse(rec.slice(nl + 1)) };
                });
                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";
    }
}