Templates keep your other apps in sync with the Noctalia palette. When the theme changes, Noctalia renders each enabled template with the new colors and writes the result into that app's config, so your terminal, launcher, editor and browser follow along.
This repo is the community template source. Merged templates are served from https://api.noctalia.dev/templates
and show up in Settings -> Templates, where users enable the ones they want. PRs are welcome.
One top-level directory per app, named after the template id:
fuzzel/
template.toml # the manifest: catalog entry, and what gets rendered where
fuzzel.conf # the input file, written in Noctalia's template syntax
apply.sh # optional hook, run after rendering
README.md # optional, for app-specific setup notes
[catalog.fuzzel] # the id users enable; must match the directory name
name = "Fuzzel" # display name in Settings
category = "launcher"
[templates.fuzzel] # one or more render entries
input_path = "fuzzel.conf" # a file in this directory
output_path = "$XDG_CONFIG_HOME/fuzzel/themes/noctalia" # where the rendered file lands
post_hook = "bash '{{ config_dir }}/apply.sh'" # optional, runs after the writecategory must be one of audio, browser, compositor, editor, launcher, system, terminal, misc. These
are the same categories Noctalia's built-in templates use, so community entries group with them instead of each
inventing a category of one.
An app can declare several entries. That is how discord/ renders three styles across nine Discord clients: one
[templates.<entry>] table per output, all under a single [catalog.discord].
Beyond the basics:
| Key | What it does |
|---|---|
output_path |
A single path, or an array of paths to write the same render to. |
output_path_dynamic |
A command whose stdout is the output path, for apps whose config location has to be discovered at apply time (see obsidian/). |
input_path_modes |
{ dark = "...", light = "..." } when dark and light need different source files. |
input_path_dynamic |
A command whose stdout is the input path. |
pre_hook / post_hook |
Shell run before or after the write. |
post_action |
Named noctalia built-in after the write (not shell). Currently: firefox-theme. |
requires_path |
Skip the entry unless this path exists, so one template can ship entries for apps a user may not have. |
Output paths expand a leading $XDG_CONFIG_HOME, $XDG_DATA_HOME, $XDG_STATE_HOME, $XDG_CACHE_HOME, or ~.
{{ config_dir }} is your template's own directory. On a user's machine your files are cached in a folder of their
own, and {{ config_dir }} points at it, so a hook names a file you ship directly: bash '{{ config_dir }}/apply.sh'.
It is not the repo root, and not the app's config directory. CI rejects a hook that reaches for a file the template
does not ship.
The input file is the app's own config, with Noctalia's template syntax wherever a color goes:
window {
background-color: {{ colors.surface.default.hex }};
color: {{ colors.on_surface.default.hex }};
}The full reference is in the docs: Templates covers the syntax, every
color token, filters (| darken(0.2), | hex_stripped), loops and conditionals.
App theming covers how templates get applied.
A pre_hook or post_hook is shell that runs on the user's machine, as them, every time the theme changes. Rendering a
file is inert; a hook is not. So:
- Keep
apply.shshort and readable. A reviewer has to be able to see everything it does. - Make it idempotent. It runs on every theme change, so it must never append the same line twice. See
walker/apply.shfor the pattern: check first, rewrite in place, append only as a last resort. - Fail loudly when the app's config is missing (non-zero exit, message on stderr) instead of creating something the user never asked for.
- Download nothing, and execute nothing you did not ship in this directory.
- Touch only that app's own config.
You do not need a fork of the shell. Point a user template at your working copy in ~/.config/noctalia/config.toml:
[theme.templates.user.my_app]
input_path = "~/dev/community-templates/my_app/my_app.css"
output_path = "$XDG_CONFIG_HOME/my_app/theme.css"
post_hook = "bash '~/dev/community-templates/my_app/apply.sh'"Confirm it is picked up:
noctalia theme --list-templatesThen change your theme, or re-apply the current one, and check the app updates. Test dark and light, and apply twice to prove the hook is idempotent.
One wrinkle: a user template's {{ config_dir }} is your Noctalia config directory, not the template folder, which is
why the hook above spells out a full path. Once merged, the community copy resolves {{ config_dir }} to its own cached
folder, so the manifest you submit uses {{ config_dir }}/apply.sh.
Open a PR against main. CI validates every manifest on each push: the catalog id matches the directory, the category is
known, every input_path exists, every entry writes somewhere, and hooks only reference files you ship.
- One app per PR.
- Include a screenshot of the app with the theme applied.
- Say which version of the app you tested against.
- Do not commit rendered output or your own personal app config.
Maintainers read hooks line by line before merging.
- Documentation
- Discord
- Bugs in Noctalia itself, rather than in a template, belong in noctalia.