From 58e37da20ed56eef1863f259e17a819105ac33f2 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Tue, 10 Nov 2026 10:06:13 +0100 Subject: feat(deckit): settings page for deck-it, the tablet dashboard A page under Desktop for deck-it's ~/.config/deck-it/config.toml and its login line. deck-it now merges that file over its shipped config key by key, so the page writes only what differs and takes its placeholders from `deck-it defaults` rather than copying them. - Start at login edits hypr-theme's autostart.lua at once, as the Autostart page does, through Autostart.js: it re-enables a commented deck-it line or adds `deck-it wifi --wait`; off comments them out. - Every config key, with launchers picked from the installed apps (desktop ids, reorderable) and module switches built from the defaults. - A status line polls `deck-it status` every 2s while the page is open; Restart runs `deck-it stop` and the login command, then reports "Restarted" once the server runs with a tablet tunnelled, or what it saw after 30s. Shared helpers it needed: Toml.js reads plain decimals (the display scale), Store keeps a list of numbers numeric (the workspaces), and ChoiceSetting takes a store as Setting and BoolSetting already do. Co-Authored-By: Claude Opus 5.5 --- desktop/modules/deckit/DeckItPage.qml | 282 ++++++++++++++++++++++++++++++++++ 1 file changed, 282 insertions(+) create mode 100644 desktop/modules/deckit/DeckItPage.qml (limited to 'desktop/modules/deckit/DeckItPage.qml') diff --git a/desktop/modules/deckit/DeckItPage.qml b/desktop/modules/deckit/DeckItPage.qml new file mode 100644 index 0000000..685d62f --- /dev/null +++ b/desktop/modules/deckit/DeckItPage.qml @@ -0,0 +1,282 @@ +// 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. + +import Quickshell +import QtQuick +import "../.." +import "../qtmaildir" + +// The deck-it settings page. Every field edits the draft of +// ~/.config/deck-it/config.toml; an emptied field unsets the key, so deck-it's +// shipped default (the placeholder) applies. +Column { + id: col + spacing: 6 + readonly property var st: DeckIt.store + function def(section, key) { return col.st.fallback(section, key); } + + // The launcher list, in button order. + readonly property var apps: col.st.value("launch", "apps") ?? [] + property string query: "" + // Installed apps not yet listed, for the picker. Terminal apps are left + // out, as the Autostart page does: gtk-launch would need a terminal for them. + readonly property var candidates: { + const q = col.query.trim().toLowerCase(); + return DesktopEntries.applications.values + .filter(a => !a.noDisplay && !a.runInTerminal && a.execString && !col.apps.includes(a.id) + && (q === "" || a.name.toLowerCase().includes(q) || a.id.toLowerCase().includes(q))) + .sort((a, b) => a.name.localeCompare(b.name)) + .slice(0, 8); + } + function setApps(list) { col.st.put("launch", "apps", list); } + + component Small: Text { + width: col.width + wrapMode: Text.Wrap + color: Theme.subtext + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } + } + + Small { text: DeckIt.path + ", only the keys that differ from deck-it's defaults. Saved changes apply when deck-it restarts." } + Small { visible: DeckIt.defaultsError !== ""; color: Theme.red; text: DeckIt.defaultsError } + Component.onCompleted: DeckIt.watchers++ + Component.onDestruction: DeckIt.watchers-- + + Item { + width: col.width + height: 34 + Rectangle { + id: dot + anchors { left: parent.left; verticalCenter: parent.verticalCenter } + width: 10; height: 10; radius: 5 + color: DeckIt.healthy ? Theme.green : DeckIt.status && (DeckIt.status.server || DeckIt.status.waiting) + ? Theme.yellow : Theme.red + } + Text { + anchors { left: dot.right; leftMargin: 10; right: restartBtn.left; rightMargin: 8; verticalCenter: parent.verticalCenter } + elide: Text.ElideRight + text: DeckIt.summary + color: Theme.text + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 3 } + } + Button { + id: restartBtn + anchors { right: parent.right; verticalCenter: parent.verticalCenter } + text: DeckIt.restartAt ? "Restarting…" : "Restart deck-it" + enabled: !DeckIt.restartAt + onClicked: DeckIt.restart() + } + } + Small { visible: DeckIt.note !== ""; color: DeckIt.noteBad ? Theme.red : Theme.green; text: DeckIt.note } + + Heading { text: "Start" } + Item { + objectName: "autostart/" + width: col.width + height: 34 + Text { + id: asLabel + anchors { left: parent.left; verticalCenter: parent.verticalCenter } + width: 160 + text: "Start at login" + color: Theme.subtext + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 3 } + } + Switch { + id: asSwitch + anchors { left: asLabel.right; leftMargin: 8; verticalCenter: parent.verticalCenter } + checked: DeckIt.autostart + onToggled: on => DeckIt.setAutostart(on) + } + Text { + anchors { left: asSwitch.right; leftMargin: 10; verticalCenter: parent.verticalCenter } + text: DeckIt.autostartCmd + ", in autostart.lua; applies at next login" + color: Theme.subtext + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } + } + } + Small { visible: DeckIt.autostartError !== ""; color: Theme.red; text: DeckIt.autostartError } + Setting { store: col.st; section: "wifi"; key: "address"; label: "Wi-Fi address" + placeholder: "ip:port" + hint: "From the tablet's Wireless debugging screen. The port changes whenever it is switched on; empty tries the last address, then mDNS." } + + Heading { text: "Display" } + BoolSetting { store: col.st; section: "display"; key: "enabled"; label: "Second display" + fallback: col.def("display", "enabled") ?? true + hint: "a headless monitor streamed to AVNC; off runs the dashboard alone" } + Setting { store: col.st; section: "display"; key: "resolution"; label: "Resolution" + placeholder: col.st.placeholder("display", "resolution"); hint: "WIDTHxHEIGHT, or auto to ask the tablet." } + Setting { store: col.st; section: "display"; key: "position"; label: "Position" + placeholder: col.st.placeholder("display", "position") + hint: "auto-below centres it under the first monitor; any Hyprland position works." } + Setting { store: col.st; section: "display"; key: "scale"; label: "Scale"; kind: "float"; min: 0.25; max: 4 + placeholder: col.st.placeholder("display", "scale") } + Setting { store: col.st; section: "display"; key: "refresh"; label: "Refresh"; kind: "int"; min: 1; max: 240 + placeholder: col.st.placeholder("display", "refresh") } + Setting { store: col.st; section: "display"; key: "name"; label: "Output name" + placeholder: col.st.placeholder("display", "name") } + + Heading { objectName: "launch/apps"; text: "Launchers" } + Small { text: "Buttons on the Windows tab, in this order. Each runs its app through gtk-launch." } + Repeater { + model: col.apps + delegate: Item { + id: row + required property string modelData + required property int index + readonly property var entry: DesktopEntries.byId(row.modelData) + width: col.width + height: 34 + Image { + id: appIcon + anchors { left: parent.left; leftMargin: 8; verticalCenter: parent.verticalCenter } + width: 20; height: 20 + sourceSize: Qt.size(20, 20) + source: row.entry ? Quickshell.iconPath(row.entry.icon, true) : "" + } + Text { + anchors { left: appIcon.right; leftMargin: 10; right: up.left; rightMargin: 8; verticalCenter: parent.verticalCenter } + elide: Text.ElideRight + text: row.entry ? row.entry.name + " " + row.modelData : row.modelData + " not installed" + color: row.entry ? Theme.text : Theme.red + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 3 } + } + Button { + id: up + anchors { right: del.left; rightMargin: 6; verticalCenter: parent.verticalCenter } + text: "Up" + enabled: row.index > 0 + onClicked: { + const a = col.apps.slice(); + a.splice(row.index - 1, 0, a.splice(row.index, 1)[0]); + col.setApps(a); + } + } + Button { + id: del + anchors { right: parent.right; verticalCenter: parent.verticalCenter } + text: "Remove" + danger: true + onClicked: col.setApps(col.apps.filter(a => a !== row.modelData)) + } + } + } + Rectangle { + width: col.width + height: 34 + radius: 8 + color: Theme.surface + TextInput { + id: search + activeFocusOnTab: true + anchors { fill: parent; leftMargin: 10; rightMargin: 10 } + verticalAlignment: TextInput.AlignVCenter + clip: true + selectByMouse: true + color: Theme.text + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2 } + onTextEdited: col.query = text + Text { + anchors.verticalCenter: parent.verticalCenter + visible: parent.text === "" + text: "add an app: search installed apps" + color: Theme.subtext + font: parent.font + } + } + } + Repeater { + model: col.candidates + delegate: Rectangle { + id: app + required property var modelData + width: col.width + height: 32 + radius: 8 + color: appArea.containsMouse ? Qt.alpha(Theme.text, 0.06) : "transparent" + Image { + id: candIcon + anchors { left: parent.left; leftMargin: 8; verticalCenter: parent.verticalCenter } + width: 20; height: 20 + sourceSize: Qt.size(20, 20) + source: Quickshell.iconPath(app.modelData.icon, true) + } + Text { + anchors { left: candIcon.right; leftMargin: 10; right: parent.right; rightMargin: 8; verticalCenter: parent.verticalCenter } + elide: Text.ElideRight + text: app.modelData.name + " " + app.modelData.id + color: Theme.text + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 3 } + } + MouseArea { + id: appArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + col.setApps(col.apps.concat([app.modelData.id])); + col.query = ""; + search.text = ""; + } + } + } + } + + Heading { text: "Modules" } + Small { text: "The dashboard's panels and tabs." } + Repeater { + model: Object.keys(col.def("modules") ?? {}) + BoolSetting { + required property string modelData + store: col.st; section: "modules"; key: modelData; label: modelData + fallback: col.def("modules", modelData) ?? true + } + } + Setting { store: col.st; section: "windows"; key: "workspaces"; label: "Workspaces" + placeholder: col.st.placeholder("windows", "workspaces") + hint: "Numbers on the Windows tab, separated by spaces, shown even when empty." } + + Heading { text: "Services" } + Setting { store: col.st; section: "obs"; key: "host"; label: "OBS host" + placeholder: col.st.placeholder("obs", "host") } + Setting { store: col.st; section: "obs"; key: "port"; label: "OBS port"; kind: "int"; min: 1; max: 65535 + placeholder: col.st.placeholder("obs", "port") } + Setting { store: col.st; section: "obs"; key: "password"; label: "OBS password" + hint: "obs-websocket's; empty reads the one OBS stores in its own config." } + Setting { store: col.st; section: "llm"; key: "url"; label: "LLM dashboard URL" + placeholder: col.st.placeholder("llm", "url") } + Setting { store: col.st; section: "sbbuilder"; key: "host"; label: "Build VM host" + placeholder: col.st.placeholder("sbbuilder", "host"); hint: "ssh host alias; runs sbb-remote jobs there." } + + Heading { text: "Hardware" } + Small { text: "Each is auto, or a fixed value such as wlan0, BAT1 or coretemp." } + Setting { store: col.st; section: "hardware"; key: "net"; label: "Network interface" + placeholder: col.st.placeholder("hardware", "net") } + Setting { store: col.st; section: "hardware"; key: "battery"; label: "Battery" + placeholder: col.st.placeholder("hardware", "battery") } + Setting { store: col.st; section: "hardware"; key: "cpu_sensor"; label: "CPU sensor" + placeholder: col.st.placeholder("hardware", "cpu_sensor") } + Setting { store: col.st; section: "hardware"; key: "gpu"; label: "GPU" + placeholder: col.st.placeholder("hardware", "gpu"); hint: "auto (Intel xe) or none." } + Setting { store: col.st; section: "hardware"; key: "gpu_mem_total"; label: "GPU memory" + placeholder: col.st.placeholder("hardware", "gpu_mem_total"); hint: "The xe driver reports no VRAM size: the card's total, like 12G." } + + Heading { text: "Advanced" } + Setting { store: col.st; section: "ports"; key: "command_center"; label: "Dashboard port"; kind: "int"; min: 1024; max: 65535 + placeholder: col.st.placeholder("ports", "command_center"); hint: "On the tablet's localhost, through adb reverse." } + Setting { store: col.st; section: "ports"; key: "display"; label: "Display port"; kind: "int"; min: 1024; max: 65535 + placeholder: col.st.placeholder("ports", "display") } + ChoiceSetting { store: col.st; section: ""; key: "hyprland"; label: "Hyprland config" + fallbackLabel: "Default (" + (col.def("", "hyprland") ?? "auto") + ")" + options: [{ value: "auto", label: "auto" }, { value: "lua", label: "lua" }, { value: "classic", label: "classic" }] } + Setting { store: col.st; section: "icons"; key: "theme"; label: "Fallback icon theme" + placeholder: col.st.placeholder("icons", "theme"); hint: "Used only when keystone sets no icon theme." } +} -- cgit v1.2.3