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
|
#+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.
|