aboutsummaryrefslogtreecommitdiffstats
path: root/volume-osd/README.md
blob: cce2ace675a60ff3bb2951ecc849147c3b31a4a8 (plain)
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
# 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 is untested.

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