aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-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.