diff options
Diffstat (limited to 'desktop/modules/kdeconnect/KdeConnectModule.qml')
| -rw-r--r-- | desktop/modules/kdeconnect/KdeConnectModule.qml | 163 |
1 files changed, 163 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 } + } + } +} |
