aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-07-02 10:18:51 -0400
committerCraig Jennings <c@cjennings.net>2026-07-02 10:18:51 -0400
commit04bac48f03c33d20a34c2532d6502f049586834e (patch)
tree71ac988e67e97b1062f04ee701ce3e6ff7897b94
parent329669c2cb3e7c7902e604c56a2df3c3557a5390 (diff)
downloadarchsetup-04bac48f03c33d20a34c2532d6502f049586834e.tar.gz
archsetup-04bac48f03c33d20a34c2532d6502f049586834e.zip
docs(spec): bluetooth panel initial spec, net-panel UX findings filed
The bluetooth panel spec clones the net panel's stack: a GTK-free engine over bluetoothctl one-shot verbs, a GTK4 layer-shell popup, two tabs with ASCII mockups, a diagnostics doctor chain, and four decisions awaiting review. The UX pass surfaced two net-panel findings, filed as tasks: error toasts auto-dismiss before an error can be read, and the V2 spec's keyboard-navigation claims aren't verifiably implemented.
-rw-r--r--docs/design/2026-07-02-bluetooth-panel-spec.org317
-rw-r--r--todo.org11
2 files changed, 328 insertions, 0 deletions
diff --git a/docs/design/2026-07-02-bluetooth-panel-spec.org b/docs/design/2026-07-02-bluetooth-panel-spec.org
new file mode 100644
index 0000000..56a27f0
--- /dev/null
+++ b/docs/design/2026-07-02-bluetooth-panel-spec.org
@@ -0,0 +1,317 @@
+#+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
+
+* DRAFT Status
+:PROPERTIES:
+:ID: 1271a845-4463-4831-9902-990eda6b2265
+:END:
+- [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 | draft |
+|--------+---------------------------------------------------------------------------------|
+| 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.
+
+Non-goals (this iteration): OBEX file transfer, audio-codec switching UI
+(diagnostics may *name* a wrong profile; switching stays with wpctl for
+now), 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
+ <mac>= 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 <mac>=.
+- Pairing: the one interactive corner. =bluetoothctl pair <mac>= 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 sink stuck in HSP/HFP when A2DP is expected:
+ evidence names it and points at the fix; switching profiles is
+ deliberately out of scope for v1 (non-goal), so this row is
+ diagnosis-only.
+
+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)
+
+** TODO Pair implies trust + connect?
+Recommendation: yes — one Pair verb does pair → trust → connect. Pairing
+a device nearly always means "use it now, auto-reconnect later"; a
+device you don't want auto-reconnecting gets untrusted from its row
+caption menu later (or we add a per-device "auto-reconnect" toggle in a
+later pass). The alternative (separate Pair / Trust / Connect buttons)
+triples the button bar for the rare case.
+
+** TODO Retire blueman entirely?
+The panel + bar module replace blueman-applet and blueman-manager
+(Super+Shift+B rebinds to the panel). Recommendation: drop the blueman
+package from archsetup and both machines once the panel's phase 2 lands;
+keep bluetoothctl as the terminal fallback. The alternative keeps blueman
+installed-but-unwired as a safety net during a bake-in period.
+
+** TODO Battery in the row caption or tooltip only?
+Recommendation: caption when the device reports it ("Connected · battery
+80%"), tooltip otherwise. It's the one datum that changes decisions
+(charge the mouse tonight?); hiding it behind hover buries the payoff.
+
+** TODO Scan burst length and auto-rescan?
+Recommendation: 8s bursts, no auto-repeat — Rescan is explicit, matching
+the net panel's Available view. The alternative (continuous scan while
+the Nearby view is open) finds slow advertisers but burns radio and
+battery, and the open-ended spinner reads as "stuck".
+
+* Implementation phases
+
+1. Engine =bt= package: adapter/device/scan probes over fake-bluetoothctl,
+ status + doctor chain (rfkill, service, powered, reachability, audio
+ profile) — 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).
diff --git a/todo.org b/todo.org
index 82317f0..6c13d6c 100644
--- a/todo.org
+++ b/todo.org
@@ -537,6 +537,11 @@ Design / open questions (propose before building):
Implementation notes: a small GTK layer-shell app (mirror pocketbook's structure: src-layout Python package, pytest, Makefile) talking to brightnessctl / hyprctl / the touchpad + airplane helpers. Lives in the dotfiles repo or in-tree like pocketbook. TDD the backing toggle/slider logic. Sizable — worth a design doc first.
+** TODO [#B] Bluetooth panel + bar module :feature:waybar:bluetooth:
+Initial spec written 2026-07-02: [[file:docs/design/2026-07-02-bluetooth-panel-spec.org]] (DRAFT — decisions await Craig's review before build).
+
+A bluetooth panel driving a CLI underneath (bluetoothctl one-shot verbs), consistent in look and feel with the net panel (GTK4 + layer-shell + Blueprint, humble-object presenter, verify-everything). Minimalistic interface, full functionality, plus a diagnostics/troubleshooting section mirroring the net panel's Diagnostics tab. Bar module glyph opens it. Craig's ask (2026-07-02): follow UX/UI best practices; where the net panel's patterns conflict with best practices, file a net-panel bug task rather than clone the flaw.
+
** TODO [#B] Local offline LLM runtime + per-host model cache :tooling:llm:
:PROPERTIES:
:LAST_REVIEWED: 2026-05-29
@@ -872,6 +877,12 @@ The wlogout config uses fixed pixel margins, which is the likely reason sizing d
Add a regression test so the square-cell fix doesn't silently break on a resolution change: assert the rendered (or computed) wlogout button cells are square across ratio's and velox's resolutions. Dropped :quick: — the cross-host test pushes this past a spare-moment fix.
Shipped 2026-07-02 (dotfiles 775771b). Keybind now calls a =wlogout-menu= wrapper computing centered margins from the focused monitor (the old fixed L/R 1200 exceeded velox's 1436 logical width). Also fixed two styling defects the geometry hid: invisible unfocused borders (now muted, so the square edge is visible) and hover/focus sharing one gold rule (lock button glowed at launch; focus is now a muted ring). Tests: unit margin-math suite across both hosts' resolutions + portrait + small + bad-geometry, CSS regression suite, and a compositor-gated =make test-wlogout= smoke that launches a no-op probe, screenshots, and measures squareness (velox: 361x361 px, PASS). Ratio's visual eyeball rides the pending ratio sync.
+** TODO [#C] Net panel: error toasts auto-dismiss unread :bug:network:waybar:
+Found during the bluetooth-panel UX pass (2026-07-02). The panel's toast revealer auto-clears after 4 seconds; an operation error that surfaces only in the toast (connect failures, as opposed to diagnostics errors that land as persistent verdict rows) can vanish before it's read. Heuristic: error messages persist until acknowledged or the failure is visible elsewhere. Matrix: Minor severity x some-users-sometimes = P3. Candidate fix: errors keep the toast revealed until the next user action (successes keep the 4s fade), or failed ops also mark the affected row.
+
+** TODO [#C] Net panel: verify claimed keyboard navigation :test:network:waybar:
+Found during the bluetooth-panel UX pass (2026-07-02). The V2 spec claims tab-between-sections, arrow-key row navigation, and type-to-filter, but no custom keyboard code exists in the panel — arrows and type-ahead may ride GTK ListBox defaults, tab-between-sections likely doesn't. Verify each claim against the live panel (AT-SPI smoke can assert focus order); implement or strike the claims from the spec so spec and panel agree.
+
** TODO [#C] Window focus lost when unhiding stashed windows :bug:hyprland:
:PROPERTIES:
:LAST_REVIEWED: 2026-06-24