diff options
Diffstat (limited to 'desktop/modules/deckit/DeckIt.qml')
| -rw-r--r-- | desktop/modules/deckit/DeckIt.qml | 198 |
1 files changed, 198 insertions, 0 deletions
diff --git a/desktop/modules/deckit/DeckIt.qml b/desktop/modules/deckit/DeckIt.qml new file mode 100644 index 0000000..8d24917 --- /dev/null +++ b/desktop/modules/deckit/DeckIt.qml @@ -0,0 +1,198 @@ +// 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 "../sbbuilder" +import "../autostart/Autostart.js" as A + +// deck-it's user config and its autostart line. The config is a sparse +// override: deck-it merges it over the shipped config.toml key by key, so a +// key left unset here keeps deck-it's default, which `deck-it defaults` +// reports. The autostart switch edits hypr-theme's autostart.lua at once, +// as the Autostart page does; config edits go through the SaveBar. +Singleton { + id: root + + readonly property string config: (Quickshell.env("XDG_CONFIG_HOME") || Quickshell.env("HOME") + "/.config") + readonly property string path: root.config + "/deck-it/config.toml" + readonly property string autostartFile: root.config + "/hypr/sections/autostart.lua" + // Wi-Fi, waiting for the tablet; it defers to a USB start. + readonly property string autostartCmd: "deck-it wifi --wait" + + readonly property Store store: Store {} + // Empty until `deck-it defaults` answered; then the page can show them. + property bool ready: false + property string defaultsError: "" + + // The autostart.lua entries that run deck-it, and whether one is enabled. + property var entries: [] + readonly property bool autostart: root.entries.some(e => e.enabled) + property string autostartError: "" + + function reload() { fv.reload(); asv.reload(); if (!root.ready) defs.running = true; } + + function save() { + const d = root.store; + 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); + } + + // `deck-it status`, polled while the page is open: {server, display, + // waiting, tablet, extend}; null until the first answer. + property var status: null + property int watchers: 0 + // The last Restart's outcome; restartAt is 0 when none is pending. + property string note: "" + property bool noteBad: false + property real restartAt: 0 + readonly property int restartTimeout: 30000 + + // One line for the page. + readonly property string summary: { + const s = root.status; + if (!s) return "Checking…"; + if (s.server && s.tablet) return "Running, tablet connected (" + s.tablet + ")" + + (s.extend ? (s.display ? ", display streaming" : ", display not streaming") : ""); + if (s.server) return "Running, but no tablet is tunnelled"; + if (s.waiting) return "Waiting for the tablet on Wi-Fi"; + return "Not running"; + } + readonly property bool healthy: !!root.status && root.status.server && root.status.tablet !== "" + && (!root.status.extend || root.status.display) + + // Stops it and starts it again the way login does, so a saved config applies. + function restart() { + Quickshell.execDetached(["sh", "-c", "deck-it stop; exec deck-it wifi --wait"]); + root.note = "Restarting…"; + root.noteBad = false; + root.restartAt = Date.now(); + // Let stop run first, or the first poll still sees the old instance. + poll.restart(); + } + + function checkRestart() { + if (root.restartAt === 0) return; + if (root.healthy) { + root.note = "Restarted at " + Qt.formatTime(new Date(), "HH:mm:ss"); + root.noteBad = false; + root.restartAt = 0; + } else if (Date.now() - root.restartAt > root.restartTimeout) { + root.note = "Not back after " + root.restartTimeout / 1000 + "s: " + root.summary; + root.noteBad = true; + root.restartAt = 0; + } + } + + Timer { + id: poll + interval: 2000 + repeat: true + triggeredOnStart: true + running: root.watchers > 0 + onTriggered: if (!stat.running) stat.running = true + } + Process { + id: stat + command: ["deck-it", "status"] + stdout: StdioCollector { id: statOut } + onExited: { + try { root.status = JSON.parse(statOut.text); } catch (e) { root.status = null; } + root.checkRestart(); + } + } + + function mine(text) { + return A.parse(text).filter(e => A.program(e.cmd, "").replace(/.*\//, "") === "deck-it"); + } + + // On: re-enable a commented-out line, else add one. Off: comment out + // every enabled one. Each edit returns null if its line moved meanwhile. + function setAutostart(on) { + let t = asv.text(); + const list = root.mine(t); + if (on) { + const off = list.find(e => !e.enabled); + t = off ? A.toggle(t, off) : A.add(t, root.autostartCmd); + } else { + // Toggling keeps every line's index, so the parsed entries stay valid. + for (const e of list.filter(e => e.enabled)) if (t !== null) t = A.toggle(t, e); + } + if (t === null) { + root.autostartError = "autostart.lua changed underneath or has no closing end), reloaded. Try again."; + asv.reload(); + return; + } + root.autostartError = ""; + asv.setText(t); + root.entries = root.mine(t); + } + + FileView { + id: fv + path: root.path + atomicWrites: true + watchChanges: true + printErrors: false + onFileChanged: reload() + onLoaded: root.store.loaded(text()) + onLoadFailed: root.store.loaded("") + onSaveFailed: err => root.store.error = "cannot write: " + FileViewError.toString(err) + } + + FileView { + id: asv + path: root.autostartFile + // Hyprland reloads its config on every change: never a half-written file. + atomicWrites: true + watchChanges: true + printErrors: false + onFileChanged: reload() + onLoaded: root.entries = root.mine(text()) + onLoadFailed: err => root.autostartError = "cannot read autostart.lua: " + FileViewError.toString(err) + onSaveFailed: err => root.autostartError = "cannot write autostart.lua: " + FileViewError.toString(err) + } + + // The shipped values, as {section: {key: value}} with top-level keys + // under "", the shape Store's placeholders read. + Process { + id: defs + command: ["deck-it", "defaults"] + stdout: StdioCollector { id: defsOut } + stderr: StdioCollector { id: defsErr } + onExited: code => { + let d = null; + try { d = JSON.parse(defsOut.text); } catch (e) {} + if (!d) { + root.defaultsError = "deck-it defaults failed: " + (defsErr.text.trim() || `exit ${code}`) + + ". Is deck-it on PATH?"; + return; + } + const out = { "": {} }; + for (const k in d) { + if (d[k] !== null && typeof d[k] === "object" && !Array.isArray(d[k])) out[k] = d[k]; + else out[""][k] = d[k]; + } + root.store.defaults = out; + root.defaultsError = ""; + root.ready = true; + } + } + + Component.onCompleted: defs.running = true +} |
