aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--desktop/modules/kdeconnect/KdeConnectModule.qml163
-rw-r--r--desktop/modules/kdeconnect/KdeConnectPage.qml78
-rw-r--r--desktop/modules/kdeconnect/KdeConnectRow.qml71
-rw-r--r--desktop/modules/kdeconnect/KdeConnectTile.qml45
-rw-r--r--desktop/shell.qml2
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 {},