I kept switching themes and finding text I couldn't read. So I built a theme manager that won't write a theme with bad contrast to disk. huemill is public now.
The design choice that makes it work is that palette.json is the seam. One
side produces it — from a named Base16-style theme or the current wallpaper —
and the other side consumes it, and they never touch. Enforce the invariants
once, in the compiler and the test suite, and every adapter downstream (kitty,
ghostty, waybar, Firefox, whatever) inherits them without knowing they exist.
The invariant
The thresholds sit at the top of the compiler: MIN_CONTENT_CONTRAST = 4.5 and
MIN_BORDER_CONTRAST = 3.0. readable_color returns a preferred foreground if
it clears the minimum, otherwise the better of black or white. contrast_color
doesn't fall back to black or white for muted text and borders — it shifts
lightness until the threshold is met, so muted text on a dark surface stays
dark, just lighter enough to pass.
The test suite asserts the artifact before it's written: content/surface
contrast ≥ 4.5, border/surface ≥ 3.0, and every on*Container/onSelection
pair ≥ 4.5. Break the invariant and the tests fail. If it's on disk, it's
readable.
The seam is load-bearing
Write is atomic: flock around a temp file, rename() over the target, so a
concurrent reader never sees a half-written palette. The current artifact is
version 4 — semantic roles (surface, text, accent, danger) with on*,
*Container, and on*Container variants, interaction colors, and a full
terminal palette for indices 0 through 255. The terminal expansion derives
from the theme colors and credits Jake Stewart's color256, which huemill vendors
with its public-domain provenance intact.
Wallpaper mode needs matugen for the initial generation on top of the
named-theme path. If ImageMagick's convert is available, huemill records
wallpaperLuminance from 0.0 to 1.0; if it isn't, that field is just omitted.
Both are optional, and huemill's job is the palette, not your application
templates — after writing the artifact it can invoke a theme-apply-all hook
your dotfiles provide.
The result is one stable JSON document, consumed everywhere. It doesn't own the application end of the seam; it just makes sure the thing between them is always readable.
Version 0.1.0, MIT, 23 tests. codeberg.org/maxronner/huemill