# AGENTS.md Guidance for agents working in this repository. ## What this is One visual identity for a Hyprland desktop: Catppuccin Macchiato, Noto Sans for UI text, Inconsolata Nerd Font Mono for monospace. It covers rofi, dunst, Hyprland window borders, conky, Qt5/Qt6, GTK3/GTK4, waybar, kitty and neovim. The repo holds the canonical copies. `./install.sh` symlinks them into place, so the working config cannot drift from what is committed. Everything outside `~/.config/rofi/udt/` and `~/bin/` was edited in place; `docs/MIGRATION.md` is the record of what changed and where the backups are. ## One palette, many consumers Colours are written down once. `palette/.conf` holds them under the scheme's own names and `palette/roles-.conf` says what each is for. `bin/udt-palette` renders that pair into every consumer's own syntax; `install.sh` runs it first, before anything is linked. **The scheme selection lives outside the repo**, in `~/.config/udt/roles.conf`, seeded from `palette/roles.conf.default` on a first install and never overwritten after. That is deliberate: which theme a machine runs is local state, so switching does not dirty the working tree and two machines sharing this repo can differ. It is the one exception to this repo holding the canonical copy of everything. Four schemes ship: `macchiato`, `tokyo-night`, `nord`, `dracula`. Switching is one line in `~/.config/udt/roles.conf` plus `./install.sh`. Generated files are gitignored, because tracking them would turn every scheme switch into a diff. Editing one is pointless: the next install overwrites it. The generated set is `rofi/udt/palette.rasi`, `templates/dunstrc`, `templates/conky.conf`, `templates/waybar/theme.css`, `templates/terminal/kitty-theme.conf` and `bin/udt_colors.py`. The last is imported by `udt-accent`, which is why its accent table follows the scheme. Roles are a flat namespace, so a name collides across sections: conky's outline is `body_outline` because waybar already has `outline`. A role value is a palette name with two optional modifiers, `name/75` for 75% alpha and `name*50` for half brightness. The second exists only because GTK's `shade(@main-bg, 0.5)` has no palette name to point at: half-brightness crust is darker than the darkest colour any of these schemes ships. **Adding a scheme means adding two files, never editing a consumer.** Every scheme must define the same role set; `udt-palette --selftest` fails if one drifts, which is what stops a switch from breaking a config nobody looked at. ## The accent One colour moves: `@accent`. `bin/udt-accent` reads the current wallpaper, extracts its signature colour, and snaps it to the nearest of ten Catppuccin accents by perceptual hue in CIELAB. `wallp` calls it on every wallpaper change. It writes five things: `~/.cache/wal/udt-accent.rasi` (rofi), `~/.cache/wal/udt-border.lua` (Hyprland), the accent substitution in `~/.cache/wal/dunstrc`, `~/.cache/wal/colors.json` (Firefox, via pywalfox), and `~/.cache/wal/udt-palette.qml` (quickshell). The QML palette is the one output that is not only the accent: it carries the whole structural palette too, parsed out of `rofi/udt/palette.rasi` rather than duplicated in the script. That file stays the single place the Macchiato values are written down. Quickshell components watch the generated file and re-read it in place, so a palette edit reaches a running shell without restarting it; see the `quickshell` repo alongside this one. **Extraction is deliberately isolated from the pywal cache.** It calls `pywal.backends.colorz.get()` directly, which returns a list and writes nothing. Running `wal -i` instead would rewrite all of `~/.cache/wal`, including the terminal's ANSI colours, and that is the exact failure this design exists to avoid. If you change `signature_color()`, verify it still writes nothing. `colors.json` is the one pywal file `udt-accent` does rewrite, and it rewrites it wholesale rather than editing it: a fixed Macchiato palette with the accent in the cursor and the two highlight slots. That is deliberate. pywalfox reads `colors.json` and nothing else, so it is the only way Firefox can follow the accent, and overwriting it costs nothing because nothing else reads it. In particular kitty reads `~/.config/kitty/current-theme.conf`, not this file, so the terminal palette stays fixed no matter what the wallpaper looks like. The write lands after `wallp` has already run `wal --theme`, so it wins; reorder those two and pywal's wallpaper-derived colours come back. Not everything tracks the wallpaper. rofi, dunst, Hyprland borders and Firefox do; conky, Qt, GTK and waybar sit on fixed lavender, which is also the fallback when a wallpaper is too grey to snap. Qt and GTK apps only reread a theme on restart, so a moving accent there would leave running apps disagreeing with new ones. ## Verifying visual changes `rofi -no-config -theme -dump-theme >/dev/null` catches syntax errors without a display. It does **not** tell you what renders: it normalises values, prints `(null)` for every image, and collapses multi-value margins. For anything visual, screenshot and measure: grim -o DP-1 /tmp/shot.png Reasoning about rofi's box model was wrong repeatedly during this project; measuring pixels was right every time. When a widget looks wrong, measure its bounding box before theorising about why. Rofi needs a display, so these steps run in the user's session. Do not judge them by exit code: rofi exits non-zero for ordinary reasons such as Escape. ## Things that are not where they look - **rofi is 2.0.0**, not the 1.7.3 its config header claims. `rofi -v` is the authority. `-width` is gone; width lives in the theme. - **`userimage` in `powermenu.rasi` only honours its margin.** A `width` with `expand: false` collapses it to a line, and a percentage margin resolves against the whole window rather than the padded column. Its 230px margin is tied to the 680px column in `mainbox`; both move together. - **A layout that hardcodes `children` silently drops `-mesg`.** That is why `list.rasi` lists the message widget even though most callers pass nothing. - **`ClearlyU` is installed and claims large parts of the Private Use Area**, so it can win fontconfig's fallback and draw a blank where another font has the glyph. `fc-match ":charset=XXXX" family` says who actually wins. The `U+F0xxx` Nerd Font ranges avoid the problem. - **GTK3's theme comes from gsettings on Wayland**, not `settings.ini`. It was pinned to `Breeze`, silently overriding the file, for who knows how long. - **GTK4 ignores `gtk-theme-name`.** Its stylesheet is symlinked into `~/.config/gtk-4.0/theme` and imported by `gtk.css`. - **Hyprland's Lua parser refuses `hyprctl keyword`** ("keyword can't work with non-legacy parsers") and has no source directive. The border colours are a Lua table read with `dofile()`. - **`wallp` starts `swaybg` in the background**, which inherits the shell's stdout, so a pipe or command substitution appears to hang after `wallp` has already finished. Redirect its output when calling it from a script. - **waybar's own CLAUDE.md documents a stale CSS load order.** The `modules-*.css` and `states.css` files it names are not imported by anything; `style.css` pulls in `styles/main.css`. One of the dead files holds a `font-family` that looks authoritative but does nothing. ## Editing rules - **conky**: change colours in the `conky.config` block only. `conky.text` is laid out with absolute `${goto}` pixel offsets tuned to label widths, so editing text means re-tuning every goto on that line. See `~/.config/conky/CLAUDE.md`. Conky does not hot-reload. - **Committed files carry no home paths.** A gitleaks hook blocks them, and it has been right every time. Where a live config mixes theme keys with personal data (`qt6ct.conf` has an `ignored_applications` list), track only the theme-relevant keys. - **Themes that reference an image cannot be committed with a real path.** `launcher.rasi` carries an `@WPAPER@` placeholder that `install.sh` rewrites. rofi does not expand `~` inside a `url()`. - After editing anything under `rofi/udt/` or `palette/`, run `./install.sh`. - `bin/udt-palette --selftest` resolves every role in every scheme. Run it after touching a palette, a role map, or the generator. - `bin/udt-accent --selftest` covers the snapping logic. Run it after touching the colour maths. ## Reloading | | How | | --- | --- | | rofi | nothing; the theme is read per launch | | dunst | `udt-accent` restarts it | | Firefox | `udt-accent` runs `pywalfox update` | | Hyprland | `hyprctl reload` | | waybar | `killall -SIGUSR2 waybar` | | kitty | `pkill -USR1 -x kitty` | | conky | restart it; no hot-reload | | Qt / GTK apps | restart the app |