From 22101e1f1bc846885798b5815318a41110110a1c Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Sat, 4 Jul 2026 12:38:10 -0500 Subject: docs(spec): sort formal specs into docs/specs/ with lifecycle status Moved the seven formal specs from docs/design/ into docs/specs/, each stamped with a lifecycle status heading: four IMPLEMENTED (bluetooth, net-other-interfaces, audio, instrument-console), one CANCELLED (file-manager-swallow), two DRAFT (desktop-settings, timer). Rewrote the seven todo.org links to the new paths. The two -spec.org files without the spec spine (waybar-network-module, waybar-timer-module) stayed in docs/design/ as notes. --- docs/design/2026-07-02-bluetooth-panel-spec.org | 470 -------------------- .../2026-07-02-desktop-settings-panel-spec.org | 128 ------ .../2026-07-02-file-manager-swallow-spec.org | 141 ------ .../2026-07-02-net-panel-other-interfaces-spec.org | 189 -------- docs/design/2026-07-02-timer-panel-spec.org | 113 ----- docs/design/2026-07-03-audio-panel-spec.org | 160 ------- .../2026-07-03-instrument-console-panels-spec.org | 158 ------- docs/specs/2026-07-02-bluetooth-panel-spec.org | 476 +++++++++++++++++++++ .../2026-07-02-desktop-settings-panel-spec.org | 134 ++++++ .../specs/2026-07-02-file-manager-swallow-spec.org | 147 +++++++ .../2026-07-02-net-panel-other-interfaces-spec.org | 195 +++++++++ docs/specs/2026-07-02-timer-panel-spec.org | 119 ++++++ docs/specs/2026-07-03-audio-panel-spec.org | 166 +++++++ .../2026-07-03-instrument-console-panels-spec.org | 164 +++++++ 14 files changed, 1401 insertions(+), 1359 deletions(-) delete mode 100644 docs/design/2026-07-02-bluetooth-panel-spec.org delete mode 100644 docs/design/2026-07-02-desktop-settings-panel-spec.org delete mode 100644 docs/design/2026-07-02-file-manager-swallow-spec.org delete mode 100644 docs/design/2026-07-02-net-panel-other-interfaces-spec.org delete mode 100644 docs/design/2026-07-02-timer-panel-spec.org delete mode 100644 docs/design/2026-07-03-audio-panel-spec.org delete mode 100644 docs/design/2026-07-03-instrument-console-panels-spec.org create mode 100644 docs/specs/2026-07-02-bluetooth-panel-spec.org create mode 100644 docs/specs/2026-07-02-desktop-settings-panel-spec.org create mode 100644 docs/specs/2026-07-02-file-manager-swallow-spec.org create mode 100644 docs/specs/2026-07-02-net-panel-other-interfaces-spec.org create mode 100644 docs/specs/2026-07-02-timer-panel-spec.org create mode 100644 docs/specs/2026-07-03-audio-panel-spec.org create mode 100644 docs/specs/2026-07-03-instrument-console-panels-spec.org (limited to 'docs') diff --git a/docs/design/2026-07-02-bluetooth-panel-spec.org b/docs/design/2026-07-02-bluetooth-panel-spec.org deleted file mode 100644 index 121197a..0000000 --- a/docs/design/2026-07-02-bluetooth-panel-spec.org +++ /dev/null @@ -1,470 +0,0 @@ -#+TITLE: Bluetooth Panel — CLI-Driven, Net-Panel Kin -#+AUTHOR: Craig Jennings -#+DATE: 2026-07-02 -#+TODO: TODO | DONE -#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED - -* IMPLEMENTED Status -:PROPERTIES: -:ID: 1271a845-4463-4831-9902-990eda6b2265 -:END: -- [2026-07-02 Thu] IMPLEMENTED — all five phases shipped the same day - (dotfiles eb2230f / 76b2c05 / e372de3 / 2a026b1; archsetup d8d8c53): - engine, panel, bar module + blueman retirement, bt-priv + package swap, - install wiring proven by VM assertions. 43 dotfiles suites green, both - AT-SPI smokes green, panels verified live; the phase 4-5 VM assertions - run on the next VM pass. -- [2026-07-02 Thu] DOING — spec-response decomposed the five phases into - build sub-tasks under the todo.org parent (:SPEC_ID: bound); build - started same day per Craig ("4 first, then 1" — bugs then bluetooth). -- [2026-07-02 Thu] READY — spec-review passed the gate: all four - decisions resolved, phases decomposable, CLI verbs verified against - bluez 5.86. Two non-blocking findings recorded and dispositioned in - the same pass (donor-pattern answers). -- [2026-07-02 Thu] DRAFT — initial spec from Craig's request: a bluetooth - module driving a CLI underneath, consistent with the net panel, minimal - interface, full functionality, diagnostics section, visual mockups. - -* Metadata - -| Field | Value | -|--------+---------------------------------------------------------------------------------| -| Status | implemented | -|--------+---------------------------------------------------------------------------------| -| Owner | Craig Jennings | -|--------+---------------------------------------------------------------------------------| -| Repo | dotfiles (bt module); archsetup (packages, sudoers, keybind defaults) | -|--------+---------------------------------------------------------------------------------| -| Kin | net panel (architecture donor), desktop-settings panel (same donor, shared css) | -|--------+---------------------------------------------------------------------------------| - -* Problem - -Bluetooth on both daily drivers runs through blueman: a tray applet plus a -GTK3 manager window (Super+Shift+B). It's the odd one out on the desktop — -a foreign visual style next to the dupre-themed panels, a tray icon where -every other indicator is a first-class waybar module, and no diagnostics -story at all. When the BT mouse fails to reconnect at boot (a recurring -gotcha — touchpad-auto exists because of it) or headphones pair but route -no audio, the fix is a terminal séance: bluetoothctl, rfkill, systemctl, -wpctl, in whatever order folklore suggests. - -The net panel proved the shape that fixes this: a minimal layer-shell -popup over a GTK-free engine that drives a CLI, with a diagnostics tab -that names the failure and offers the repair. Bluetooth is the same -problem with a smaller surface: one adapter, a handful of devices, a -short list of well-known failure modes. - -* Goals - -1. Visibility: adapter power state and every known device with live state - (connected, battery, signal) in one glance — panel and bar module agree. -2. Control: power, scan, pair, connect, disconnect, forget — full - functionality from the panel, zero terminals (the net panel's V2 - contract). -3. Diagnostics: a doctor that walks the known failure chain (adapter → - rfkill → service → power → device → audio profile), names the broken - link in evidence rows, and offers tiered repairs. -4. Consistency: same stack, same window shape, same interaction grammar, - same palette as the net panel. A user who knows one panel knows both. - -Audio-profile switching is in scope for v1 (Craig, 2026-07-02 — "bitten -by this too many times to count"): the doctor's audio-profile step -carries a one-click repair, not just a diagnosis, and connected audio -devices surface their active profile (details in the doctor chain below). - -Non-goals (this iteration): OBEX file transfer, multi-adapter support -(both machines have one controller), BLE sensor/GATT browsing. - -* Design sketch - -** Architecture — the net panel's stack, verbatim - -- GTK4 + gtk4-layer-shell, Blueprint .blp compiled to committed .ui - (=make ui=), PyGObject at runtime. -- Humble-object split: GTK-free =PanelModel= presenter (unit-tested like - net's), thin composite-widget pages, =bg(work, done)= worker-thread - helper for every slow call. -- Engine: a new =bt= package in dotfiles (=bluetooth/src/bt/=, sibling of - =net/=), CLI entry =bt= with =bt status= / =bt panel= / =bt doctor= — - the same cmd/cli layout as net. -- Layer-shell OVERLAY popup anchored TOP+RIGHT, 380x520, Esc closes, - focus-out auto-hides, single-instance toggle via a =bt-panel= wrapper. - Dupre palette css shared with the net panel (the factored css asset the - desktop-settings spec calls for — three consumers now, so the factoring - happens in this project's phase 1 if settings hasn't landed it). -- Testing: engine TDD with fake binaries on a temp PATH (fake-bluetoothctl, - fake-rfkill, fake-systemctl, fake-wpctl); PanelModel unit suite; one - gated AT-SPI smoke (=make test-panel= pattern). - -** CLI backing — bluetoothctl one-shot verbs - -bluez 5.86 (installed) supports everything non-interactive: - -- Adapter: =bluetoothctl show= (powered, discoverable, pairable), - =bluetoothctl power on|off=. -- Device lists: =bluetoothctl devices Paired|Connected|Trusted= — the - Paired view is a merge of Paired + Connected states; =bluetoothctl info - = per row fills caption detail (battery percentage rides bluez's - built-in Battery1 profile and appears in info output; RSSI appears - during discovery). -- Scan: =bluetoothctl --timeout N scan on= (bounded discovery burst), - then =devices= diffed against Paired for the Nearby list. The panel - scans in 8s bursts with a live "Scanning…" state rather than an - unbounded scan. -- Connect/disconnect/forget: =bluetoothctl connect|disconnect|remove =. -- Pairing: the one interactive corner. =bluetoothctl pair = can demand - a passkey confirmation. The engine drives bluetoothctl's line protocol - over a pty with a bounded state machine (expect "Confirm passkey", - reply yes/no); a passkey prompt surfaces as a panel dialog showing the - six digits, mirroring the net panel's password dialog. NoInputNoOutput - devices (mice, most headphones) sail through without the dialog. -- rfkill: the user is in the =rfkill= group, so block/unblock is - unprivileged (=rfkill unblock bluetooth=). -- Privileged path: exactly one verb needs root — =systemctl restart - bluetooth= — so =bt-priv= is a one-verb closed helper with its own - NOPASSWD sudoers rule placed by archsetup, cloning net-priv's - regex-validated pattern rather than widening net-priv's scope. - -** Panel anatomy - -Two tabs. Devices is the panel; Diagnostics is the escape hatch. - -Devices tab, Paired sub-view (the default — daily use is reconnecting -known devices, not discovering new ones): - -#+begin_example -╭──────────────────────────────────────────────╮ -│ [ Devices ] [ Diagnostics ] │ ← top switcher -│ │ -│ Bluetooth ●──○ hci0 on │ ← adapter row: power switch -│ ──────────────────────────────────────────── │ -│ [ Paired ] [ Nearby ] │ ← sub-view switcher -│ ┌──────────────────────────────────────────┐ │ -│ │ 󰍽 MX Master 3 │ │ -│ │ Connected · battery 80% │ │ -│ │ 󰋋 WH-1000XM4 │ │ -│ │ Paired, not connected │ │ -│ │ 󰌌 K380 Keyboard │ │ -│ │ Paired, not connected │ │ -│ │ │ │ -│ └──────────────────────────────────────────┘ │ -│ [ Disconnect ] [ Forget ] │ ← acts on selected row -╰──────────────────────────────────────────────╯ -#+end_example - -The primary button is one control with a state-following label: -"Connect" when the selection is disconnected (suggested-action styling), -"Disconnect" when connected. Row-activate (Enter / double-click) -connects — never disconnects — matching the net panel's asymmetry. -Captions carry the human state line; the MAC lives in the row tooltip, -not the visible caption. - -Devices tab, Nearby sub-view: - -#+begin_example -╭──────────────────────────────────────────────╮ -│ [ Devices ] [ Diagnostics ] │ -│ │ -│ Bluetooth ●──○ hci0 on │ -│ ──────────────────────────────────────────── │ -│ [ Paired ] [ Nearby ] │ -│ ┌──────────────────────────────────────────┐ │ -│ │ Scanning… (6s) │ │ ← overlay state label -│ │ 󰋋 JBL Flip 6 −58 dBm │ │ -│ │ 󰄜 Pixel 9 −71 dBm │ │ -│ │ 󰂱 (unnamed) 74:A5:… −83 dBm │ │ -│ └──────────────────────────────────────────┘ │ -│ [ Pair ] [ Rescan ] [ Discoverable ⊙ ] │ -╰──────────────────────────────────────────────╯ -#+end_example - -Pair does the whole intended thing — pair, then trust, then connect — -because pairing a device means "use it now and reconnect on its own -later" (decision below). Discoverable is a toggle for the inbound case -(pairing a phone TO the laptop), off by default, auto-off with bluez's -discoverable-timeout. Rows sort by RSSI, strongest first; named devices -above unnamed ones. - -Diagnostics tab (mirrors the net panel's shape: one big verb + streaming -evidence rows + tiered repairs behind confirmation): - -#+begin_example -╭──────────────────────────────────────────────╮ -│ [ Devices ] [ Diagnostics ] │ -│ │ -│ [ Get Bluetooth Working ] [ Advanced ▸]│ -│ ┌──────────────────────────────────────────┐ │ -│ │ ✓ Adapter present (hci0) │ │ -│ │ ✓ Not blocked (rfkill clear) │ │ -│ │ ✓ bluetooth.service active │ │ -│ │ ✓ Adapter powered │ │ -│ │ ✗ MX Master 3: paired but unreachable │ │ -│ │ … Re-pair suggested — see below │ │ -│ │ │ │ -│ │ Fix: [ Reconnect ] [ Re-pair device ] │ │ -│ └──────────────────────────────────────────┘ │ -│ power-cycle · restart service · unblock │ ← tiered repairs (confirm) -╰──────────────────────────────────────────────╯ -#+end_example - -The doctor chain, in order, each an evidence row: - -1. Adapter present — =bluetoothctl list= / rfkill has an hci entry. - Absent → hardware/driver verdict, no repair offered. -2. rfkill state — soft-blocked names the likely cause when the - airplane-mode state file says airplane is on ("Blocked by airplane - mode — turn airplane mode off"), otherwise offers Unblock (no root - needed, rfkill group). -3. bluetooth.service — inactive/failed → offer restart (the one bt-priv - verb), evidence quotes the last journal line. -4. Adapter powered — off → offer power on (and note if a boot-time - policy keeps turning it off). -5. Per-device reachability — paired-but-connect-fails distinguishes - "device off/out of range" (RSSI absent in a scan burst) from "bond - corrupt" (connect error string), and only the latter suggests the - re-pair repair (remove + pair + trust + connect, confirmed first — - it's the destructive tier). -6. Audio profile (audio devices only) — device connected but no wpctl - sink/source, or the card stuck in HSP/HFP when A2DP is expected: - evidence names the active profile and offers the repair inline — - "Switch to A2DP" drives =wpctl set-profile = (profile - inventory from =pw-dump= — ground truth 2026-07-02: wpctl can't - enumerate a card's profiles, and the card's =bluez5.profile= prop - reads "off" mid-stream; the card's Profile param and the sink node's - =api.bluez5.profile= are authoritative), verifies the sink came back - in the expected profile, and reports fixed or no-change. In v1 per - Craig (2026-07-02): this failure mode has bitten repeatedly, so it - gets the one-click fix, not just a diagnosis. Connected audio-device - row captions also show the profile when it's the degraded one - ("Connected · mic mode (HSP)") so the state is visible before the - doctor runs. - -Repairs confirm with the net panel's future-tense scope copy ("This will -restart the Bluetooth service. Connected devices will drop and -reconnect."), run on the worker thread, verify after (re-read state, -report "fixed" or "no change"), and never chain silently. - -** Bar module - -=custom/bluetooth= replacing the blueman-applet tray icon: the panel's -glanceable layer, one glyph, state-following like =custom/net=: - -#+begin_example - 󰂲 off / blocked (dim; red slash variant when rfkill-blocked) - 󰂯 on, nothing connected (dim) - 󰂱 connected (white; tooltip lists devices + battery) -#+end_example - -Tooltip carries device names, battery percentages, and the keybind hints -(the module-tooltip convention shipped 2026-07-02). Click opens the -panel (=bt-panel= toggle wrapper); the existing Super+Shift+B bind moves -from blueman-manager to =bt panel=. Low-battery on a connected device -(<15%) adds a red percentage to the glyph text — the mouse dying -mid-meeting is the one state worth surfacing unprompted. - -** UX conformance notes - -Named against the heuristics the panel family follows (Nielsen's ten, -plus the rulesets patterns catalog): - -- Visibility of status: live captions, scan countdown, elapsed ticker on - long ops, verify-after-repair rows. -- Match to the real world: device-kind glyphs + plain state lines; MACs - demoted to tooltips; "Forget" not "Remove bond". -- User control: Esc closes, Rescan is idempotent, scan bursts are - bounded, repairs confirm, running ops show a Stop where stoppable. -- Consistency: interaction grammar is the net panel's — same switcher - layout, same primary-button contract, same confirm copy shape. -- Error prevention: Forget and Re-pair confirm; power-off while devices - are connected states the consequence in the confirm body. -- Recognition over recall: every action is a visible button; no context - menus, no hidden gestures (transient-state-buttons pattern). -- Minimalism: two tabs, one primary action per view, detail behind - tooltips and the Advanced reveal. -- Help users recover: the doctor's evidence rows name the broken link - and carry the repair inline (default-most-common-friction-proportional: - the likely fix is one click, the destructive one is confirmed). - -Tension found with the net panel while writing this (filed as todo.org -tasks per Craig's instruction, 2026-07-02): transient error toasts -auto-dismiss in 4s, and the V2 spec's keyboard-navigation claims -(tab-between-sections, arrow rows, type-to-filter) aren't verifiably -implemented. Both filed against the net panel rather than cloned here; -this panel adopts whatever resolution those tasks land on. - -* Decisions (Craig) [4/4] - -** DONE Pair implies trust + connect? -CLOSED: [2026-07-02 Thu] -Decided (Craig, 2026-07-02): yes — one Pair verb does pair → trust → -connect. A device that shouldn't auto-reconnect gets untrusted later; a -per-device auto-reconnect toggle can ride a later pass. - -** DONE Retire blueman entirely? -CLOSED: [2026-07-02 Thu] -Decided (Craig, 2026-07-02): drop it outright, no bake-in period — the -package leaves archsetup and both machines once phase 2 lands, -bluetoothctl stays as the terminal fallback. Craig's framing: any issue -after retirement is a signal the doctor needs another check or the panel -has a real bug, and it gets fixed there rather than papered over by -keeping blueman around. - -** DONE Battery in the row caption or tooltip only? -CLOSED: [2026-07-02 Thu] -Approved (Craig, 2026-07-02): caption when the device reports it -("Connected · battery 80%"), tooltip otherwise. - -** DONE Scan burst length and auto-rescan? -CLOSED: [2026-07-02 Thu] -Approved (Craig, 2026-07-02): 8s bursts, no auto-repeat — Rescan stays -explicit, matching the net panel's Available view. - -* Review findings [2/2] - -** DONE Empty-state and no-adapter presentation copy undefined :nonblocking: -CLOSED: [2026-07-02 Thu] -The mockups show populated lists; the spec didn't say what an empty Paired -list, an empty post-scan Nearby list, or a machine with no adapter shows -in the panel and on the bar glyph. Dispositioned same pass: clone the -donor — the net panel's in-box overlay message pattern (=show_loading= / -placeholder label) carries the copy. Paired empty: "No paired devices — -switch to Nearby to pair one." Nearby post-scan empty: "Nothing found — -Rescan, or make the device discoverable." No adapter: adapter row reads -"No Bluetooth adapter", Devices controls disable, Diagnostics stays -usable (the doctor's step 1 names the hardware/driver verdict); bar -glyph shows the off/blocked state. Non-blocking; recorded so the -implementer doesn't invent copy mid-build. - -** DONE Logging/redaction carry-over unstated :nonblocking: -CLOSED: [2026-07-02 Thu] -The spec says "the net panel's stack, verbatim" but didn't name whether -the engine adopts net's =eventlog= (structured op log) and =redact= -(sensitive-field scrubbing) modules. Dispositioned same pass: yes, both -carry over — every mutating verb (pair/connect/forget/repair) logs an -eventlog entry, and MACs are the redaction surface (device names stay, -MACs redact in copied reports, mirroring net's report redaction). -Non-blocking; it's the donor default made explicit. - -* Implementation phases - -1. Engine =bt= package: adapter/device/scan probes over fake-bluetoothctl, - status + doctor chain (rfkill, service, powered, reachability, audio - profile probe + A2DP switch repair over fake-wpctl) — pure TDD, no - GTK. =bt status= and =bt doctor= work from a terminal. Shared dupre - css factored to the common asset if the settings panel hasn't already - done it. -2. Panel: PanelModel presenter + Blueprint pages (Devices with - Paired/Nearby, Diagnostics), worker-thread wiring, pairing-dialog - state machine, bt-panel toggle wrapper, AT-SPI smoke. Super+Shift+B - rebind. -3. Bar module =custom/bluetooth= (glyph states, tooltip, low-battery - surface, refresh signal), waybar config + suite coverage; blueman - retirement per the decision. -4. bt-priv one-verb helper + sudoers rule in archsetup; package-list - swap (blueman out per decision, bluez-utils stays); VM test - assertions. -5. archsetup keybind/config defaults so a fresh install lands the panel - wired (waybar module present, bind set, sudoers placed). - -* Review and iteration history - -** 2026-07-02 Thu @ 15:19:58 -0400 — Claude Code (archsetup) — phase 5 builder, spec closed -- *What changed or was recommended:* Phase 5 shipped and the spec flipped - to IMPLEMENTED. No new install code was needed — the waybar module, the - =Super+Shift+B= bind, and the shared panel css all ride the dotfiles - hyprland tier that a fresh install already clones and stows, and sudoers - is covered by the blanket grant. The phase's substance is proof: - =test_desktop.py= gained hyprland-gated assertions for the four stowed - bt bins, the =custom/bluetooth= waybar entry, the =bt-panel= keybind, - and the stowed =panel.css=. -- *Why:* Final phase of the DOING decomposition; with it the todo parent - closed and the lifecycle keyword flipped with a history line. -- *Artifacts:* archsetup =scripts/testing/tests/test_desktop.py=; todo.org - parent DONE + dated phase 5 / test-surface entries; this spec's Status - heading. - -** 2026-07-02 Thu @ 15:16:51 -0400 — Claude Code (archsetup) — phase 4 builder -- *What changed or was recommended:* Phase 4 shipped. Dotfiles =2a026b1=: - the stowed =bt-priv= shim (one verb, verified against the fake-systemctl) - and the sxhkd =Super+Shift+B= bind repointed from blueman-manager to - =st -e bluetoothctl= (terminal fallback per the retirement decision — the - panel is Wayland-only). archsetup: blueman dropped from the - =desktop_environment= package loop; VM assertions added (bluez/bluez-utils - present, blueman absent). blueman also removed live from velox. -- *Why:* Build order per the DOING decomposition. The spec's "sudoers rule" - item resolved as net-priv's did: archsetup already grants the primary - user blanket =NOPASSWD: ALL= (archsetup:1089), so a narrow bt-priv rule - would be dead config — no new sudoers needed, and phase 5's "sudoers - placed" is satisfied by the existing grant. -- *Artifacts:* dotfiles =hyprland/.local/bin/bt-priv=, - =common/.config/sxhkd/sxhkdrc=; archsetup =archsetup= (bluetooth loop), - =scripts/testing/tests/test_packages.py=; dated phase 4 entry under the - todo.org parent. - -** 2026-07-02 Thu @ 15:06:00 -0400 — Claude Code (archsetup) — phase 3 builder -- *What changed or was recommended:* Phase 3 shipped (dotfiles =e372de3=): - the =custom/bluetooth= bar module (state-following glyph, low-battery red - percentage, device+battery tooltip with the keybind hint, signal 10 with - the panel poking it after each reload) and the blueman retirement from the - Hyprland session (exec-once + windowrules removed, applet killed live). - The phase 2 deferred items also closed this pass: both AT-SPI smokes green - (the bt smoke's primary-button assertion fixed for the state-following - label, =c1a8219=), both panels eyeballed correct in dupre, and the - net-panel keyboard claims verified live (archsetup =e80df2b= — false - claims struck from the net spec). -- *Why:* Build order per the DOING decomposition; the Zoom meeting ended, - unblocking the visual work. Phases 4-5 (bt-priv/sudoers/packages, install - defaults — archsetup side) remain. -- *Artifacts:* dotfiles =bluetooth/src/bt/indicator.py=, =waybar-bt=, - waybar config + three css files; dated phase 3 entry under the todo.org - parent. - -** 2026-07-02 Thu @ 14:15:27 -0400 — Claude Code (archsetup) — phase 2 builder -- *What changed or was recommended:* Phase 2 shipped (dotfiles =76b2c05=): - the GTK panel — PanelModel/viewmodel presenter pair (69 tests), Blueprint - pages, pairing pty state machine with default-deny passkey confirms, - manage.py op envelopes shared by CLI and panel (power + discoverable verbs - added), =bt-panel= toggle, Super+Shift+B rebind. The shared dupre css - factoring landed as planned: net's inline =_CSS= became - =themes/dupre/panel.css= with =dupre-*= classes, both panels consume it. - 43 suites green. The AT-SPI smoke (=make test-panel-bt=) is written but - not yet run live — a Zoom meeting occupied the compositor; it runs when - the meeting ends, along with a visual check of both panels. -- *Why:* Build order per the DOING decomposition; phases 3-5 (bar module, - bt-priv/sudoers, install defaults) remain. -- *Artifacts:* dotfiles =bluetooth/src/bt/{panel,viewmodel,pairing,manage, - gui,pages}.py=, =ui/*.blp=, =tests/bt/test_btpanel.py=, the panel smoke; - dated phase 2 entry under the todo.org parent. - -** 2026-07-02 Thu @ 13:31:00 -0400 — Claude Code (archsetup) — phase 1 builder -- *What changed or was recommended:* Phase 1 shipped (dotfiles =eb2230f=): - the =bt= engine package, 101 tests over fakes, live-verified read-only - on velox. Two spec corrections from ground truth: profile inventory - comes from =pw-dump= (wpctl can't enumerate profiles), and the active - profile reads from the card's Profile param / sink's - =api.bluez5.profile= (the card's =bluez5.profile= prop is unreliable). - The shared-css factoring moved into phase 2 — net's css is an inline - string in its =gui.py=, so extracting it belongs with the first second - consumer rather than as a standalone poke at the working net panel. -- *Why:* Build order per the DOING decomposition; corrections keep the - spec honest for the phase 2 implementer. -- *Artifacts:* dotfiles =bluetooth/src/bt/=, =tests/bt/=, the stowed - =bt= shim; dated phase 1 entry under the todo.org parent. - -** 2026-07-02 Thu @ 13:10:00 -0400 — Claude Code (archsetup) — reviewer + responder -- *What changed or was recommended:* Ran the spec-review gate: passed. - All four decisions were already DONE (cookie added to the heading); - the five phases are each a clean single-session stop; CLI verbs are - verified against installed bluez 5.86. Two non-blocking findings - recorded and dispositioned in the same fused pass (empty-state / - no-adapter copy, eventlog + redaction carry-over) — both resolve to - "clone the net-panel donor," now stated explicitly. Flipped DRAFT → - READY → DOING and decomposed the phases into build sub-tasks under the - todo.org parent with :SPEC_ID: bound. -- *Why:* Craig queued the build ("4 first, then 1", 2026-07-02) after - resolving all decisions the same morning; the gate held nothing back, - so review and response fused to keep the speedrun moving. -- *Artifacts:* Findings in =* Review findings [2/2]= above; build parent - in todo.org ("Bluetooth panel + bar module"); net-panel toast fix the - UX-conformance note references landed as dotfiles =0f017d4=. diff --git a/docs/design/2026-07-02-desktop-settings-panel-spec.org b/docs/design/2026-07-02-desktop-settings-panel-spec.org deleted file mode 100644 index 8becf71..0000000 --- a/docs/design/2026-07-02-desktop-settings-panel-spec.org +++ /dev/null @@ -1,128 +0,0 @@ -#+TITLE: Desktop-Settings Dropdown Panel -#+AUTHOR: Craig Jennings -#+DATE: 2026-07-02 -#+TODO: TODO | DONE -#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED - -* DRAFT Status -:PROPERTIES: -:ID: fb7eec22-a214-4568-82c4-903612f4832f -:END: -- [2026-07-02 Thu] DRAFT — initial spec from the todo.org task "Desktop-settings - dropdown panel" (2026-06-24 review), updated for the Blueprint/GTK4 pipeline - the net panel stood up 2026-07-01. - -* Metadata - -| Field | Value | -|--------+----------------------------------------------| -| Status | draft | -|--------+----------------------------------------------| -| Owner | Craig Jennings | -|--------+----------------------------------------------| -| Repo | dotfiles | -|--------+----------------------------------------------| -| Kin | net panel (architecture donor), theme studio | -|--------+----------------------------------------------| - -* Problem - -Desktop toggles are scattered: dim, caffeine/idle, touchpad/mouse, airplane -mode each own a bar module and a keybind; brightness and keyboard-backlight -have keybinds but no visible control or level readout. The bar is running out -of glanceable width (hence the collapse arrows), and sliders can't live in -waybar at all. One settings dropdown — a gear glyph opening a small panel — -gathers them. - -* Goals - -1. One panel with every desktop toggle + slider: auto-dim, idle/caffeine, - touchpad, mouse, airplane (laptop-only), screen brightness, keyboard - backlight. -2. Conditional rows appear only when the hardware/context applies (mouse - present, trackpad present, battery present) — reuse the detection the - airplane/touchpad indicators already do. -3. Every control reflects live state and verifies its action took (the net - panel's verify-everything contract). -4. Bar stays the quick layer: which standalone indicators survive is a - decision below. - -* Design sketch - -** Architecture — clone the net panel's proven stack - -- GTK4 + gtk4-layer-shell, Blueprint .blp sources compiled to committed .ui - (=make ui=; dev-only build dependency, fresh clones run without the - compiler). -- Humble-object split: a GTK-free PanelModel presenter (unit-tested to 100% - like the net PanelModel) + thin composite-widget pages. Backing actions in - a GTK-free settings.py that shells out to brightnessctl / hyprctl / the - existing toggle scripts, TDD'd with fake binaries like every dotfiles - suite. -- One gated AT-SPI smoke (the run-panel-smoke.sh pattern), no bespoke - headless widget suite. -- Dupre WIP palette CSS, shared with the net panel — factor the palette - block into a common css asset both panels load rather than duplicating - (feeds the theme-studio task later). - -** Controls and their backings - -| Control | Backing | -|--------------------+----------------------------------------------| -| Auto-dim toggle | hyprctl decoration:dim_inactive (dim-toggle) | -|--------------------+----------------------------------------------| -| Idle / caffeine | hypridle start/stop (caffeine-toggle) | -|--------------------+----------------------------------------------| -| Touchpad toggle | toggle-touchpad + touchpad-state file | -|--------------------+----------------------------------------------| -| Mouse toggle | same mechanism, mouse-state file | -|--------------------+----------------------------------------------| -| Airplane mode | airplane-mode script (laptop-only row) | -|--------------------+----------------------------------------------| -| Screen brightness | brightnessctl (backlight class), slider + % | -|--------------------+----------------------------------------------| -| Keyboard backlight | brightnessctl (kbd_backlight class), slider | -|--------------------+----------------------------------------------| - -Slider changes apply live (throttled) and read back the actual level after -apply — verify-everything. Toggles re-read their source of truth after -firing, same as the bar indicators do, and the bar modules get their refresh -signals so both surfaces agree. - -** Open/close behavior - -Gear glyph module on the bar right cluster; click toggles the panel -(layer-shell anchored under the bar, right-aligned). Focus-out auto-hide + -Close button, matching the net panel. Keybind decision below. - -* Decisions (Craig) - -** TODO Which standalone bar indicators collapse into the panel? -Options per module (dim, touchpad, caffeine): keep on bar + mirrored in -panel; or panel-only (frees bar width). Recommendation: keep touchpad and -caffeine visible on the bar (state you glance at), move dim into the panel -(you set it rarely), keep airplane where it is. - -** TODO Keybind for the panel? -Super+Shift+G (gear) is free. Or no keybind — mouse-only surface. - -** TODO Where does the code live? -Recommendation: dotfiles =settings/= sibling to =net/= (same src-layout, -tests in tests/settings/), sharing the palette css. In-tree pocketbook-style -was the old note; the net panel is the better donor now. - -** TODO Slider granularity and floor -brightnessctl exposes 0-100%; a 5% floor stops "screen went black in a dark -room" lockouts. Confirm the floor (or allow 0 with a long-press escape -hatch). - -* Implementation phases - -1. settings.py backings (brightness get/set, kbd backlight, toggle - state readers) — pure engine, TDD with fake brightnessctl/hyprctl. -2. PanelModel presenter (rows, conditional visibility, verify-after-apply - semantics) — unit-tested, no GTK. -3. Blueprint UI + gear bar module + open/close wiring; palette css factored - to a shared asset; AT-SPI smoke. -4. Bar-module consolidation per the decision above (drop/keep indicators, - refresh-signal wiring, keybind). diff --git a/docs/design/2026-07-02-file-manager-swallow-spec.org b/docs/design/2026-07-02-file-manager-swallow-spec.org deleted file mode 100644 index 4c61be1..0000000 --- a/docs/design/2026-07-02-file-manager-swallow-spec.org +++ /dev/null @@ -1,141 +0,0 @@ -#+TITLE: File-Manager Swallow Pattern -#+AUTHOR: Craig Jennings -#+DATE: 2026-07-02 -#+TODO: TODO | DONE -#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED - -* CANCELLED Status -:PROPERTIES: -:ID: d92e0074-f594-4e83-81a0-faf282e15ed0 -:END: -- [2026-07-02 Thu] CANCELLED — targeted the wrong file manager. Craig's ask - is about the dirvish popup (Super+F, an Emacs frame), not nautilus (the - Super+Shift+F bind that misled the grounding). For dirvish the right - design is elisp-side and strictly better: Emacs is the launcher, so it - can spawn the handler directly (=start-process=), hide the popup frame, - and restore it from a process sentinel — exact exit tracking plus a - failure notify, no window-event heuristics. Reassigned to .emacs.d via - its inbox (2026-07-02-2231-from-archsetup-dirvish-popup-swallow-handoff). - The gio double-fork finding below stands for any gio-launching file - manager; the daemon design is kept for reference only. -- [2026-07-02 Thu] DRAFT — initial spec from Craig's roam capture ("when the - file manager launches another app, it should hide and return when that - process ends"). Feasibility ground truth sampled live on velox same - evening: Hyprland's native swallow cannot work here (see Problem), so the - design is an event-listener daemon. - -* Metadata - -| Field | Value | -|--------+----------------------------------------------------| -| Status | cancelled | -|--------+----------------------------------------------------| -| Owner | Craig Jennings | -|--------+----------------------------------------------------| -| Repo | dotfiles (daemon + config); archsetup (none) | -|--------+----------------------------------------------------| -| Kin | touchpad-auto (socket-listener donor), | -| | hypr-refocus-scratchpad (event-daemon sibling) | -|--------+----------------------------------------------------| - -* Problem - -Opening a file from nautilus (Super+Shift+F, tiled, class -=org.gnome.Nautilus=) spawns a viewer window while nautilus stays in the -layout. The wanted behavior is the swallow pattern: the file manager hides -while the app it launched runs, and returns when that app exits. Today -there's no signal connecting the two windows — the viewer lands wherever -the layout puts it, nautilus lingers, and quitting is manual. - -*Hyprland's native swallow is ruled out — measured, not assumed.* -=misc:enable_swallow= + =swallow_regex= would be exactly this feature in two -config lines, but it matches by walking the new window's PID ancestry to -the swallow candidate's PID. Nautilus launches handlers through GLib -(=g_app_info_launch_default_for_uri=), and that path orphans the child: -reproduced live on velox 2026-07-02 with a python-gi launcher — feh came up -with PPID 1 (reparented to init) while the launcher was still alive. The -ancestry walk hits init before it hits nautilus, every time, for every -handler. Any design that depends on PID parentage is dead on arrival; the -signal has to come from window events instead. - -Ground truth on handlers (velox, 2026-07-02): pdf → zathura, image → feh, -video → mpv, text/code → emacsclient (window belongs to the emacs daemon). -Side-note, out of scope here: feh is X11 — an XWayland viewer on a -no-XWayland-by-preference setup; a default-handler review is its own task. - -* Goals - -- Double-click a file in nautilus → the viewer takes its place; nautilus is - gone (special workspace, not killed — state and tabs survive). -- Quit the viewer → nautilus returns and has focus. -- Nothing else changes: terminals, scratchpads, and every other window keep - their current behavior. -- Config-driven, testable logic, one small daemon — the touchpad-auto shape. - -* Design sketch - -A =hypr-swallow= daemon (dotfiles, =hyprland/.local/bin/=) listening on the -Hyprland IPC event socket (socket2), same as =touchpad-auto=: - -- Track the active window (=activewindow>>= events carry class + title; - =activewindowv2>>= carries the address). -- On =openwindow>>= (address, workspace, class, title) while the active - window's class is a configured *parent* (nautilus): dispatch - =movetoworkspacesilent special:swallow,address:0x=, record - child-address → {parent-address, origin workspace}. -- On =closewindow>>= of a recorded child: bring the parent back - (=movetoworkspace=) and focus it; drop the record. -- On =closewindow>>= of a hidden parent (nautilus quit while hidden): drop - the record, nothing to restore. -- Exception classes (fuzzel, dunst, scratchpad classes, the panels) never - trigger a swallow even when they open over nautilus. -- Pure event-machine core (parse lines → state transitions → dispatch list), - unit-tested against recorded event streams; a thin socket loop around it. - -Known edge, handled: Super+Shift+F while nautilus is hidden re-runs -=nautilus=, which activates the existing (hidden) instance instead of -opening a window. The daemon (or the bind) must restore-and-untrack in that -case so the bind never appears dead. - -Known limitation, accepted: the emacsclient case never swallows — the -window belongs to the long-running emacs daemon and =closewindow= for it -means a frame closed, not "the file is done." The parent-class trigger plus -exception list naturally leaves it alone only if we exclude it explicitly — -see decision 2. - -* Decisions (Craig) - -** TODO Trigger breadth: any new window while nautilus is active, or an allowlist of viewer classes? -"Any window" is simple and catches every handler, but a false positive -exists: an app you launched seconds earlier from elsewhere finishes starting -while you're focused on nautilus → nautilus gets swallowed by an unrelated -window. An allowlist (zathura, mpv, imv, feh, …) can't be surprised but -needs maintaining. Recommendation: any-window + exception list — the false -positive is rare and self-healing (close the window or refocus). - -** TODO The emacs frame case: swallow or exempt? -Opening a text file from nautilus raises/creates an emacs frame. Swallowing -nautilus under it "works" going in, but the restore fires when *any* frame -closes, which may be much later or never. Recommendation: exempt =emacs= — -text files just open, nautilus stays. - -** TODO Restore destination: the workspace nautilus came from, or the one you're on when the viewer closes? -If you move the viewer to another workspace and quit it there, "origin" -teleports you back; "current" brings nautilus to you. Recommendation: -current workspace — the restore should land where your attention is. - -** TODO Multiple children: refcount or single-slot? -You can only launch a second file after restoring nautilus manually, so -overlap is rare — but a fast double-launch can record two children. -Recommendation: refcount — restore when the last tracked child closes. - -* Implementation phases - -1. =hypr-swallow= core: pure event-machine (TDD over recorded event - streams; fake hyprctl for dispatch assertions), config block at the top - (parent classes, exception classes), unittest suite in =tests/=. -2. Socket loop + wiring: exec-once in hyprland.conf, the Super+Shift+F - restore-if-hidden interplay, daemon single-instance guard. -3. Live verification on velox (zathura + mpv round-trips, the emacs case, - the false-positive probe) + manual-testing entries; ratio rides the - dotfiles pull. diff --git a/docs/design/2026-07-02-net-panel-other-interfaces-spec.org b/docs/design/2026-07-02-net-panel-other-interfaces-spec.org deleted file mode 100644 index 6b0a72d..0000000 --- a/docs/design/2026-07-02-net-panel-other-interfaces-spec.org +++ /dev/null @@ -1,189 +0,0 @@ -#+TITLE: Net Panel — Tailscale, VPN, and WireGuard Interfaces -#+AUTHOR: Craig Jennings -#+DATE: 2026-07-02 -#+TODO: TODO | DONE -#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED - -* IMPLEMENTED Status -:PROPERTIES: -:ID: 79a1075a-4b56-4f25-a861-b69f120a636a -:END: -- [2026-07-02 Thu] IMPLEMENTED — all six phases shipped (dotfiles 2d9d060, - 21db05a, 31ba056, b4010bf, b5c8442; archsetup 0389790 + the wireguard - import script): probes, panel Tunnels view, diagnose/doctor route - awareness, bar badge, installer swap + operator, velox config migration. - Residual human steps filed under todo.org "Manual testing and - validation": proton CLI sign-in (per machine) and the first live - badge/tunnel round-trip. Ratio picks up the import + package swap on its - trip. -- [2026-07-02 Thu] DOING — decomposed into six build phases under the - todo.org parent (:SPEC_ID: bound); build started same evening per Craig - ("tunnels build now + audio-panel spec alongside"). -- [2026-07-02 Thu] READY — fused review passed the gate: 4/4 decisions - resolved, phases decomposable, claims re-verified live (proton-vpn-cli - 1.0.1 in extra, binary =/usr/bin/protonvpn=, no package conflict with the - GTK app; =tailscale status --json= shape confirmed on velox — Self/Peer/ - CurrentTailnet.Name/MagicDNSSuffix; zero NM wireguard connections yet, - seven configs in assets awaiting the phase 6 import). -- [2026-07-02 Thu] DRAFT — initial spec from the roam capture "other network - interfaces (tailscale, VPNs, wireguard)" filed in todo.org 2026-07-02. - -* Metadata - -| Field | Value | -|--------+---------------------------------------------------| -| Status | implemented | -|--------+---------------------------------------------------| -| Owner | Craig Jennings | -|--------+---------------------------------------------------| -| Repo | dotfiles (net module); archsetup (packages) | -|--------+---------------------------------------------------| -| Parent | Waybar network module spec (2026-06-29), V2 panel | -|--------+---------------------------------------------------| - -* Problem - -The net panel's Connections tab shows what NetworkManager knows: WiFi networks -and wired links. The machines also run overlay and tunnel interfaces the panel -is blind to: - -- Tailscale (tailscaled, both daily drivers; the tailnet is how the machines - reach each other; not an NM device) -- WireGuard configs (assets/wireguard-config/ carries Proton VPN configs; - importable as NM connections of type wireguard or run via wg-quick) -- Commercial VPN clients (Proton VPN GTK app is installed on velox; owns its - own tunnel device) - -When one of these is up it changes routing, DNS, and reachability — exactly -the things the Diagnostics tab reasons about — yet the panel neither shows nor -controls them, and the doctor can misattribute a VPN-caused failure to the -underlying link. - -* Goals - -1. Visibility: the Connections tab shows overlay/tunnel interfaces with live - state (up/down, address, and for tailscale the tailnet peers summary). -2. Control: bring each up or down from the panel row, same interaction shape - as Join/Disconnect on WiFi rows (no terminals — V2 contract). -3. Diagnostics awareness: diagnose/doctor know when a tunnel owns the default - route or DNS, name it in evidence rows, and stop misattributing its - failures to the physical link. - -Non-goals (this iteration): installing or configuring VPN providers, tailnet -ACL management, exit-node selection UI (a "use exit node" affordance can ride -a later pass), kill-switch management (tracked separately in the spec's -failure catalog). - -* Design sketch - -** Data sources — one probe per backend, engine-side - -New GTK-free module net/src/net/overlays.py with one probe per backend, -each returning the same small dict shape ({kind, name, state, addr, detail, -can_toggle}): - -- tailscale: =tailscale status --json= (rich: self, peers, exit node, health - messages). Daemon down → state "stopped". Binary absent → backend absent. -- wireguard-nm: =nmcli -t connection show= filtered to type wireguard — - up/down via the existing nmcli wrapper (activate/deactivate connection). - The seven Proton configs in assets/wireguard-config/ import cleanly - (=nmcli connection import type wireguard file =, then - =connection.autoconnect no= immediately — imports default to autoconnect - yes). They use only PrivateKey/Address/DNS + PublicKey/AllowedIPs/Endpoint, - no PostUp/PostDown anywhere, so no wg-quick path is needed at all - (Craig, 2026-07-02). All are full-tunnel (AllowedIPs 0.0.0.0/0) — the - panel should treat them as mutually exclusive. -- proton: drive the official proton-vpn-cli (Arch extra repo, v1.0.x, - stable since 2026-04) — connect/disconnect/status verbs. It drives NM - underneath (python-proton-vpn-network-manager), so the panel still sees - connection events through NM. Runtime-exclusive with the GTK app, which - gets dropped from the install. The imported NM wireguard configs remain - a raw fallback when the CLI/API path is down; the CLI stays primary - because the raw configs lack kill switch, port forwarding, and server - rotation. - -** Panel - -A fourth Connections group "Tunnels" (after Saved / Available now / Wired) -using the existing group-header + row machinery. Row: glyph per kind, name, -state caption; primary action Up/Down where can_toggle, else Open app. -Tailscale row detail (subtitle or tooltip): tailnet name, peer count online, -exit node if any. - -** Privileged path - -- tailscale up/down: needs root or operator — =tailscale set --operator= at - install time (archsetup) makes the user an operator, so no sudo needed at - runtime. Fallback: the V2 net-priv helper gains tailscale-up/down verbs. -- NM wireguard connections: no privilege needed (NM polkit default for the - active user). - -** Diagnostics awareness - -- diag gains an "overlay owns default route/DNS" detection step: when the - default route or resolv.conf points at a tunnel interface, evidence names - it ("default route via tailscale0") and failure classification runs the - physical-link checks against the underlying device instead. -- doctor: a tunnel-caused egress failure (VPN up but its endpoint dead) - classifies fixable with next_action "bring the tunnel down / reconnect", - not a WiFi reset. - -** Bar indicator - -Part of v1 (Craig, 2026-07-02 — "shouldn't be optional"): a small overlay -badge on the net glyph when a tunnel owns the default route. Rides the same -route/DNS-ownership detection the diagnostics step adds. - -* Decisions (Craig) - -** DONE Which backends ship in the first pass? -CLOSED: [2026-07-02 Thu] -Approved (Craig, 2026-07-02): tailscale + NM-managed wireguard. Craig asked -whether the wireguard configs can be ported to NM so wg-quick drops out -entirely — yes: all seven configs in assets/wireguard-config/ use only the -six directives NM imports cleanly (verified 2026-07-02; import command and -autoconnect caveat now in the design sketch). wg-quick is out of the spec, -not deferred. Proton control is CLI-driven per the Proton decision below, -superseding the detection-only recommendation here. - -** DONE Tailscale control path: operator flag at install vs net-priv verbs? -CLOSED: [2026-07-02 Thu] -Approved (Craig, 2026-07-02): =tailscale set --operator=$USER= in archsetup's -tailscale step (declarative, no sudo at runtime); net-priv verbs only if -operator mode proves insufficient (e.g. up with flags). -** DONE Does "Tunnels" belong in Connections or its own tab? -CLOSED: [2026-07-02 Thu] -Approved (Craig, 2026-07-02): a Connections group. A fourth top tab dilutes -the V2 nav for three rows. - -** DONE Proton VPN: detect-only or drive its CLI? -CLOSED: [2026-07-02 Thu] -Decided (Craig, 2026-07-02): drive it through a CLI. Research (2026-07-02): -Proton shipped an official Linux CLI — first release 2025-11, stable v1.0.0 -2026-04, packaged in Arch extra as proton-vpn-cli (1.0.1 at check time), -with kill switch, port forwarding, NetShield, server selection, and a -status command. It drives NM underneath, so the panel sees its connections -through the existing NM event path. Spec changes: the proton backend calls -protonvpn connect/disconnect/status instead of device-detection -(can_toggle true); archsetup installs proton-vpn-cli and drops -proton-vpn-gtk-app (the two can't run concurrently per the project README — -untested locally); the imported NM wireguard configs stay as a raw fallback. -Sources: [[https://protonvpn.com/support/linux-cli][Proton Linux CLI guide]], -[[https://protonvpn.com/support/release-notes-linux-cli][CLI release notes]], -[[https://github.com/ProtonVPN/proton-vpn-cli][proton-vpn-cli repo]]. -* Implementation phases - -1. overlays.py probes (tailscale JSON, nmcli wireguard filter, proton-vpn-cli - status) — pure engine, TDD with fake binaries; =net status= grows an - overlays section. -2. Panel Tunnels group + Up/Down wiring through the worker thread; AT-SPI - smoke extension. -3. Diagnose/doctor overlay awareness (route/DNS ownership step, classifier - rows, evidence text) — TDD against the diag harness. -4. waybar-net tunnel badge on the net glyph (v1 per the bar-indicator - decision), riding phase 3's route-ownership detection; suite coverage. -5. archsetup: tailscale operator flag in the tailscale install step; - proton-vpn-cli replaces proton-vpn-gtk-app in the package list; VM test - assertions. -6. One-time per-machine migration: import the seven assets/wireguard-config - configs into NM with autoconnect off (scriptable; both daily drivers). diff --git a/docs/design/2026-07-02-timer-panel-spec.org b/docs/design/2026-07-02-timer-panel-spec.org deleted file mode 100644 index 2c9f7d4..0000000 --- a/docs/design/2026-07-02-timer-panel-spec.org +++ /dev/null @@ -1,113 +0,0 @@ -#+TITLE: Timer GTK Panel -#+AUTHOR: Craig Jennings -#+DATE: 2026-07-02 -#+TODO: TODO | DONE -#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED - -* DRAFT Status -:PROPERTIES: -:ID: 1770af2e-b093-4024-a512-ae4324a2869f -:END: -- [2026-07-02 Thu] DRAFT — initial spec from Craig's roam capture "give the - timer a gtk UI/UX like the network panel. spec this out." - -* Metadata - -| Field | Value | -|--------+---------------------------------------------------| -| Status | draft | -|--------+---------------------------------------------------| -| Owner | Craig Jennings | -|--------+---------------------------------------------------| -| Repo | dotfiles | -|--------+---------------------------------------------------| -| Kin | net panel (architecture donor), wtimer (backing), | -| | desktop-settings panel spec (sibling) | -|--------+---------------------------------------------------| - -* Problem - -The timer's whole UI is a chain of three fuzzel prompts (type, value, label) -plus a fourth for cancel. That flow can't show what's already running while -you create, can't offer one-tap presets, gives no feedback on a typo until -the add silently fails, and pomodoro state (phase, cycle) is only visible in -a tooltip. The 2026-07-02 styling pass made the dialogs presentable, but the -shape is still four blind modals for what is really one small control -surface. - -* Goals - -1. One panel, opened from the bar's timer module, that shows everything - running (live countdowns, pomodoro phase/cycle, paused state) and creates - new items without leaving it. -2. One-tap presets for the common cases (tea, pomodoro, quick alarm) next to - freeform entry, with inline validation before the add. -3. Per-item controls: pause/resume, cancel, promote to primary (the bar - glyph slot). -4. wtimer stays the single owner of timer state and the notification path; - the panel is a view over it, never a second engine. - -* Design sketch - -** Architecture — clone the net panel's proven stack - -- GTK4 + gtk4-layer-shell dropdown anchored under the timer module, Blueprint - .blp compiled to committed .ui (=make ui=; compiler is dev-only). -- Humble-object split: GTK-free PanelModel presenter, unit-tested to 100%, - with thin widget bindings; one gated AT-SPI smoke via the - run-panel-smoke.sh pattern. -- Backing: shell out to the existing wtimer CLI (=add=, =toggle=, =cancel=, - =cycle=, =render=). =render= already emits a JSON payload; the panel polls - it (or subscribes to the same RTMIN+14 refresh signal) for live state. - wtimer's 89-case suite keeps owning the logic; panel tests fake the CLI - like every dotfiles suite fakes binaries. -- Dupre WIP palette CSS shared with the net panel (same factoring the - desktop-settings spec calls for — one palette asset, three panels). - -** Layout sketch - -- Header row: running-item count + a Clear All button (maps to cancel-all). -- Item list: one row per item — type glyph, label, live countdown / clock - time / phase+cycle for pomodoro, pause and cancel buttons, click-to-promote. -- Create strip: four type buttons (the wtimer glyphs), preset chips per type - (e.g. 5m / 15m / 25m / 60m for timers), a freeform entry validated with - wtimer's own parsers, an optional label field. -- Empty state: the create strip alone, centered. - -** What happens to the fuzzel flow - -The keybind/fuzzel path stays as the keyboard-fast lane (it's now styled and -tested); the panel replaces the click-driven path on the bar module. Whether -the fuzzel chain eventually retires is a decision below. - -* Decisions (Craig) - -** TODO Panel scope: standalone timer panel, or a page in the desktop-settings panel? -The desktop-settings spec (sibling DRAFT) could host timers as a page. -Standalone matches the net panel's one-domain-one-panel shape and keeps the -timer dropdown small; folding in means one panel binary fewer. Recommend -standalone, sharing the palette/css asset. - -** TODO Fuzzel flow: keep as keyboard fast lane, or retire once the panel lands? -Keeping both costs two creation paths to maintain (though the fuzzel chain is -small and freshly tested). Recommend keep until the panel proves itself, then -revisit. - -** TODO Presets: which chips per type? -Strawman: timer 5m/15m/25m/60m; alarm +30m/top-of-hour/07:00; pomodoro -default cycle only; stopwatch needs none. Adjust to taste. - -** TODO Live updates: poll render (1s, like the bar) or a wtimer "watch" mode? -Polling reuses what exists and matches the bar's cadence; a watch/subscribe -mode is cleaner but grows wtimer. Recommend polling first. - -* Implementation phases - -1. PanelModel presenter + CLI-backing seam (TDD, GTK-free, 100% like the net - PanelModel). -2. Blueprint UI: item list + create strip, wired to the presenter; palette - css factored to the shared asset. -3. Bar integration: timer module left-click opens the panel (replacing the - fuzzel menu binding there), RTMIN+14 refresh keeps bar and panel in step. -4. AT-SPI smoke + manual-testing checklist; decide the fuzzel flow's future - after a week of real use. diff --git a/docs/design/2026-07-03-audio-panel-spec.org b/docs/design/2026-07-03-audio-panel-spec.org deleted file mode 100644 index 16a087d..0000000 --- a/docs/design/2026-07-03-audio-panel-spec.org +++ /dev/null @@ -1,160 +0,0 @@ -#+TITLE: Audio Panel — the pulsemixer console -#+AUTHOR: Craig Jennings -#+DATE: 2026-07-03 -#+TODO: TODO | DONE -#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED - -* IMPLEMENTED Status -:PROPERTIES: -:ID: 71f556c6-ee02-47cc-a3be-68c8289380f3 -:END: -- [2026-07-03 Fri] IMPLEMENTED — built in a no-approvals speedrun in the - dotfiles repo (branch panel-bugfixing): engine (pactl), presenter, GTK - panel, PTT arming, bar indicator, and the bar/keybind wiring, across four - commits 65e5bb0..9601420. 102 unit tests + a passing AT-SPI smoke on velox. - All five Decisions below resolved. Live-eyeball validation (visual polish, - PTT-in-a-meeting, fader feel, the master-mute hardware key) is the one open - follow-up, tracked as a manual-testing task in todo.org. -- [2026-07-03 Fri] DRAFT — stub from the todo.org task "Audio panel spec" - (roam ask 2026-07-02) plus the 2026-07-03 waybar/sound design discussion. - Written to iterate alongside the prototype - (=working/sound-panel/sound-panel-prototype.html=). Spine is present; the - Decisions and Design detail get filled in as we go. - -* Metadata - -| Field | Value | -|--------+---------------------------------------------------| -| Status | implemented | -|--------+---------------------------------------------------| -| Owner | Craig Jennings | -|--------+---------------------------------------------------| -| Repo | dotfiles | -|--------+---------------------------------------------------| -| Kin | net panel + bt panel (architecture + aesthetic | -| | donors), desktop-settings panel (sibling) | -|--------+---------------------------------------------------| - -* Problem - -Audio control today is the pyprland audio scratchpad (Super+A) — a floating -pulsemixer TUI — plus scattered bar affordances: =pulseaudio= (volume, click -to mute sink), =pulseaudio#mic= (mic glyph + mic-toggle), Super+M audio-cycle -ring, Super+Shift+A mic-toggle. There's no single glanceable surface that -shows every sink and source, lets you set the default output/input, and -carries the meeting-grade mic controls Craig wants (a clean muted mode and a -hold-to-talk mode). The net + bluetooth panels set the pattern for exactly -this shape; audio is the third instrument in the family. - -* Goals - -1. One panel, opened from the bar's sound glyph, exposing the full pulsemixer - surface: every sink and source, per-device volume, per-device mute, and - switching the default output and input. -2. Replace the pyprland audio scratchpad (Super+A) as the primary audio UI. -3. Mic modes for meetings: *live*, *muted*, and *push-to-talk* (mic stays - muted except while Space is held, releasing re-mutes). -4. A *master quick-mute* — one action mutes all output — reachable from the - faceplate and a keybind. -5. Instrument-console aesthetic and architecture consistent with net + bt: - same faceplate, lamps, engraved sections, console keys, needle gauges, - verify-everything contract. -6. The bar glyph reflects live state: speaker + three arcs normally, a - speaker-with-✕ when muted (Craig's called glyphs). - -* Design sketch - -Prototype: =working/sound-panel/sound-panel-prototype.html= (the reference for -layout + idioms below). - -** Surface (from the prototype) - -- *Faceplate* — status lamp, sound glyph, state word (PLAYBACK / MUTED), a - MUTED badge, the SND·01 unit label, and the *master quick-mute switch* - (same switch idiom as net wifi / bt power), plus the close ✕. -- *OUTPUTS section* — one row per sink. Row body click = set default (gold - DEF tag). A machined fader sets that sink's volume; the trailing glyph - mutes just that device. Active/default row is emphasized (cream name, gold - lamp/glyph). -- *INPUTS section* — one row per source, same idioms. -- *Mic mode* — three console keys: LIVE / MUTED / PUSH·TALK. Push-to-talk - keeps the mic muted (red IN needle) and un-mutes only while Space is held. -- *Twin VU needles* — output level + input level, the sound analog of net - throughput and bt battery gauges. Needle goes red when its side is muted. - -** Architecture — clone the net/bt panel stack - -- GTK4 + gtk4-layer-shell, Blueprint =.blp= → committed =.ui= (=make ui=, - dev-only build dep). -- Humble-object split: a GTK-free PanelModel presenter (unit-tested like the - net/bt PanelModels) + thin composite-widget pages. Backing actions in a - GTK-free =audio.py= that shells to the audio control layer (pactl / - wpctl / pulsemixer — pick below), TDD'd with fake binaries. -- One gated AT-SPI smoke (=run-panel-smoke.sh= pattern). -- Shared instrument-console palette CSS asset (the one net/bt/settings all - load) — do not duplicate the palette block. -- Code lives in dotfiles =audio/= sibling to =net/= (src-layout, tests in - =tests/audio/=). - -* Decisions (Craig) - -** DONE Audio control backend — pactl vs wpctl vs pulsemixer -CLOSED: [2026-07-03 Fri] -Resolved: =pactl= (the engine module is =pactl.py=). Both ratio and velox run -PipeWire with the pipewire-pulse compat layer and no PulseAudio daemon, so -pactl and wpctl hit the same graph — but =pactl -f json= gives structured, -name-addressable output where wpctl offers only a volatile-id tree. Reads go -through =pactl -f json list sinks|sources= + =get-default-*=; writes target -devices by stable name behind an argv-charset guard. - -** DONE Push-to-talk mechanism under Wayland (feasibility — phase 1) -CLOSED: [2026-07-03 Fri] -Resolved: route (a), Hyprland dynamic binds. The phase-1 spike confirmed all -three primitives on velox (Hyprland 0.55.4): =hyprctl keyword bind/unbind= -adds and removes a bind live, =bindr= fires on release, and =pactl -set-source-mute @DEFAULT_SOURCE@ 0|1= toggles the mic cleanly. =ptt.py= arms a -press bind (un-mute) + a bindr (re-mute) on entering PTT mode and unbinds on -leaving, so the talk key isn't grabbed globally otherwise. No evdev needed. -Documented behavior: while PTT is armed, the talk key is the talk key. - -** DONE Quick-mute keybind + scope -CLOSED: [2026-07-03 Fri] -Resolved: the XF86AudioMute hardware key (Super+Shift+M turned out to be taken -by the monocle-layout bind, so the spec's assumption was wrong). The mute key -now runs =audio quick-mute=, which mutes every output (master), not just the -default sink — identical on a single-sink machine, correct on a multi-sink -one. Also reachable from the faceplate master switch and the panel. Scope: -master mute of all sinks, with verify-after-apply per sink. - -** DONE Bar glyph click map -CLOSED: [2026-07-03 Fri] -Resolved with the low-regret wiring: kept the existing =pulseaudio= waybar -module (left-click mute, scroll volume — no regression) and repointed its -right-click from the retired pulsemixer scratchpad to =audio-panel=. So: left -= mute, right = open panel, scroll = volume. A fuller =custom/audio= indicator -(state-following speaker glyph + its own click map) is built and tested -(=indicator.py= + =waybar-audio=) but stays unwired until the new bar glyph -gets a live eyeball — the swap is a one-line waybar edit when Craig's ready. - -** DONE Fate of the existing audio affordances -CLOSED: [2026-07-03 Fri] -Resolved: Super+A repurposed from =pypr toggle audio= (the pulsemixer -scratchpad) to =audio-panel= — the panel is the primary audio UI now, so the -scratchpad is retired. Its definition still sits in the machine-local -=pyprland.toml= (not stowed) and can be deleted by hand. Kept: =pulseaudio= + -=pulseaudio#mic= waybar modules (glance + scroll + the mic-mute glance), -Super+M cycle, Super+Shift+A + XF86AudioMicMute mic-toggle. Changed: -XF86AudioMute → master quick-mute (see the quick-mute decision above). - -* Implementation phases - -1. Push-to-talk feasibility spike (decision above) — the one unknown; settle - the mechanism before committing the mic-mode design. -2. =audio.py= backings (list/get/set/mute/default for sinks + sources) — - pure engine, TDD with a fake audio backend. -3. PanelModel presenter (rows, default tracking, mic modes, master mute, - verify-after-apply) — unit-tested, no GTK. -4. Blueprint UI + sound bar glyph (normal / muted / ptt states) + open/close - wiring; shared palette css; AT-SPI smoke. -5. Bar-affordance consolidation per the decision above; retire the Super+A - scratchpad; keybinds. diff --git a/docs/design/2026-07-03-instrument-console-panels-spec.org b/docs/design/2026-07-03-instrument-console-panels-spec.org deleted file mode 100644 index 315e0b4..0000000 --- a/docs/design/2026-07-03-instrument-console-panels-spec.org +++ /dev/null @@ -1,158 +0,0 @@ -#+TITLE: Instrument-console rebuild — net + bluetooth panels -#+DATE: 2026-07-03 -#+TODO: TODO | DONE -#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED - -* IMPLEMENTED Status -:PROPERTIES: -:ID: e73877f5-4f5b-4f81-b946-dbaa6145e0d5 -:END: -- 2026-07-03 Fri @ 06:49 -0400 :: DOING → IMPLEMENTED: all six phases shipped - (net GTK-free layer 81ec9c3, net view 800ef60; bt GTK-free layer 5318b34, bt - view 66f03d9; phase-6 dead-code removal f4e688e). Both panels are single-screen - instrument consoles, verified live on velox — 46 suites + full make test green, - both AT-SPI smokes green end to end, render matching the approved prototype. The - three folded tasks (network panel redesign, bt switch placement + title, bt - rename devices) closed with the build. -- 2026-07-03 Fri @ 02:07 -0400 :: DRAFT → READY → DOING in one stroke: Craig - approved the design through five interactive prototype iterations and - authorized the no-approvals speedrun ("let's build them now... go"). The - review gate was the live prototype session itself. -- 2026-07-03 Fri @ 02:07 -0400 :: Created (DRAFT) from the prototype session. - -* Metadata - -| Field | Value | -|---------------------+------------------------------------------------------------| -| Status | implemented | -|---------------------+------------------------------------------------------------| -| Owner | Craig Jennings | -|---------------------+------------------------------------------------------------| -| Repos | dotfiles (net/, bluetooth/, themes), archsetup | -|---------------------+------------------------------------------------------------| -| Normative reference | [[file:../../assets/2026-07-03-instrument-console-panels-prototype.html][assets/2026-07-03-instrument-console-panels-prototype.html]] | -|---------------------+------------------------------------------------------------| - -* Summary - -Rebuild both GTK layer-shell panels (net, bluetooth) from the tabbed layout -to the instrument-console design: one screen, no tabs, a faceplate with a -state word + badges + radio switch + close, engraved section labels, lamp -rows that act on click, dial meters under the console keys, and a doctor -that does it all. The interactive prototype =panel-console-v3.html= is the -normative design reference — when this spec and the prototype disagree on a -visual or interaction, the prototype wins. - -* Decisions (all resolved — Craig, prototype session 2026-07-02/03) - -- Replace the tabbed panels outright. No fallback flag; git history is the - rollback. Net panel first, bluetooth second. -- Advanced repair tiers leave the panel entirely. DOCTOR runs the full - diagnose → classify → lightest-repair → re-verify escalation (the engine - already does this). The surgical tiers stay CLI-only (=net repair ...=). -- Faceplate (both panels): state lamp + state word, badges, unit label - (NET·01 / BT·01), radio switch (wifi radio / bt adapter power), close ✕. - Badges: TUNNEL (gold, net), AIRPLANE (gold, both), LOW BATT (red, bt). -- Sections in order — net: CHANNEL, NETWORKS (+ hidden action), TUNNELS, - CONSOLE (DOCTOR / SPEED TEST keys), meters, output. bt: ADAPTER (with - clickable =discoverable= chip), PAIRED, NEARBY (+ scanning note), CONSOLE - (DOCTOR / SCAN), battery gauges, output. -- Section row budgets, half-row peek, internal scroll (thin slate - scrollbar): NETWORKS 5.5 rows, TUNNELS 4.5, PAIRED 5.5, NEARBY 4.5. - In-range networks sort active-first then strongest-signal-first. Counts on - the engraved headers ("networks · 12 in range", "tunnels · 1 up of 9", - "paired · 3", "nearby · 12"). The panel silhouette never grows with list - length; only the output well is variable and it caps at ~170px. -- Lamp-row grammar: green = live/connected, gold = available/actionable, - off = down/stored, red = failed; busy = pulsing gold during transitions. - Rows act on click (tunnels toggle, networks join, paired devices - connect/disconnect toggle, nearby devices pair). -- Arm-first for anything disruptive or destructive, 3s auto-disarm: - - forget (network or bt device): hover reveals ✕; first click arms the - row terracotta ("forget? click ✕ again"), second fires. No dialog. - - disconnect (active network): click the active row; first click arms in - GOLD ("disconnect? click again") — disruptive, not destructive — second - fires. -- Meters (net): two dials, RX·DOWN / TX·UP, gold needles, mode tag top-left - (LIVE green / TEST gold), HOLD tag top-right. Idle: live link throughput. - Speed test: cards flash gold, needles sweep the measured rate, then PIN - the final value with HOLD; clicking a held meter releases it to LIVE. - Scale 0–100 Mbps, auto-relabel to 0–1000 when a reading exceeds 100. - Dial top inset ~13px so the corner tags never touch the arc. -- Speed test output well gets ONLY: location line ("location: by - "), ping (+jitter), final line, conditioned tip(s). The rates - live in the meters, not the text. -- Battery gauges (bt): same dial chrome; one per connected device (two - slots; empty slot dim "NO DEVICE"/"ADAPTER OFF"); needle+value red under - 15% and the LOW BATT faceplate badge lights. -- Output well: doctor streams the checks with their narration lines - (viewmodel.STEP_NARRATION) and repair steps in gold; verdict line closes - (olive for pass/fixed). A dismiss ✕ appears in the well's corner whenever - it has content. Both panels. -- WiFi radio switch: =nmcli radio wifi on|off=. Off empties NETWORKS to one - dim "wifi radio off" row, drops the connection, kills tailscale rows' - reachability; on rejoins the last network (NM autoconnect does this for - real). Airplane mode is system-level (Super+Shift+A owns it): both panels - reflect it (state word AIRPLANE, gold badge, switches down); a switch - flipped under airplane refuses with a toast naming the exit. A routed - ethernet link keeps the net panel ONLINE through airplane mode. -- Ethernet: presence-based row pinned atop NETWORKS when a cable is up - ("enp… · active · wired · 1.0 Gbps" / "wired · standby"); CHANNEL swaps - the signal ladder for "wired · full-duplex" when routed; clicking - the row toggles route ownership via device disconnect/connect. -- Pairing (bt): nearby row click → busy → passkey-confirm dialog (large - gold digits) → device moves to PAIRED and connects. SCAN key refreshes - with a "scanning…" note on the header. -- Rename (bt): hover ✎ on a paired row → dialog prefilled → bluez - =set-alias= (closes the filed rename task). -- Tooltips: any ellipsized row label carries its full text as the tooltip. -- Dialogs (join / hidden SSID / passkey / rename) keep the in-panel dupre - dialog style (gold border, dark well inputs, gold caret). -- Close: ✕ on the faceplate + Esc (already shipped; keep). -- Folded tasks: "Network panel redesign", "Bluetooth panel: switch placement - + panel title", "Bluetooth panel: rename devices" — all close with this - build's phases. - -* Engine gaps (small, close during phases) - -- radio verb: =nmcli radio wifi on|off= helper (manage or sysio) + tests. -- hidden-SSID join: =manage.add= grows a hidden flag - (=802-11-wireless.hidden yes=). -- ethernet: device rows from =nmcli dev= (type ethernet) + disconnect/ - connect verbs (device-level; =net down --iface= already disconnects). -- bt rename: btctl =set-alias= one-shot verb + verify-after read. -- bt battery: already exposed (indicator uses it). -- speedtest meters: =run_speedtest_stream= on_update already ticks (pty). -- link speed for wired channel line: =ethtool=-free read from - =/sys/class/net//speed=. - -* Implementation phases - -1. [X] Spec + task wiring (this file; todo.org parent task with :SPEC_ID:). -2. [X] Net GTK-free layer (TDD): viewmodel row composers for the console - sections (network rows sorted+counted, tunnel rows, channel facts, - faceplate state word derivation, meter scale logic, arm state machines - for forget/disconnect), PanelModel restructure (sections, no tabs). - Engine gaps: radio verb, hidden join, ethernet rows, wired link speed. -3. [X] Net view rebuild: gui.py single-page console built in Python - (faceplate, engraved scrolled sections, console keys, cairo dial meters - with mode/hold tags, output well + dismiss), panel.css additions - (engrave, lamps, dial, badges, arm tints). AT-SPI smoke + driver - rewritten for the console layout. Shipped with phase 4 (dotfiles - 800ef60): a view-only intermediate is a broken panel (rows and switches - that do nothing), so view + interactions landed together. -4. [X] Net interactions: join/hidden/forget (arm terracotta)/disconnect - (arm gold)/radio switch/ethernet toggle/doctor stream/speed-test-drives- - meters, toasts. Verified live on velox (DOCTOR streams, SPEED TEST sweeps - both dials then HOLD). Shipped in dotfiles 800ef60 with phase 3. -5. [X] Bluetooth panel: same treatment end to end (faceplate + power - switch, adapter chip, paired/nearby lamp rows, pair passkey flow, - rename via set-alias, forget arm, battery gauges + LOW BATT, DOCTOR / - SCAN keys, output). bt smoke rewritten. Shipped in two commits mirroring - net: dotfiles 5318b34 (GTK-free layer + engine gaps) and 66f03d9 (view + - interactions + smoke). rename lands on the bluez Alias via busctl - (set-alias has no MAC-addressed one-shot); verified live on velox (smoke - green end to end, screenshot matches the prototype). -6. [X] Live verification both panels on velox + all suites + smokes green; - summary of findings written to file; folded tasks closed; dead code - removed; session context finalized. diff --git a/docs/specs/2026-07-02-bluetooth-panel-spec.org b/docs/specs/2026-07-02-bluetooth-panel-spec.org new file mode 100644 index 0000000..f1b3ac1 --- /dev/null +++ b/docs/specs/2026-07-02-bluetooth-panel-spec.org @@ -0,0 +1,476 @@ +#+TITLE: Bluetooth Panel — CLI-Driven, Net-Panel Kin +#+AUTHOR: Craig Jennings +#+DATE: 2026-07-02 +#+TODO: TODO | DONE +#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED + +* IMPLEMENTED Bluetooth Panel — CLI-Driven, Net-Panel Kin +:PROPERTIES: +:ID: 8af6a76a-5665-4d20-9efd-ffdf7460c981 +:END: +- 2026-07-04 Sat @ 12:36:56 -0500 — retrofitted by spec-sort; status set to IMPLEMENTED (reason: Shipped through phase 3; build task DONE and manual tests filed.) + +* IMPLEMENTED Status +:PROPERTIES: +:ID: 1271a845-4463-4831-9902-990eda6b2265 +:END: +- [2026-07-02 Thu] IMPLEMENTED — all five phases shipped the same day + (dotfiles eb2230f / 76b2c05 / e372de3 / 2a026b1; archsetup d8d8c53): + engine, panel, bar module + blueman retirement, bt-priv + package swap, + install wiring proven by VM assertions. 43 dotfiles suites green, both + AT-SPI smokes green, panels verified live; the phase 4-5 VM assertions + run on the next VM pass. +- [2026-07-02 Thu] DOING — spec-response decomposed the five phases into + build sub-tasks under the todo.org parent (:SPEC_ID: bound); build + started same day per Craig ("4 first, then 1" — bugs then bluetooth). +- [2026-07-02 Thu] READY — spec-review passed the gate: all four + decisions resolved, phases decomposable, CLI verbs verified against + bluez 5.86. Two non-blocking findings recorded and dispositioned in + the same pass (donor-pattern answers). +- [2026-07-02 Thu] DRAFT — initial spec from Craig's request: a bluetooth + module driving a CLI underneath, consistent with the net panel, minimal + interface, full functionality, diagnostics section, visual mockups. + +* Metadata + +| Field | Value | +|--------+---------------------------------------------------------------------------------| +| Status | implemented | +|--------+---------------------------------------------------------------------------------| +| Owner | Craig Jennings | +|--------+---------------------------------------------------------------------------------| +| Repo | dotfiles (bt module); archsetup (packages, sudoers, keybind defaults) | +|--------+---------------------------------------------------------------------------------| +| Kin | net panel (architecture donor), desktop-settings panel (same donor, shared css) | +|--------+---------------------------------------------------------------------------------| + +* Problem + +Bluetooth on both daily drivers runs through blueman: a tray applet plus a +GTK3 manager window (Super+Shift+B). It's the odd one out on the desktop — +a foreign visual style next to the dupre-themed panels, a tray icon where +every other indicator is a first-class waybar module, and no diagnostics +story at all. When the BT mouse fails to reconnect at boot (a recurring +gotcha — touchpad-auto exists because of it) or headphones pair but route +no audio, the fix is a terminal séance: bluetoothctl, rfkill, systemctl, +wpctl, in whatever order folklore suggests. + +The net panel proved the shape that fixes this: a minimal layer-shell +popup over a GTK-free engine that drives a CLI, with a diagnostics tab +that names the failure and offers the repair. Bluetooth is the same +problem with a smaller surface: one adapter, a handful of devices, a +short list of well-known failure modes. + +* Goals + +1. Visibility: adapter power state and every known device with live state + (connected, battery, signal) in one glance — panel and bar module agree. +2. Control: power, scan, pair, connect, disconnect, forget — full + functionality from the panel, zero terminals (the net panel's V2 + contract). +3. Diagnostics: a doctor that walks the known failure chain (adapter → + rfkill → service → power → device → audio profile), names the broken + link in evidence rows, and offers tiered repairs. +4. Consistency: same stack, same window shape, same interaction grammar, + same palette as the net panel. A user who knows one panel knows both. + +Audio-profile switching is in scope for v1 (Craig, 2026-07-02 — "bitten +by this too many times to count"): the doctor's audio-profile step +carries a one-click repair, not just a diagnosis, and connected audio +devices surface their active profile (details in the doctor chain below). + +Non-goals (this iteration): OBEX file transfer, multi-adapter support +(both machines have one controller), BLE sensor/GATT browsing. + +* Design sketch + +** Architecture — the net panel's stack, verbatim + +- GTK4 + gtk4-layer-shell, Blueprint .blp compiled to committed .ui + (=make ui=), PyGObject at runtime. +- Humble-object split: GTK-free =PanelModel= presenter (unit-tested like + net's), thin composite-widget pages, =bg(work, done)= worker-thread + helper for every slow call. +- Engine: a new =bt= package in dotfiles (=bluetooth/src/bt/=, sibling of + =net/=), CLI entry =bt= with =bt status= / =bt panel= / =bt doctor= — + the same cmd/cli layout as net. +- Layer-shell OVERLAY popup anchored TOP+RIGHT, 380x520, Esc closes, + focus-out auto-hides, single-instance toggle via a =bt-panel= wrapper. + Dupre palette css shared with the net panel (the factored css asset the + desktop-settings spec calls for — three consumers now, so the factoring + happens in this project's phase 1 if settings hasn't landed it). +- Testing: engine TDD with fake binaries on a temp PATH (fake-bluetoothctl, + fake-rfkill, fake-systemctl, fake-wpctl); PanelModel unit suite; one + gated AT-SPI smoke (=make test-panel= pattern). + +** CLI backing — bluetoothctl one-shot verbs + +bluez 5.86 (installed) supports everything non-interactive: + +- Adapter: =bluetoothctl show= (powered, discoverable, pairable), + =bluetoothctl power on|off=. +- Device lists: =bluetoothctl devices Paired|Connected|Trusted= — the + Paired view is a merge of Paired + Connected states; =bluetoothctl info + = per row fills caption detail (battery percentage rides bluez's + built-in Battery1 profile and appears in info output; RSSI appears + during discovery). +- Scan: =bluetoothctl --timeout N scan on= (bounded discovery burst), + then =devices= diffed against Paired for the Nearby list. The panel + scans in 8s bursts with a live "Scanning…" state rather than an + unbounded scan. +- Connect/disconnect/forget: =bluetoothctl connect|disconnect|remove =. +- Pairing: the one interactive corner. =bluetoothctl pair = can demand + a passkey confirmation. The engine drives bluetoothctl's line protocol + over a pty with a bounded state machine (expect "Confirm passkey", + reply yes/no); a passkey prompt surfaces as a panel dialog showing the + six digits, mirroring the net panel's password dialog. NoInputNoOutput + devices (mice, most headphones) sail through without the dialog. +- rfkill: the user is in the =rfkill= group, so block/unblock is + unprivileged (=rfkill unblock bluetooth=). +- Privileged path: exactly one verb needs root — =systemctl restart + bluetooth= — so =bt-priv= is a one-verb closed helper with its own + NOPASSWD sudoers rule placed by archsetup, cloning net-priv's + regex-validated pattern rather than widening net-priv's scope. + +** Panel anatomy + +Two tabs. Devices is the panel; Diagnostics is the escape hatch. + +Devices tab, Paired sub-view (the default — daily use is reconnecting +known devices, not discovering new ones): + +#+begin_example +╭──────────────────────────────────────────────╮ +│ [ Devices ] [ Diagnostics ] │ ← top switcher +│ │ +│ Bluetooth ●──○ hci0 on │ ← adapter row: power switch +│ ──────────────────────────────────────────── │ +│ [ Paired ] [ Nearby ] │ ← sub-view switcher +│ ┌──────────────────────────────────────────┐ │ +│ │ 󰍽 MX Master 3 │ │ +│ │ Connected · battery 80% │ │ +│ │ 󰋋 WH-1000XM4 │ │ +│ │ Paired, not connected │ │ +│ │ 󰌌 K380 Keyboard │ │ +│ │ Paired, not connected │ │ +│ │ │ │ +│ └──────────────────────────────────────────┘ │ +│ [ Disconnect ] [ Forget ] │ ← acts on selected row +╰──────────────────────────────────────────────╯ +#+end_example + +The primary button is one control with a state-following label: +"Connect" when the selection is disconnected (suggested-action styling), +"Disconnect" when connected. Row-activate (Enter / double-click) +connects — never disconnects — matching the net panel's asymmetry. +Captions carry the human state line; the MAC lives in the row tooltip, +not the visible caption. + +Devices tab, Nearby sub-view: + +#+begin_example +╭──────────────────────────────────────────────╮ +│ [ Devices ] [ Diagnostics ] │ +│ │ +│ Bluetooth ●──○ hci0 on │ +│ ──────────────────────────────────────────── │ +│ [ Paired ] [ Nearby ] │ +│ ┌──────────────────────────────────────────┐ │ +│ │ Scanning… (6s) │ │ ← overlay state label +│ │ 󰋋 JBL Flip 6 −58 dBm │ │ +│ │ 󰄜 Pixel 9 −71 dBm │ │ +│ │ 󰂱 (unnamed) 74:A5:… −83 dBm │ │ +│ └──────────────────────────────────────────┘ │ +│ [ Pair ] [ Rescan ] [ Discoverable ⊙ ] │ +╰──────────────────────────────────────────────╯ +#+end_example + +Pair does the whole intended thing — pair, then trust, then connect — +because pairing a device means "use it now and reconnect on its own +later" (decision below). Discoverable is a toggle for the inbound case +(pairing a phone TO the laptop), off by default, auto-off with bluez's +discoverable-timeout. Rows sort by RSSI, strongest first; named devices +above unnamed ones. + +Diagnostics tab (mirrors the net panel's shape: one big verb + streaming +evidence rows + tiered repairs behind confirmation): + +#+begin_example +╭──────────────────────────────────────────────╮ +│ [ Devices ] [ Diagnostics ] │ +│ │ +│ [ Get Bluetooth Working ] [ Advanced ▸]│ +│ ┌──────────────────────────────────────────┐ │ +│ │ ✓ Adapter present (hci0) │ │ +│ │ ✓ Not blocked (rfkill clear) │ │ +│ │ ✓ bluetooth.service active │ │ +│ │ ✓ Adapter powered │ │ +│ │ ✗ MX Master 3: paired but unreachable │ │ +│ │ … Re-pair suggested — see below │ │ +│ │ │ │ +│ │ Fix: [ Reconnect ] [ Re-pair device ] │ │ +│ └──────────────────────────────────────────┘ │ +│ power-cycle · restart service · unblock │ ← tiered repairs (confirm) +╰──────────────────────────────────────────────╯ +#+end_example + +The doctor chain, in order, each an evidence row: + +1. Adapter present — =bluetoothctl list= / rfkill has an hci entry. + Absent → hardware/driver verdict, no repair offered. +2. rfkill state — soft-blocked names the likely cause when the + airplane-mode state file says airplane is on ("Blocked by airplane + mode — turn airplane mode off"), otherwise offers Unblock (no root + needed, rfkill group). +3. bluetooth.service — inactive/failed → offer restart (the one bt-priv + verb), evidence quotes the last journal line. +4. Adapter powered — off → offer power on (and note if a boot-time + policy keeps turning it off). +5. Per-device reachability — paired-but-connect-fails distinguishes + "device off/out of range" (RSSI absent in a scan burst) from "bond + corrupt" (connect error string), and only the latter suggests the + re-pair repair (remove + pair + trust + connect, confirmed first — + it's the destructive tier). +6. Audio profile (audio devices only) — device connected but no wpctl + sink/source, or the card stuck in HSP/HFP when A2DP is expected: + evidence names the active profile and offers the repair inline — + "Switch to A2DP" drives =wpctl set-profile = (profile + inventory from =pw-dump= — ground truth 2026-07-02: wpctl can't + enumerate a card's profiles, and the card's =bluez5.profile= prop + reads "off" mid-stream; the card's Profile param and the sink node's + =api.bluez5.profile= are authoritative), verifies the sink came back + in the expected profile, and reports fixed or no-change. In v1 per + Craig (2026-07-02): this failure mode has bitten repeatedly, so it + gets the one-click fix, not just a diagnosis. Connected audio-device + row captions also show the profile when it's the degraded one + ("Connected · mic mode (HSP)") so the state is visible before the + doctor runs. + +Repairs confirm with the net panel's future-tense scope copy ("This will +restart the Bluetooth service. Connected devices will drop and +reconnect."), run on the worker thread, verify after (re-read state, +report "fixed" or "no change"), and never chain silently. + +** Bar module + +=custom/bluetooth= replacing the blueman-applet tray icon: the panel's +glanceable layer, one glyph, state-following like =custom/net=: + +#+begin_example + 󰂲 off / blocked (dim; red slash variant when rfkill-blocked) + 󰂯 on, nothing connected (dim) + 󰂱 connected (white; tooltip lists devices + battery) +#+end_example + +Tooltip carries device names, battery percentages, and the keybind hints +(the module-tooltip convention shipped 2026-07-02). Click opens the +panel (=bt-panel= toggle wrapper); the existing Super+Shift+B bind moves +from blueman-manager to =bt panel=. Low-battery on a connected device +(<15%) adds a red percentage to the glyph text — the mouse dying +mid-meeting is the one state worth surfacing unprompted. + +** UX conformance notes + +Named against the heuristics the panel family follows (Nielsen's ten, +plus the rulesets patterns catalog): + +- Visibility of status: live captions, scan countdown, elapsed ticker on + long ops, verify-after-repair rows. +- Match to the real world: device-kind glyphs + plain state lines; MACs + demoted to tooltips; "Forget" not "Remove bond". +- User control: Esc closes, Rescan is idempotent, scan bursts are + bounded, repairs confirm, running ops show a Stop where stoppable. +- Consistency: interaction grammar is the net panel's — same switcher + layout, same primary-button contract, same confirm copy shape. +- Error prevention: Forget and Re-pair confirm; power-off while devices + are connected states the consequence in the confirm body. +- Recognition over recall: every action is a visible button; no context + menus, no hidden gestures (transient-state-buttons pattern). +- Minimalism: two tabs, one primary action per view, detail behind + tooltips and the Advanced reveal. +- Help users recover: the doctor's evidence rows name the broken link + and carry the repair inline (default-most-common-friction-proportional: + the likely fix is one click, the destructive one is confirmed). + +Tension found with the net panel while writing this (filed as todo.org +tasks per Craig's instruction, 2026-07-02): transient error toasts +auto-dismiss in 4s, and the V2 spec's keyboard-navigation claims +(tab-between-sections, arrow rows, type-to-filter) aren't verifiably +implemented. Both filed against the net panel rather than cloned here; +this panel adopts whatever resolution those tasks land on. + +* Decisions (Craig) [4/4] + +** DONE Pair implies trust + connect? +CLOSED: [2026-07-02 Thu] +Decided (Craig, 2026-07-02): yes — one Pair verb does pair → trust → +connect. A device that shouldn't auto-reconnect gets untrusted later; a +per-device auto-reconnect toggle can ride a later pass. + +** DONE Retire blueman entirely? +CLOSED: [2026-07-02 Thu] +Decided (Craig, 2026-07-02): drop it outright, no bake-in period — the +package leaves archsetup and both machines once phase 2 lands, +bluetoothctl stays as the terminal fallback. Craig's framing: any issue +after retirement is a signal the doctor needs another check or the panel +has a real bug, and it gets fixed there rather than papered over by +keeping blueman around. + +** DONE Battery in the row caption or tooltip only? +CLOSED: [2026-07-02 Thu] +Approved (Craig, 2026-07-02): caption when the device reports it +("Connected · battery 80%"), tooltip otherwise. + +** DONE Scan burst length and auto-rescan? +CLOSED: [2026-07-02 Thu] +Approved (Craig, 2026-07-02): 8s bursts, no auto-repeat — Rescan stays +explicit, matching the net panel's Available view. + +* Review findings [2/2] + +** DONE Empty-state and no-adapter presentation copy undefined :nonblocking: +CLOSED: [2026-07-02 Thu] +The mockups show populated lists; the spec didn't say what an empty Paired +list, an empty post-scan Nearby list, or a machine with no adapter shows +in the panel and on the bar glyph. Dispositioned same pass: clone the +donor — the net panel's in-box overlay message pattern (=show_loading= / +placeholder label) carries the copy. Paired empty: "No paired devices — +switch to Nearby to pair one." Nearby post-scan empty: "Nothing found — +Rescan, or make the device discoverable." No adapter: adapter row reads +"No Bluetooth adapter", Devices controls disable, Diagnostics stays +usable (the doctor's step 1 names the hardware/driver verdict); bar +glyph shows the off/blocked state. Non-blocking; recorded so the +implementer doesn't invent copy mid-build. + +** DONE Logging/redaction carry-over unstated :nonblocking: +CLOSED: [2026-07-02 Thu] +The spec says "the net panel's stack, verbatim" but didn't name whether +the engine adopts net's =eventlog= (structured op log) and =redact= +(sensitive-field scrubbing) modules. Dispositioned same pass: yes, both +carry over — every mutating verb (pair/connect/forget/repair) logs an +eventlog entry, and MACs are the redaction surface (device names stay, +MACs redact in copied reports, mirroring net's report redaction). +Non-blocking; it's the donor default made explicit. + +* Implementation phases + +1. Engine =bt= package: adapter/device/scan probes over fake-bluetoothctl, + status + doctor chain (rfkill, service, powered, reachability, audio + profile probe + A2DP switch repair over fake-wpctl) — pure TDD, no + GTK. =bt status= and =bt doctor= work from a terminal. Shared dupre + css factored to the common asset if the settings panel hasn't already + done it. +2. Panel: PanelModel presenter + Blueprint pages (Devices with + Paired/Nearby, Diagnostics), worker-thread wiring, pairing-dialog + state machine, bt-panel toggle wrapper, AT-SPI smoke. Super+Shift+B + rebind. +3. Bar module =custom/bluetooth= (glyph states, tooltip, low-battery + surface, refresh signal), waybar config + suite coverage; blueman + retirement per the decision. +4. bt-priv one-verb helper + sudoers rule in archsetup; package-list + swap (blueman out per decision, bluez-utils stays); VM test + assertions. +5. archsetup keybind/config defaults so a fresh install lands the panel + wired (waybar module present, bind set, sudoers placed). + +* Review and iteration history + +** 2026-07-02 Thu @ 15:19:58 -0400 — Claude Code (archsetup) — phase 5 builder, spec closed +- *What changed or was recommended:* Phase 5 shipped and the spec flipped + to IMPLEMENTED. No new install code was needed — the waybar module, the + =Super+Shift+B= bind, and the shared panel css all ride the dotfiles + hyprland tier that a fresh install already clones and stows, and sudoers + is covered by the blanket grant. The phase's substance is proof: + =test_desktop.py= gained hyprland-gated assertions for the four stowed + bt bins, the =custom/bluetooth= waybar entry, the =bt-panel= keybind, + and the stowed =panel.css=. +- *Why:* Final phase of the DOING decomposition; with it the todo parent + closed and the lifecycle keyword flipped with a history line. +- *Artifacts:* archsetup =scripts/testing/tests/test_desktop.py=; todo.org + parent DONE + dated phase 5 / test-surface entries; this spec's Status + heading. + +** 2026-07-02 Thu @ 15:16:51 -0400 — Claude Code (archsetup) — phase 4 builder +- *What changed or was recommended:* Phase 4 shipped. Dotfiles =2a026b1=: + the stowed =bt-priv= shim (one verb, verified against the fake-systemctl) + and the sxhkd =Super+Shift+B= bind repointed from blueman-manager to + =st -e bluetoothctl= (terminal fallback per the retirement decision — the + panel is Wayland-only). archsetup: blueman dropped from the + =desktop_environment= package loop; VM assertions added (bluez/bluez-utils + present, blueman absent). blueman also removed live from velox. +- *Why:* Build order per the DOING decomposition. The spec's "sudoers rule" + item resolved as net-priv's did: archsetup already grants the primary + user blanket =NOPASSWD: ALL= (archsetup:1089), so a narrow bt-priv rule + would be dead config — no new sudoers needed, and phase 5's "sudoers + placed" is satisfied by the existing grant. +- *Artifacts:* dotfiles =hyprland/.local/bin/bt-priv=, + =common/.config/sxhkd/sxhkdrc=; archsetup =archsetup= (bluetooth loop), + =scripts/testing/tests/test_packages.py=; dated phase 4 entry under the + todo.org parent. + +** 2026-07-02 Thu @ 15:06:00 -0400 — Claude Code (archsetup) — phase 3 builder +- *What changed or was recommended:* Phase 3 shipped (dotfiles =e372de3=): + the =custom/bluetooth= bar module (state-following glyph, low-battery red + percentage, device+battery tooltip with the keybind hint, signal 10 with + the panel poking it after each reload) and the blueman retirement from the + Hyprland session (exec-once + windowrules removed, applet killed live). + The phase 2 deferred items also closed this pass: both AT-SPI smokes green + (the bt smoke's primary-button assertion fixed for the state-following + label, =c1a8219=), both panels eyeballed correct in dupre, and the + net-panel keyboard claims verified live (archsetup =e80df2b= — false + claims struck from the net spec). +- *Why:* Build order per the DOING decomposition; the Zoom meeting ended, + unblocking the visual work. Phases 4-5 (bt-priv/sudoers/packages, install + defaults — archsetup side) remain. +- *Artifacts:* dotfiles =bluetooth/src/bt/indicator.py=, =waybar-bt=, + waybar config + three css files; dated phase 3 entry under the todo.org + parent. + +** 2026-07-02 Thu @ 14:15:27 -0400 — Claude Code (archsetup) — phase 2 builder +- *What changed or was recommended:* Phase 2 shipped (dotfiles =76b2c05=): + the GTK panel — PanelModel/viewmodel presenter pair (69 tests), Blueprint + pages, pairing pty state machine with default-deny passkey confirms, + manage.py op envelopes shared by CLI and panel (power + discoverable verbs + added), =bt-panel= toggle, Super+Shift+B rebind. The shared dupre css + factoring landed as planned: net's inline =_CSS= became + =themes/dupre/panel.css= with =dupre-*= classes, both panels consume it. + 43 suites green. The AT-SPI smoke (=make test-panel-bt=) is written but + not yet run live — a Zoom meeting occupied the compositor; it runs when + the meeting ends, along with a visual check of both panels. +- *Why:* Build order per the DOING decomposition; phases 3-5 (bar module, + bt-priv/sudoers, install defaults) remain. +- *Artifacts:* dotfiles =bluetooth/src/bt/{panel,viewmodel,pairing,manage, + gui,pages}.py=, =ui/*.blp=, =tests/bt/test_btpanel.py=, the panel smoke; + dated phase 2 entry under the todo.org parent. + +** 2026-07-02 Thu @ 13:31:00 -0400 — Claude Code (archsetup) — phase 1 builder +- *What changed or was recommended:* Phase 1 shipped (dotfiles =eb2230f=): + the =bt= engine package, 101 tests over fakes, live-verified read-only + on velox. Two spec corrections from ground truth: profile inventory + comes from =pw-dump= (wpctl can't enumerate profiles), and the active + profile reads from the card's Profile param / sink's + =api.bluez5.profile= (the card's =bluez5.profile= prop is unreliable). + The shared-css factoring moved into phase 2 — net's css is an inline + string in its =gui.py=, so extracting it belongs with the first second + consumer rather than as a standalone poke at the working net panel. +- *Why:* Build order per the DOING decomposition; corrections keep the + spec honest for the phase 2 implementer. +- *Artifacts:* dotfiles =bluetooth/src/bt/=, =tests/bt/=, the stowed + =bt= shim; dated phase 1 entry under the todo.org parent. + +** 2026-07-02 Thu @ 13:10:00 -0400 — Claude Code (archsetup) — reviewer + responder +- *What changed or was recommended:* Ran the spec-review gate: passed. + All four decisions were already DONE (cookie added to the heading); + the five phases are each a clean single-session stop; CLI verbs are + verified against installed bluez 5.86. Two non-blocking findings + recorded and dispositioned in the same fused pass (empty-state / + no-adapter copy, eventlog + redaction carry-over) — both resolve to + "clone the net-panel donor," now stated explicitly. Flipped DRAFT → + READY → DOING and decomposed the phases into build sub-tasks under the + todo.org parent with :SPEC_ID: bound. +- *Why:* Craig queued the build ("4 first, then 1", 2026-07-02) after + resolving all decisions the same morning; the gate held nothing back, + so review and response fused to keep the speedrun moving. +- *Artifacts:* Findings in =* Review findings [2/2]= above; build parent + in todo.org ("Bluetooth panel + bar module"); net-panel toast fix the + UX-conformance note references landed as dotfiles =0f017d4=. diff --git a/docs/specs/2026-07-02-desktop-settings-panel-spec.org b/docs/specs/2026-07-02-desktop-settings-panel-spec.org new file mode 100644 index 0000000..d147249 --- /dev/null +++ b/docs/specs/2026-07-02-desktop-settings-panel-spec.org @@ -0,0 +1,134 @@ +#+TITLE: Desktop-Settings Dropdown Panel +#+AUTHOR: Craig Jennings +#+DATE: 2026-07-02 +#+TODO: TODO | DONE +#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED + +* DRAFT Desktop-Settings Dropdown Panel +:PROPERTIES: +:ID: d6bb1e73-ec90-4327-85ee-bfa762da5bce +:END: +- 2026-07-04 Sat @ 12:36:56 -0500 — retrofitted by spec-sort; status set to DRAFT (evidence-based, human-confirmed) + +* DRAFT Status +:PROPERTIES: +:ID: fb7eec22-a214-4568-82c4-903612f4832f +:END: +- [2026-07-02 Thu] DRAFT — initial spec from the todo.org task "Desktop-settings + dropdown panel" (2026-06-24 review), updated for the Blueprint/GTK4 pipeline + the net panel stood up 2026-07-01. + +* Metadata + +| Field | Value | +|--------+----------------------------------------------| +| Status | draft | +|--------+----------------------------------------------| +| Owner | Craig Jennings | +|--------+----------------------------------------------| +| Repo | dotfiles | +|--------+----------------------------------------------| +| Kin | net panel (architecture donor), theme studio | +|--------+----------------------------------------------| + +* Problem + +Desktop toggles are scattered: dim, caffeine/idle, touchpad/mouse, airplane +mode each own a bar module and a keybind; brightness and keyboard-backlight +have keybinds but no visible control or level readout. The bar is running out +of glanceable width (hence the collapse arrows), and sliders can't live in +waybar at all. One settings dropdown — a gear glyph opening a small panel — +gathers them. + +* Goals + +1. One panel with every desktop toggle + slider: auto-dim, idle/caffeine, + touchpad, mouse, airplane (laptop-only), screen brightness, keyboard + backlight. +2. Conditional rows appear only when the hardware/context applies (mouse + present, trackpad present, battery present) — reuse the detection the + airplane/touchpad indicators already do. +3. Every control reflects live state and verifies its action took (the net + panel's verify-everything contract). +4. Bar stays the quick layer: which standalone indicators survive is a + decision below. + +* Design sketch + +** Architecture — clone the net panel's proven stack + +- GTK4 + gtk4-layer-shell, Blueprint .blp sources compiled to committed .ui + (=make ui=; dev-only build dependency, fresh clones run without the + compiler). +- Humble-object split: a GTK-free PanelModel presenter (unit-tested to 100% + like the net PanelModel) + thin composite-widget pages. Backing actions in + a GTK-free settings.py that shells out to brightnessctl / hyprctl / the + existing toggle scripts, TDD'd with fake binaries like every dotfiles + suite. +- One gated AT-SPI smoke (the run-panel-smoke.sh pattern), no bespoke + headless widget suite. +- Dupre WIP palette CSS, shared with the net panel — factor the palette + block into a common css asset both panels load rather than duplicating + (feeds the theme-studio task later). + +** Controls and their backings + +| Control | Backing | +|--------------------+----------------------------------------------| +| Auto-dim toggle | hyprctl decoration:dim_inactive (dim-toggle) | +|--------------------+----------------------------------------------| +| Idle / caffeine | hypridle start/stop (caffeine-toggle) | +|--------------------+----------------------------------------------| +| Touchpad toggle | toggle-touchpad + touchpad-state file | +|--------------------+----------------------------------------------| +| Mouse toggle | same mechanism, mouse-state file | +|--------------------+----------------------------------------------| +| Airplane mode | airplane-mode script (laptop-only row) | +|--------------------+----------------------------------------------| +| Screen brightness | brightnessctl (backlight class), slider + % | +|--------------------+----------------------------------------------| +| Keyboard backlight | brightnessctl (kbd_backlight class), slider | +|--------------------+----------------------------------------------| + +Slider changes apply live (throttled) and read back the actual level after +apply — verify-everything. Toggles re-read their source of truth after +firing, same as the bar indicators do, and the bar modules get their refresh +signals so both surfaces agree. + +** Open/close behavior + +Gear glyph module on the bar right cluster; click toggles the panel +(layer-shell anchored under the bar, right-aligned). Focus-out auto-hide + +Close button, matching the net panel. Keybind decision below. + +* Decisions (Craig) + +** TODO Which standalone bar indicators collapse into the panel? +Options per module (dim, touchpad, caffeine): keep on bar + mirrored in +panel; or panel-only (frees bar width). Recommendation: keep touchpad and +caffeine visible on the bar (state you glance at), move dim into the panel +(you set it rarely), keep airplane where it is. + +** TODO Keybind for the panel? +Super+Shift+G (gear) is free. Or no keybind — mouse-only surface. + +** TODO Where does the code live? +Recommendation: dotfiles =settings/= sibling to =net/= (same src-layout, +tests in tests/settings/), sharing the palette css. In-tree pocketbook-style +was the old note; the net panel is the better donor now. + +** TODO Slider granularity and floor +brightnessctl exposes 0-100%; a 5% floor stops "screen went black in a dark +room" lockouts. Confirm the floor (or allow 0 with a long-press escape +hatch). + +* Implementation phases + +1. settings.py backings (brightness get/set, kbd backlight, toggle + state readers) — pure engine, TDD with fake brightnessctl/hyprctl. +2. PanelModel presenter (rows, conditional visibility, verify-after-apply + semantics) — unit-tested, no GTK. +3. Blueprint UI + gear bar module + open/close wiring; palette css factored + to a shared asset; AT-SPI smoke. +4. Bar-module consolidation per the decision above (drop/keep indicators, + refresh-signal wiring, keybind). diff --git a/docs/specs/2026-07-02-file-manager-swallow-spec.org b/docs/specs/2026-07-02-file-manager-swallow-spec.org new file mode 100644 index 0000000..b898f11 --- /dev/null +++ b/docs/specs/2026-07-02-file-manager-swallow-spec.org @@ -0,0 +1,147 @@ +#+TITLE: File-Manager Swallow Pattern +#+AUTHOR: Craig Jennings +#+DATE: 2026-07-02 +#+TODO: TODO | DONE +#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED + +* CANCELLED File-Manager Swallow Pattern +:PROPERTIES: +:ID: 179a1cd2-7a02-4c44-a09d-685c5a154895 +:END: +- 2026-07-04 Sat @ 12:36:56 -0500 — retrofitted by spec-sort; status set to CANCELLED (reason: Native swallow ruled out by test; reassigned to .emacs.d dirvish handling.) + +* CANCELLED Status +:PROPERTIES: +:ID: d92e0074-f594-4e83-81a0-faf282e15ed0 +:END: +- [2026-07-02 Thu] CANCELLED — targeted the wrong file manager. Craig's ask + is about the dirvish popup (Super+F, an Emacs frame), not nautilus (the + Super+Shift+F bind that misled the grounding). For dirvish the right + design is elisp-side and strictly better: Emacs is the launcher, so it + can spawn the handler directly (=start-process=), hide the popup frame, + and restore it from a process sentinel — exact exit tracking plus a + failure notify, no window-event heuristics. Reassigned to .emacs.d via + its inbox (2026-07-02-2231-from-archsetup-dirvish-popup-swallow-handoff). + The gio double-fork finding below stands for any gio-launching file + manager; the daemon design is kept for reference only. +- [2026-07-02 Thu] DRAFT — initial spec from Craig's roam capture ("when the + file manager launches another app, it should hide and return when that + process ends"). Feasibility ground truth sampled live on velox same + evening: Hyprland's native swallow cannot work here (see Problem), so the + design is an event-listener daemon. + +* Metadata + +| Field | Value | +|--------+----------------------------------------------------| +| Status | cancelled | +|--------+----------------------------------------------------| +| Owner | Craig Jennings | +|--------+----------------------------------------------------| +| Repo | dotfiles (daemon + config); archsetup (none) | +|--------+----------------------------------------------------| +| Kin | touchpad-auto (socket-listener donor), | +| | hypr-refocus-scratchpad (event-daemon sibling) | +|--------+----------------------------------------------------| + +* Problem + +Opening a file from nautilus (Super+Shift+F, tiled, class +=org.gnome.Nautilus=) spawns a viewer window while nautilus stays in the +layout. The wanted behavior is the swallow pattern: the file manager hides +while the app it launched runs, and returns when that app exits. Today +there's no signal connecting the two windows — the viewer lands wherever +the layout puts it, nautilus lingers, and quitting is manual. + +*Hyprland's native swallow is ruled out — measured, not assumed.* +=misc:enable_swallow= + =swallow_regex= would be exactly this feature in two +config lines, but it matches by walking the new window's PID ancestry to +the swallow candidate's PID. Nautilus launches handlers through GLib +(=g_app_info_launch_default_for_uri=), and that path orphans the child: +reproduced live on velox 2026-07-02 with a python-gi launcher — feh came up +with PPID 1 (reparented to init) while the launcher was still alive. The +ancestry walk hits init before it hits nautilus, every time, for every +handler. Any design that depends on PID parentage is dead on arrival; the +signal has to come from window events instead. + +Ground truth on handlers (velox, 2026-07-02): pdf → zathura, image → feh, +video → mpv, text/code → emacsclient (window belongs to the emacs daemon). +Side-note, out of scope here: feh is X11 — an XWayland viewer on a +no-XWayland-by-preference setup; a default-handler review is its own task. + +* Goals + +- Double-click a file in nautilus → the viewer takes its place; nautilus is + gone (special workspace, not killed — state and tabs survive). +- Quit the viewer → nautilus returns and has focus. +- Nothing else changes: terminals, scratchpads, and every other window keep + their current behavior. +- Config-driven, testable logic, one small daemon — the touchpad-auto shape. + +* Design sketch + +A =hypr-swallow= daemon (dotfiles, =hyprland/.local/bin/=) listening on the +Hyprland IPC event socket (socket2), same as =touchpad-auto=: + +- Track the active window (=activewindow>>= events carry class + title; + =activewindowv2>>= carries the address). +- On =openwindow>>= (address, workspace, class, title) while the active + window's class is a configured *parent* (nautilus): dispatch + =movetoworkspacesilent special:swallow,address:0x=, record + child-address → {parent-address, origin workspace}. +- On =closewindow>>= of a recorded child: bring the parent back + (=movetoworkspace=) and focus it; drop the record. +- On =closewindow>>= of a hidden parent (nautilus quit while hidden): drop + the record, nothing to restore. +- Exception classes (fuzzel, dunst, scratchpad classes, the panels) never + trigger a swallow even when they open over nautilus. +- Pure event-machine core (parse lines → state transitions → dispatch list), + unit-tested against recorded event streams; a thin socket loop around it. + +Known edge, handled: Super+Shift+F while nautilus is hidden re-runs +=nautilus=, which activates the existing (hidden) instance instead of +opening a window. The daemon (or the bind) must restore-and-untrack in that +case so the bind never appears dead. + +Known limitation, accepted: the emacsclient case never swallows — the +window belongs to the long-running emacs daemon and =closewindow= for it +means a frame closed, not "the file is done." The parent-class trigger plus +exception list naturally leaves it alone only if we exclude it explicitly — +see decision 2. + +* Decisions (Craig) + +** TODO Trigger breadth: any new window while nautilus is active, or an allowlist of viewer classes? +"Any window" is simple and catches every handler, but a false positive +exists: an app you launched seconds earlier from elsewhere finishes starting +while you're focused on nautilus → nautilus gets swallowed by an unrelated +window. An allowlist (zathura, mpv, imv, feh, …) can't be surprised but +needs maintaining. Recommendation: any-window + exception list — the false +positive is rare and self-healing (close the window or refocus). + +** TODO The emacs frame case: swallow or exempt? +Opening a text file from nautilus raises/creates an emacs frame. Swallowing +nautilus under it "works" going in, but the restore fires when *any* frame +closes, which may be much later or never. Recommendation: exempt =emacs= — +text files just open, nautilus stays. + +** TODO Restore destination: the workspace nautilus came from, or the one you're on when the viewer closes? +If you move the viewer to another workspace and quit it there, "origin" +teleports you back; "current" brings nautilus to you. Recommendation: +current workspace — the restore should land where your attention is. + +** TODO Multiple children: refcount or single-slot? +You can only launch a second file after restoring nautilus manually, so +overlap is rare — but a fast double-launch can record two children. +Recommendation: refcount — restore when the last tracked child closes. + +* Implementation phases + +1. =hypr-swallow= core: pure event-machine (TDD over recorded event + streams; fake hyprctl for dispatch assertions), config block at the top + (parent classes, exception classes), unittest suite in =tests/=. +2. Socket loop + wiring: exec-once in hyprland.conf, the Super+Shift+F + restore-if-hidden interplay, daemon single-instance guard. +3. Live verification on velox (zathura + mpv round-trips, the emacs case, + the false-positive probe) + manual-testing entries; ratio rides the + dotfiles pull. diff --git a/docs/specs/2026-07-02-net-panel-other-interfaces-spec.org b/docs/specs/2026-07-02-net-panel-other-interfaces-spec.org new file mode 100644 index 0000000..0d63feb --- /dev/null +++ b/docs/specs/2026-07-02-net-panel-other-interfaces-spec.org @@ -0,0 +1,195 @@ +#+TITLE: Net Panel — Tailscale, VPN, and WireGuard Interfaces +#+AUTHOR: Craig Jennings +#+DATE: 2026-07-02 +#+TODO: TODO | DONE +#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED + +* IMPLEMENTED Net Panel — Tailscale, VPN, and WireGuard Interfaces +:PROPERTIES: +:ID: 09f4cd40-f391-4eba-a4ff-c22bad00ad7f +:END: +- 2026-07-04 Sat @ 12:36:56 -0500 — retrofitted by spec-sort; status set to IMPLEMENTED (reason: Tunnels track shipped detection, diagnose, and panel bring-up; build task DONE.) + +* IMPLEMENTED Status +:PROPERTIES: +:ID: 79a1075a-4b56-4f25-a861-b69f120a636a +:END: +- [2026-07-02 Thu] IMPLEMENTED — all six phases shipped (dotfiles 2d9d060, + 21db05a, 31ba056, b4010bf, b5c8442; archsetup 0389790 + the wireguard + import script): probes, panel Tunnels view, diagnose/doctor route + awareness, bar badge, installer swap + operator, velox config migration. + Residual human steps filed under todo.org "Manual testing and + validation": proton CLI sign-in (per machine) and the first live + badge/tunnel round-trip. Ratio picks up the import + package swap on its + trip. +- [2026-07-02 Thu] DOING — decomposed into six build phases under the + todo.org parent (:SPEC_ID: bound); build started same evening per Craig + ("tunnels build now + audio-panel spec alongside"). +- [2026-07-02 Thu] READY — fused review passed the gate: 4/4 decisions + resolved, phases decomposable, claims re-verified live (proton-vpn-cli + 1.0.1 in extra, binary =/usr/bin/protonvpn=, no package conflict with the + GTK app; =tailscale status --json= shape confirmed on velox — Self/Peer/ + CurrentTailnet.Name/MagicDNSSuffix; zero NM wireguard connections yet, + seven configs in assets awaiting the phase 6 import). +- [2026-07-02 Thu] DRAFT — initial spec from the roam capture "other network + interfaces (tailscale, VPNs, wireguard)" filed in todo.org 2026-07-02. + +* Metadata + +| Field | Value | +|--------+---------------------------------------------------| +| Status | implemented | +|--------+---------------------------------------------------| +| Owner | Craig Jennings | +|--------+---------------------------------------------------| +| Repo | dotfiles (net module); archsetup (packages) | +|--------+---------------------------------------------------| +| Parent | Waybar network module spec (2026-06-29), V2 panel | +|--------+---------------------------------------------------| + +* Problem + +The net panel's Connections tab shows what NetworkManager knows: WiFi networks +and wired links. The machines also run overlay and tunnel interfaces the panel +is blind to: + +- Tailscale (tailscaled, both daily drivers; the tailnet is how the machines + reach each other; not an NM device) +- WireGuard configs (assets/wireguard-config/ carries Proton VPN configs; + importable as NM connections of type wireguard or run via wg-quick) +- Commercial VPN clients (Proton VPN GTK app is installed on velox; owns its + own tunnel device) + +When one of these is up it changes routing, DNS, and reachability — exactly +the things the Diagnostics tab reasons about — yet the panel neither shows nor +controls them, and the doctor can misattribute a VPN-caused failure to the +underlying link. + +* Goals + +1. Visibility: the Connections tab shows overlay/tunnel interfaces with live + state (up/down, address, and for tailscale the tailnet peers summary). +2. Control: bring each up or down from the panel row, same interaction shape + as Join/Disconnect on WiFi rows (no terminals — V2 contract). +3. Diagnostics awareness: diagnose/doctor know when a tunnel owns the default + route or DNS, name it in evidence rows, and stop misattributing its + failures to the physical link. + +Non-goals (this iteration): installing or configuring VPN providers, tailnet +ACL management, exit-node selection UI (a "use exit node" affordance can ride +a later pass), kill-switch management (tracked separately in the spec's +failure catalog). + +* Design sketch + +** Data sources — one probe per backend, engine-side + +New GTK-free module net/src/net/overlays.py with one probe per backend, +each returning the same small dict shape ({kind, name, state, addr, detail, +can_toggle}): + +- tailscale: =tailscale status --json= (rich: self, peers, exit node, health + messages). Daemon down → state "stopped". Binary absent → backend absent. +- wireguard-nm: =nmcli -t connection show= filtered to type wireguard — + up/down via the existing nmcli wrapper (activate/deactivate connection). + The seven Proton configs in assets/wireguard-config/ import cleanly + (=nmcli connection import type wireguard file =, then + =connection.autoconnect no= immediately — imports default to autoconnect + yes). They use only PrivateKey/Address/DNS + PublicKey/AllowedIPs/Endpoint, + no PostUp/PostDown anywhere, so no wg-quick path is needed at all + (Craig, 2026-07-02). All are full-tunnel (AllowedIPs 0.0.0.0/0) — the + panel should treat them as mutually exclusive. +- proton: drive the official proton-vpn-cli (Arch extra repo, v1.0.x, + stable since 2026-04) — connect/disconnect/status verbs. It drives NM + underneath (python-proton-vpn-network-manager), so the panel still sees + connection events through NM. Runtime-exclusive with the GTK app, which + gets dropped from the install. The imported NM wireguard configs remain + a raw fallback when the CLI/API path is down; the CLI stays primary + because the raw configs lack kill switch, port forwarding, and server + rotation. + +** Panel + +A fourth Connections group "Tunnels" (after Saved / Available now / Wired) +using the existing group-header + row machinery. Row: glyph per kind, name, +state caption; primary action Up/Down where can_toggle, else Open app. +Tailscale row detail (subtitle or tooltip): tailnet name, peer count online, +exit node if any. + +** Privileged path + +- tailscale up/down: needs root or operator — =tailscale set --operator= at + install time (archsetup) makes the user an operator, so no sudo needed at + runtime. Fallback: the V2 net-priv helper gains tailscale-up/down verbs. +- NM wireguard connections: no privilege needed (NM polkit default for the + active user). + +** Diagnostics awareness + +- diag gains an "overlay owns default route/DNS" detection step: when the + default route or resolv.conf points at a tunnel interface, evidence names + it ("default route via tailscale0") and failure classification runs the + physical-link checks against the underlying device instead. +- doctor: a tunnel-caused egress failure (VPN up but its endpoint dead) + classifies fixable with next_action "bring the tunnel down / reconnect", + not a WiFi reset. + +** Bar indicator + +Part of v1 (Craig, 2026-07-02 — "shouldn't be optional"): a small overlay +badge on the net glyph when a tunnel owns the default route. Rides the same +route/DNS-ownership detection the diagnostics step adds. + +* Decisions (Craig) + +** DONE Which backends ship in the first pass? +CLOSED: [2026-07-02 Thu] +Approved (Craig, 2026-07-02): tailscale + NM-managed wireguard. Craig asked +whether the wireguard configs can be ported to NM so wg-quick drops out +entirely — yes: all seven configs in assets/wireguard-config/ use only the +six directives NM imports cleanly (verified 2026-07-02; import command and +autoconnect caveat now in the design sketch). wg-quick is out of the spec, +not deferred. Proton control is CLI-driven per the Proton decision below, +superseding the detection-only recommendation here. + +** DONE Tailscale control path: operator flag at install vs net-priv verbs? +CLOSED: [2026-07-02 Thu] +Approved (Craig, 2026-07-02): =tailscale set --operator=$USER= in archsetup's +tailscale step (declarative, no sudo at runtime); net-priv verbs only if +operator mode proves insufficient (e.g. up with flags). +** DONE Does "Tunnels" belong in Connections or its own tab? +CLOSED: [2026-07-02 Thu] +Approved (Craig, 2026-07-02): a Connections group. A fourth top tab dilutes +the V2 nav for three rows. + +** DONE Proton VPN: detect-only or drive its CLI? +CLOSED: [2026-07-02 Thu] +Decided (Craig, 2026-07-02): drive it through a CLI. Research (2026-07-02): +Proton shipped an official Linux CLI — first release 2025-11, stable v1.0.0 +2026-04, packaged in Arch extra as proton-vpn-cli (1.0.1 at check time), +with kill switch, port forwarding, NetShield, server selection, and a +status command. It drives NM underneath, so the panel sees its connections +through the existing NM event path. Spec changes: the proton backend calls +protonvpn connect/disconnect/status instead of device-detection +(can_toggle true); archsetup installs proton-vpn-cli and drops +proton-vpn-gtk-app (the two can't run concurrently per the project README — +untested locally); the imported NM wireguard configs stay as a raw fallback. +Sources: [[https://protonvpn.com/support/linux-cli][Proton Linux CLI guide]], +[[https://protonvpn.com/support/release-notes-linux-cli][CLI release notes]], +[[https://github.com/ProtonVPN/proton-vpn-cli][proton-vpn-cli repo]]. +* Implementation phases + +1. overlays.py probes (tailscale JSON, nmcli wireguard filter, proton-vpn-cli + status) — pure engine, TDD with fake binaries; =net status= grows an + overlays section. +2. Panel Tunnels group + Up/Down wiring through the worker thread; AT-SPI + smoke extension. +3. Diagnose/doctor overlay awareness (route/DNS ownership step, classifier + rows, evidence text) — TDD against the diag harness. +4. waybar-net tunnel badge on the net glyph (v1 per the bar-indicator + decision), riding phase 3's route-ownership detection; suite coverage. +5. archsetup: tailscale operator flag in the tailscale install step; + proton-vpn-cli replaces proton-vpn-gtk-app in the package list; VM test + assertions. +6. One-time per-machine migration: import the seven assets/wireguard-config + configs into NM with autoconnect off (scriptable; both daily drivers). diff --git a/docs/specs/2026-07-02-timer-panel-spec.org b/docs/specs/2026-07-02-timer-panel-spec.org new file mode 100644 index 0000000..c0dbd2c --- /dev/null +++ b/docs/specs/2026-07-02-timer-panel-spec.org @@ -0,0 +1,119 @@ +#+TITLE: Timer GTK Panel +#+AUTHOR: Craig Jennings +#+DATE: 2026-07-02 +#+TODO: TODO | DONE +#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED + +* DRAFT Timer GTK Panel +:PROPERTIES: +:ID: 25ed5321-f035-42b3-b115-69364d775f41 +:END: +- 2026-07-04 Sat @ 12:36:56 -0500 — retrofitted by spec-sort; status set to DRAFT (evidence-based, human-confirmed) + +* DRAFT Status +:PROPERTIES: +:ID: 1770af2e-b093-4024-a512-ae4324a2869f +:END: +- [2026-07-02 Thu] DRAFT — initial spec from Craig's roam capture "give the + timer a gtk UI/UX like the network panel. spec this out." + +* Metadata + +| Field | Value | +|--------+---------------------------------------------------| +| Status | draft | +|--------+---------------------------------------------------| +| Owner | Craig Jennings | +|--------+---------------------------------------------------| +| Repo | dotfiles | +|--------+---------------------------------------------------| +| Kin | net panel (architecture donor), wtimer (backing), | +| | desktop-settings panel spec (sibling) | +|--------+---------------------------------------------------| + +* Problem + +The timer's whole UI is a chain of three fuzzel prompts (type, value, label) +plus a fourth for cancel. That flow can't show what's already running while +you create, can't offer one-tap presets, gives no feedback on a typo until +the add silently fails, and pomodoro state (phase, cycle) is only visible in +a tooltip. The 2026-07-02 styling pass made the dialogs presentable, but the +shape is still four blind modals for what is really one small control +surface. + +* Goals + +1. One panel, opened from the bar's timer module, that shows everything + running (live countdowns, pomodoro phase/cycle, paused state) and creates + new items without leaving it. +2. One-tap presets for the common cases (tea, pomodoro, quick alarm) next to + freeform entry, with inline validation before the add. +3. Per-item controls: pause/resume, cancel, promote to primary (the bar + glyph slot). +4. wtimer stays the single owner of timer state and the notification path; + the panel is a view over it, never a second engine. + +* Design sketch + +** Architecture — clone the net panel's proven stack + +- GTK4 + gtk4-layer-shell dropdown anchored under the timer module, Blueprint + .blp compiled to committed .ui (=make ui=; compiler is dev-only). +- Humble-object split: GTK-free PanelModel presenter, unit-tested to 100%, + with thin widget bindings; one gated AT-SPI smoke via the + run-panel-smoke.sh pattern. +- Backing: shell out to the existing wtimer CLI (=add=, =toggle=, =cancel=, + =cycle=, =render=). =render= already emits a JSON payload; the panel polls + it (or subscribes to the same RTMIN+14 refresh signal) for live state. + wtimer's 89-case suite keeps owning the logic; panel tests fake the CLI + like every dotfiles suite fakes binaries. +- Dupre WIP palette CSS shared with the net panel (same factoring the + desktop-settings spec calls for — one palette asset, three panels). + +** Layout sketch + +- Header row: running-item count + a Clear All button (maps to cancel-all). +- Item list: one row per item — type glyph, label, live countdown / clock + time / phase+cycle for pomodoro, pause and cancel buttons, click-to-promote. +- Create strip: four type buttons (the wtimer glyphs), preset chips per type + (e.g. 5m / 15m / 25m / 60m for timers), a freeform entry validated with + wtimer's own parsers, an optional label field. +- Empty state: the create strip alone, centered. + +** What happens to the fuzzel flow + +The keybind/fuzzel path stays as the keyboard-fast lane (it's now styled and +tested); the panel replaces the click-driven path on the bar module. Whether +the fuzzel chain eventually retires is a decision below. + +* Decisions (Craig) + +** TODO Panel scope: standalone timer panel, or a page in the desktop-settings panel? +The desktop-settings spec (sibling DRAFT) could host timers as a page. +Standalone matches the net panel's one-domain-one-panel shape and keeps the +timer dropdown small; folding in means one panel binary fewer. Recommend +standalone, sharing the palette/css asset. + +** TODO Fuzzel flow: keep as keyboard fast lane, or retire once the panel lands? +Keeping both costs two creation paths to maintain (though the fuzzel chain is +small and freshly tested). Recommend keep until the panel proves itself, then +revisit. + +** TODO Presets: which chips per type? +Strawman: timer 5m/15m/25m/60m; alarm +30m/top-of-hour/07:00; pomodoro +default cycle only; stopwatch needs none. Adjust to taste. + +** TODO Live updates: poll render (1s, like the bar) or a wtimer "watch" mode? +Polling reuses what exists and matches the bar's cadence; a watch/subscribe +mode is cleaner but grows wtimer. Recommend polling first. + +* Implementation phases + +1. PanelModel presenter + CLI-backing seam (TDD, GTK-free, 100% like the net + PanelModel). +2. Blueprint UI: item list + create strip, wired to the presenter; palette + css factored to the shared asset. +3. Bar integration: timer module left-click opens the panel (replacing the + fuzzel menu binding there), RTMIN+14 refresh keeps bar and panel in step. +4. AT-SPI smoke + manual-testing checklist; decide the fuzzel flow's future + after a week of real use. diff --git a/docs/specs/2026-07-03-audio-panel-spec.org b/docs/specs/2026-07-03-audio-panel-spec.org new file mode 100644 index 0000000..82041ed --- /dev/null +++ b/docs/specs/2026-07-03-audio-panel-spec.org @@ -0,0 +1,166 @@ +#+TITLE: Audio Panel — the pulsemixer console +#+AUTHOR: Craig Jennings +#+DATE: 2026-07-03 +#+TODO: TODO | DONE +#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED + +* IMPLEMENTED Audio Panel — the pulsemixer console +:PROPERTIES: +:ID: 9175e017-46ad-4887-ae45-887e9551c005 +:END: +- 2026-07-04 Sat @ 12:36:56 -0500 — retrofitted by spec-sort; status set to IMPLEMENTED (reason: Shipped; build task DONE, manual tests filed.) + +* IMPLEMENTED Status +:PROPERTIES: +:ID: 71f556c6-ee02-47cc-a3be-68c8289380f3 +:END: +- [2026-07-03 Fri] IMPLEMENTED — built in a no-approvals speedrun in the + dotfiles repo (branch panel-bugfixing): engine (pactl), presenter, GTK + panel, PTT arming, bar indicator, and the bar/keybind wiring, across four + commits 65e5bb0..9601420. 102 unit tests + a passing AT-SPI smoke on velox. + All five Decisions below resolved. Live-eyeball validation (visual polish, + PTT-in-a-meeting, fader feel, the master-mute hardware key) is the one open + follow-up, tracked as a manual-testing task in todo.org. +- [2026-07-03 Fri] DRAFT — stub from the todo.org task "Audio panel spec" + (roam ask 2026-07-02) plus the 2026-07-03 waybar/sound design discussion. + Written to iterate alongside the prototype + (=working/sound-panel/sound-panel-prototype.html=). Spine is present; the + Decisions and Design detail get filled in as we go. + +* Metadata + +| Field | Value | +|--------+---------------------------------------------------| +| Status | implemented | +|--------+---------------------------------------------------| +| Owner | Craig Jennings | +|--------+---------------------------------------------------| +| Repo | dotfiles | +|--------+---------------------------------------------------| +| Kin | net panel + bt panel (architecture + aesthetic | +| | donors), desktop-settings panel (sibling) | +|--------+---------------------------------------------------| + +* Problem + +Audio control today is the pyprland audio scratchpad (Super+A) — a floating +pulsemixer TUI — plus scattered bar affordances: =pulseaudio= (volume, click +to mute sink), =pulseaudio#mic= (mic glyph + mic-toggle), Super+M audio-cycle +ring, Super+Shift+A mic-toggle. There's no single glanceable surface that +shows every sink and source, lets you set the default output/input, and +carries the meeting-grade mic controls Craig wants (a clean muted mode and a +hold-to-talk mode). The net + bluetooth panels set the pattern for exactly +this shape; audio is the third instrument in the family. + +* Goals + +1. One panel, opened from the bar's sound glyph, exposing the full pulsemixer + surface: every sink and source, per-device volume, per-device mute, and + switching the default output and input. +2. Replace the pyprland audio scratchpad (Super+A) as the primary audio UI. +3. Mic modes for meetings: *live*, *muted*, and *push-to-talk* (mic stays + muted except while Space is held, releasing re-mutes). +4. A *master quick-mute* — one action mutes all output — reachable from the + faceplate and a keybind. +5. Instrument-console aesthetic and architecture consistent with net + bt: + same faceplate, lamps, engraved sections, console keys, needle gauges, + verify-everything contract. +6. The bar glyph reflects live state: speaker + three arcs normally, a + speaker-with-✕ when muted (Craig's called glyphs). + +* Design sketch + +Prototype: =working/sound-panel/sound-panel-prototype.html= (the reference for +layout + idioms below). + +** Surface (from the prototype) + +- *Faceplate* — status lamp, sound glyph, state word (PLAYBACK / MUTED), a + MUTED badge, the SND·01 unit label, and the *master quick-mute switch* + (same switch idiom as net wifi / bt power), plus the close ✕. +- *OUTPUTS section* — one row per sink. Row body click = set default (gold + DEF tag). A machined fader sets that sink's volume; the trailing glyph + mutes just that device. Active/default row is emphasized (cream name, gold + lamp/glyph). +- *INPUTS section* — one row per source, same idioms. +- *Mic mode* — three console keys: LIVE / MUTED / PUSH·TALK. Push-to-talk + keeps the mic muted (red IN needle) and un-mutes only while Space is held. +- *Twin VU needles* — output level + input level, the sound analog of net + throughput and bt battery gauges. Needle goes red when its side is muted. + +** Architecture — clone the net/bt panel stack + +- GTK4 + gtk4-layer-shell, Blueprint =.blp= → committed =.ui= (=make ui=, + dev-only build dep). +- Humble-object split: a GTK-free PanelModel presenter (unit-tested like the + net/bt PanelModels) + thin composite-widget pages. Backing actions in a + GTK-free =audio.py= that shells to the audio control layer (pactl / + wpctl / pulsemixer — pick below), TDD'd with fake binaries. +- One gated AT-SPI smoke (=run-panel-smoke.sh= pattern). +- Shared instrument-console palette CSS asset (the one net/bt/settings all + load) — do not duplicate the palette block. +- Code lives in dotfiles =audio/= sibling to =net/= (src-layout, tests in + =tests/audio/=). + +* Decisions (Craig) + +** DONE Audio control backend — pactl vs wpctl vs pulsemixer +CLOSED: [2026-07-03 Fri] +Resolved: =pactl= (the engine module is =pactl.py=). Both ratio and velox run +PipeWire with the pipewire-pulse compat layer and no PulseAudio daemon, so +pactl and wpctl hit the same graph — but =pactl -f json= gives structured, +name-addressable output where wpctl offers only a volatile-id tree. Reads go +through =pactl -f json list sinks|sources= + =get-default-*=; writes target +devices by stable name behind an argv-charset guard. + +** DONE Push-to-talk mechanism under Wayland (feasibility — phase 1) +CLOSED: [2026-07-03 Fri] +Resolved: route (a), Hyprland dynamic binds. The phase-1 spike confirmed all +three primitives on velox (Hyprland 0.55.4): =hyprctl keyword bind/unbind= +adds and removes a bind live, =bindr= fires on release, and =pactl +set-source-mute @DEFAULT_SOURCE@ 0|1= toggles the mic cleanly. =ptt.py= arms a +press bind (un-mute) + a bindr (re-mute) on entering PTT mode and unbinds on +leaving, so the talk key isn't grabbed globally otherwise. No evdev needed. +Documented behavior: while PTT is armed, the talk key is the talk key. + +** DONE Quick-mute keybind + scope +CLOSED: [2026-07-03 Fri] +Resolved: the XF86AudioMute hardware key (Super+Shift+M turned out to be taken +by the monocle-layout bind, so the spec's assumption was wrong). The mute key +now runs =audio quick-mute=, which mutes every output (master), not just the +default sink — identical on a single-sink machine, correct on a multi-sink +one. Also reachable from the faceplate master switch and the panel. Scope: +master mute of all sinks, with verify-after-apply per sink. + +** DONE Bar glyph click map +CLOSED: [2026-07-03 Fri] +Resolved with the low-regret wiring: kept the existing =pulseaudio= waybar +module (left-click mute, scroll volume — no regression) and repointed its +right-click from the retired pulsemixer scratchpad to =audio-panel=. So: left += mute, right = open panel, scroll = volume. A fuller =custom/audio= indicator +(state-following speaker glyph + its own click map) is built and tested +(=indicator.py= + =waybar-audio=) but stays unwired until the new bar glyph +gets a live eyeball — the swap is a one-line waybar edit when Craig's ready. + +** DONE Fate of the existing audio affordances +CLOSED: [2026-07-03 Fri] +Resolved: Super+A repurposed from =pypr toggle audio= (the pulsemixer +scratchpad) to =audio-panel= — the panel is the primary audio UI now, so the +scratchpad is retired. Its definition still sits in the machine-local +=pyprland.toml= (not stowed) and can be deleted by hand. Kept: =pulseaudio= + +=pulseaudio#mic= waybar modules (glance + scroll + the mic-mute glance), +Super+M cycle, Super+Shift+A + XF86AudioMicMute mic-toggle. Changed: +XF86AudioMute → master quick-mute (see the quick-mute decision above). + +* Implementation phases + +1. Push-to-talk feasibility spike (decision above) — the one unknown; settle + the mechanism before committing the mic-mode design. +2. =audio.py= backings (list/get/set/mute/default for sinks + sources) — + pure engine, TDD with a fake audio backend. +3. PanelModel presenter (rows, default tracking, mic modes, master mute, + verify-after-apply) — unit-tested, no GTK. +4. Blueprint UI + sound bar glyph (normal / muted / ptt states) + open/close + wiring; shared palette css; AT-SPI smoke. +5. Bar-affordance consolidation per the decision above; retire the Super+A + scratchpad; keybinds. diff --git a/docs/specs/2026-07-03-instrument-console-panels-spec.org b/docs/specs/2026-07-03-instrument-console-panels-spec.org new file mode 100644 index 0000000..c0a0c56 --- /dev/null +++ b/docs/specs/2026-07-03-instrument-console-panels-spec.org @@ -0,0 +1,164 @@ +#+TITLE: Instrument-console rebuild — net + bluetooth panels +#+DATE: 2026-07-03 +#+TODO: TODO | DONE +#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED + +* IMPLEMENTED Instrument-console rebuild — net + bluetooth panels +:PROPERTIES: +:ID: ac23e996-a51a-466b-ad80-2faff46447bf +:END: +- 2026-07-04 Sat @ 12:36:56 -0500 — retrofitted by spec-sort; status set to IMPLEMENTED (reason: Panel rebuild shipped (dotfiles e993c3f); build task DONE.) + +* IMPLEMENTED Status +:PROPERTIES: +:ID: e73877f5-4f5b-4f81-b946-dbaa6145e0d5 +:END: +- 2026-07-03 Fri @ 06:49 -0400 :: DOING → IMPLEMENTED: all six phases shipped + (net GTK-free layer 81ec9c3, net view 800ef60; bt GTK-free layer 5318b34, bt + view 66f03d9; phase-6 dead-code removal f4e688e). Both panels are single-screen + instrument consoles, verified live on velox — 46 suites + full make test green, + both AT-SPI smokes green end to end, render matching the approved prototype. The + three folded tasks (network panel redesign, bt switch placement + title, bt + rename devices) closed with the build. +- 2026-07-03 Fri @ 02:07 -0400 :: DRAFT → READY → DOING in one stroke: Craig + approved the design through five interactive prototype iterations and + authorized the no-approvals speedrun ("let's build them now... go"). The + review gate was the live prototype session itself. +- 2026-07-03 Fri @ 02:07 -0400 :: Created (DRAFT) from the prototype session. + +* Metadata + +| Field | Value | +|---------------------+------------------------------------------------------------| +| Status | implemented | +|---------------------+------------------------------------------------------------| +| Owner | Craig Jennings | +|---------------------+------------------------------------------------------------| +| Repos | dotfiles (net/, bluetooth/, themes), archsetup | +|---------------------+------------------------------------------------------------| +| Normative reference | [[file:../../assets/2026-07-03-instrument-console-panels-prototype.html][assets/2026-07-03-instrument-console-panels-prototype.html]] | +|---------------------+------------------------------------------------------------| + +* Summary + +Rebuild both GTK layer-shell panels (net, bluetooth) from the tabbed layout +to the instrument-console design: one screen, no tabs, a faceplate with a +state word + badges + radio switch + close, engraved section labels, lamp +rows that act on click, dial meters under the console keys, and a doctor +that does it all. The interactive prototype =panel-console-v3.html= is the +normative design reference — when this spec and the prototype disagree on a +visual or interaction, the prototype wins. + +* Decisions (all resolved — Craig, prototype session 2026-07-02/03) + +- Replace the tabbed panels outright. No fallback flag; git history is the + rollback. Net panel first, bluetooth second. +- Advanced repair tiers leave the panel entirely. DOCTOR runs the full + diagnose → classify → lightest-repair → re-verify escalation (the engine + already does this). The surgical tiers stay CLI-only (=net repair ...=). +- Faceplate (both panels): state lamp + state word, badges, unit label + (NET·01 / BT·01), radio switch (wifi radio / bt adapter power), close ✕. + Badges: TUNNEL (gold, net), AIRPLANE (gold, both), LOW BATT (red, bt). +- Sections in order — net: CHANNEL, NETWORKS (+ hidden action), TUNNELS, + CONSOLE (DOCTOR / SPEED TEST keys), meters, output. bt: ADAPTER (with + clickable =discoverable= chip), PAIRED, NEARBY (+ scanning note), CONSOLE + (DOCTOR / SCAN), battery gauges, output. +- Section row budgets, half-row peek, internal scroll (thin slate + scrollbar): NETWORKS 5.5 rows, TUNNELS 4.5, PAIRED 5.5, NEARBY 4.5. + In-range networks sort active-first then strongest-signal-first. Counts on + the engraved headers ("networks · 12 in range", "tunnels · 1 up of 9", + "paired · 3", "nearby · 12"). The panel silhouette never grows with list + length; only the output well is variable and it caps at ~170px. +- Lamp-row grammar: green = live/connected, gold = available/actionable, + off = down/stored, red = failed; busy = pulsing gold during transitions. + Rows act on click (tunnels toggle, networks join, paired devices + connect/disconnect toggle, nearby devices pair). +- Arm-first for anything disruptive or destructive, 3s auto-disarm: + - forget (network or bt device): hover reveals ✕; first click arms the + row terracotta ("forget? click ✕ again"), second fires. No dialog. + - disconnect (active network): click the active row; first click arms in + GOLD ("disconnect? click again") — disruptive, not destructive — second + fires. +- Meters (net): two dials, RX·DOWN / TX·UP, gold needles, mode tag top-left + (LIVE green / TEST gold), HOLD tag top-right. Idle: live link throughput. + Speed test: cards flash gold, needles sweep the measured rate, then PIN + the final value with HOLD; clicking a held meter releases it to LIVE. + Scale 0–100 Mbps, auto-relabel to 0–1000 when a reading exceeds 100. + Dial top inset ~13px so the corner tags never touch the arc. +- Speed test output well gets ONLY: location line ("location: by + "), ping (+jitter), final line, conditioned tip(s). The rates + live in the meters, not the text. +- Battery gauges (bt): same dial chrome; one per connected device (two + slots; empty slot dim "NO DEVICE"/"ADAPTER OFF"); needle+value red under + 15% and the LOW BATT faceplate badge lights. +- Output well: doctor streams the checks with their narration lines + (viewmodel.STEP_NARRATION) and repair steps in gold; verdict line closes + (olive for pass/fixed). A dismiss ✕ appears in the well's corner whenever + it has content. Both panels. +- WiFi radio switch: =nmcli radio wifi on|off=. Off empties NETWORKS to one + dim "wifi radio off" row, drops the connection, kills tailscale rows' + reachability; on rejoins the last network (NM autoconnect does this for + real). Airplane mode is system-level (Super+Shift+A owns it): both panels + reflect it (state word AIRPLANE, gold badge, switches down); a switch + flipped under airplane refuses with a toast naming the exit. A routed + ethernet link keeps the net panel ONLINE through airplane mode. +- Ethernet: presence-based row pinned atop NETWORKS when a cable is up + ("enp… · active · wired · 1.0 Gbps" / "wired · standby"); CHANNEL swaps + the signal ladder for "wired · full-duplex" when routed; clicking + the row toggles route ownership via device disconnect/connect. +- Pairing (bt): nearby row click → busy → passkey-confirm dialog (large + gold digits) → device moves to PAIRED and connects. SCAN key refreshes + with a "scanning…" note on the header. +- Rename (bt): hover ✎ on a paired row → dialog prefilled → bluez + =set-alias= (closes the filed rename task). +- Tooltips: any ellipsized row label carries its full text as the tooltip. +- Dialogs (join / hidden SSID / passkey / rename) keep the in-panel dupre + dialog style (gold border, dark well inputs, gold caret). +- Close: ✕ on the faceplate + Esc (already shipped; keep). +- Folded tasks: "Network panel redesign", "Bluetooth panel: switch placement + + panel title", "Bluetooth panel: rename devices" — all close with this + build's phases. + +* Engine gaps (small, close during phases) + +- radio verb: =nmcli radio wifi on|off= helper (manage or sysio) + tests. +- hidden-SSID join: =manage.add= grows a hidden flag + (=802-11-wireless.hidden yes=). +- ethernet: device rows from =nmcli dev= (type ethernet) + disconnect/ + connect verbs (device-level; =net down --iface= already disconnects). +- bt rename: btctl =set-alias= one-shot verb + verify-after read. +- bt battery: already exposed (indicator uses it). +- speedtest meters: =run_speedtest_stream= on_update already ticks (pty). +- link speed for wired channel line: =ethtool=-free read from + =/sys/class/net//speed=. + +* Implementation phases + +1. [X] Spec + task wiring (this file; todo.org parent task with :SPEC_ID:). +2. [X] Net GTK-free layer (TDD): viewmodel row composers for the console + sections (network rows sorted+counted, tunnel rows, channel facts, + faceplate state word derivation, meter scale logic, arm state machines + for forget/disconnect), PanelModel restructure (sections, no tabs). + Engine gaps: radio verb, hidden join, ethernet rows, wired link speed. +3. [X] Net view rebuild: gui.py single-page console built in Python + (faceplate, engraved scrolled sections, console keys, cairo dial meters + with mode/hold tags, output well + dismiss), panel.css additions + (engrave, lamps, dial, badges, arm tints). AT-SPI smoke + driver + rewritten for the console layout. Shipped with phase 4 (dotfiles + 800ef60): a view-only intermediate is a broken panel (rows and switches + that do nothing), so view + interactions landed together. +4. [X] Net interactions: join/hidden/forget (arm terracotta)/disconnect + (arm gold)/radio switch/ethernet toggle/doctor stream/speed-test-drives- + meters, toasts. Verified live on velox (DOCTOR streams, SPEED TEST sweeps + both dials then HOLD). Shipped in dotfiles 800ef60 with phase 3. +5. [X] Bluetooth panel: same treatment end to end (faceplate + power + switch, adapter chip, paired/nearby lamp rows, pair passkey flow, + rename via set-alias, forget arm, battery gauges + LOW BATT, DOCTOR / + SCAN keys, output). bt smoke rewritten. Shipped in two commits mirroring + net: dotfiles 5318b34 (GTK-free layer + engine gaps) and 66f03d9 (view + + interactions + smoke). rename lands on the bluez Alias via busctl + (set-alias has no MAC-addressed one-shot); verified live on velox (smoke + green end to end, screenshot matches the prototype). +6. [X] Live verification both panels on velox + all suites + smokes green; + summary of findings written to file; folded tasks closed; dead code + removed; session context finalized. -- cgit v1.2.3