1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
|
# 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`; 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.
|