diff options
Diffstat (limited to 'docs/superpowers')
| -rw-r--r-- | docs/superpowers/specs/2026-09-14-kdeconnect-design.md | 217 |
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. |
