Skip to content

Theming templates

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

These operate on colors:

  • grayscale
  • invert
  • set_alpha
  • set_lightness
  • set_hue
  • set_saturation
  • set_red
  • set_green
  • set_blue
  • lighten
  • darken
  • saturate
  • desaturate
  • auto_lightness

These accept another color:

  • blend
  • harmonize
  • to_color

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 and downstream reloads.

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

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

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

[templates.qt]
input_path = "./qtct.conf"
output_path = [
"~/.config/qt5ct/colors/noctalia.conf",
"~/.config/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 = "~/.config/foo/theme.css"

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

index controls processing order and defaults to 0.

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 rendering, but only when at least one output file changed
  • 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 shipped built-in templates:

Terminal window
noctalia theme --list-builtins

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