aboutsummaryrefslogtreecommitdiffstats
path: root/desktop/modules/kdeconnect/KdeConnectModule.qml
diff options
context:
space:
mode:
Diffstat (limited to 'desktop/modules/kdeconnect/KdeConnectModule.qml')
-rw-r--r--desktop/modules/kdeconnect/KdeConnectModule.qml163
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 }
+ }
+ }
+}