aboutsummaryrefslogtreecommitdiffstats
path: root/desktop/modules/kdeconnect
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-09-14 19:11:14 +0200
committerDanilo M. <danix@danix.xyz>2026-09-14 19:11:14 +0200
commitc035080a9929f419941605b08c5052b35127fa2e (patch)
tree3e96eb7d232161bf7f037b50b66e5a20ccd38bee /desktop/modules/kdeconnect
parentbdfbcb869b18b084d57008c9a80b2c7d5843b2f4 (diff)
downloadquickshell-c035080a9929f419941605b08c5052b35127fa2e.tar.gz
quickshell-c035080a9929f419941605b08c5052b35127fa2e.zip
feat(desktop): KDE Connect state script
Quickshell 0.3.1 has no generic D-Bus module, so device state comes from qdbus6 shell-outs. The script emits a tab line protocol for the QML side and is checked by a stub-daemon test, since the live daemon is not an oracle an agent can rely on. A failed daemon query exits non-zero with no output so the module keeps its last list, which is not the same as a valid empty device list.
Diffstat (limited to 'desktop/modules/kdeconnect')
-rwxr-xr-xdesktop/modules/kdeconnect/kdeconnect-state.sh106
-rwxr-xr-xdesktop/modules/kdeconnect/test-kdeconnect-state.sh112
2 files changed, 218 insertions, 0 deletions
diff --git a/desktop/modules/kdeconnect/kdeconnect-state.sh b/desktop/modules/kdeconnect/kdeconnect-state.sh
new file mode 100755
index 0000000..1bf3e4d
--- /dev/null
+++ b/desktop/modules/kdeconnect/kdeconnect-state.sh
@@ -0,0 +1,106 @@
+#!/bin/bash
+#
+# 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.
+#
+# One state dump for the KDE Connect module: the daemon's devices and any
+# pending pairing requests, as tab-separated lines on stdout.
+#
+# device<TAB>id<TAB>name<TAB>type<TAB>paired<TAB>reachable<TAB>pairRequested<TAB>pairRequestedByPeer<TAB>key<TAB>charge<TAB>charging
+# request<TAB>id
+#
+# request lines come first. A daemon query that fails exits non-zero with no
+# output, so the caller keeps its last list rather than showing an empty one;
+# a query that succeeds with no devices exits zero and also prints nothing.
+#
+# The test stubs qdbus6 on PATH; see test-kdeconnect-state.sh.
+
+set -u
+
+BUS=org.kde.kdeconnect
+ROOT=/modules/kdeconnect
+QDBUS=qdbus6
+
+# A scalar property or method value for a device, empty on any failure.
+prop() {
+ "$QDBUS" "$BUS" "$ROOT/devices/$1" "org.kde.kdeconnect.device.$2" 2>/dev/null
+}
+
+bool() {
+ case "$(prop "$1" "$2")" in
+ true) printf 1 ;;
+ *) printf 0 ;;
+ esac
+}
+
+# A device name is attacker-adjacent text; a tab or newline in it must not
+# break the line protocol.
+sanitize() {
+ printf '%s' "$1" | tr '\t\n' ' '
+}
+
+emit_device() {
+ local id="$1" name type paired reachable pr pbp key charge charging
+ name="$(sanitize "$(prop "$id" name)")"
+ type="$(sanitize "$(prop "$id" type)")"
+ paired="$(bool "$id" isPaired)"
+ reachable="$(bool "$id" isReachable)"
+ pr="$(bool "$id" isPairRequested)"
+ pbp="$(bool "$id" isPairRequestedByPeer)"
+
+ # The key only means anything while a pairing is in flight.
+ key=""
+ if [[ "$pr" == 1 || "$pbp" == 1 ]]; then
+ key="$(sanitize "$(prop "$id" verificationKey)")"
+ fi
+
+ # The battery plugin is not on every device: a desktop without a battery
+ # object already returned qdbus an error. An absent query is an empty
+ # field, never a zero.
+ charge="$("$QDBUS" "$BUS" "$ROOT/devices/$id/battery" \
+ org.freedesktop.DBus.Properties.Get \
+ org.kde.kdeconnect.device.battery charge 2>/dev/null)"
+ [[ "$charge" =~ ^[0-9]+$ ]] || charge=""
+
+ charging=0
+ if [[ -n "$charge" ]]; then
+ case "$("$QDBUS" "$BUS" "$ROOT/devices/$id/battery" \
+ org.freedesktop.DBus.Properties.Get \
+ org.kde.kdeconnect.device.battery isCharging 2>/dev/null)" in
+ true) charging=1 ;;
+ esac
+ fi
+
+ printf 'device\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
+ "$id" "$name" "$type" "$paired" "$reachable" "$pr" "$pbp" \
+ "$key" "$charge" "$charging"
+}
+
+main() {
+ local ids
+ # Command substitution carries qdbus's exit status; a failed list is the
+ # error case, not an empty device list.
+ ids="$("$QDBUS" "$BUS" "$ROOT" org.kde.kdeconnect.daemon.devices 2>/dev/null)" || exit 1
+
+ local reqs r
+ reqs="$("$QDBUS" "$BUS" "$ROOT" org.freedesktop.DBus.Properties.Get \
+ org.kde.kdeconnect.daemon pairingRequests 2>/dev/null)" || exit 1
+ for r in $reqs; do
+ printf 'request\t%s\n' "$r"
+ done
+
+ local id
+ for id in $ids; do
+ [[ -n "$id" ]] && emit_device "$id"
+ done
+}
+
+main "$@"
diff --git a/desktop/modules/kdeconnect/test-kdeconnect-state.sh b/desktop/modules/kdeconnect/test-kdeconnect-state.sh
new file mode 100755
index 0000000..77354db
--- /dev/null
+++ b/desktop/modules/kdeconnect/test-kdeconnect-state.sh
@@ -0,0 +1,112 @@
+#!/bin/bash
+#
+# 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.
+#
+# The one runnable check for kdeconnect-state.sh. It puts a stub qdbus6 on
+# PATH and runs the real script against it, so nothing here touches the live
+# daemon and the whole thing runs in milliseconds.
+#
+# Fixture: idA a reachable phone with a battery, idB a laptop whose name
+# contains a tab and which has no battery plugin, idC an unpaired phone with
+# an incoming pairing request and a verification key.
+#
+# Usage: ./test-kdeconnect-state.sh (exit 0 = all passed)
+
+set -u
+
+here="$(cd "$(dirname "$0")" && pwd)"
+stub="$(mktemp -d)"
+trap 'rm -rf "$stub"' EXIT
+
+cat > "$stub/qdbus6" <<'STUB'
+#!/bin/bash
+path="$2"; m="$3"; a="$4"; b="$5"
+
+if [[ "${FAIL_DEVICES:-}" == 1 && "$m" == "org.kde.kdeconnect.daemon.devices" ]]; then
+ exit 1
+fi
+if [[ "${ZERO_DEVICES:-}" == 1 && "$m" == "org.kde.kdeconnect.daemon.devices" ]]; then
+ exit 0
+fi
+
+case "$path:$m" in
+ "/modules/kdeconnect:org.kde.kdeconnect.daemon.devices")
+ printf '%s\n' idA idB idC ;;
+ "/modules/kdeconnect:org.freedesktop.DBus.Properties.Get")
+ if [[ "$a" == "org.kde.kdeconnect.daemon" && "$b" == "pairingRequests" ]]; then
+ [[ "${ZERO_DEVICES:-}" == 1 ]] || printf '%s\n' idC
+ fi ;;
+ "/modules/kdeconnect/devices/idA:org.kde.kdeconnect.device.name") printf 'Pixel 6 Pro\n' ;;
+ "/modules/kdeconnect/devices/idA:org.kde.kdeconnect.device.type") printf 'phone\n' ;;
+ "/modules/kdeconnect/devices/idA:org.kde.kdeconnect.device.isPaired") printf 'true\n' ;;
+ "/modules/kdeconnect/devices/idA:org.kde.kdeconnect.device.isReachable") printf 'true\n' ;;
+ "/modules/kdeconnect/devices/idA:org.kde.kdeconnect.device.isPairRequested") printf 'false\n' ;;
+ "/modules/kdeconnect/devices/idA:org.kde.kdeconnect.device.isPairRequestedByPeer") printf 'false\n' ;;
+ "/modules/kdeconnect/devices/idA/battery:org.freedesktop.DBus.Properties.Get")
+ [[ "$b" == charge ]] && printf '50\n' || printf 'true\n' ;;
+ "/modules/kdeconnect/devices/idB:org.kde.kdeconnect.device.name") printf 'kali\tlaptop\n' ;;
+ "/modules/kdeconnect/devices/idB:org.kde.kdeconnect.device.type") printf 'desktop\n' ;;
+ "/modules/kdeconnect/devices/idB:org.kde.kdeconnect.device.isPaired") printf 'true\n' ;;
+ "/modules/kdeconnect/devices/idB:org.kde.kdeconnect.device.isReachable") printf 'false\n' ;;
+ "/modules/kdeconnect/devices/idB:org.kde.kdeconnect.device.isPairRequested") printf 'false\n' ;;
+ "/modules/kdeconnect/devices/idB:org.kde.kdeconnect.device.isPairRequestedByPeer") printf 'false\n' ;;
+ "/modules/kdeconnect/devices/idB/battery:org.freedesktop.DBus.Properties.Get") exit 1 ;;
+ "/modules/kdeconnect/devices/idC:org.kde.kdeconnect.device.name") printf 'New Phone\n' ;;
+ "/modules/kdeconnect/devices/idC:org.kde.kdeconnect.device.type") printf 'phone\n' ;;
+ "/modules/kdeconnect/devices/idC:org.kde.kdeconnect.device.isPaired") printf 'false\n' ;;
+ "/modules/kdeconnect/devices/idC:org.kde.kdeconnect.device.isReachable") printf 'true\n' ;;
+ "/modules/kdeconnect/devices/idC:org.kde.kdeconnect.device.isPairRequested") printf 'false\n' ;;
+ "/modules/kdeconnect/devices/idC:org.kde.kdeconnect.device.isPairRequestedByPeer") printf 'true\n' ;;
+ "/modules/kdeconnect/devices/idC:org.kde.kdeconnect.device.verificationKey") printf '1826C6D4\n' ;;
+ "/modules/kdeconnect/devices/idC/battery:org.freedesktop.DBus.Properties.Get") exit 1 ;;
+esac
+STUB
+chmod +x "$stub/qdbus6"
+
+pass=0
+fail=0
+
+check() {
+ local name="$1" want="$2" got="$3"
+ if [[ "$want" == "$got" ]]; then
+ pass=$((pass + 1))
+ else
+ fail=$((fail + 1))
+ printf 'FAIL: %s\n want: %q\n got: %q\n' "$name" "$want" "$got"
+ fi
+}
+
+# The full line protocol for the fixture. The name with a tab is one field
+# after sanitizing; the two battery-less devices leave the charge and
+# charging fields empty and zero respectively.
+want="$(printf 'request\tidC')
+$(printf 'device\tidA\tPixel 6 Pro\tphone\t1\t1\t0\t0\t\t50\t1')
+$(printf 'device\tidB\tkali laptop\tdesktop\t1\t0\t0\t0\t\t\t0')
+$(printf 'device\tidC\tNew Phone\tphone\t0\t1\t0\t1\t1826C6D4\t\t0')"
+
+got="$(PATH="$stub:$PATH" bash "$here/kdeconnect-state.sh")"
+check "the line protocol" "$want" "$got"
+
+# A daemon that is down is not an empty device list.
+got="$(PATH="$stub:$PATH" FAIL_DEVICES=1 bash "$here/kdeconnect-state.sh")"
+rc=$?
+check "a failed query prints nothing" "" "$got"
+check "a failed query exits non-zero" "1" "$([[ $rc -ne 0 ]] && echo 1 || echo 0)"
+
+# A reachable daemon with no devices is a valid empty list.
+got="$(PATH="$stub:$PATH" ZERO_DEVICES=1 bash "$here/kdeconnect-state.sh")"
+rc=$?
+check "zero devices prints nothing" "" "$got"
+check "zero devices exits zero" "0" "$rc"
+
+printf '\n%d passed, %d failed\n' "$pass" "$fail"
+[[ "$fail" -eq 0 ]]