aboutsummaryrefslogtreecommitdiffstats
path: root/docs/superpowers
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-09-14 19:03:55 +0200
committerDanilo M. <danix@danix.xyz>2026-09-14 19:03:55 +0200
commit03b2f9feb965a4576a84e2935daf178f058cfc76 (patch)
treefee6f54c699e76621921da16b1a22f0a5a116233 /docs/superpowers
parent15707ecbc83d23cc0cae96f308359bb671ff93ad (diff)
downloadquickshell-03b2f9feb965a4576a84e2935daf178f058cfc76.tar.gz
quickshell-03b2f9feb965a4576a84e2935daf178f058cfc76.zip
docs: KDE Connect module design
Status plus basic actions, absorbed from the indicator. State comes from qdbus6 over kdeconnectd, since quickshell 0.3.1 has no generic DBus module. Polled while the drawer is open, driven by the tile lifecycle. Notes the indicator is not what serves the phone-to-PC clipboard, and the FileDialog focus risk under an Exclusive layer surface.
Diffstat (limited to 'docs/superpowers')
-rw-r--r--docs/superpowers/specs/2026-09-14-kdeconnect-design.md217
1 files changed, 217 insertions, 0 deletions
diff --git a/docs/superpowers/specs/2026-09-14-kdeconnect-design.md b/docs/superpowers/specs/2026-09-14-kdeconnect-design.md
new file mode 100644
index 0000000..a49a026
--- /dev/null
+++ b/docs/superpowers/specs/2026-09-14-kdeconnect-design.md
@@ -0,0 +1,217 @@
+# KDE Connect module: design
+
+A `kdeconnect` module under `desktop/`, absorbing the job of the
+`kdeconnect-indicator` tray icon into the drawer. Status plus basic actions:
+reachability, battery, pairing, ping, clipboard send, file share, refresh and
+filesystem mount.
+
+## Why
+
+The drawer is now the only network and Bluetooth UI: `nm-applet` and the waybar
+bluetooth module were removed in the previous project. `kdeconnect-indicator`
+is the same class of loose indicator, a tray icon outside the shell that shows
+device state nothing else shows. This module moves that state and the actions
+worth having into the drawer and removes the indicator.
+
+## Confirmed facts
+
+- `kdeconnectd` runs from a system-wide autostart entry,
+ `/etc/xdg/autostart/org.kde.kdeconnect.daemon.desktop`. It is not started by
+ the indicator, so removing the indicator does not stop the daemon.
+- `kdeconnect-indicator` is started from the live Hyprland config,
+ `~/.config/hypr/sections/autostart.lua` (its parent process is Hyprland). That
+ one line is the removal.
+- Quickshell 0.3.1 exposes **no generic D-Bus QML module**, only
+ `Quickshell.DBusMenu`. Everything here goes through the `kdeconnect-cli` and
+ `qdbus6` binaries, the same ceiling as the bluetooth module's `bluetoothctl`
+ pairing.
+- The daemon API is complete for this: `org.kde.kdeconnect.daemon.devices()`
+ lists device ids, `daemon.pairingRequests` is the incoming request list,
+ each device object carries `name`, `type`, `isPaired`, `isReachable`,
+ `isPairRequested`, `isPairRequestedByPeer` and `verificationKey`, and
+ `.../battery` carries `charge` and `isCharging`. A full read over the two
+ paired devices measured about 5ms.
+- The clipboard plugin is loaded and enabled in `kdeconnectd`, and links
+ `libKF6GuiAddons` (`KSystemClipboard`) and `Qt6Gui`. Phone to PC clipboard is
+ the daemon's, not the indicator's, and is untouched by this work.
+
+## Scope
+
+In:
+
+- A drawer tile with device state, and a page listing devices with actions.
+- Pairing, unpairing, and accepting an incoming pairing request.
+- Ping/find, send clipboard, share a file or URL, refresh/discovery, mount the
+ device filesystem.
+- Removing the indicator from autostart.
+
+Out, documented as ceilings:
+
+- SMS read or send, notification forwarding, remote input (keyboard, mouse,
+ presenter, digitizer), media remote control, find-this-device, run-command.
+ The daemon supports all of them and the indicator surfaced some. The drawer
+ does not.
+- Noticing an incoming pairing request while the drawer is closed. The poll
+ only runs while the drawer is open; the request is still there when it opens.
+- Phone to PC clipboard. It is the daemon's own plugin, nothing to build. If it
+ fails on Hyprland it is because the compositor refuses `set_selection` from a
+ daemon holding no keyboard focus, which no code in this module can fix.
+
+## Architecture
+
+Three layers, mirroring the existing modules.
+
+**State, in `kdeconnect-state.sh`.** One helper script queries the daemon and
+prints a line protocol on stdout. It does not watch: it is run on demand and on
+a timer. It calls `qdbus6` for the device id list, then per device the scalar
+properties and the battery properties, and prints one record per line,
+tab-separated:
+
+```
+device<TAB><id><TAB><name><TAB><type><TAB><paired 0|1><TAB><reachable 0|1><TAB><charge or empty><TAB><charging 0|1>
+request<TAB><id>
+```
+
+`request` lines come first, one per incoming pairing request. Tabs and newlines
+inside a device name are replaced with spaces, so a hostile name cannot break
+the protocol. Empty or unparseable `qdbus6` output is an error and produces no
+lines, never a plausible-looking empty list, the notmuch lesson from
+AGENTS.md: validate the shape rather than trusting the absence of output.
+
+**The module, in `KdeConnectModule.qml`.** `alwaysActive: false`. A `Process`
+runs the script on a `SplitParser` and rebuilds a JS array of device objects
+from the lines. A 5s `Timer` drives it. The timer is active only
+while the drawer is open.
+
+The tile drives that lifetime. The tile content is instantiated whenever the
+drawer panel is loaded, whether the grid or a page is showing, and destroyed
+when the drawer closes (the drawer's `LazyLoader` unloads its `PanelWindow`).
+So the tile sets a `polling` flag on the module from `Component.onCompleted`
+and clears it from `Component.onDestruction`, and the timer runs on that flag.
+This refreshes once as the drawer opens and then every 5s while it is open,
+with no polling while it is closed. Same spirit as the vm module gating its
+stats poll on the page.
+
+**The page, in `KdeConnectPage.qml`.** Full height, the standard `Page` header
+with a back arrow, a refresh control, an incoming-pairing-request banner, and
+the device list. `KdeConnectRow.qml` is one device.
+
+## Tile
+
+`KdeConnectTile.qml`. A phone glyph, verified present in the Inconsolata Nerd
+Font cmap, rendered with `Theme.iconFamily` per the glyph rule in AGENTS.md.
+Active accent while any device is reachable. `tileContent` shows the reachable
+device name and its battery where there is one, the reachable count where there
+are several, `Offline` where devices are paired but none reachable, and
+`No devices` where none are paired.
+
+## Page and rows
+
+One row per device, paired first and unpaired discovered devices after. Row
+content: a type glyph (phone or laptop), the name, a status line (Reachable /
+Not reachable / Paired), and the battery percentage when the battery plugin
+reports one. Null or unknown values render a dash, never a substituted number,
+the libvirt rule.
+
+Actions are per row, as small buttons:
+
+| action | shown when | command |
+|---|---|---|
+| ping / find | reachable | `kdeconnect-cli --ring -d <id>` |
+| send clipboard | reachable | `kdeconnect-cli --send-clipboard -d <id>` |
+| share | reachable | file dialog, then `kdeconnect-cli --share <path> -d <id>` |
+| mount | reachable | `kdeconnect-cli --mount -d <id>`, then open the `--get-mount-point` path |
+| unpair | paired | `kdeconnect-cli --unpair -d <id>` |
+| pair | unpaired and reachable | `kdeconnect-cli --pair -d <id>` |
+
+Refresh is a page-level control, not per row: `kdeconnect-cli --refresh`.
+
+## Pairing
+
+Pairing is the reason the indicator cannot simply be deleted. The flow:
+
+- Outgoing: a `pair` button on an unpaired device runs `kdeconnect-cli --pair`.
+ The daemon asks the phone, the user confirms there, and the next poll shows
+ the device paired.
+- Incoming: `daemon.pairingRequests` lists ids that asked to pair with this
+ machine. The page shows a banner naming the device with Accept and Reject.
+ Accept calls `qdbus6 org.kde.kdeconnect
+ /modules/kdeconnect/devices/<id> org.kde.kdeconnect.device.acceptPairing`,
+ Reject calls `cancelPairing()`.
+- The verification key (`verificationKey`) is shown on the row while a pairing
+ is in flight, so the two keys can be compared.
+- Unpair calls `unpair()` on the device object.
+
+`isPairRequested` and `isPairRequestedByPeer` are read for display. The raw
+`pairState` integer is not mapped in QML, since the booleans carry the meaning.
+
+## File sharing and the dialog risk
+
+`share` opens a `QtQuick.Dialogs` `FileDialog` and, on accept, runs
+`kdeconnect-cli --share <path> -d <id>`. This is the one unproven piece: the
+dialog is a separate top-level window, and the drawer holds the keyboard with
+`WlrKeyboardFocus.Exclusive` on a layer surface. It is plausible the dialog
+renders but takes no focus, or does not render at all. This is probed during
+implementation. If it fails, the fallback is a text field for a path or URL,
+which is one rung lazier and already good enough; the design does not hinge on
+the dialog.
+
+## Failure handling
+
+Every action that fails raises `notify-send` with `--urgency=critical`, the
+pattern the vm and bluetooth modules use, because the drawer may have closed by
+the time the command returns. Pairing failure is read from
+`org.kde.kdeconnect.device.pairingFailed`, or from the pair button's process
+exit, and shown the same way. A failed `--mount` notifies rather than silently
+opening nothing.
+
+## Files
+
+```
+desktop/modules/kdeconnect/
+ KdeConnectModule.qml
+ KdeConnectTile.qml
+ KdeConnectPage.qml
+ KdeConnectRow.qml
+ kdeconnect-state.sh
+ test-kdeconnect-state.sh
+ README.md
+```
+
+`desktop/shell.qml` gains the module in the registry. `desktop/README.md` gains
+it in the module list, in grid order. With this tile the grid holds seven of
+its ceiling of nine.
+
+## Changes outside this repo
+
+| file | change |
+|---|---|
+| `~/.config/hypr/sections/autostart.lua` | remove the `kdeconnect-indicator` line |
+
+The system `kdeconnectd` autostart stays. No new blur rule is needed: the
+drawer already has `blur-desktop`.
+
+## Verification
+
+The only headless oracle is `test-kdeconnect-state.sh`. It puts a stub `qdbus6`
+on `PATH` that returns canned property dumps for a fixed two-device set, runs
+`kdeconnect-state.sh` against it, and asserts the exact tab-separated output,
+including a name carrying a tab to prove the sanitiser, a device with no
+battery plugin to prove the empty field, and a device list of zero to prove the
+error case emits nothing rather than a clean empty list. No shell, no daemon
+needed.
+
+Everything else is visual and is the user's to look at, per AGENTS.md. The
+specifics to walk: the tile on open before and after a poll, a reachable phone
+showing name and battery, an offline device, ping, a clipboard send landing on
+the phone, a file share, a mount opening the folder, an outgoing pair, and an
+incoming request accepted from the banner.
+
+The phone to PC clipboard direction is checked once, by sending a clipboard
+from the phone and pasting on the PC, to confirm whether Hyprland accepts the
+daemon's selection. It is expected to work but is not guaranteed, and if it
+does not, it is a Wayland focus limit rather than a fault in this module.
+
+Process checks follow AGENTS.md: the binary is `qs`, `pkill -x qs` and
+`pgrep -cx qs`, never `pkill -f`; a detached `qs` does not survive an agent's
+tool call, so it is started so the harness owns it and confirmed from the log.