Look targets
One look themes every app together, including apps scoot has no
built-in fragment for. A target is a template plus a line saying where the
rendered file goes and how to tell the app. Make one per app you want
themed; scoot look apply renders each into the active look beside the
built-in fragments and runs your reload command.
Make a target
Section titled “Make a target”Targets live in ~/. ($XDG_CONFIG_HOME
when set, else ~/.), one directory per target:
mkdir -p ~/.config/scoot/look-targets/kitty-colorsWrite target. (what the target is) beside the template (what the
app reads):
name = "kitty-colors"output = "kitty/colors.conf"template = "colors.tmpl"
[reload]argv = ["kitty", "@", "set-colors", "--all"]timeout_secs = 10background {{ palette.background }}foreground {{ palette.foreground }}color4 {{ terminal.color4 }}Apply the look. The template renders into the active look under
targets/kitty/colors., next to scoot. and bar., and
the reload runs:
scoot look apply moonriseThe swap is atomic: every target renders before anything is recorded,
under the same swap, cache key and rollback as the built-in fragments.
look validate and look build cover targets the same way. A sync
with targets configured (hand-written or Nix-declared) renders fresh
instead of pointing at the Nix catalog render (built without the
templates) and says so. An in-tree example (kitty, all sixteen terminal
colors) is in docs/examples/look-targets/kitty-colors/, beside the
six-app starter pack below.
target.toml
Section titled “target.toml”| Option | Type / default | Reload |
|---|---|---|
name |
string, required. Matches the directory name ([a-z0-9-]{1,32}). |
Validated on every apply; a mismatch refuses. |
output |
string, required. Relative to the config home (kitty/colors.). ASCII only (a Unicode name refuses); no .., no absolute path, no hidden segments; must stay under the config home through symlinks. |
Rendered to targets/<output> in the active look on every apply. Two targets naming the same output refuse, as do two where one output is a directory on the way to the other (x and x/y); both compared case-insensitively. |
template |
string, required. The template file inside the target directory. | Read on every apply (symlinks refuse); an unreadable template refuses the swap. |
reload. |
array of 1 to 16 strings, optional. The program plus its arguments: a bare name on PATH or an absolute path. |
Run directly after a successful swap, never through a shell (no splitting, no globs, no $VAR). |
reload. |
whole number, default 30, 1 to 300. |
The hung child and the processes it started are killed after the wait (a process that starts its own session is not reached); the swap stands either way. |
Unknown keys refuse: target. takes only the keys above. A reload
needs no entry to render; without [reload] the file still renders and
nothing runs.
Wire the app’s own config at the rendered file once, with an include or a link. Keys you set after the include win per key. For kitty:
include ~/.local/state/scoot/look/targets/kitty/colors.conf(~/. is the default state dir; the look link
inside it points at the active render.) Leave a file that another tool
owns alone: when the app’s own module or another tool owns the path,
keep that ownership and let the look stay out of it.
Template tokens
Section titled “Template tokens”A token is {{ dotted., exactly the look’s own spelling
(palette., terminal., fonts.). Tokens a look
does not set (a missing [terminal] table, an unset [effects] key)
refuse instead of rendering empty. There are no conditionals, loops, or
includes: it is pure substitution, and nothing in a template ever
executes. A template needing a literal {{ has no escape in v1;
a stray }} with no opening {{ stays literal (JSON nests }}).
| Token | Renders |
|---|---|
palette., palette., palette., palette., palette., palette., palette., palette., palette., palette., palette., palette., palette. |
the look’s #rrggbb roles |
terminal. … terminal. |
the look’s sixteen hues, only when it has a [terminal] table |
fonts., fonts., fonts. |
family names |
fonts., fonts., fonts., fonts. |
points (10, never 10.) |
opacity., opacity., opacity. |
0 to 1 |
shape., shape., shape. |
whole numbers |
effects., effects., effects., effects., effects. |
only when the look sets them |
cursor., cursor., cursor., cursor. |
body only when the look sets it |
polarity |
dark or light |
wallpaper., wallpaper., wallpaper., wallpaper. |
only when the look sets them |
icons. |
the icon theme name |
night. |
only when the look sets it |
Color filters re-spell a #rrggbb token: {{ palette.
(#FF9A30), {{ palette. (same),
{{ palette. (FF9A30),
{{ palette. (255,154,48),
{{ palette. (rgba(255,154,48,0.). A filter on
anything but a color refuses, as does an unknown filter or token.
The reload runs after the swap stands: a zero exit counts as reloaded, anything else (missing binary, timeout, non-zero exit) is a note naming what happened. A reload that writes past 64 KiB on stdout or stderr is killed instead, and the note says its output was truncated. A failing reload never undoes the swap. A failing render does the opposite (unknown token, unreadable template, duplicate or escaping output): the swap refuses before anything is recorded.
Overrides from a look
Section titled “Overrides from a look”A look can tune the colors, fonts and font sizes one of your targets
renders, per target. It adds a [targets. table to its
look., where <name> is the name of a target you created or added:
[targets.kitty-colors]"palette.background" = "#101820""terminal.color1" = "#E06C75""fonts.mono" = "Iosevka Term""fonts.terminal-size" = 13| Option | Type / default | Reload |
|---|---|---|
"palette. |
#rrggbb. Any of the thirteen palette roles. |
next swap |
"terminal. to "terminal. |
#rrggbb. |
next swap |
"fonts., "fonts., "fonts. |
A font family: the same text the look’s own [fonts] table takes. |
next swap |
"fonts., "fonts., "fonts., "fonts. |
A number from 4 to 192 (points), not a string. | next swap |
A font family is 1 to 64 ASCII letters, digits, spaces and -_.,
starting with a letter or digit. Nothing else is accepted: no quotes,
backslashes, #, ;, =, braces, slashes, newlines or other control,
format or non-ASCII characters, so a family can end neither a config line
nor a quoted value and cannot name a path. The font must still be
installed on your system for the app to use it; scoot does not check an
override’s family the way it checks the look’s own, and an app falls
back when it cannot find one.
Rules:
- The override replaces that token for that target only. Your other
targets and the look’s own fragments (
scoot.,toml bar., …) keep the look’s colors and fonts.toml - A
terminal.override adds the token when the look has nocolorN [terminal]table, so a template that names it renders instead of refusing. - It does nothing when you have no target of that name. A look is shared and your targets are yours, so an unknown name is ignored, not an error.
- A child look inherits its base’s overrides and replaces them key by key.
- Only the keys above are accepted. Paths (
fonts.,files fonts.),nix opacity.,* shape.,* cursor.,* output,template,reloadand every other key refuse when the look is parsed, invalidate,build,installand every swap alike. A value must fit its key: a color is exactly#rrggbb, a family is the text above, a size is a number in range. - A size renders like the look’s own sizes:
13, never13.;0 12.stays5 12..5
Installed looks (someone else’s) can never supply templates, output
paths, or reload commands. The override table carries colors, font
families and sizes and nothing else, and an [apps. table for a
target refuses through the allowlist, so only these user-owned
directories are ever read.
Symptom:
look validateorlook installrefuses a[targets.key with<name>] not an overridable token.A look’s target table takes only
palette.,* terminal.tocolor0 terminal.,color15 fonts.,ui fonts.,sans fonts.and the four font sizes. Remove any other key; a look cannot set a path or a command for your target.mono
Symptom:
look validateorlook installrefusestargets.with<name>[fonts. mono] must be an identifier: ASCII letters, digits, space and -_..+, 1 to 64 bytes, starting with a letter or digit The family has a character outside that set (a quote, a newline, a non-ASCII letter). A family that needs one is not expressible in an override; the look can carry the font file in its own
[fonts.instead.files]
Symptom:
look validateorlook installrefusestargets.with<name>[fonts. terminal-size] must be a number from 4 to 192 points.The size is quoted (
"13"), out of range, or not a number. Write it bare:"fonts..terminal-size" = 13
Symptom:
look validateorlook installrefuses a[targets.value with<name>] invalid type: booleantrue, expected a quoted string (colors, font families) or a number (font sizes); booleans, arrays, tables and dates are not accepted.The value is a boolean, array, table or date (the message says which, after
invalid type:) and points at the line. Colors and families are quoted strings; sizes are bare numbers.
Symptom: a look’s override has no effect.
You have no target with that name (
look-targets/<name>/), or the look’s table is spelled for another one. The override is ignored by design.
Symptom:
validatenames an unknown token.The template spells a token no look carries (
palette.,backround terminal.against a look with nocolor4 [terminal]table). Fix the spelling or pick a look that sets it; a typo never swaps.
Symptom: the reload did not run and the reply says the app is not installed.
The reload binary is missing from
PATH(or the absolute path is wrong). Install the app or fix the argv; the rendered file is already in the active look, so a manual reload (or the next swap) picks it up.
Symptom:
validatesays two targets write the same output, or a file where another needs a directory.Both
target.files name the sametoml output(the compare is case-insensitive, soKitty/xandkitty/xcollide), or one namesxand the otherx/y. Rename one: the second write would silently win, and no wiring could name both.
Symptom:
validatesays an output escapes the config home or a symlink.The output names
.., an absolute path, or a directory that is a symlink out of the config home. Point it inside the home; through a symlinked directory refuses even when the spelling looks relative.
Starter pack
Section titled “Starter pack”Seven maintained targets ship in-tree in
docs/examples/look-targets/<name>/: the kitty example above plus the
six below, each with its target. and template. Add the ones you
want with scoot look target add (or copy a directory
into ~/. by hand), then add the
wiring line to the app’s own config, once, by hand. Keys you set in
your own config after the include win per key. All six need a look
with a [terminal] table; against a look without one validate names
the missing token. None of the six declares a [reload]: the last
column says how each app picks up the re-render.
| Target | What it themes | Picks up a swap |
|---|---|---|
alacritty |
palette, all 16 terminal colors, mono font | live: Alacritty reloads on file change (live_config_reload, on by default), imports included |
wezterm |
palette, all 16 terminal colors, mono font and size | live: WezTerm reloads on file change (automatically_reload_config, on by default) |
ghostty |
palette, all 16 terminal colors, mono font | by keybind: the reload_config action (ctrl+shift+, by default); Ghostty has no external reload command |
tmux |
status line, window list, pane borders, messages | by hand: run tmux source-file <the rendered file> (or restart the server); the path is machine-specific |
zathura |
page, status bar, input bar, completion, index, search highlight, font | restart: R reloads the document, not the config |
rofi |
window, input bar, list, selected and urgent rows, messages, font | next launch: rofi reads its theme on every start |
Wire each one at its rendered path (the look state dir’s look link
plus targets/<output>):
import = ["~/.local/state/scoot/look/targets/alacritty/colors.toml"]local scoot_colors = dofile(os.getenv("HOME") .. "/.local/state/scoot/look/targets/wezterm/colors.lua")scoot_colors.apply_to_config(config)config-file = "~/.local/state/scoot/look/targets/ghostty/theme"source-file ~/.local/state/scoot/look/targets/tmux/theme.confln -s ~/.local/state/scoot/look/targets/zathura/theme ~/.config/zathura/scoot-themeinclude scoot-theme@import "~/.local/state/scoot/look/targets/rofi/colors.rasi"zathura’s include resolves relative to the file doing the including,
so the link above keeps the wiring portable; an absolute include of
the state path works too. tmux reads ~/. when its server
starts ($XDG_CONFIG_HOME/tmux/tmux. on tmux 3.2+); :source-file
applies a re-render to the running server.
Nix users declare the same targets with
programs.: each entry lands
as scoot/look-targets/<name>/{target. in the config
home. A sync with any target configured (declared or hand-written)
renders fresh instead of pointing at the catalog render, which was
built without the templates.
Add a pack
Section titled “Add a pack”A pack is a directory of targets someone else wrote: a directory holding
target directories (the starter pack above is one), or a single target
directory. target add copies it into your own look-targets/. A pack
is not part of a look and not part of look install: installed looks
still cannot carry templates, outputs or reload commands.
scoot look target add ./pack # print the review, add nothingscoot look target add ./pack --trust # print the review, then addTargets render files into your desktop’s config and may run a command on
every swap, so the add is gated like look install --trust. Without
--trust it prints, for each target, what it writes, its RELOAD command
on a line of its own (or reload: none), and the whole template, then
exits 1 having written nothing. With --trust the same review prints
first and the targets land. Nothing runs at add time: the files are read,
checked and copied, and the bytes you read are the bytes written.
| Check | What refuses |
|---|---|
| Source shape | a symlink anywhere (the pack, a target directory, target. or a template), a hard link, a look directory (it has look.: use look install), a file beside the targets other than README*, LICENSE* or COPYING*, a file in a target beyond target. and its template, a template naming a subdirectory |
| Limits | more than 32 targets, more than 4 MiB in total, a template over 64 KiB or not UTF-8 |
| Each target | the same checks look validate runs: name matches its directory, output stays under the config home, reload shape, template syntax, and every token spelled by at least one look in your catalog (a token only some looks set refuses later, per look, as usual) |
| Collisions | a target name that already exists in look-targets/ (a Nix-declared one included: it is never written through), an output that an existing target or another target in the pack already writes, or that is a file where another needs a directory (x against x/y), compared case-insensitively |
On failure the add is all or nothing: every target is checked before the
first one lands, and if one cannot land the ones already moved are
removed again (a crash mid-add is not covered). The review shows letters,
digits and ordinary punctuation as they are and escapes everything else
(controls, zero-width and bidi characters, odd spaces, combining marks) as
\u{1b}, so a template or file name cannot rewrite the terminal or
imitate the RELOAD line. Each reload argument is quoted when it holds
anything beyond plain path characters, so echo "a b" and echo a b
read differently. --json prints the review as a document after the add
succeeds (added is true), or with added set to false when you
omitted --trust; a failed add prints only the error. The template and
reload arguments are raw there.
Symptom:
target addprints the review and exits 1 with “nothing added”.That is the gate working: you ran it without
--trust. Read the templates and theRELOADlines; if you trust the pack, rerun with--trust.
Symptom:
target addsays a target named<name>already exists.
look-targets/<name>is already there (yours, or declared in Nix). Remove or rename it, or rename the target in the pack; nothing was added.
Symptom:
target addsays a target writes an output an existing target already writes, or a file where another needs a directory.Two targets may not write the same file (the second write would silently win), and one may not write
xwhile another writesx/y(the render could not makexa directory). Change oneoutput, or remove the existing target.
Symptom:
target addrefuses a symlink, a hard link or an unexpected file.A pack is foreign input and is read as plain files only. Re-export it with
cp -Rortar(macOStaradds.files; set_* COPYFILE_DISABLE=1), without links.
Not here yet
Section titled “Not here yet”Browsers (no live reload for Firefox without an extension, and the Chrome policy needs a root or managed path), look overrides for tokens other than colors, and keeping look sources and template sources in separate imports. Each is a later slice; the ticket lists them.
