# 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`. **The process is called `qs`, not `quickshell`.** Both binaries ship, but `qs` is what runs, so `pkill -x quickshell` and `pgrep -x quickshell` match nothing and silently succeed. During development this meant every "stopped" was a lie and every restart stacked another instance: 47 accumulated before the user noticed. It also produced "0 processes" readings that were mistaken twice for the shell exiting, and sent a debugging session after a bug that was not there. Use `pkill -x qs` and `pgrep -cx qs`, and check the count after. **`pkill -f` matches the agent's own shell.** The working directory is in the command line, so `pkill -f qs` or `pkill -f "foo.qml"` kills the caller and returns 144. Always `-x`, never `-f`. 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 `; 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 ` runs one directly. `qs -p ipc call ` 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.