// 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." } }