Skip to content

Template Reference

TemplateEngine renders text and files from a theme token map. It supports inline expressions, block syntax for loops and conditionals, color formatting, filter pipelines, and TOML-driven batch processing.

This document describes the supported template syntax and data model.

TemplateEngine is responsible for:

  • Rendering template strings and files
  • Reading values from theme token maps
  • Applying color and string filters
  • Executing loops and conditionals
  • Processing TOML template configs
  • Computing custom colors and closest_color

It is not responsible for image loading or palette extraction.

Inline expressions use:

{{ ... }}

Examples:

{{ colors.primary.default.hex }}
{{ colors.surface.dark.rgb }}
{{ mode }}
{{ image }}

Control blocks use:

<* ... *>

Supported block tags:

  • for
  • if
  • else
  • endif
  • endfor

Examples:

<* for name, value in colors *>
{{ name }}={{ value.default.hex }}
<* endfor *>
<* if {{ loop.first }} *>
first
<* else *>
not first
<* endif *>

If a block tag appears alone on a line, with only whitespace around it, that whole line is removed from the rendered output. This matters for whitespace-sensitive templates.

Colors are accessed with:

{{ colors.<name>.<mode>.<format> }}

Examples:

{{ colors.primary.default.hex }}
{{ colors.surface.light.hsl }}
{{ colors.terminal_foreground.default.hex_stripped }}

Supported modes:

  • dark
  • light
  • default

default resolves to the configured default mode.

The following values are available directly:

  • mode
  • image
  • closest_color

mode is the current default mode.

image is the source image path when rendering from an image-driven theme.

closest_color is populated during config-driven rendering when compare_to and colors_to_compare are used.

Supported aliases:

  • hover -> surface_container_high
  • on_hover -> on_surface

Supported output formats:

  • hex
  • hex_stripped
  • rgb
  • rgb_csv
  • rgba
  • hsl
  • hsla
  • red
  • green
  • blue
  • alpha
  • hue
  • saturation
  • lightness

Format behavior:

  • hex: #rrggbb
  • hex_stripped: rrggbb
  • rgb: rgb(r, g, b)
  • rgb_csv: r,g,b
  • rgba: rgba(r, g, b, a)
  • hsl: hsl(h, s%, l%)
  • hsla: hsla(h, s%, l%, a)
  • red, green, blue: integer channels
  • alpha: floating-point alpha
  • hue: integer hue
  • saturation, lightness: integer percentages

Filters are chained with pipe syntax:

{{ colors.primary.default.hex | grayscale }}
{{ colors.primary.default.hex | set_alpha 0.5 }}
{{ colors.primary.default.hex | blend: "#ff0000", 0.5 }}

Supported syntaxes:

  • | filter
  • | filter arg
  • | filter: arg

Numeric filter arguments are locale-invariant and use . as the decimal separator, for example set_alpha 0.5.

These operate on colors:

  • grayscale — no argument; converts to gray using luminance weighting
  • invert — no argument; inverts each RGB channel
  • set_alpha — alpha 01, e.g. set_alpha 0.5
  • set_lightness — sets HSL lightness, 0100
  • set_hue — sets absolute hue in degrees, 0360
  • rotate_hue — rotates hue by a relative amount in degrees, e.g. rotate_hue 30
  • set_saturation — sets HSL saturation, 0100
  • set_red — sets the red channel, 0255
  • set_green — sets the green channel, 0255
  • set_blue — sets the blue channel, 0255
  • lighten — adds to lightness in percentage points, e.g. lighten 10
  • darken — subtracts from lightness in percentage points, e.g. darken 10
  • saturate — adds to saturation in percentage points, e.g. saturate 10
  • desaturate — subtracts from saturation in percentage points, e.g. desaturate 10
  • auto_lightness — shifts lightness by the given percentage points away from mid (lightens dark colors, darkens light ones)

These accept another color:

  • blend — interpolates hue toward the given color by an amount 01 (default 1), e.g. blend: "#ff0000", 0.5
  • harmonize — nudges hue toward the given color, capped at 15°
  • to_color — reinterprets a string value as a color so color filters can follow

Examples:

{{ colors.primary.default.hex | blend: "#ff0000", 0.5 }}
{{ colors.primary.default.hex | harmonize: "#00ff88" }}
{{ "#ffaa00" | to_color | darken 0.1 }}

These operate on strings:

  • replace
  • lower_case
  • camel_case
  • pascal_case
  • snake_case
  • kebab_case

Examples:

{{ mode | replace: "dark", "night" }}
{{ "surface container high" | snake_case }}

Supported forms:

<* for item in iterable *>
...
<* endfor *>
<* for key, value in colors *>
...
<* endfor *>

Supported forms:

<* if {{ expr }} *>
...
<* endif *>
<* if not {{ expr }} *>
...
<* else *>
...
<* endif *>

Truthiness rules:

  • false is false
  • numeric 0 is false
  • empty strings are false
  • case-insensitive "false", "0", and "none" are false
  • empty arrays and maps are false
  • everything else is true

Supported iterable expressions:

  • colors
  • palettes.primary
  • palettes.secondary
  • palettes.tertiary
  • palettes.error
  • palettes.neutral
  • palettes.neutral_variant
  • numeric ranges like 0..10 and -5..5
  • arrays and maps from the current scope

colors iterates all available token names and their mode maps:

<* for name, value in colors *>
{{ name }}={{ value.default.hex }}
<* endfor *>

palettes.* iterates derived tone steps for the selected palette family.

Supported families:

  • primary
  • secondary
  • tertiary
  • error
  • neutral
  • neutral_variant

