diff options
| author | Craig Jennings <c@cjennings.net> | 2026-06-29 16:50:15 -0400 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-06-29 16:50:15 -0400 |
| commit | b248644109b79d91c3f2a8603cbaefaca05ced81 (patch) | |
| tree | 444bb579c4cb65647be1402cf097811591c76178 /docs/design | |
| parent | 3ff5148fc5942af6fc27315cd7cad0a41abad053 (diff) | |
| download | archsetup-b248644109b79d91c3f2a8603cbaefaca05ced81.tar.gz archsetup-b248644109b79d91c3f2a8603cbaefaca05ced81.zip | |
docs: add unified waybar network module design spec
The wifi-no-internet indicator, the nmcli network-manager dropdown, and the captive-portal diagnostics are one feature, so the spec designs them as a single custom/net module instead of three. It splits into three layers: a tested Python net engine wrapping nmcli plus the diagnostics, a thin bar indicator, and a GTK4 layer-shell panel. The captive script becomes the diagnostics engine.
It records the locked decisions (panel toolkit, split probe cadence, GPG store supplements NetworkManager, librespeed for speed test) and a four-phase plan, indicator first. I linked it from both todo tasks.
Diffstat (limited to 'docs/design')
| -rw-r--r-- | docs/design/2026-06-29-waybar-network-module-spec.org | 298 |
1 files changed, 298 insertions, 0 deletions
diff --git a/docs/design/2026-06-29-waybar-network-module-spec.org b/docs/design/2026-06-29-waybar-network-module-spec.org new file mode 100644 index 0000000..bf195b8 --- /dev/null +++ b/docs/design/2026-06-29-waybar-network-module-spec.org @@ -0,0 +1,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. |
