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
|
# 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.
┌──────────────────────────────────┐
│ 🔊 Output 75% │
│ ████████████████░░░░░░░░ │
└──────────────────────────────────┘
## Running it
qs -p .
From Hyprland, to start it with the session:
exec-once = qs -p ~/Programming/GIT/quickshell/volume-osd
## 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 the Catppuccin Macchiato palette. The accent is read from
`~/.cache/wal/udt-accent.rasi`, the file `udt-accent` writes on every wallpaper
change, and is watched, so the OSD recolours without a restart. Lavender
(`#b7bdf8`) is the fallback when that file is absent, which is also what makes
this directory runnable on a machine that has no 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.
## 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.
|