docs: implementation plan for volume notification
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,227 @@
|
|||||||
|
# Volume Notification Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Replace the three `pamixer`-direct media-key bindings with calls to a wrapper script that also fires a mako notification showing the new volume level (or "Muted") with a progress bar.
|
||||||
|
|
||||||
|
**Architecture:** New `scripts/volume.sh` wrapper accepting `up|down|mute`. Adjusts master sink via `pamixer`, then calls `notify-send` with mako's `x-canonical-private-synchronous` and `int:value` hints so successive notifications replace each other and render as a progress bar. niri keybinds call the wrapper through its `~/.local/bin/volume` symlink. install.sh symlinks the script alongside the existing entries.
|
||||||
|
|
||||||
|
**Tech Stack:** bash, pamixer, notify-send, mako (notification daemon), niri.
|
||||||
|
|
||||||
|
**Spec:** `docs/superpowers/specs/2026-05-11-volume-notification-design.md`
|
||||||
|
|
||||||
|
**Verification model:** Bash syntax check + dry-run script invocation with each subcommand. niri config validity via `niri validate`. End-to-end (actually pressing the volume key and seeing a mako bubble) requires the live desktop — the controller verifies after both task commits.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
| File | Role |
|
||||||
|
|---|---|
|
||||||
|
| `scripts/volume.sh` | New wrapper: pamixer adjust + notify-send. Three subcommands: `up`, `down`, `mute`. |
|
||||||
|
| `install.sh` | One new `ln -sf` line in the `==> Installing scripts` block. |
|
||||||
|
| `niri/config.kdl` | Three lines (97–99) modified to call `volume up/down/mute` instead of `pamixer` directly. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 1: Wrapper script + install.sh symlink
|
||||||
|
|
||||||
|
After this task, `~/.local/bin/volume up|down|mute` works from any shell and produces both the volume change and the mako notification. niri keybinds still call `pamixer` directly — that swap happens in Task 2.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `scripts/volume.sh`
|
||||||
|
- Modify: `install.sh` (append one line in the scripts symlink block)
|
||||||
|
|
||||||
|
- [ ] **Step 1: Create `scripts/volume.sh`**
|
||||||
|
|
||||||
|
Write `scripts/volume.sh` with this exact content:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
up) pamixer -i 5 ;;
|
||||||
|
down) pamixer -d 5 ;;
|
||||||
|
mute) pamixer -t ;;
|
||||||
|
*) echo "usage: $0 up|down|mute" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ "$(pamixer --get-mute)" = "true" ]; then
|
||||||
|
notify-send -h string:x-canonical-private-synchronous:volume \
|
||||||
|
-h int:value:0 \
|
||||||
|
-t 1500 \
|
||||||
|
"Muted"
|
||||||
|
else
|
||||||
|
LEVEL=$(pamixer --get-volume)
|
||||||
|
notify-send -h string:x-canonical-private-synchronous:volume \
|
||||||
|
-h int:value:"$LEVEL" \
|
||||||
|
-t 1500 \
|
||||||
|
"Volume: ${LEVEL}%"
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Make it executable**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x scripts/volume.sh
|
||||||
|
ls -la scripts/volume.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: `-rwxr-xr-x` mode.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Syntax-check**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash -n scripts/volume.sh && echo "syntax ok"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: `syntax ok`, exit 0.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Dry-run the usage-error branch**
|
||||||
|
|
||||||
|
The unrecognised-arg branch is safe to exercise without side effects (pamixer never called):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash scripts/volume.sh 2>&1
|
||||||
|
echo "exit=$?"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
```
|
||||||
|
usage: scripts/volume.sh up|down|mute
|
||||||
|
exit=1
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Symlink it into `~/.local/bin`**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ln -sf "$PWD/scripts/volume.sh" ~/.local/bin/volume
|
||||||
|
ls -la ~/.local/bin/volume
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: symlink pointing to `$PWD/scripts/volume.sh` (absolute path of repo's script).
|
||||||
|
|
||||||
|
- [ ] **Step 6: Add the symlink line to `install.sh`**
|
||||||
|
|
||||||
|
Find the `==> Installing scripts` block in `install.sh`. It currently ends like this:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ln -sf "$(pwd)/scripts/waybar-restart.sh" ~/.local/bin/waybar-restart
|
||||||
|
ln -sf "$(pwd)/scripts/screenshot.sh" ~/.local/bin/screenshot
|
||||||
|
```
|
||||||
|
|
||||||
|
Append one line after `screenshot.sh`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ln -sf "$(pwd)/scripts/volume.sh" ~/.local/bin/volume
|
||||||
|
```
|
||||||
|
|
||||||
|
The block then ends with three `ln -sf` lines (waybar-restart, screenshot, volume).
|
||||||
|
|
||||||
|
- [ ] **Step 7: Sanity-check install.sh parses**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash -n install.sh && echo "syntax ok"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: `syntax ok`, exit 0.
|
||||||
|
|
||||||
|
- [ ] **Step 8: Functional smoke test (controller verifies; subagent skip if no audio session)**
|
||||||
|
|
||||||
|
If you have an audio session, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
~/.local/bin/volume up
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: volume rises 5%, a mako bubble appears with `Volume: NN%` and a progress bar, the bubble auto-dismisses in 1.5 s. Then run `~/.local/bin/volume down` and `~/.local/bin/volume mute` and confirm each behaves.
|
||||||
|
|
||||||
|
If your sandbox has no audio session or no notification daemon, skip; the controller will verify after the commit.
|
||||||
|
|
||||||
|
- [ ] **Step 9: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add scripts/volume.sh install.sh
|
||||||
|
git commit -m "volume: add notification wrapper script"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 2: Point niri keybinds at the wrapper
|
||||||
|
|
||||||
|
After this task, pressing the three XF86Audio keys runs the wrapper instead of `pamixer` directly. Volume changes now produce notifications.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `niri/config.kdl` (lines 97–99)
|
||||||
|
|
||||||
|
- [ ] **Step 1: Replace the three keybind lines**
|
||||||
|
|
||||||
|
Find lines 97–99 in `niri/config.kdl`:
|
||||||
|
|
||||||
|
```kdl
|
||||||
|
XF86AudioRaiseVolume allow-when-locked=true { spawn "pamixer" "-i" "5"; }
|
||||||
|
XF86AudioLowerVolume allow-when-locked=true { spawn "pamixer" "-d" "5"; }
|
||||||
|
XF86AudioMute allow-when-locked=true { spawn "pamixer" "-t"; }
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace with:
|
||||||
|
|
||||||
|
```kdl
|
||||||
|
XF86AudioRaiseVolume allow-when-locked=true { spawn "volume" "up"; }
|
||||||
|
XF86AudioLowerVolume allow-when-locked=true { spawn "volume" "down"; }
|
||||||
|
XF86AudioMute allow-when-locked=true { spawn "volume" "mute"; }
|
||||||
|
```
|
||||||
|
|
||||||
|
`allow-when-locked=true` stays — same behaviour, just routed through the wrapper.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Validate niri config**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
niri validate 2>&1 | tail -2
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: a final line `INFO niri: config is valid`. Non-zero exit or `ERROR` means a syntax issue.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Confirm pamixer is no longer referenced in keybinds**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
grep -n "pamixer" niri/config.kdl
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: zero matches (the wrapper is invoked via `volume`, pamixer is implementation detail).
|
||||||
|
|
||||||
|
- [ ] **Step 4: Confirm the three new lines are in place**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
grep -n 'spawn "volume"' niri/config.kdl
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: three lines, one each for `"up"`, `"down"`, `"mute"`:
|
||||||
|
```
|
||||||
|
97: XF86AudioRaiseVolume allow-when-locked=true { spawn "volume" "up"; }
|
||||||
|
98: XF86AudioLowerVolume allow-when-locked=true { spawn "volume" "down"; }
|
||||||
|
99: XF86AudioMute allow-when-locked=true { spawn "volume" "mute"; }
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Functional smoke test (controller verifies)**
|
||||||
|
|
||||||
|
niri picks up `config.kdl` changes live; no reload needed. Press the volume up key — a mako notification should appear within ~100 ms showing the new level + progress bar. Press mute — notification shows `Muted` with empty bar. Press volume up again — notification returns to `Volume: NN%`.
|
||||||
|
|
||||||
|
Each key press should replace the previous notification (no stacking), thanks to the `x-canonical-private-synchronous` hint.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add niri/config.kdl
|
||||||
|
git commit -m "niri: route volume keys through notification wrapper"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Final verification (controller, post-merge)
|
||||||
|
|
||||||
|
- [ ] Pressing XF86AudioRaiseVolume / XF86AudioLowerVolume produces a mako notification with the new percentage and a progress bar.
|
||||||
|
- [ ] Pressing XF86AudioMute toggles the mute state; the notification shows `Muted` (with empty bar) when muting and `Volume: NN%` when unmuting.
|
||||||
|
- [ ] Holding the key (rapid repeat) produces a single updating bubble, not a stack.
|
||||||
|
- [ ] Volume changes still work on the lock screen (gtklock). The bubble may not be visible while locked depending on mako's layer ordering relative to gtklock; the volume change itself fires regardless.
|
||||||
|
- [ ] `~/.local/bin/volume bogus` prints the usage message and exits 1.
|
||||||
Reference in New Issue
Block a user