diff options
Diffstat (limited to 'desktop')
| -rw-r--r-- | desktop/modules/kdeconnect/KdeConnectModule.qml | 163 | ||||
| -rw-r--r-- | desktop/modules/kdeconnect/KdeConnectPage.qml | 78 | ||||
| -rw-r--r-- | desktop/modules/kdeconnect/KdeConnectRow.qml | 71 | ||||
| -rw-r--r-- | desktop/modules/kdeconnect/KdeConnectTile.qml | 45 | ||||
| -rw-r--r-- | desktop/shell.qml | 2 |
5 files changed, 359 insertions, 0 deletions
diff --git a/desktop/modules/kdeconnect/KdeConnectModule.qml b/desktop/modules/kdeconnect/KdeConnectModule.qml new file mode 100644 index 0000000..1175b81 --- /dev/null +++ b/desktop/modules/kdeconnect/KdeConnectModule.qml @@ -0,0 +1,163 @@ +// 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 Quickshell.Io +import QtQuick +import "../.." + +// Not always active: there is no push to listen to without a generic D-Bus +// module, so the state is polled while the drawer is open and not otherwise. +// The poll's lifetime is the tile's lifetime; see KdeConnectTile.qml. +Module { + id: mod + + name: "kdeconnect" + label: "KDE Connect" + alwaysActive: false + + // Rebuilt from kdeconnect-state.sh on every poll. See the script for the + // line protocol; the field order here must match it. + property var devices: [] + property var requests: [] + property string error: "" + + readonly property var paired: devices.filter(d => d.paired) + readonly property var unpaired: devices.filter(d => !d.paired) + readonly property var reachable: devices.filter(d => d.reachable) + readonly property bool anyReachable: reachable.length > 0 + + icon: "\uf10b" + active: mod.anyReachable + + function deviceName(id) { + const d = devices.find(x => x.id === id); + return d ? d.name : id; + } + + // The tile is created when the drawer panel loads, grid or page, and + // destroyed when it unloads, so its presence is the poll's on/off switch. + property bool polling: false + onPollingChanged: if (polling) refresh() + + function refresh() { + stateProc.devices = []; + stateProc.reqs = []; + stateProc.running = false; + stateProc.running = true; + } + + property Process stateProc: Process { + property var devices: [] + property var reqs: [] + command: ["bash", `${Quickshell.shellDir}/modules/kdeconnect/kdeconnect-state.sh`] + stdout: SplitParser { + onRead: line => { + const f = line.split("\t"); + if (f[0] === "request" && f[1]) + stateProc.reqs.push(f[1]); + else if (f[0] === "device" && f.length === 11) + stateProc.devices.push({ + id: f[1], name: f[2], type: f[3], + paired: f[4] === "1", reachable: f[5] === "1", + pairRequested: f[6] === "1", + pairRequestedByPeer: f[7] === "1", + key: f[8], charge: f[9], charging: f[10] === "1", + }); + } + } + onExited: code => { + if (code !== 0) { mod.error = "kdeconnectd is not responding"; return; } + mod.error = ""; + mod.devices = stateProc.devices; + mod.requests = stateProc.reqs; + } + } + + property Timer pollTimer: Timer { + interval: 5000 + repeat: true + running: mod.polling + onTriggered: mod.refresh() + } + + // --- actions --- + + // A failure after the drawer closed would otherwise go unseen, so it also + // raises a notification. Same cached-Process shape as the other modules. + function notify(title, body) { + notifyProc.command = ["notify-send", "--app-name=kdeconnect", + "--urgency=critical", "--icon=error", title, body]; + notifyProc.running = false; + notifyProc.running = true; + } + + property Process notifyProc: Process {} + + // One reusable action process. Actions are serialized; a second click + // while one runs replaces it rather than racing. A failure notifies. + function run(label, cmd) { + actProc.label = label; + actProc.command = cmd; + actProc.running = false; + actProc.running = true; + } + + property Process actProc: Process { + property string label: "" + stderr: StdioCollector { id: actErr } + onExited: code => { + if (code !== 0) + mod.notify(actProc.label + " failed", + actErr.text.trim() || ("exited " + code)); + mod.refresh(); + } + } + + function discover() { run("Refresh", ["kdeconnect-cli", "--refresh"]); } + function ring(id) { run("Ring", ["kdeconnect-cli", "--ring", "-d", id]); } + function clipboard(id) { + run("Clipboard", ["kdeconnect-cli", "--send-clipboard", "-d", id]); + } + function pair(id) { run("Pair", ["kdeconnect-cli", "--pair", "-d", id]); } + function unpair(id) { run("Unpair", ["kdeconnect-cli", "--unpair", "-d", id]); } + + // Accept and reject are the two actions kdeconnect-cli does not offer, so + // they go straight to the daemon. + function accept(id) { + run("Accept", ["qdbus6", "org.kde.kdeconnect", + `/modules/kdeconnect/devices/${id}`, + "org.kde.kdeconnect.device.acceptPairing"]); + } + function reject(id) { + run("Reject", ["qdbus6", "org.kde.kdeconnect", + `/modules/kdeconnect/devices/${id}`, + "org.kde.kdeconnect.device.cancelPairing"]); + } + + // --mount prints nothing and the mount point comes from --get-mount-point, + // so both steps run in one shell. The id is machine-generated hex; quoting + // it through JSON.stringify keeps the shell from being the injection path. + function mount(id) { + run("Mount", ["sh", "-c", + `kdeconnect-cli --mount -d ${JSON.stringify(id)} && ` + + `xdg-open "$(kdeconnect-cli --get-mount-point -d ${JSON.stringify(id)})"`]); + } + + tileContent: Component { KdeConnectTile { mod: mod } } + + page: Component { + Page { + title: "KDE Connect" + KdeConnectPage { width: parent.width; mod: mod } + } + } +} diff --git a/desktop/modules/kdeconnect/KdeConnectPage.qml b/desktop/modules/kdeconnect/KdeConnectPage.qml new file mode 100644 index 0000000..46a42f9 --- /dev/null +++ b/desktop/modules/kdeconnect/KdeConnectPage.qml @@ -0,0 +1,78 @@ +// 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 "../.." + +Column { + id: page + + required property var mod + + spacing: 14 + + Row { + width: page.width + spacing: 8 + + Button { + text: "Refresh" + onClicked: page.mod.discover() + } + } + + Text { + width: page.width + visible: page.mod.error !== "" + wrapMode: Text.Wrap + text: page.mod.error + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 3 } + color: Theme.red + } + + Text { + width: page.width + visible: page.mod.paired.length > 0 + text: "Paired" + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2; bold: true } + color: Theme.subtext + } + + Repeater { + model: page.mod.paired + + KdeConnectRow { + required property var modelData + mod: page.mod + device: modelData + width: page.width + } + } + + Text { + width: page.width + visible: page.mod.unpaired.length > 0 + text: "Available" + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2; bold: true } + color: Theme.subtext + } + + Repeater { + model: page.mod.unpaired + + KdeConnectRow { + required property var modelData + mod: page.mod + device: modelData + width: page.width + } + } +} diff --git a/desktop/modules/kdeconnect/KdeConnectRow.qml b/desktop/modules/kdeconnect/KdeConnectRow.qml new file mode 100644 index 0000000..45daae9 --- /dev/null +++ b/desktop/modules/kdeconnect/KdeConnectRow.qml @@ -0,0 +1,71 @@ +// 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 "../.." + +// One KDE Connect device. The type glyph is the phone for anything that is +// not a laptop or desktop, since the daemon's type strings are not a closed +// set. +Rectangle { + id: row + + required property var mod + required property var device + + implicitHeight: col.implicitHeight + 16 + radius: 8 + color: row.device.reachable ? Qt.alpha(Theme.accent, 0.10) : Qt.alpha(Theme.surface, 0.35) + + Column { + id: col + anchors { left: parent.left; right: parent.right; top: parent.top; margins: 10 } + spacing: 8 + + Item { + width: parent.width + implicitHeight: Math.max(name.implicitHeight, glyph.implicitHeight) + + Text { + id: glyph + anchors.verticalCenter: parent.verticalCenter + text: row.device.type === "desktop" ? "\uf109" : "\uf10b" + font { family: Theme.iconFamily; pixelSize: Theme.fontSize - 2 } + color: Theme.subtext + } + + Text { + id: name + anchors { left: glyph.right; leftMargin: 8; right: parent.right; verticalCenter: parent.verticalCenter } + elide: Text.ElideRight + text: row.device.name || row.device.id + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2; bold: row.device.reachable } + color: row.device.reachable ? Theme.accent : Theme.text + } + } + + Text { + id: status + width: parent.width + text: { + let s = row.device.paired + ? (row.device.reachable ? "Reachable" : "Not reachable") + : (row.device.reachable ? "Found" : "Not reachable"); + if (row.device.charge !== "") + s += " " + row.device.charge + "%" + (row.device.charging ? " charging" : ""); + return s; + } + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } + color: Theme.subtext + elide: Text.ElideRight + } + } +} diff --git a/desktop/modules/kdeconnect/KdeConnectTile.qml b/desktop/modules/kdeconnect/KdeConnectTile.qml new file mode 100644 index 0000000..ae74fbc --- /dev/null +++ b/desktop/modules/kdeconnect/KdeConnectTile.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 "../.." + +// The state line under the tile label, and the poll's lifetime: the tile +// exists exactly while the drawer panel is loaded, so it turns polling on and +// off. Without this the module would poll while the drawer is closed, which +// nothing needs. +Item { + id: tile + + required property var mod + + width: parent ? parent.width : implicitWidth + implicitHeight: state.implicitHeight + + Component.onCompleted: mod.polling = true + Component.onDestruction: mod.polling = false + + Text { + id: state + width: parent.width + elide: Text.ElideRight + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } + color: tile.mod.anyReachable ? Theme.text : Theme.subtext + text: { + const r = tile.mod.reachable; + if (r.length === 1) + return r[0].charge !== "" ? r[0].name + " " + r[0].charge + "%" : r[0].name; + if (r.length > 1) return r.length + " connected"; + if (tile.mod.paired.length > 0) return "Offline"; + return "No devices"; + } + } +} diff --git a/desktop/shell.qml b/desktop/shell.qml index d191983..6a37827 100644 --- a/desktop/shell.qml +++ b/desktop/shell.qml @@ -14,6 +14,7 @@ import Quickshell.Io import Quickshell.Wayland import "modules/appearance" import "modules/bluetooth" +import "modules/kdeconnect" import "modules/mail" import "modules/network" import "modules/sound" @@ -38,6 +39,7 @@ ShellRoot { SoundModule {}, NetworkModule {}, BluetoothModule {}, + KdeConnectModule {}, MailModule {}, AppearanceModule {}, VmModule {}, |
