diff options
| author | Danilo M. <danix@danix.xyz> | 2026-09-14 19:11:14 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-09-14 19:11:14 +0200 |
| commit | c035080a9929f419941605b08c5052b35127fa2e (patch) | |
| tree | 3e96eb7d232161bf7f037b50b66e5a20ccd38bee | |
| parent | bdfbcb869b18b084d57008c9a80b2c7d5843b2f4 (diff) | |
| download | quickshell-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.
| -rwxr-xr-x | desktop/modules/kdeconnect/kdeconnect-state.sh | 106 | ||||
| -rwxr-xr-x | desktop/modules/kdeconnect/test-kdeconnect-state.sh | 112 |
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 ]] |
