aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--desktop/modules/deckit/DeckItDashboardModule.qml47
-rw-r--r--desktop/modules/deckit/DeckItDashboardPage.qml82
-rw-r--r--desktop/modules/deckit/DeckItGeneralPage.qml89
-rw-r--r--desktop/modules/deckit/DeckItModule.qml20
-rw-r--r--desktop/modules/deckit/DeckItPage.qml291
-rw-r--r--desktop/modules/deckit/DeckItServicesModule.qml45
-rw-r--r--desktop/modules/deckit/DeckItServicesPage.qml37
-rw-r--r--desktop/modules/deckit/DeckItWindowsModule.qml43
-rw-r--r--desktop/modules/deckit/DeckItWindowsPage.qml151
-rw-r--r--desktop/modules/deckit/README.md28
-rw-r--r--desktop/modules/deckit/Small.qml21
-rw-r--r--desktop/shell.qml4
12 files changed, 549 insertions, 309 deletions
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. <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.
+
+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. <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.
+
+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. <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.
+
+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. <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.
-
-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. <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.
+
+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. <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.
+
+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. <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.
+
+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. <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.
+
+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. <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.
+
+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 {},