Colours
caelestia picks the scheme. Whatever you choose in the shell’s scheme picker,
dynamic included, recolours the desktop on the spot. A rebuild then brings
the parts only Nix can reach (the boot screens, the greeter and the tty) up
to the same scheme.
flowchart TB
pick["caelestia scheme set<br/>(the shell's picker)"]
state["scheme.json<br/>$XDG_STATE_HOME/caelestia"]
render["caelestia renders<br/>~/.config/caelestia/templates"]
hook["caelestia-theme-hook<br/>(theme.postHook)"]
repo["current.json · dynamic.txt<br/>in the checkout"]
palette["palette.nix"]
pick --> state
pick --> render
pick --> hook
hook --> repo
repo -->|nixos-rebuild| palette
state --> l_apps["caelestia · neovim · tmux · Floorp, Firefox, Zen<br/>gtk · qt · Spotify (caelestia's own appliers)"]
render --> t_apps["kitty · hypr · waybar · wofi · wlogout · swaylock<br/>mako · lazygit · vesktop · MangoHud · starship · ccstatusline · KDE apps"]
palette --> b_apps["stylix: grub · plymouth · tty<br/>SDDM · the seeded scheme.json"]
Choosing a scheme
The picker, or caelestia scheme set -n <name> -f <flavour> -m <mode>. Ours
is hu-tao, flavour red, mode dark:
dotfiles/caelestia/schemes/hu-tao/red/dark.txt, 110 keys in caelestia’s own
format. dynamic generates its colours from the wallpaper. Every scheme
caelestia ships works too, since they all carry the same 110 keys.
The CLI lists schemes only out of its own package (data/schemes), never
from ~/.config. So home/apps/caelestia.nix overrides the CLI to copy
dotfiles/caelestia/schemes/ in beside upstream’s. A new scheme is a new
<name>/<flavour>/<mode>.txt there and a rebuild.
<mode> has to be dark or light: caelestia feeds it to GTK’s
prefer-<mode> and to its qt<mode>.colors template. A variant goes in the
flavour, which is why ours is hu-tao/red/dark and not hu-tao/default/dark-red.
What follows a switch, and when
| When | What | How |
|---|---|---|
| at once | caelestia | its own |
| at once | neovim | utils/colors.lua watches scheme.json and re-applies catppuccin, lualine |
| at once | kitty, waybar, mako, tmux, Hyprland | a template, then the hook’s USR1, USR2, makoctl reload, tmux-apply-colors.sh, hyprctl reload |
| at once | vesktop, starship, ccstatusline | a template; the hook touches the theme links Vencord watches, the rest read per render |
| at once | Floorp, Firefox | CaelestiaFox from AMO, fed by its native app (pkgs/caelestiafox.nix) |
| at once | Zen’s window | caelestia-tab’s Zen mod, reloaded live by the autoconfig lib.wrapZen adds |
| at once | Spotify | caelestia’s enableSpicetify, then the hook’s spicetify refresh; theme.js re-links the CSS |
| preview reload | markdown-preview.nvim | plugins/markdown-preview.lua rewrites its CSS from the same watch, while Neovim runs |
| next time the app starts | GTK and Qt apps | caelestia’s enableGtk, enableQt: gtk.css, ~/.config/qtengine |
| next time the app starts | lazygit, wofi, wlogout, swaylock, MangoHud, KDE apps (kdeglobals) | a template |
| next rebuild | grub, plymouth, the tty, SDDM | palette.nix, off current.json |
| next rebuild | the Limine and SDDM backgrounds, the wallpaper | stylix.image, off current.json’s wallpaper, looked up in third-party-assets |
Templates
A template is a config file whose colours are caelestia fields,
{{ primary.hex }} for a bare rrggbb, {{ primary.red }} and friends for a
channel. caelestia renders every file in ~/.config/caelestia/templates/ into
$XDG_STATE_HOME/caelestia/theme/ of the same name, on every switch, and the
app’s own path is a link to the rendered file.
hutao.caelestiaTemplates (in home/apps/caelestia.nix) is the one list of
them: a name, a source, and the path to link. home/apps/dotfiles.nix feeds
it from recolour, the table of every colour literal in the dotfiles by the
scheme key it means, substituted with palette.template’s fields instead
of hex. The literals stay in the files, so the configs remain valid when
~/.config points at the raw tree, and --replace-fail still turns a moved
literal into a failed build. theme.nix adds kdeglobals, and
ccstatusline.nix the status line’s settings.
A rebuild can change a template without the scheme changing, and caelestia
only renders on a switch, so activation renders them too
(caelestia-render-templates, the same fields) and runs the hook’s reloads.
Hyprland is the odd one: ~/.config/hypr is linked whole, so
look_and_feel.lua dofile()s a rendered hypr-colours.lua instead of being
a template itself. It falls back to its own literals, which the build still
substitutes with the scheme of that build.
Off in cli.json, because something here does the job: enableTerm (kitty
reloads mocha.conf, where caelestia’s escape sequences would paint its own
terminal mapping over it), enableHypr and enableDiscord (both templates
here).
Recording the pick
caelestia-theme-hook runs after every switch. It writes the scheme’s name,
flavour, mode and variant to dotfiles/caelestia/current.json, and on
dynamic the generated colours to dotfiles/caelestia/dynamic.txt, in
caelestia’s format. current.json also gets the wallpaper’s file name, read
off caelestia’s wallpaper/path.txt: caelestia wallpaper runs the same
hook. modules/desktop/stylix.nix looks that name up in third-party-assets’
Wallpapers/ for stylix.image, which is Limine’s background through
stylix’s limine target and SDDM’s through pkgs/sddm-hu-tao.nix. Both are tracked, so the next rebuild builds the scheme
you are looking at and the pick is in git once you commit it. A flake reads a
tracked file’s working-tree contents, so the rebuild does not wait for the
commit.
The hook finds the checkout through $XDG_STATE_HOME/hutao/flake-path, which
activation writes from hutao.flakePath (default
~/Projects/nixos-dotfiles). A clone elsewhere sets that option, or edits the
file for a quick fix. With no checkout there it records nothing and says so in
a notification. The path cannot be worked out: a flake is evaluated from its
copy in the store.
GTK and Qt
caelestia owns both. Stylix’s gtk and qt targets are off, so there is one writer per file:
- GTK: caelestia writes
gtk-3.0/gtk.cssandgtk-4.0/gtk.cssand setsadw-gtk3-darkin dconf. Home Manager keeps the theme package, the font (fromstylix.fonts) and the icons, and sets no GTK 4 theme, because that would make it writegtk-4.0/gtk.csstoo. - Qt:
QT_QPA_PLATFORMTHEME=qtengine, with the Darkly style. caelestia writes~/.config/qtengine/config.jsonand the colours beside it.
Worth knowing
- qtengine is Qt 6 only. Nothing here is Qt 5; see
modules/desktop/fcitx5.nixfor the one thing that was. - The Qt font is set in caelestia’s own template. The CLI override in
home/apps/caelestia.nixpatchesqtengine.jsontostylix.fonts.monospace(JetBrainsMono Nerd Font) at weight 300, Light, and the applications size. caelestia rewritesconfig.jsonon every switch, so editing that file does nothing lasting. - Every switch dirties the checkout when the scheme differs from the
committed one. Commit
current.jsonanddynamic.txtwhen you want the pick kept. - A wallpaper that is not in third-party-assets fails the rebuild. A
flake cannot read
~/Pictures/Wallpapers, so a picked wallpaper has to be added to that repo’sassets/third-party/Wallpapers/under the same name, thennix flake update third-party-assets. The error names the file. - ANSI green, blue, cyan and magenta are one colour in hu-tao. The scheme
sets
term2,term4,term6,term10,term12andterm14to the sameff9b8a. Retune those keys to tell them apart in the terminal. - CaelestiaFox is two halves, and only one is Nix’s. Install the
extension by hand from
AMO.
Each browser’s wrapper (
home/apps/packages.nix) links the native app’s manifest into~/.mozilla/native-messaging-hostswhen the browser starts, so restart it after the first rebuild that brings it. All three read that directory: Floorp though its profile is in~/.config/floorp, and Zen (from thezen-browserflake input, not nixpkgs) though its profile is in~/.zen— confirmed for Zen 1.22.3b on 2026-09-27. - Zen’s window isn’t CaelestiaFox’s. Zen ignores the extension theme API,
so CaelestiaFox recolours Firefox and Floorp but not Zen. caelestia-tab’s
helper writes a Zen mod instead (see its handbook), and the
zenpackage inhome/apps/packages.nixgoes throughcaelestia-tab.lib.wrapZen, which adds the autoconfig script that reloads the mod live. palette.nixholds no hex of its own. A missing colour is a key to add to the scheme, not a constant to inline.