aboutsummaryrefslogtreecommitdiffstats
path: root/AGENTS.md
blob: b022a38500549facf01cb6edf2ff7171c8c0cc46 (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
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
# 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/<scheme>.conf` holds them under the
scheme's own names and `palette/roles-<scheme>.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.

Nine schemes ship: `macchiato`, `frappe`, `mocha`, `tokyo-night`, `nord`, `dracula`,
`material-ocean`, `material-palenight`, `material-darker`. Switching is one line in
`~/.config/udt/roles.conf` plus `./install.sh`.

All nine are dark. A light scheme is not a role remap: homepage is pinned
`theme: dark` and its generated card overrides target the dark-mode class, and
`install.sh` would need to flip that setting too.

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`, `templates/homepage/custom.css` and
`bin/udt_colors.py`.

The homepage stylesheet is the one output that leaves this machine.
`install.sh` scp's it to `homepage:hp-stage/`, with a five second timeout and a
warning rather than a failure when the host is down, so installing the local
theme still works off that network. It stages only: `/opt/homepage/config` is
root-owned, and the final copy plus a `systemctl restart homepage` is a manual
step the script prints. homepage's `settings.yaml` must keep `color: gray`, the
theme class the generated CSS targets. 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, and `udt-palette --selftest` enforces
that, then has rofi parse every theme under every scheme. Both checks exist
because both failures shipped: a role set can drift silently, and a palette
that resolves can still emit a theme rofi refuses to parse, which stops every
launcher on the desktop.

## 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 <absolute-path> -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

`./install.sh` reloads everything running, so a scheme switch is one command.
The table is what it does, and what to run if you change a config by hand.

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