#+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 = / =net down [--iface IF]= — switch / disconnect. - =net add= / =net edit = / =net remove = — 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.= — 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: =, =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.