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