1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
|
#+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
* DOING Status
:PROPERTIES:
:ID: 1271a845-4463-4831-9902-990eda6b2265
:END:
- [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 | doing |
|--------+---------------------------------------------------------------------------------|
| 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
<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 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 <card> <index>= (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: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=.
|