diff options
Diffstat (limited to 'docs/superpowers')
| -rw-r--r-- | docs/superpowers/specs/2026-09-14-network-bluetooth-design.md | 160 |
1 files changed, 160 insertions, 0 deletions
diff --git a/docs/superpowers/specs/2026-09-14-network-bluetooth-design.md b/docs/superpowers/specs/2026-09-14-network-bluetooth-design.md new file mode 100644 index 0000000..53c79ac --- /dev/null +++ b/docs/superpowers/specs/2026-09-14-network-bluetooth-design.md @@ -0,0 +1,160 @@ +# Network and Bluetooth modules for the desktop shell + +Two new modules under the `desktop/` drawer: `network`, covering wired and +wifi, and `bluetooth`. They follow the module contract the desktop shell +already defines, and they use the native Quickshell backends rather than +shelling out to `nmcli` or `bluetoothctl`, with one deliberate exception for +Bluetooth pairing. + +This is the first of the projects the desktop shell design named but left +unbuilt. Do not disturb, breaktimer and kdeconnect remain later projects. + +## Why + +The drawer is where a desktop's adjustable things are visible together. Wifi +and Bluetooth are the two most-used of those and are currently not in it. The +design also chose to replace waybar's own network and Bluetooth indicators, so +the drawer becomes the single place both are managed. + +## Scope + +In: + +- `network`: wired devices and wifi, radio switch, scan, join a network with a + password, join a hidden network, connect, disconnect, forget. +- `bluetooth`: adapter power, discoverable, scan, pair, connect, disconnect, + forget, trust, device battery where reported. +- Removing waybar's network and Bluetooth indicators from the live config. + +Out, and noted as ceilings: + +- Enterprise / 802.1x wifi. The profile model is far heavier than the drawer + wants, and nothing here uses it. +- Joining a hidden network, if `NMSettings` turns out not to be constructible + in QML. Nothing else can create a profile from scratch, so this one is + conditional on a feasibility check during implementation. Dropped with a + note if it does not hold. + +## Backends + +Quickshell 0.3.1 ships the two APIs built in; verified against the 0.3.1 +reference and against the installed binary: + +- `Quickshell.Networking` (NetworkManager D-Bus). `Networking` has + `connectivity`, `devices`, `wifiEnabled`, `wifiHardwareEnabled`. + `NetworkDevice` has `autoconnect`, `name`, `address`, `networks`, `state`, + `type`, `connected`, `disconnect()`. `WifiDevice` adds `scannerEnabled` and + `mode`. `Network` has `known`, `nmSettings`, `state`, `connected`, `device`, + `name`, `connect()`, `connectWithSettings()`, `disconnect()`, `forget()` and + the `connectionFailed` signal. `WifiNetwork` adds `security`, + `signalStrength` and `connectWithPsk(psk)`. +- `Quickshell.Bluetooth` (BlueZ D-Bus). `Bluetooth` has `adapters`, + `defaultAdapter`, `devices`. `BluetoothAdapter` has `enabled`, + `discoverable`, `discoverableTimeout`, `pairable`, `pairableTimeout`, + `discovering`, `devices`, `name`, `state`, `adapterId`. `BluetoothDevice` + has `paired`, `bonded`, `connected`, `trusted`, `blocked`, `battery`, + `batteryAvailable`, `icon`, `address`, `name`, `deviceName`, `state`, + `adapter`, and `pair()`, `cancelPair()`, `connect()`, `disconnect()`, + `forget()`. + +Because Quickshell has no generic D-Bus module and no BlueZ pairing agent, +`pair()` cannot handle a device that requires a passkey or PIN confirmation. +That one action shells out to `bluetoothctl`, whose own agent does. + +## Module shape + +Both modules are `alwaysActive: true` and declare it truthfully. There is no +poll to gate: the backends are push, and the tile needs live state the moment +the drawer opens. Unlike sound and vm, neither needs a `Service.qml`: there is +no polling loop to own, so the module root references the singleton directly, +which is also what instantiates it at shell start. + +``` +modules/network/ NetworkModule.qml NetworkTile.qml NetworkPage.qml NetworkRow.qml README.md +modules/bluetooth/ BluetoothModule.qml BluetoothTile.qml BluetoothPage.qml BluetoothRow.qml Pairing.qml README.md +``` + +`shell.qml` gains the two imports and two registry entries. The drawer's +`open(<name>)` IPC reaches the pages with no further wiring; the module names +are `network` and `bluetooth`. + +Grid order becomes `Sound, Network, Bluetooth, Mail, Appearance, Machines`, +six tiles, two rows of three in the 600px panel. + +## The network page + +**Wired.** The managed `WiredDevice`s only, filtered by `type` so `docker0`, +`virbr0` and `veth*` never appear. Each is a row: interface, connected state, +and disconnect. + +**Wifi.** A radio switch on `Networking.wifiEnabled`. Off hides the list and +shows `Wi-Fi off`. On shows a scan toggle on `device.scannerEnabled` and a +list ordered connected first, then known, then the rest by `signalStrength`. +A lock glyph marks a secured network. Row click connects an open or known +network directly; a secured unknown network reveals an inline password field +on the row and submits through `connectWithPsk()`. A connected row offers +Disconnect, a known row offers Forget. + +**Hidden network.** A `Join hidden network` row taking SSID, security and +password, subject to the ceiling above. + +## The Bluetooth page + +Adapter power switch, then when on a `Make visible` toggle (`discoverable`) +and a scan toggle (`discovering`). Connected devices come first, each with +icon, name and battery when `batteryAvailable`, and click disconnects. Paired +devices follow: connect, disconnect, forget, and a trust toggle. Devices found +while scanning offer Pair, which starts `Pairing.qml`. + +## Tiles + +**Network.** The icon swaps wired, wifi, off. With one link up the state line +names it (`eth0`, or the SSID). With both wired and wifi up the content is two +compact lines, one each, because the API exposes no route metric and the tile +will not guess which link carries traffic. `active` while any link is up. + +**Bluetooth.** The icon swaps on and off. The state line names the connected +device, or a count when several, `No devices` when powered but idle, `Off` +when the adapter is down. `active` while anything is connected. + +## Failures + +An inline status line on the page beside the affected row: a rejected PSK +arrives as `connectionFailed`, and forgetting or pairing can fail too. A +failure that lands after the drawer has closed also fires `notify-send`, +using the cached-`Process` pattern already in `VmModule.qml`. The error clears +on the next action. + +## Files outside this repo + +The live waybar configuration loses its network and Bluetooth indicators, +applied by the user. Nothing else outside the repo changes: both pages render +inside the existing `quickshell-desktop` window, so there is no new namespace +and no new blur rule, and no new IPC. + +## Verification + +No automated oracle reaches a D-Bus backend, so verification is visual, by the +user, following the AGENTS.md measurement rules (the process is `qs`, a +detached one does not survive a tool call, `pkill -f` kills the caller): + +- radio toggles for both wifi and the Bluetooth adapter +- join an open network and a secured one, once with a wrong PSK and once right +- forget a network +- wired shows connected; with wired and wifi both up the tile shows two lines +- Bluetooth power, discoverable, scan +- pair a Just Works device, then forget and re-trust it + +If `Pairing.qml` grows a parser for `bluetoothctl` output, that parser leaves +one runnable `assert` check fed sample lines. `test-mail-notify.sh` is +unaffected. + +## Traps carried forward + +- The PipeWire "initial value arrives before `ready`" pattern may apply to + these singletons too. Verify before trusting a first paint rather than + assuming a guard is needed. +- QML's JS engine has no `String.matchAll`, so any `bluetoothctl` parsing uses + an `exec` loop. +- `bluetoothctl` output parsing is brittle. Mark it with a `ponytail:` comment + naming the ceiling and the upgrade path. |
