From cd7035be76c82c15d172a6827c3c8f81df723424 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Tue, 10 Nov 2026 20:09:46 +0100 Subject: feat(deckit): split the settings into a deck-it group of four pages The single long deck-it page becomes its own sidebar group: General (status, Restart, Start, Display, Advanced), Dashboard (Modules, action icons, Hardware), Windows (launchers, workspaces) and Services (OBS, LLM, K1C, build VM). All four share DeckIt.store, so Save on any page writes the whole draft, as the Mail pages do. Each module lists its own fields, so a search jumps to the page holding the field. The small-print text moves to a shared Small.qml. Co-Authored-By: Claude Opus 5.5 --- desktop/modules/deckit/DeckItDashboardModule.qml | 47 ++++ desktop/modules/deckit/DeckItDashboardPage.qml | 82 +++++++ desktop/modules/deckit/DeckItGeneralPage.qml | 89 +++++++ desktop/modules/deckit/DeckItModule.qml | 20 +- desktop/modules/deckit/DeckItPage.qml | 291 ----------------------- desktop/modules/deckit/DeckItServicesModule.qml | 45 ++++ desktop/modules/deckit/DeckItServicesPage.qml | 37 +++ desktop/modules/deckit/DeckItWindowsModule.qml | 43 ++++ desktop/modules/deckit/DeckItWindowsPage.qml | 151 ++++++++++++ desktop/modules/deckit/README.md | 28 ++- desktop/modules/deckit/Small.qml | 21 ++ desktop/shell.qml | 4 + 12 files changed, 549 insertions(+), 309 deletions(-) create mode 100644 desktop/modules/deckit/DeckItDashboardModule.qml create mode 100644 desktop/modules/deckit/DeckItDashboardPage.qml create mode 100644 desktop/modules/deckit/DeckItGeneralPage.qml delete mode 100644 desktop/modules/deckit/DeckItPage.qml create mode 100644 desktop/modules/deckit/DeckItServicesModule.qml create mode 100644 desktop/modules/deckit/DeckItServicesPage.qml create mode 100644 desktop/modules/deckit/DeckItWindowsModule.qml create mode 100644 desktop/modules/deckit/DeckItWindowsPage.qml create mode 100644 desktop/modules/deckit/Small.qml diff --git a/desktop/modules/deckit/DeckItDashboardModule.qml b/desktop/modules/deckit/DeckItDashboardModule.qml new file mode 100644 index 0000000..4517723 --- /dev/null +++ b/desktop/modules/deckit/DeckItDashboardModule.qml @@ -0,0 +1,47 @@ +// 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 QtQuick +import "../.." + +// deck-it's dashboard: its panels, its action icons and the hardware it reads. +Module { + name: "deckit-dashboard" + label: "Dashboard" + // nf-md-view_dashboard, present in Inconsolata Nerd Font. + icon: "󰕮" + settings: true + group: "deck-it" + dirty: DeckIt.store.dirty + keywords: ["deck-it", "tablet", "dashboard", "modules", "panels", "actions", "icons", "hardware", + "network", "battery", "cpu", "gpu", "sensor"] + readonly property var fields: [ + ["modules", "", "Modules"], ["dashboard", "actions", "Dashboard actions"], + ["hardware", "net", "Network interface"], ["hardware", "battery", "Battery"], + ["hardware", "cpu_sensor", "CPU sensor"], ["hardware", "gpu", "GPU"], + ["hardware", "gpu_mem_total", "GPU memory"]] + searchItems: fields.map(f => ({ text: f[2], key: f[0] + "/" + f[1] })) + function refresh() { DeckIt.reload(); } + + page: Component { + Page { + title: "Dashboard" + // One draft behind all four deck-it pages: Save writes them all. + actions: Component { + SaveBar { + dirty: DeckIt.store.dirty; valid: DeckIt.store.valid; error: DeckIt.store.error + onSave: DeckIt.save(); onDiscard: DeckIt.store.discard() + } + } + DeckItDashboardPage { width: parent.width } + } + } +} diff --git a/desktop/modules/deckit/DeckItDashboardPage.qml b/desktop/modules/deckit/DeckItDashboardPage.qml new file mode 100644 index 0000000..658f07b --- /dev/null +++ b/desktop/modules/deckit/DeckItDashboardPage.qml @@ -0,0 +1,82 @@ +// 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 QtQuick +import "../.." +import "../qtmaildir" + +// deck-it's Dashboard page: its panels, its action icons, the hardware it reads. +Column { + id: col + spacing: 6 + readonly property var st: DeckIt.store + function def(section, key) { return col.st.fallback(section, key); } + + // The dashboard's action icons: every id deck-it knows is in the + // defaults' list; the file keeps the ones switched on, in that order. + readonly property var allActions: col.def("dashboard", "actions") ?? [] + readonly property var actions: col.st.value("dashboard", "actions") ?? col.allActions + function setAction(id, on) { + col.st.put("dashboard", "actions", + col.allActions.filter(a => a === id ? on : col.actions.includes(a))); + } + + Small { visible: DeckIt.defaultsError !== ""; color: Theme.red; text: DeckIt.defaultsError } + + Heading { objectName: "modules/"; 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 + } + } + + Heading { objectName: "dashboard/actions"; text: "Dashboard actions" } + Small { text: "Icon buttons at the bottom of the tablet's dashboard." } + Repeater { + model: col.allActions + Item { + id: act + required property string modelData + width: col.width + height: 34 + Text { + id: actLabel + anchors { left: parent.left; verticalCenter: parent.verticalCenter } + width: 160 + text: act.modelData + color: Theme.subtext + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 3 } + } + Switch { + anchors { left: actLabel.right; leftMargin: 8; verticalCenter: parent.verticalCenter } + checked: col.actions.includes(act.modelData) + onToggled: on => col.setAction(act.modelData, on) + } + } + } + + 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." } +} diff --git a/desktop/modules/deckit/DeckItGeneralPage.qml b/desktop/modules/deckit/DeckItGeneralPage.qml new file mode 100644 index 0000000..358574f --- /dev/null +++ b/desktop/modules/deckit/DeckItGeneralPage.qml @@ -0,0 +1,89 @@ +// 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" + +// deck-it's General page: the intro, Start, Display and Advanced. Every field +// edits the draft of ~/.config/deck-it/config.toml shared by the four deck-it +// pages; 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); } + + 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-- + + 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 { 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." } +} diff --git a/desktop/modules/deckit/DeckItModule.qml b/desktop/modules/deckit/DeckItModule.qml index fbf6bd0..973ef1c 100644 --- a/desktop/modules/deckit/DeckItModule.qml +++ b/desktop/modules/deckit/DeckItModule.qml @@ -14,8 +14,8 @@ import QtQuick import "../.." import "../qtmaildir" -// deck-it, the tablet dashboard: whether it starts at login, and its config. -// See README.md. +// deck-it, the tablet dashboard: General, the first of its four pages, with +// its state and Restart in the header. See README.md. Module { id: mod name: "deckit" @@ -23,22 +23,17 @@ Module { // nf-md-tablet, present in Inconsolata Nerd Font. icon: "󰓶" settings: true - group: "Desktop" + group: "deck-it" dirty: DeckIt.store.dirty - keywords: ["deck-it", "tablet", "android", "dashboard", "stream deck", "adb", "wifi", "avnc", - "wayvnc", "second monitor", "launcher", "autostart", "printer", "k1c", "klipper"] + keywords: ["deck-it", "tablet", "android", "stream deck", "adb", "wifi", "avnc", "wayvnc", + "second monitor", "autostart", "display", "resolution", "port", "hyprland", "icon theme"] readonly property var fields: [ ["autostart", "", "Start at login"], ["wifi", "address", "Wi-Fi address"], ["display", "enabled", "Second display"], ["display", "resolution", "Resolution"], ["display", "position", "Position"], ["display", "scale", "Scale"], ["display", "refresh", "Refresh"], ["display", "name", "Output name"], ["ports", "command_center", "Dashboard port"], ["ports", "display", "Display port"], - ["launch", "apps", "Launchers"], ["windows", "workspaces", "Workspaces"], - ["obs", "host", "OBS host"], ["obs", "port", "OBS port"], ["obs", "password", "OBS password"], - ["llm", "url", "LLM dashboard URL"], ["sbbuilder", "host", "Build VM host"], ["k1c", "url", "K1C Moonraker"], - ["hardware", "net", "Network interface"], ["hardware", "battery", "Battery"], - ["hardware", "cpu_sensor", "CPU sensor"], ["hardware", "gpu", "GPU"], - ["hardware", "gpu_mem_total", "GPU memory"], ["icons", "theme", "Fallback icon theme"]] + ["", "hyprland", "Hyprland config"], ["icons", "theme", "Fallback icon theme"]] searchItems: fields.map(f => ({ text: f[2], key: f[0] + "/" + f[1] })) function refresh() { DeckIt.reload(); } @@ -47,6 +42,7 @@ Module { title: "deck-it" // Its state in the header, as the Wallpaper engine page does: // `deck-it status`, polled while the page is open (DeckIt.watchers). + // One draft behind all four deck-it pages: Save writes them all. actions: Component { Row { spacing: 8 @@ -72,7 +68,7 @@ Module { } } } - DeckItPage { width: parent.width } + DeckItGeneralPage { width: parent.width } } } } diff --git a/desktop/modules/deckit/DeckItPage.qml b/desktop/modules/deckit/DeckItPage.qml deleted file mode 100644 index 4368ca4..0000000 --- a/desktop/modules/deckit/DeckItPage.qml +++ /dev/null @@ -1,291 +0,0 @@ -// 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); } - - // The dashboard's action icons: every id deck-it knows is in the - // defaults' list; the file keeps the ones switched on, in that order. - readonly property var allActions: col.def("dashboard", "actions") ?? [] - readonly property var actions: col.st.value("dashboard", "actions") ?? col.allActions - function setAction(id, on) { - col.st.put("dashboard", "actions", - col.allActions.filter(a => a === id ? on : col.actions.includes(a))); - } - - 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-- - - 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 { objectName: "dashboard/actions"; text: "Dashboard actions" } - Small { text: "Icon buttons at the bottom of the tablet's dashboard." } - Repeater { - model: col.allActions - Item { - id: act - required property string modelData - width: col.width - height: 34 - Text { - id: actLabel - anchors { left: parent.left; verticalCenter: parent.verticalCenter } - width: 160 - text: act.modelData - color: Theme.subtext - font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 3 } - } - Switch { - anchors { left: actLabel.right; leftMargin: 8; verticalCenter: parent.verticalCenter } - checked: col.actions.includes(act.modelData) - onToggled: on => col.setAction(act.modelData, on) - } - } - } - - 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: "k1c"; key: "url"; label: "K1C Moonraker" - placeholder: "http://ip:7125"; hint: "The printer's Moonraker address, for the K1C tab. Empty: no printer." } - 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." } -} diff --git a/desktop/modules/deckit/DeckItServicesModule.qml b/desktop/modules/deckit/DeckItServicesModule.qml new file mode 100644 index 0000000..5ad3c3e --- /dev/null +++ b/desktop/modules/deckit/DeckItServicesModule.qml @@ -0,0 +1,45 @@ +// 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 QtQuick +import "../.." + +// Where deck-it's OBS, LLM, printer and build VM panels connect. +Module { + name: "deckit-services" + label: "Services" + // nf-md-server, present in Inconsolata Nerd Font. + icon: "󰒋" + settings: true + group: "deck-it" + dirty: DeckIt.store.dirty + keywords: ["deck-it", "tablet", "services", "obs", "websocket", "llm", "printer", "k1c", "klipper", + "moonraker", "build vm", "sbbuilder", "ssh"] + readonly property var fields: [ + ["obs", "host", "OBS host"], ["obs", "port", "OBS port"], ["obs", "password", "OBS password"], + ["llm", "url", "LLM dashboard URL"], ["k1c", "url", "K1C Moonraker"], ["sbbuilder", "host", "Build VM host"]] + searchItems: fields.map(f => ({ text: f[2], key: f[0] + "/" + f[1] })) + function refresh() { DeckIt.reload(); } + + page: Component { + Page { + title: "Services" + // One draft behind all four deck-it pages: Save writes them all. + actions: Component { + SaveBar { + dirty: DeckIt.store.dirty; valid: DeckIt.store.valid; error: DeckIt.store.error + onSave: DeckIt.save(); onDiscard: DeckIt.store.discard() + } + } + DeckItServicesPage { width: parent.width } + } + } +} diff --git a/desktop/modules/deckit/DeckItServicesPage.qml b/desktop/modules/deckit/DeckItServicesPage.qml new file mode 100644 index 0000000..e02cf16 --- /dev/null +++ b/desktop/modules/deckit/DeckItServicesPage.qml @@ -0,0 +1,37 @@ +// 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 QtQuick +import "../.." +import "../qtmaildir" + +// deck-it's Services page: where the OBS, LLM, printer and build VM panels connect. +Column { + id: col + spacing: 6 + readonly property var st: DeckIt.store + function def(section, key) { return col.st.fallback(section, key); } + + Small { visible: DeckIt.defaultsError !== ""; color: Theme.red; text: DeckIt.defaultsError } + + 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: "k1c"; key: "url"; label: "K1C Moonraker" + placeholder: "http://ip:7125"; hint: "The printer's Moonraker address, for the K1C tab. Empty: no printer." } + 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." } +} diff --git a/desktop/modules/deckit/DeckItWindowsModule.qml b/desktop/modules/deckit/DeckItWindowsModule.qml new file mode 100644 index 0000000..3c75c28 --- /dev/null +++ b/desktop/modules/deckit/DeckItWindowsModule.qml @@ -0,0 +1,43 @@ +// 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 QtQuick +import "../.." + +// deck-it's Windows tab: its launcher buttons and the workspaces it shows. +Module { + name: "deckit-windows" + label: "Windows" + // nf-md-apps, present in Inconsolata Nerd Font. + icon: "󰀻" + settings: true + group: "deck-it" + dirty: DeckIt.store.dirty + keywords: ["deck-it", "tablet", "windows", "launcher", "launchers", "apps", "gtk-launch", "workspaces"] + readonly property var fields: [ + ["launch", "apps", "Launchers"], ["windows", "workspaces", "Workspaces"]] + searchItems: fields.map(f => ({ text: f[2], key: f[0] + "/" + f[1] })) + function refresh() { DeckIt.reload(); } + + page: Component { + Page { + title: "Windows" + // One draft behind all four deck-it pages: Save writes them all. + actions: Component { + SaveBar { + dirty: DeckIt.store.dirty; valid: DeckIt.store.valid; error: DeckIt.store.error + onSave: DeckIt.save(); onDiscard: DeckIt.store.discard() + } + } + DeckItWindowsPage { width: parent.width } + } + } +} diff --git a/desktop/modules/deckit/DeckItWindowsPage.qml b/desktop/modules/deckit/DeckItWindowsPage.qml new file mode 100644 index 0000000..c7cd11c --- /dev/null +++ b/desktop/modules/deckit/DeckItWindowsPage.qml @@ -0,0 +1,151 @@ +// 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" + +// deck-it's Windows page: the launcher buttons and the workspaces shown. +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); } + + Small { visible: DeckIt.defaultsError !== ""; color: Theme.red; text: DeckIt.defaultsError } + + 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: "Workspaces" } + 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." } +} diff --git a/desktop/modules/deckit/README.md b/desktop/modules/deckit/README.md index abfd7be..51aed37 100644 --- a/desktop/modules/deckit/README.md +++ b/desktop/modules/deckit/README.md @@ -1,12 +1,28 @@ # deckit -One settings page, under Desktop, for deck-it (the tablet dashboard, its own -repo): whether it starts at login, and its `~/.config/deck-it/config.toml`. +Four settings pages, in their own deck-it sidebar group, for deck-it (the +tablet dashboard, its own repo): whether it starts at login, and its +`~/.config/deck-it/config.toml`. + +- General (`DeckItModule`, `DeckItGeneralPage`): the config path, Start + (start at login, Wi-Fi address), Display and Advanced (ports, Hyprland + config, fallback icon theme). Its header carries deck-it's state and + Restart. +- Dashboard (`DeckItDashboardModule`, `DeckItDashboardPage`): the Modules + toggles, the dashboard's action icons, and Hardware. +- Windows (`DeckItWindowsModule`, `DeckItWindowsPage`): the launchers and the + workspaces the Windows tab shows. +- Services (`DeckItServicesModule`, `DeckItServicesPage`): OBS, the LLM + dashboard URL, the K1C's Moonraker and the build VM host. + +The four pages edit one draft, `DeckIt.store`, as the Mail pages do: each has +a SaveBar, and Save or Discard on any page acts on all of them. Each module +lists its own fields, so a search lands on the page that holds the field. The config file is a sparse override. deck-it merges it over its shipped -`config.toml` key by key, so this page writes only what differs, and an +`config.toml` key by key, so these pages write only what differs, and an emptied field unsets the key and brings the default back. The defaults come -from `deck-it defaults` (JSON), run once when the page first loads, so they +from `deck-it defaults` (JSON), run once when deck-it's settings first load, so they are never copied here; deck-it must be on PATH (`~/bin/deck-it`, a link to the repo's launcher). The draft rules are the settings window's, through sbbuilder's `Store` and `Toml.js`, which gained plain decimals for the display @@ -25,8 +41,8 @@ the list; the order is the button order on the tablet. deck-it reads its config once at start, so a save applies on Restart deck-it, which runs `deck-it stop` and then the login command. -While the page is open it polls `deck-it status` (JSON: server, display, -waiting, tablet, extend) every 2s and shows one line in the page header, +While the General page is open it polls `deck-it status` (JSON: server, display, +waiting, tablet, extend) every 2s and shows one line in that page's header, beside Restart and Save, as the Wallpaper engine page does: green when the server runs with a tablet tunnelled (and the display streams, if enabled), yellow while starting or waiting for the tablet, red when stopped. A Restart's diff --git a/desktop/modules/deckit/Small.qml b/desktop/modules/deckit/Small.qml new file mode 100644 index 0000000..6f31001 --- /dev/null +++ b/desktop/modules/deckit/Small.qml @@ -0,0 +1,21 @@ +// 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 QtQuick +import "../.." + +// A line of small print on the deck-it pages. +Text { + width: parent ? parent.width : 0 + wrapMode: Text.Wrap + color: Theme.subtext + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } +} diff --git a/desktop/shell.qml b/desktop/shell.qml index 798aa2d..f5ca7d4 100644 --- a/desktop/shell.qml +++ b/desktop/shell.qml @@ -112,7 +112,11 @@ ShellRoot { OptionsModule {}, MonitorsModule {}, ObsScenesModule {}, + // deck-it's four pages, one draft. DeckItModule {}, + DeckItDashboardModule {}, + DeckItWindowsModule {}, + DeckItServicesModule {}, AssistantModule { id: asstSettings }, PresetsModule {}, MailGeneralModule {}, -- cgit v1.2.3