# 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. `Quickshell.Networking` exposes no local IP, only the MAC (`NetworkDevice.address`). The network tile's IP line therefore reads `ip -j -4 addr show`, refreshed on link change and on a slow timer while a link is up to catch a DHCP renew. That is the second shell-out, and the only other one. ## 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()` 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 tile is a globe; the wired/wifi distinction belongs to the page's section headers, which carry the ethernet and wifi glyphs. Each active link is a line of a green dot, the interface name and its local IPv4 (`eth0 10.0.0.5`). With both wired and wifi up there are two lines, one each, because the API exposes no route metric and the tile will not guess which link carries traffic. Nothing up shows `Disconnected`, or `Off` when the radio is down. `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`; a pairing failure is the `bluetoothctl` exit, surfaced by `Pairing.qml`. Disconnect and forget expose no failure signal in either backend, so a failure there shows only as the list not changing. That is a ceiling, noted rather than faked. 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.