Tone values:

  • 0
  • 5
  • 10
  • 15
  • 20
  • 25
  • 30
  • 35
  • 40
  • 50
  • 60
  • 70
  • 80
  • 90
  • 95
  • 98
  • 99
  • 100

Inside for loops, the loop object provides:

  • loop.index
  • loop.first
  • loop.last

loop.index is zero-based.

When rendering files, TemplateEngine:

  • reads the template file
  • renders the output
  • creates parent directories when needed
  • skips rewriting unchanged files
  • reports render failures through the file render result

Skipping unchanged output avoids unnecessary timestamp updates.

TemplateEngine supports TOML-driven template processing.

Top-level sections:

  • [config]
  • [templates]

[config.custom_colors] supports both forms:

[config.custom_colors]
brand = "#ff0000"
[config.custom_colors.brand]
color = "#ff0000"
blend = true

In Noctalia’s main config, use [theme.templates.custom_colors] with the same value forms.

Generated tokens per mode:

  • {name}_source
  • {name}_value
  • {name}
  • on_{name}
  • {name}_container
  • on_{name}_container

Each template entry supports:

  • input_path
  • output_path
  • output_path_dynamic
  • colors_to_compare
  • compare_to
  • pre_hook
  • post_hook
  • input_path_modes
  • index
  • requires_path

output_path accepts either a single string or an array of strings:

[templates.qt]
input_path = "./qtct.conf"
output_path = [
"$XDG_CONFIG_HOME/qt5ct/colors/noctalia.conf",
"$XDG_CONFIG_HOME/qt6ct/colors/noctalia.conf",
]

output_path_dynamic is optional. It is a shell command string (same templating as hooks: {{ config_dir }}, {{ mode }}, etc.). After rendering, the shell runs it; on exit status 0, each non-empty line of stdout is appended as an output path (after resolveConfigPath). Lines starting with # are ignored. Use this when destinations depend on the machine (for example Emacs config layout) without hard-coding app-specific logic in the engine. Static output_path entries, if any, are kept; dynamic lines are added after them.

input_path_modes selects a different template input for dark and light mode:

[templates.foo]
input_path_modes = { dark = "./dark.css", light = "./light.css" }
output_path = "$XDG_CONFIG_HOME/foo/theme.css"

Relative input_path and output_path values are resolved from the config file directory.

A leading $XDG_CONFIG_HOME, $XDG_DATA_HOME, $XDG_STATE_HOME, or $XDG_CACHE_HOME token in input_path/output_path is expanded per the XDG Base Directory spec: the environment variable when it is set, otherwise the spec default (~/.config, ~/.local/share, ~/.local/state, ~/.cache). Prefer these tokens over a hardcoded ~/.config so output lands in the right place when a config/data/cache home is relocated. A leading ~ still expands to $HOME only.

index controls processing order and defaults to 0.

requires_path is optional. When set, the entry is skipped unless that path already exists on disk. Use it for explicit install checks (for example a Flatpak data directory).

When several entries share the same input_path but write to different client config roots (for example Discord forks or VS Code vs VSCodium), the engine infers each output’s client root and skips outputs whose root is missing. Single-app templates (one entry per input) still create missing directories as before.

compare_to and colors_to_compare can be used to compute closest_color.

Behavior:

  • compare_to is rendered first
  • colors_to_compare provides named comparison candidates
  • the closest candidate becomes available as {{ closest_color }}

Supported hooks:

  • pre_hook
  • post_hook

Behavior:

  • hook commands are rendered through the template engine first
  • pre_hook runs before any output files are written
  • post_hook runs after a template is processed successfully, even when rendered output was unchanged
  • closest_color is available inside hooks

Additional variables available inside hooks and templates:

  • {{ mode }}
  • {{ image }}
  • {{ closest_color }}
  • {{ config_dir }}
  • {{ config_file }}

Every resolved theme exposes flattened terminal tokens. Built-in and community palettes provide curated terminal colors, while wallpaper-derived themes synthesize terminal colors from the generated palette.

  • terminal_foreground
  • terminal_background
  • terminal_cursor
  • terminal_cursor_text
  • terminal_selection_fg
  • terminal_selection_bg
  • terminal_normal_black
  • terminal_normal_red
  • terminal_normal_green
  • terminal_normal_yellow
  • terminal_normal_blue
  • terminal_normal_magenta
  • terminal_normal_cyan
  • terminal_normal_white
  • terminal_bright_black
  • terminal_bright_red
  • terminal_bright_green
  • terminal_bright_yellow
  • terminal_bright_blue
  • terminal_bright_magenta
  • terminal_bright_cyan
  • terminal_bright_white

These are accessed like any other color token:

{{ colors.terminal_background.default.hex }}
{{ colors.terminal_normal_red.default.rgb }}

Template rendering is available through noctalia theme.

Render one file:

Terminal window
noctalia theme <image> -r input.txt:output.txt

Process a TOML config:

Terminal window
noctalia theme <image> -c templates.toml

List available templates:

Terminal window
noctalia theme --list-templates

Pass -c <file> with --list-templates to include entries from a standalone template TOML config.

Process the shipped built-in template catalog:

Terminal window
noctalia theme <image> --builtin-config

The shipped built-in catalog lives at assets/templates/builtin.toml.

Render from a precomputed theme JSON:

Terminal window
noctalia theme --theme-json theme.json -r input.txt:output.txt

Process a TOML config:

Terminal window
noctalia theme <image> -c templates.toml

Set the default template mode:

Terminal window
noctalia theme <image> --default-mode light -r input.txt:output.txt