aboutsummaryrefslogtreecommitdiff
path: root/docs/design/2026-06-29-waybar-network-module-spec.org
blob: bf195b8debc93740b5ff42621ad6b90301e9b4cc (plain)
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
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
#+TITLE: Waybar Network Module — Design Spec
#+AUTHOR: Craig Jennings & Claude
#+DATE: 2026-06-29

* Goal

One waybar network component that does the whole job: shows connection state
(including the missing "associated but no internet / captive portal" state),
manages connections from a dropdown (nmcli-backed, optional GPG-encrypted secret
store), and runs the network diagnostics and remediation off the same place
(captive-portal detection + forcing, bounce/reset, gateway/DNS checks, speed
test).

It unifies three todo tasks that are really one feature:
- =[#C]= "archsetup Waybar Wi-Fi module should show no-internet state" — the
  indicator state plus the 2026-06-22 roam expansion (bounce, diagnostics, speed
  test off the component).
- =[#B]= "Network-manager dropdown, nmcli-backed with GPG-stored secrets" — the
  management dropdown.
- The network diagnostics already shipped in =captive= (the hotel/captive-portal
  tool, formerly =login-page=) become this module's diagnostics engine rather
  than a standalone CLI.

* Scope

** In
- *Indicator* — wifi/ethernet icon + signal + SSID, plus an internet sub-state:
  online / captive / no-internet / connecting / disconnected / airplane.
- *Interface-correct* — targets the wifi (or chosen) device, not the
  default-route interface, so an active USB tether or wired link can't mask
  wifi state. (Same lesson =captive= fixed; the current =custom/netspeed= keys
  off the default route and has the bug.)
- *Connection management (panel)* — list saved connections most-recently-used
  first, live signal for in-range wifi, click to switch; add / edit / remove;
  ethernet↔wifi and wifi↔wifi switching even when a link appears mid-session.
- *Diagnostics (panel)* — captive probe (204-vs-portal) with the extracted
  portal URL and an Open button; bounce/reset (fresh MAC); gateway ping; DNS
  config + temporary 1.1.1.1 override test.
- *Speed test (panel)* — down/up/ping with a progress indicator and last-result
  shown.
- *Credential store* — optional GPG-encrypted connection+secret file under
  =~/.config=, opt-in (default unencrypted), passphrase cadence via gpg-agent
  TTL. Supplements NetworkManager, does not replace it.
- *Persistence* — connectivity probe result cached in the runtime dir so the
  bar reads it cheaply between probes.

** Out (v1, note for later)
- No replacement of NetworkManager's connection engine. NM stays the thing that
  connects; we drive it via nmcli.
- No VPN / wireguard management (separate tooling already exists).
- No per-connection captive-portal auto-login automation beyond opening the
  portal page.
- No graphing/history of speed-test results beyond the last run.
- The desktop-settings dropdown (sibling =[#B]=) is a separate module, but it
  shares the GTK4 layer-shell panel shell built here.

* Architecture

Three layers. Keep the bar cheap, the panel rich, the logic in one tested place.

1. *Engine* — a =net= Python package (src-layout, pytest), exposing a CLI. Wraps
   every nmcli op and owns the diagnostics. Emits JSON. This is the testable
   core (fake =nmcli= / =curl= on PATH, like the existing =waybar-netspeed= and
   =waybar-sysmon= test harnesses). Precedent: pocketbook is Python in the
   dotfiles repo; =wtimer= is Python for the same testability reason.
2. *Indicator* — a thin =waybar-net= script that calls =net status --json= and
   renders icon + signal + state + tooltip. Replaces =custom/netspeed=
   (throughput folds into the tooltip).
3. *Panel* — a GTK4 + gtk4-layer-shell app (mirrors pocketbook's structure)
   that imports the engine. Hosts connection management, diagnostics, and the
   speed test.

How the existing pieces map in:
- =captive= (bash, shipped) — the engine shells out to it for the heavy,
  interactive portal-force flow (sudo reset, DNS override, browser launch). Its
  cheap portal-detection logic is mirrored natively in the engine for the fast
  status path so the bar never blocks on a subprocess. =captive= stays a usable
  standalone CLI.
- =waybar-netspeed= (sh, shipped) — retired; its throughput sampling moves into
  the engine's status output and renders in the indicator tooltip only.
- =nmcli= — the connection backend for every op.

Language note: the engine is Python; the indicator is a thin Python or sh
wrapper over =net status --json=. The bar path must stay fast (sub-100ms for the
cheap poll), so the indicator does no network I/O itself — it reads link state
and the cached connectivity result.

* Connectivity model — split cadence

The indicator polls every ~2s, but a real internet/captive probe every 2s wastes
battery and can re-trigger a captive portal. So split it:

- *Fast path (every poll, cheap, no network)* — interface, type, SSID, signal,
  IPv4 presence, throughput sample. From nmcli / sysfs only.
- *Slow path (cached, TTL ~45s)* — the actual internet/captive probe (the 204
  check + meta-refresh portal extraction). Result cached at
  =$XDG_RUNTIME_DIR/waybar/net-connectivity.json= with a timestamp.

The indicator reads the cache each poll. When the cache is older than the TTL,
=net status= kicks =net probe= in the background (non-blocking) and renders the
last cached sub-state meanwhile. A user-triggered diagnose/reconnect refreshes
the cache immediately. This keeps the bar responsive and the portal un-poked.

* Engine — =net= CLI surface

All subcommands take =--json= where a machine reads them. Pure formatting/state
functions under the CLI; IO (nmcli, curl, file) at the edges.

- =net status [--json] [--iface IF]= — fast link state + cached connectivity
  sub-state + throughput. The indicator's source.
- =net probe [--iface IF]= — run the connectivity/captive probe now, update the
  cache, print online | captive (+ portal URL) | no-internet. Mirrors =captive='s
  cheap detection natively.
- =net list [--json]= — saved connections, MRU order, active flag, plus in-range
  wifi with signal.
- =net up <id|ssid>= / =net down [--iface IF]= — switch / disconnect.
- =net add= / =net edit <id>= / =net remove <id>= — manage connections; sync the
  GPG store (below).
- =net rescan [--iface IF]= — wifi rescan.
- =net diagnose [--json]= — full report: gateway ping, DNS config, DNS 1.1.1.1
  override test, captive probe. Shells to =captive= for the interactive/sudo
  parts; native for the read-only parts.
- =net portal= — run =captive='s portal-force flow (reset if needed, extract +
  open the portal page).
- =net reset [--hardware-mac]= — fresh-MAC reconnect (=captive='s =fresh_mac=).
- =net speedtest [--json]= — librespeed run; down/up/ping.

* Indicator (task #C — Phase 1, the fast win)

** States (internet sub-state on top of link state)
- online — associated and the probe returned 204. Normal icon.
- captive — associated, probe hit a portal. Distinct glyph + warning CSS class;
  tooltip names the portal host; left-click opens the panel's diagnostics with
  the portal ready to open.
- no-internet — associated, probe failed (no portal, no 204). Distinct glyph +
  warning class.
- connecting / disconnected / airplane / wired — as today, plus wired shown
  correctly even when it appears after session start.

** Glyphs
Nerd-font codepoints, final values verified live before merge (same discipline
as wtimer). Reuse the signal-strength ramp already in =waybar-netspeed=; add a
captive/no-internet overlay glyph.

** Tooltip
SSID + signal + IPv4 + gateway + the throughput readout (absorbed from
netspeed) + the last probe result and age.

** Interactions (no keyboard-modifier clicks — waybar can't qualify clicks by
modifier; the panel hosts the rich actions)
- left-click — open the panel.
- right-click — quick reconnect / bounce (=net reset=, no panel).
- middle-click — run =net portal= (force the captive page).
- scroll — cycle nothing in v1 (reserved; could cycle saved connections later).

* Panel (tasks #B + #C diagnostics — Phases 2-3)

GTK4 + gtk4-layer-shell, pocketbook scaffold (src-layout package, pytest,
Makefile, gtk4-layer-shell anchored dropdown under the bar). One panel shell,
reused by the future desktop-settings panel.

Sections:
1. *Connections* — list, MRU-first, active marked, live signal bars for in-range
   wifi; row click switches; buttons for add / edit / remove; a rescan control.
2. *Diagnostics* — buttons: Probe (204/captive, shows portal URL + Open),
   Bounce/Reset, Gateway ping, DNS override test. Streaming output area.
3. *Speed test* — Run button, progress, down/up/ping result + last-run line.

Interaction-pattern catalog (=~/code/rulesets/patterns/=) principles that apply:
- transient-state-buttons — all the network levers in one place, reachable by
  one chord (the bar click), state visible.
- default-most-common-friction-proportional — connections MRU-ordered so the
  common pick is first; destructive ops (remove) get a confirm, switching does
  not.
- one-prompt-picker-typed-prefix — if the connection picker ever goes
  keyboard-driven, kind (wifi/eth/saved/in-range) + name in one typed picker.

* Connection management (nmcli)

- Every op via nmcli: =device status=, =connection show=, =con up/down=,
  =con add/modify/delete=, =dev wifi rescan/list=.
- MRU ordering from NM's =connection.timestamp= (last activated), descending.
- Ethernet appears in the list whenever a wired device is present, selectable at
  any time; switching just brings the chosen connection up.
- TDD with a fake =nmcli= on PATH returning canned output, asserting the exact
  nmcli command sequence (behavior, not implementation) — the established
  pattern in =tests/waybar-netspeed= and =tests/layout-navigate=.

* Credential storage (GPG)

- Store: =~/.config/net/connections.<ext>= — connection definitions + secrets
  (PSK/EAP), one record per connection, with =last_used=.
- *Default unencrypted* (=connections.json=). Encryption is opt-in: when enabled,
  =connections.json.gpg= encrypted to Craig's private key (=c@cjennings.net=).
- Passphrase cadence via gpg-agent: once per session (long cache TTL or
  decrypt-and-hold), once per hour (=default-cache-ttl=), or never (plaintext).
  Configured in =~/.config/net/config=.
- *Supplements NM, does not replace it.* NM's own store
  (=/etc/NetworkManager/system-connections=, root-only) stays the source of
  truth that actually connects. The GPG store is a portable, user-owned export +
  secret vault. =net add/edit/remove= writes both (nmcli + store). =net import=
  rebuilds NM connections from the store on a fresh machine. They sync on every
  mutating op; NM wins on conflict for the live connection.

* Diagnostics + speed test

- Diagnostics reuse =captive= verbatim for the interactive flow (=net portal=,
  =net reset=, the 1.1.1.1 DNS override test) and mirror its cheap probe natively
  for =net status= / =net probe=. No logic is duplicated by hand beyond the small
  portal-URL parser, which is already unit-tested in =tests/captive=.
- Speed test: *librespeed-cli* (no account, self-hostable, AUR), chosen over
  Ookla speedtest-cli. =net speedtest --json= parses its JSON; the panel shows a
  progress indicator and the down/up/ping result.

* Waybar wiring

- Replace =custom/netspeed= with =custom/net= in the bar's module list (same
  slot).
- Module def: =exec: waybar-net=, =return-type: json=, =interval: 2=, a =signal=
  for on-demand refresh (next free signal after wtimer's 14), =on-click: <open
  panel>=, =on-click-right: net reset=, =on-click-middle: net portal=.
- Remove the old =on-click: pypr toggle network= scratchpad once the panel
  replaces it.

* Testing plan (TDD)

- *Engine* — fake =nmcli= + fake =curl= on PATH; assert command sequences and
  parsed/emitted JSON for status, list, up/down, add/edit/remove, probe,
  diagnose, speedtest. Pure state/format functions tested directly.
- *Portal parser* — already covered in =tests/captive= (Normal/Boundary/Error +
  the real SONIFI body). The engine's native probe reuses the same cases.
- *Indicator* — drive =net status --json= through =waybar-net=, assert the JSON
  the bar renders for each state (online / captive / no-internet / wired /
  disconnected), interface override via env like =WAYBAR_NETSPEED_IFACE=.
- *Panel* — pocketbook-style: test the backing logic (list ordering, op
  dispatch, store read/write, gpg round-trip with a test key), not the GTK
  widgets.
- *GPG store* — round-trip encrypt/decrypt with a throwaway test key; sync
  on add/edit/remove; import rebuilds NM ops (asserted against fake nmcli).

* Files touched (planned)

- =hyprland/.local/bin/waybar-net= — the indicator (replaces =waybar-netspeed=).
- =hyprland/.local/bin/net= — engine CLI entry (or a package console-script).
- =net/= package (src-layout, like pocketbook) — engine + panel, in the dotfiles
  repo (or in-tree as pocketbook is).
- =hyprland/.config/waybar/config= — swap =custom/netspeed= → =custom/net=.
- =hyprland/.config/waybar/style.css= — captive / no-internet state classes.
- =tests/net/=, =tests/waybar-net/= — suites.
- =~/.config/net/= — config + connection store (machine-local; not stowed
  content beyond a seed config).
- =captive= — minor refactor so the engine can reuse its probe cleanly.

* Resolved decisions (this session, Craig's calls)

1. Panel UI tech → GTK4 + gtk4-layer-shell, shared pocketbook scaffold (one
   panel shell, reused by the desktop-settings sibling).
2. Engine language → Python =net= package; shells out to =captive= for the
   portal-force flow, native cheap probe for the bar path.
3. Connectivity probe → split cadence (fast link poll every 2s + slow cached
   internet/captive probe, TTL ~45s).
4. No keyboard-modifier clicks (waybar can't qualify them) — the panel hosts the
   rich actions; bar clicks are left=panel, right=reset, middle=portal.
5. GPG store supplements NM; NM stays the source of truth.
6. =custom/netspeed= absorbed into =custom/net=; throughput moves to the tooltip.
7. Speed-test backend → librespeed-cli.

* Phasing

- *Phase 1 — Indicator (task #C).* =net status= + =net probe= (native cheap
  probe, reusing captive's logic) + =waybar-net= + the split-cadence cache + CSS
  states. Ships the no-internet/captive state on the bar. Smallest, highest
  value, fully testable without the GTK panel.
- *Phase 2 — Panel shell + connection management (task #B core).* GTK4
  layer-shell scaffold + =net list/up/down/add/edit/remove/rescan= + MRU list.
- *Phase 3 — Diagnostics + speed test in the panel.* Wire =net diagnose= /
  =net portal= / =net reset= / =net speedtest= into the panel; portal Open
  button.
- *Phase 4 — GPG credential store.* Opt-in encryption, cadence config, NM sync,
  import.

* Open items / risks

- gtk4-layer-shell dropdown anchoring under a waybar module needs the same
  positioning work pocketbook solved; reuse it.
- librespeed-cli availability + a default server choice (public list vs a pinned
  server) to confirm before Phase 3.
- The background-probe kick from =net status= must be truly non-blocking (spawn +
  detach) so a slow/hanging probe never stalls the bar.
- NM↔GPG-store conflict policy on edit needs a concrete rule (NM wins for the
  live connection) — confirm during Phase 4.

* Rollback

Each phase is independent. The indicator (Phase 1) is a drop-in replacement for
=custom/netspeed=; reverting is swapping the module back in the config. The panel
and store are additive — not wiring their clicks / not enabling encryption leaves
the bar working as before.