v0.1.1 7141aff

themes.md RawHistoryFolder

zut themes

zut themes are JSON files that override any subset of the built-in light/dark TUI theme. Nothing is required: a theme can change one color, only the spinner, only syntax highlighting, or all of them. Missing values inherit from zut's built-in default for the detected terminal background.

Where themes live

User themes are discovered from:

$ZUT_HOME/themes/*.json

Open /settings and choose color theme to switch. Changes are saved in $ZUT_HOME/config.json and apply immediately. If the selected file is deleted later, zut resets the setting to the built-in auto (default) theme.

The built-in choices are:

  • auto — detect the terminal background and use zut's built-in dark or light palette. This remains the default.
  • inherited (from terminal) — use the terminal's reported foreground, background, and ANSI palette. When COLORTERM=truecolor/24bit or TERM ends in -direct, zut emits truecolor; otherwise it quantizes derived colors to the xterm-256 palette as a best effort. Terminals that do not answer OSC color queries fall back to zut's built-in palette while still using the advertised color depth.
  • dark / light — use zut's fixed built-in palettes.

The inherited terminal snapshot is captured at startup and reused when the setting is changed live; restart zut to pick up terminal palette changes.

All theme color roles use the same terminal-color model. JSON themes may use xterm-256 indexes, ANSI SGR colors, or RGB values for any role. On terminals that advertise truecolor, zut emits RGB values directly and expands indexed values through the xterm palette; otherwise RGB values fall back to the nearest xterm-256 color while indexed values remain indexed. ANSI values keep their SGR semantics, with standard foreground/background ranges normalized to the role where necessary.

Theme proposals

  • shades of gray (truecolor) — a neutral grayscale palette for the UI and syntax highlighting, with an xterm-256 fallback when direct color is unavailable.

Theme files bundled with extensions are discovered in-place from loaded extension directories:

$ZUT_HOME/extensions/<extension>/theme.json
$ZUT_HOME/extensions/<extension>/themes/theme.json
<project>/.zut/extensions/<extension>/theme.json
<project>/.zut/extensions/<extension>/themes/theme.json

zut does not copy extension themes into $ZUT_HOME/themes; extension owned themes stay in the extension directory. The settings picker shows source info such as from extension my-theme-extension.

Minimal themes

All of these are valid.

Metadata only:

{
  "name": "my-theme",
  "description": "Metadata only; all visuals inherit zut defaults."
}

One shared color for both light and dark terminals:

{
  "name": "pink-accent",
  "colors": {
    "accent": 204
  }
}

One color per mode:

{
  "name": "split-accent",
  "colors": {
    "dark": { "accent": 204 },
    "light": { "accent": 161 }
  }
}

Spinner-only:

{
  "name": "custom-spinner",
  "description": "Only changes the busy spinner.",
  "spinner_frames": ["◢", "◣", "◤", "◥"],
  "spinner_messages": ["working"],
  "spinner_interval_ms": 120
}

Dark-only themes still work on light terminals. If colors.light is missing, zut applies colors.dark overrides on top of the built-in light default. The inverse also works.

{
  "name": "custom-spinner",
  "description": "An alternative spinner for zut that only displays a single spinner text.",
  "colors": {
    "dark": {
      "spinner_frames": ["◢", "◣", "◤", "◥"],
      "spinner_messages": ["working"],
      "spinner_interval_ms": 120
    }
  }
}

Full shape

All fields are optional.

{
  "name": "my-theme",
  "description": "Shown in /settings → color theme.",
  "color_descriptions": {
    "accent": "Optional documentation for humans. zut ignores this object."
  },
  "colors": {
    "dark": {
      "fg": 253,
      "muted": 244,
      "accent": 111,
      "background": "#0b1020",
      "user": 180,
      "user_bubble_bg": "#42454b",
      "user_bubble_fg": 248,
      "assistant": 117,
      "tool": 114,
      "tool_out": 245,
      "error": 203,
      "warning": 214,
      "spinner": 183,
      "selection_bg": 24,
      "selection_fg": 231,
      "spinner_frames": ["⠋", "⠙", "⠚", "⠞", "⠖", "⠦", "⠴", "⠲", "⠳", "⠓"],
      "spinner_messages": ["thinking", "working"],
      "spinner_interval_ms": 80,
      "syntax_base_style": "monokai",
      "syntax": {
        "keyword": "#81a1c1 bold",
        "keyword_constant": "#81a1c1",
        "keyword_declaration": "#81a1c1",
        "keyword_namespace": "#81a1c1",
        "keyword_reserved": "#81a1c1 bold",
        "keyword_type": "#88c0d0",
        "name_builtin": "#88c0d0",
        "name_function": "#8fbcbb",
        "name_class": "#a3be8c bold",
        "name_decorator": "#b48ead",
        "literal_string": "#a3be8c",
        "literal_string_escape": "#bf616a",
        "literal_number": "#d08770",
        "comment": "#616e88 italic",
        "comment_preproc": "#b48ead",
        "operator": "#eceff4",
        "punctuation": "#d8dee9",
        "text": "#e5e9f0"
      }
    },
    "light": {
      "fg": 236,
      "muted": 244,
      "accent": 33
    }
  }
}

You may also put overrides directly at the top level or directly under colors when they should apply to both modes:

{
  "name": "tiny",
  "accent": 204,
  "colors": {
    "spinner_messages": ["shipping"]
  }
}

Color fields

Every color field accepts the following JSON forms:

254
"#42454b"
{ "mode": "256", "index": 254 }
{ "mode": "ansi", "index": 100 }
{ "mode": "rgb", "r": 66, "g": 69, "b": 75 }

Numeric values are xterm-256 indexes. ANSI values use SGR codes, with standard foreground/background ranges normalized to the role where necessary. RGB values are emitted directly when truecolor is advertised and quantized to xterm-256 otherwise. The built-in inherited theme additionally resolves semantic accents through the terminal's ANSI palette and derives muted and selected surfaces from the terminal foreground/background.

  • fg — default foreground text.
  • muted — secondary text, dividers, gutters, inactive hints.
  • accent — prompt bar, bullets, links, headings, active markers.
  • background — optional full-row TUI background. If missing, zut uses the terminal's existing background. Experimental: terminal background colors can vary by emulator and scrollback behavior; for the most reliable result, change your terminal background color in your terminal settings instead.
  • user — user role label color; mostly compatibility.
  • user_bubble_bg — background behind user message rows.
  • user_bubble_fg — foreground inside user message rows.
  • assistant — assistant/zut accent and spinner text.
  • tool — tool names, success marks, diff additions.
  • tool_out — plain tool-output text.
  • error — errors, refused calls, diff deletions.
  • warning — warnings and high context-usage state.
  • spinner — reserved spinner color slot.
  • thinkingMax or thinking_max (names retained for compatibility) - status color used when the opt-in max reasoning level is active. Missing values inherit the built-in theme color.
  • selection_bg — highlighted row background.
  • selection_fg — highlighted row foreground.

The same forms apply to background, user_bubble_bg, and every other semantic color field listed above.

Spinner fields

Spinner settings can appear at top level, under colors, or under colors.dark / colors.light.

  • spinner_frames — list of frame strings. Single-cell glyphs keep status-bar alignment clean.
  • spinner_messages — list of messages; zut picks one per turn.
  • spinner_interval_ms — frame interval in milliseconds. Missing or invalid falls back to 80ms.

Syntax fields

Syntax highlighting uses Chroma style entries. Values may include attributes after the color, such as bold, italic, or underline.

Supported syntax override keys:

keyword, keyword_constant, keyword_declaration, keyword_namespace,
keyword_reserved, keyword_type, name_builtin, name_function,
name_class, name_decorator, literal_string, literal_string_escape,
literal_number, comment, comment_preproc, operator, punctuation, text

Example:

{
  "colors": {
    "dark": {
      "syntax_base_style": "monokai",
      "syntax": {
        "keyword": "#f05b8d",
        "name_function": "#b675f1",
        "literal_string": "#58c760",
        "comment": "#a1a1a1 italic"
      }
    }
  }
}

Theme-only extensions

An extension can exist only to ship a theme. No slash command, subprocess, or executable is required when the extension contains a valid theme file.

$ZUT_HOME/extensions/my-theme-extension/
├── extension.json
└── theme.json

extension.json:

{
  "name": "my-theme-extension",
  "version": "1.0.0",
  "description": "Ships a zut color theme",
  "enabled": true
}

No exec is needed when theme.json or themes/theme.json exists. If exec is present, zut treats it as a normal extension too.

Validate

zut theme files are plain JSON, not JSONC. Validate before installing:

python3 -m json.tool theme.json >/dev/null
source
1# zut themes
2
3zut themes are JSON files that override any subset of the built-in
4light/dark TUI theme. Nothing is required: a theme can change one
5color, only the spinner, only syntax highlighting, or all of them.
6Missing values inherit from zut's built-in default for the detected
7terminal background.
8
9## Where themes live
10
11User themes are discovered from:
12
13```text
14$ZUT_HOME/themes/*.json
15```
16
17Open `/settings` and choose **color theme** to switch. Changes are
18saved in `$ZUT_HOME/config.json` and apply immediately. If the selected
19file is deleted later, zut resets the setting to the built-in auto
20(default) theme.
21
22The built-in choices are:
23
24- **auto** — detect the terminal background and use zut's built-in dark or
25  light palette. This remains the default.
26- **inherited (from terminal)** — use the terminal's reported foreground,
27  background, and ANSI palette. When `COLORTERM=truecolor`/`24bit` or
28  `TERM` ends in `-direct`, zut emits truecolor; otherwise it quantizes
29  derived colors to the xterm-256 palette as a best effort. Terminals that
30  do not answer OSC color queries fall back to zut's built-in palette while
31  still using the advertised color depth.
32- **dark** / **light** — use zut's fixed built-in palettes.
33
34The inherited terminal snapshot is captured at startup and reused when the
35setting is changed live; restart zut to pick up terminal palette changes.
36
37All theme color roles use the same terminal-color model. JSON themes may use
38xterm-256 indexes, ANSI SGR colors, or RGB values for any role. On terminals
39that advertise truecolor, zut emits RGB values directly and expands indexed
40values through the xterm palette; otherwise RGB values fall back to the
41nearest xterm-256 color while indexed values remain indexed. ANSI values keep
42their SGR semantics, with standard foreground/background ranges normalized to
43the role where necessary.
44
45## Theme proposals
46
47- **shades of gray (truecolor)** — a neutral grayscale palette for the UI and
48  syntax highlighting, with an xterm-256 fallback when direct color is
49  unavailable.
50
51Theme files bundled with extensions are discovered in-place from loaded
52extension directories:
53
54```text
55$ZUT_HOME/extensions/<extension>/theme.json
56$ZUT_HOME/extensions/<extension>/themes/theme.json
57<project>/.zut/extensions/<extension>/theme.json
58<project>/.zut/extensions/<extension>/themes/theme.json
59```
60
61zut does **not** copy extension themes into `$ZUT_HOME/themes`; extension
62owned themes stay in the extension directory. The settings picker shows
63source info such as `from extension my-theme-extension`.
64
65## Minimal themes
66
67All of these are valid.
68
69Metadata only:
70
71```json
72{
73  "name": "my-theme",
74  "description": "Metadata only; all visuals inherit zut defaults."
75}
76```
77
78One shared color for both light and dark terminals:
79
80```json
81{
82  "name": "pink-accent",
83  "colors": {
84    "accent": 204
85  }
86}
87```
88
89One color per mode:
90
91```json
92{
93  "name": "split-accent",
94  "colors": {
95    "dark": { "accent": 204 },
96    "light": { "accent": 161 }
97  }
98}
99```
100
101Spinner-only:
102
103```json
104{
105  "name": "custom-spinner",
106  "description": "Only changes the busy spinner.",
107  "spinner_frames": ["◢", "◣", "◤", "◥"],
108  "spinner_messages": ["working"],
109  "spinner_interval_ms": 120
110}
111```
112
113Dark-only themes still work on light terminals. If `colors.light` is
114missing, zut applies `colors.dark` overrides on top of the built-in
115light default. The inverse also works.
116
117```json
118{
119  "name": "custom-spinner",
120  "description": "An alternative spinner for zut that only displays a single spinner text.",
121  "colors": {
122    "dark": {
123      "spinner_frames": ["◢", "◣", "◤", "◥"],
124      "spinner_messages": ["working"],
125      "spinner_interval_ms": 120
126    }
127  }
128}
129```
130
131## Full shape
132
133All fields are optional.
134
135```json
136{
137  "name": "my-theme",
138  "description": "Shown in /settings → color theme.",
139  "color_descriptions": {
140    "accent": "Optional documentation for humans. zut ignores this object."
141  },
142  "colors": {
143    "dark": {
144      "fg": 253,
145      "muted": 244,
146      "accent": 111,
147      "background": "#0b1020",
148      "user": 180,
149      "user_bubble_bg": "#42454b",
150      "user_bubble_fg": 248,
151      "assistant": 117,
152      "tool": 114,
153      "tool_out": 245,
154      "error": 203,
155      "warning": 214,
156      "spinner": 183,
157      "selection_bg": 24,
158      "selection_fg": 231,
159      "spinner_frames": ["⠋", "⠙", "⠚", "⠞", "⠖", "⠦", "⠴", "⠲", "⠳", "⠓"],
160      "spinner_messages": ["thinking", "working"],
161      "spinner_interval_ms": 80,
162      "syntax_base_style": "monokai",
163      "syntax": {
164        "keyword": "#81a1c1 bold",
165        "keyword_constant": "#81a1c1",
166        "keyword_declaration": "#81a1c1",
167        "keyword_namespace": "#81a1c1",
168        "keyword_reserved": "#81a1c1 bold",
169        "keyword_type": "#88c0d0",
170        "name_builtin": "#88c0d0",
171        "name_function": "#8fbcbb",
172        "name_class": "#a3be8c bold",
173        "name_decorator": "#b48ead",
174        "literal_string": "#a3be8c",
175        "literal_string_escape": "#bf616a",
176        "literal_number": "#d08770",
177        "comment": "#616e88 italic",
178        "comment_preproc": "#b48ead",
179        "operator": "#eceff4",
180        "punctuation": "#d8dee9",
181        "text": "#e5e9f0"
182      }
183    },
184    "light": {
185      "fg": 236,
186      "muted": 244,
187      "accent": 33
188    }
189  }
190}
191```
192
193You may also put overrides directly at the top level or directly under
194`colors` when they should apply to both modes:
195
196```json
197{
198  "name": "tiny",
199  "accent": 204,
200  "colors": {
201    "spinner_messages": ["shipping"]
202  }
203}
204```
205
206## Color fields
207
208Every color field accepts the following JSON forms:
209
210```json
211254
212"#42454b"
213{ "mode": "256", "index": 254 }
214{ "mode": "ansi", "index": 100 }
215{ "mode": "rgb", "r": 66, "g": 69, "b": 75 }
216```
217
218Numeric values are xterm-256 indexes. ANSI values use SGR codes, with
219standard foreground/background ranges normalized to the role where necessary.
220RGB values are emitted directly when truecolor is advertised and quantized to
221xterm-256 otherwise. The built-in inherited theme additionally resolves
222semantic accents through the terminal's ANSI palette and derives muted and
223selected surfaces from the terminal foreground/background.
224
225- `fg` — default foreground text.
226- `muted` — secondary text, dividers, gutters, inactive hints.
227- `accent` — prompt bar, bullets, links, headings, active markers.
228- `background` — optional full-row TUI background. If missing, zut uses the terminal's existing background. Experimental: terminal background colors can vary by emulator and scrollback behavior; for the most reliable result, change your terminal background color in your terminal settings instead.
229- `user` — user role label color; mostly compatibility.
230- `user_bubble_bg` — background behind user message rows.
231- `user_bubble_fg` — foreground inside user message rows.
232- `assistant` — assistant/zut accent and spinner text.
233- `tool` — tool names, success marks, diff additions.
234- `tool_out` — plain tool-output text.
235- `error` — errors, refused calls, diff deletions.
236- `warning` — warnings and high context-usage state.
237- `spinner` — reserved spinner color slot.
238- `thinkingMax` or `thinking_max` (names retained for compatibility) - status color used when the opt-in `max` reasoning level is active. Missing values inherit the built-in theme color.
239- `selection_bg` — highlighted row background.
240- `selection_fg` — highlighted row foreground.
241
242The same forms apply to `background`, `user_bubble_bg`, and every other
243semantic color field listed above.
244
245## Spinner fields
246
247Spinner settings can appear at top level, under `colors`, or under
248`colors.dark` / `colors.light`.
249
250- `spinner_frames` — list of frame strings. Single-cell glyphs keep
251  status-bar alignment clean.
252- `spinner_messages` — list of messages; zut picks one per turn.
253- `spinner_interval_ms` — frame interval in milliseconds. Missing or
254  invalid falls back to 80ms.
255
256## Syntax fields
257
258Syntax highlighting uses Chroma style entries. Values may include
259attributes after the color, such as `bold`, `italic`, or `underline`.
260
261Supported syntax override keys:
262
263```text
264keyword, keyword_constant, keyword_declaration, keyword_namespace,
265keyword_reserved, keyword_type, name_builtin, name_function,
266name_class, name_decorator, literal_string, literal_string_escape,
267literal_number, comment, comment_preproc, operator, punctuation, text
268```
269
270Example:
271
272```json
273{
274  "colors": {
275    "dark": {
276      "syntax_base_style": "monokai",
277      "syntax": {
278        "keyword": "#f05b8d",
279        "name_function": "#b675f1",
280        "literal_string": "#58c760",
281        "comment": "#a1a1a1 italic"
282      }
283    }
284  }
285}
286```
287
288## Theme-only extensions
289
290An extension can exist only to ship a theme. No slash command,
291subprocess, or executable is required when the extension contains a
292valid theme file.
293
294```text
295$ZUT_HOME/extensions/my-theme-extension/
296├── extension.json
297└── theme.json
298```
299
300`extension.json`:
301
302```json
303{
304  "name": "my-theme-extension",
305  "version": "1.0.0",
306  "description": "Ships a zut color theme",
307  "enabled": true
308}
309```
310
311No `exec` is needed when `theme.json` or `themes/theme.json` exists.
312If `exec` is present, zut treats it as a normal extension too.
313
314## Validate
315
316zut theme files are plain JSON, not JSONC. Validate before installing:
317
318```bash
319python3 -m json.tool theme.json >/dev/null
320```

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close