aboutsummaryrefslogtreecommitdiff
path: root/docs/design
diff options
context:
space:
mode:
Diffstat (limited to 'docs/design')
-rw-r--r--docs/design/2026-06-29-waybar-network-module-spec.org298
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.