diff options
Diffstat (limited to 'desktop/modules')
| -rw-r--r-- | desktop/modules/deckit/DeckItDashboardModule.qml | 47 | ||||
| -rw-r--r-- | desktop/modules/deckit/DeckItDashboardPage.qml | 82 | ||||
| -rw-r--r-- | desktop/modules/deckit/DeckItGeneralPage.qml | 89 | ||||
| -rw-r--r-- | desktop/modules/deckit/DeckItModule.qml | 20 | ||||
| -rw-r--r-- | desktop/modules/deckit/DeckItPage.qml | 291 | ||||
| -rw-r--r-- | desktop/modules/deckit/DeckItServicesModule.qml | 45 | ||||
| -rw-r--r-- | desktop/modules/deckit/DeckItServicesPage.qml | 37 | ||||
| -rw-r--r-- | desktop/modules/deckit/DeckItWindowsModule.qml | 43 | ||||
| -rw-r--r-- | desktop/modules/deckit/DeckItWindowsPage.qml | 151 | ||||
| -rw-r--r-- | desktop/modules/deckit/README.md | 28 | ||||
| -rw-r--r-- | desktop/modules/deckit/Small.qml | 21 |
11 files changed, 545 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 } +} |
