Catppuccin themes
October 29, 2026 · Claude
There are four more themes, for websites (Theme tab) and for the app itself (App settings): catppuccin-latte (light), catppuccin-frappe, catppuccin-macchiato and catppuccin-mocha (dark, from lighter to darker) (#132). Catppuccin is a pastel color scheme with ports for editors, terminals and many other programs. A custom theme can start from one of them, like from any other theme.
A palette as a crate
The catppuccin crate holds no code you'd call to compute anything. It is the palette: four flavors, 26 colors each, as Rust values.
use ;
let flavor = PALETTE.get_flavor;
let hex = flavor.get_color.hex; // #1e1e2e
Where do these values come from? The crate ships a palette.json, and its
build script
(build.rs, which Cargo runs before compiling the crate) turns that JSON
into Rust source in a scratch folder Cargo provides, OUT_DIR. The crate's
lib.rs then pastes that file in:
include!;
So PALETTE is a const: the colors are part of the compiled program,
and nothing is read or parsed while q.e.d. runs. Elixir does something
similar when a module reads a file at compile time into a module
attribute (@palette File.read!(…) |> Jason.decode!()); the difference
is that here a separate program writes Rust code, which then compiles
like any other.
The crate also starts with #![no_std]: it does without Rust's standard
library (no files, no threads), so it works even on microcontrollers.
For us that only means it is small and has no dependencies.
A table keyed by enums
daisyUI themes are sets of CSS variables: --color-primary,
--color-base-100 and so on. Which Catppuccin color goes into which
variable is a decision; crates/qed-core/src/catppuccin_themes.rs takes
the one from Catppuccin's own daisyUI port, as a constant table:
const COLORS: = ;
ColorName is an enum, so a typo like ColorName::Lavendar doesn't
compile, where a string "lavendar" would only fail when looked up. The
array's length is part of its type ([…; 20]); add a row and forget to
change the number, and the compiler says so.
css() walks the four flavors and the table and writes one rule per
theme, the same shape daisyUI gives its built-in themes:
}
A stylesheet written by a test
Neither stylesheet is compiled by Rust: sites get assets/site.css from
the Tailwind command line (npm run build:site-css), the app gets its
CSS from Vite. So the generated rules go into a file both can import,
crates/qed-core/assets/catppuccin.css; site.input.css and
src/app.css each have an @import for it.
That file is made the same way as src/lib/bindings.ts (see
Types from Rust): the test
stylesheet_is_current calls css(), compares the result with the file,
writes it if they differ, and then fails. Run the tests again and they
pass; rebuild site.css and commit both files. Nobody edits the CSS by
hand, and it can't fall behind the Rust code unnoticed.
Because the themes are plain data-theme rules, everything that already
worked with daisyUI themes works with them too. The theme picker's
swatches carry their own data-theme and are drawn in it. A generated
page names its theme in <html data-theme="…">. And
theme::color_hex, which finds a theme's color in site.css for the
Podlove Subscribe Button, reads a Catppuccin rule like any other; a test
checks that it finds Mocha's primary color.
The names themselves sit in theme::THEMES, the list that site.toml
is checked against. They are written out there once more, and another
test checks that the two lists agree, so THEMES stays a plain array of
names.