// 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 "../.." 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 // A pending Restart, 0 when none; its outcome goes to the window's snackbar. 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"]); Snack.wait("Restarting deck-it…"); 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) { Snack.ok("deck-it restarted"); root.restartAt = 0; } else if (Date.now() - root.restartAt > root.restartTimeout) { Snack.error("deck-it not back after " + root.restartTimeout / 1000 + "s: " + root.summary); 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 }