My Color Scheme
My terminal, editor, window manager, and this site all share a single color palette at all times. The pipeline that keeps them in sync is built on Noctalia, a quickshell-based desktop shell that manages theming as a first-class concern. Everything flows from one event: picking a wallpaper.
the color pipeline
When I press Super+W, a wallpaper selector opens up and allows me to select from a catalogue of saved wallpapers. Noctalia, my shell manager, then checks what color scheme is assigned to that wallpaper’s folder in its settings. There are two cases. If the folder maps to one of the 12 designed presets (Miasma, Oxide, Rose Pine Moon, and so on), Noctalia loads that preset’s JSON directly from ~/.config/noctalia/colorschemes/. If the folder is set to a dynamic scheme, Noctalia instead runs matugen against the image to derive a Material Design 3 color palette from the wallpaper’s dominant hues.
Either way, the result is the same: a set of semantic M3 color roles (mSurface, mOnSurface, mPrimary, mOutline, and so on) written to ~/.config/noctalia/colors.json. Noctalia then processes every registered template, including GTK CSS, Hyprland colors, Ghostty terminal, Zed editor, btop, Yazi, Discord, and Qt5/Qt6 palettes, substituting {{colors.field.hex}} placeholders with the resolved hex values. Each template entry can declare a post_hook to reload its target app. The whole system template list lives in two auto-generated cache files, one for predefined schemes and one for matugen runs.
My personal site hooks into this via ~/.config/noctalia/user-templates.toml:
[templates.personal-site]
input_path = "~/Code/Personal/Site/scripts/noctalia-theme.tmpl"
output_path = "~/Code/Personal/Site/public/theme.json"
post_hook = "~/Code/Personal/Site/scripts/sync-wallpaper.sh"
The template file maps the ten M3 roles the site cares about to a flat JSON object of named CSS-variable keys:
{
"bg": "{{colors.surface.default.hex}}",
"fg": "{{colors.on_surface.default.hex}}",
"muted": "{{colors.on_surface_variant.default.hex}}",
"accent": "{{colors.primary.default.hex}}",
"accentFg": "{{colors.on_primary.default.hex}}",
"surface2": "{{colors.surface_container.default.hex}}",
"outline": "{{colors.outline.default.hex}}",
"secondary": "{{colors.secondary.default.hex}}",
"tertiary": "{{colors.tertiary.default.hex}}",
"error": "{{colors.error.default.hex}}"
}
Noctalia renders the template and writes the result to public/theme.json in the site repo. That file is the shared contract between the desktop environment and the website.
the wallpaper pipeline
Colors change only when the scheme changes. Switching between two wallpapers in the same folder, say two Miasma images, leaves the colors identical, so the template system never fires. For that case, Noctalia exposes a wallpaperChange hook in its settings that fires on every wallpaper selection regardless of whether the scheme changed. Both paths converge on the same script: sync-wallpaper.sh.
The script opens ~/.cache/noctalia/wallpapers.json, which maps each connected display to its current wallpaper path, and copies the file for the primary display (eDP-1, falling back to whatever display is present) into public/. It also writes a small public/wallpaper.json manifest so the site can discover the filename without hardcoding the extension. A one-second sleep at the top of the script guards against a race condition where the hook fires before Noctalia has finished writing the updated cache.
So in practice there are two triggers that keep the site in sync with the desktop:
- Scheme change: the template system regenerates
theme.json, then thepost_hookrunssync-wallpaper.shto update the wallpaper. - Same-scheme wallpaper swap: the
wallpaperChangehook runssync-wallpaper.shdirectly, skipping the template step since colors haven’t moved.
site integration
The site reads both files at runtime, not build time. Before the first paint, an inline script in the base layout checks localStorage for a cached palette and applies it as CSS custom properties on :root. If the visitor has “live” mode selected, it fetches /theme.json fresh and updates the properties, meaning the site reflects my exact current desktop colors for anyone who visits while I have live mode on. The color scheme page shown below fetches /theme.json and compares its bg and fg values against the 15 known preset definitions to identify the scheme by name, falling back to “dynamic” if the palette was matugen-generated and doesn’t match any preset fingerprint. The wallpaper is loaded via /wallpaper.json → /wallpaper.jpg the same way.
My current scheme
All 15 presets
Select one to preview.