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
|
# AGENTS.md
Guidance for agents working in this repository.
## What this is
Quickshell components for a Hyprland desktop, one per directory, each a
complete shell in its own right. They are not modules of a single bar: any of
them runs alone, and running one does not require the others.
volume-osd/ volume for output and input, plus what is playing
vm-manager/ libvirt drawer: state, live stats, snapshots
appearance/ wallpaper picker and colour scheme switcher
Both are started from `~/.config/hypr/sections/autostart.lua` and keep running
for the whole session.
## The rule that bites first
**A quickshell config with no visible window exits.** Both components here are
hidden most of the time, so each holds itself open with a 1x1 transparent
`PanelWindow` with `mask: Region {}`, which is click-through and draws nothing.
Without it the shell loads, logs `Configuration Loaded`, reports no error, and
quits. The symptom is never an error message: it is a keybind that appears to
do nothing, or a panel that never paints. This was removed once during
development because a process check was measuring the wrong thing, and both
components broke in exactly that way. Any new component needs the same window.
## Verifying, and how not to
Two traps cost real time in this repo, both about measurement rather than code.
**A detached `qs` does not survive an agent's tool call.** Starting one with
`&`, `nohup` or `setsid -f` and then checking `pgrep` in a later call reports
`DEAD` regardless of whether the config is sound. That reads exactly like the
no-visible-window exit above and sent a debugging session in the wrong
direction for a dozen calls. Start it so the harness owns the process, and
confirm with the log rather than a later `pgrep`.
**`pkill -f` matches the agent's own shell.** The working directory is in the
command line, so `pkill -f quickshell` or `pkill -f "foo.qml"` kills the caller
and returns 144. Use `pkill -x quickshell`.
For anything visual, ask. Screenshots of a transient OSD are a race, and the
user has the screen.
## Per-component notes
Each directory's README carries its own, and they are worth reading before
changing that component. The ones that generalise:
- **PipeWire nodes report their initial volume before `ready` goes true.** The
`ready` check alone suppresses the startup values. An extra guard on top of
it ate the user's first keypress instead.
- **`PwObjectTracker` is not optional.** Node properties only stay current
while something binds the node; without it the volume reads once and goes
stale.
- **playerctld publishes a duplicate of every MPRIS player** under its own bus
name. Filter by `dbusName`, not by identity.
- **Key events reach a focused item, not a window.** Setting
`WlrLayershell.keyboardFocus` is necessary but not sufficient:
`Keys.onEscapePressed` on a `PanelWindow` never fires.
- **A `Row` sizes to its children, not its parent.** Fixed child widths inside
one overflowed the panel and pushed a button past its edge.
- **QML's JS engine has no `String.matchAll`.** It throws, and inside a `try`
that looks like a parser quietly returning nothing. Use an `exec` loop.
- **Assigning `running = true` to a `Process` that is already running does
nothing.** Reusing one `Process` for a sequence of commands needs
`running = false` immediately before each start.
- **libvirt's own memory and disk figures are not what they look like.**
`balloon.current` is memory allocated to the VM and reads full forever;
`block.allocation` is qcow2 growth on the host, not usage inside the guest.
The real numbers come from qemu-guest-agent, and the panel shows a dash
rather than substituting the host-side ones.
## Theme
No component defines a palette. `udt-accent`, in the
`unified-desktop-theme` repo alongside this one, writes
`~/.cache/wal/udt-palette.qml` from its `palette.rasi`, carrying the whole
Catppuccin Macchiato palette plus the accent snapped from the current
wallpaper. Each `Theme.qml` parses and watches that file, so a palette edit
recolours a running shell with no restart.
What is left in `Theme.qml` is a fallback for before the file is read, and for
a machine without unified-desktop-theme. Do not grow it into a second palette:
that duplication is what generating the file removed.
The file is parsed rather than imported because a generated QML singleton
cannot be imported without a `qmldir` beside it, and the wal cache has no
reason to carry one.
## Blur
Translucency is set in QML; the frosting is the compositor's. Hyprland blurs a
layer surface only when a rule names it, matched on the namespace the window
sets, in `~/.config/hypr/sections/decorations.lua`. A new component that wants
frosting needs its own `hl.layer_rule` and a distinct
`WlrLayershell.namespace`. Without the rule it still works, rendering flat
translucent.
## Reloading
| | How |
| --- | --- |
| a component's QML | quickshell hot-reloads on save; no restart |
| palette | regenerate with `udt-accent <wallpaper>`; watched, no restart |
| Hyprland binds, layer rules, autostart | `hyprctl reload` |
Hot reload covers adding and removing windows too: the keepalive window above
was added to a running shell and took effect on save.
`qs -p <dir>` runs one directly. `qs -p <dir> ipc call <target> <fn>` reaches a
running one, which is how `SUPER+v` opens the VM drawer.
## Conventions
- GPLv2 only, with the header notice in every source file.
- Commits carry the reasoning, not just the change: several of the notes above
exist because a commit message explained why something was the way it was.
- No home paths in committed files. A gitleaks hook blocks them and has been
right every time; `~` in documentation, absolute paths only in the live
Hyprland config, which is not in this repo.
|