From d203d938f005d7542bf13e217ac51917866a8ace Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Mon, 14 Sep 2026 12:24:13 +0200 Subject: feat(desktop): move volume-osd in as the sound module VolumeOsd.qml did three jobs in one file: PipeWire tracking, the OSD surface and the player transport. They become Service, Osd and the page. The OSD keeps its namespace so the existing Hyprland blur rule still matches, and the service stays always-active because the OSD has to answer a keypress with no drawer open. --- desktop/modules/sound/README.md | 159 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 desktop/modules/sound/README.md (limited to 'desktop/modules/sound/README.md') diff --git a/desktop/modules/sound/README.md b/desktop/modules/sound/README.md new file mode 100644 index 0000000..f0a8313 --- /dev/null +++ b/desktop/modules/sound/README.md @@ -0,0 +1,159 @@ +# volume-osd + +An on-screen display for volume, covering both output (speakers) and input +(microphone). It appears at the bottom of the screen when the level or mute +state changes, and fades out 1.5 seconds later. + + ┌────────────────────────────────────┐ + │ ▪ Outside World ⏮ ⏸ ⏭ │ + │ Sunbeam │ + │ ──────────────────────────────── │ + │ 🔊 Output 75% │ + │ ████████████████░░░░░░░░░░ │ + └────────────────────────────────────┘ + + (the track row only exists while a player does) + +## Running it + + qs -p . + +From Hyprland, to start it with the session: + + exec-once = qs -p ~/Programming/GIT/quickshell/volume-osd + +## Now playing + +When an MPRIS player is running, a track row sits above the volume bar: album +art, title, artist, and prev/play/next. With no player the panel is exactly +the volume OSD, at its original size. A track change or a play/pause shows the +panel too, so the row is not something you only see by touching the volume. + +Hovering the panel freezes its fade so the buttons can be clicked; moving away +starts the countdown again. Without a hover it behaves exactly as it did before +there was anything clickable on it. It still takes no keyboard focus. + +Two things about MPRIS that are not obvious: + +**Proxies publish duplicates.** playerctld proxies whichever player is +active and republishes it under `org.mpris.MediaPlayer2.playerctld`. With a +browser, plasma-browser-integration does the same, so one Navidrome tab was +live on three bus names at once. `Player.qml` drops both proxies and talks to +the browser's own entry. None of the three exists until playback starts, and +an open but silent tab publishes nothing at all. The plasma name is a browser +thing only: Feishin, playing from the same server, publishes just its own +entry and playerctld, so dropping it costs a native player nothing. + +**Album art can be a reused temp path.** Audacious extracts embedded art to a +file in its cache and rewrites that same path on each track, so the URL repeats +while the image behind it changes. The source carries the track title as a +cache buster and `cache: false`, or the previous track's cover stays on screen. + +Verified against audacious, Feishin, and Firefox playing Navidrome. Feishin is +the reference case, the only player here that fills the metadata in properly: +a real `mpris:artUrl`, `xesam:artist`, and a clean `xesam:title`. It is also +the only one whose art is a remote HTTP URL rather than a local file, so the +cover depends on reaching the Navidrome host. + +The same server through Firefox gives much less, which is a browser limit and +not something this component can fix. Navidrome's web player sets no +`mpris:artUrl` at all, so the row renders with no cover, and it packs +everything into `xesam:title` ("Roxanne - The Police - Navidrome") leaving +`xesam:artist` empty. Neither is worked around, a title like that cannot be +split back apart without guessing where a real dash ends. + +Signal publishes no MPRIS bus at all: playing an attachment claims no +`org.mpris.MediaPlayer2.*` name, only its tray `StatusNotifierItem`, so the +track row stays hidden and there is nothing here to fix. Any player that never +publishes is invisible to this component by construction. + +## No keybinds to change + +The OSD watches PipeWire rather than being triggered by a hotkey, so existing +volume binds keep working untouched: + + bind = , XF86AudioRaiseVolume, exec, wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+ + +Because the source of truth is PipeWire and not the keypress, the OSD also +appears for volume changed from anywhere else: pavucontrol, a per-application +slider, or another machine's remote control. + +One widget serves both directions. Whichever device changed last is the one +displayed, with a speaker icon for output and a microphone for input. + +Icons are Nerd Font glyphs (speaker and microphone), so the font stack needs a +Nerd Font available for fallback. Inconsolata Nerd Font Mono, which +unified-desktop-theme already installs, covers them. + +## Frosted glass + +The panel is drawn translucent (65% over the Macchiato base) and the blur +behind it comes from the compositor, not from QML. Hyprland blurs a layer +surface only when a rule says to, matched on the namespace this window sets +(`quickshell-volume-osd`): + + hl.layer_rule({ + name = "blur-volume-osd", + match = { namespace = "^(quickshell-volume-osd)$" }, + blur = true, + xray = false, + ignore_alpha = 0.1, + }) + +`xray = false` frosts the windows actually behind the OSD rather than jumping +straight to the wallpaper. `ignore_alpha = 0.1` leaves near-transparent pixels +unblurred, which keeps the rounded corners from picking up a halo. + +Doing it this way costs nothing in the shell: no `MultiEffect`, no live +blur pass in QML, no offscreen buffer. Without the rule the OSD still works, +it just renders flat translucent instead of frosted. + +## Theme + +`Theme.qml` holds no palette of its own beyond a fallback. The colours come +from `~/.cache/wal/udt-palette.qml`, which `udt-accent` generates on every +wallpaper change from unified-desktop-theme's `palette.rasi`, carrying the +whole Macchiato palette plus the accent snapped from the wallpaper. + +That file is watched, so editing the palette in unified-desktop-theme and +regenerating recolours a running OSD with no restart. Where the file does not +exist, the hardcoded defaults in `Theme.qml` apply, which is what keeps this +directory runnable on a machine without unified-desktop-theme. + +## Two details worth knowing + +**A node's volume arrives before it is ready.** When a `PwNode` binds, its +volume populates and emits a change signal, and that happens while `ready` is +still false. Those first signals are state being read, not the user turning a +knob, so `show()` checks `ready` and ignores them, which is why there is no +OSD at login. + +That one check is the whole guard, and it is tempting to add a second. An +earlier version also swallowed the first change per node, on the assumption +that the startup values arrived *after* ready. They do not, so the extra guard +ate the user's first keypress instead: the OSD only appeared from the second +change onward. If this symptom comes back, trace the signal order before +adding a filter. + +**`PwObjectTracker` is not optional.** PipeWire node properties are only kept +current while something binds the node. Without the tracker the volume reads +once and then goes stale, which looks like an OSD that displays a number from +several changes ago. + +**A config with no visible window exits.** The OSD is hidden most of the time, +so it holds itself open with a 1x1 transparent window with an empty mask, +which is click-through and draws nothing. Without it the shell loads, reports +no error, and quits, and the symptom is a keybind that appears to do nothing +or a panel that never paints. This was removed once during development after +misreading a process check, and the bug came straight back. + +**A Row sizes to its children, not to its parent.** The track row's text +column originally had a fixed width, and art + text + buttons + spacing came +to 356px inside a 328px content box, so the `next` button hung over the panel +edge. The column now takes whatever the art and transport buttons leave, which +holds at any panel width. Fixed widths inside a Row are worth distrusting. + +## Volume above 100% + +PipeWire allows volume over 1.0. The percentage is reported as-is, so it can +read above 100%, while the bar stops at full rather than overflowing its track. -- cgit v1.2.3