v0.22.0 fae6c20

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_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 busy-spinner animation for zut.",
  "colors": {
    "dark": {
      "spinner_frames": ["◢", "◣", "◤", "◥"],
      "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_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_frames": ["◐", "◓", "◑", "◒"]
  }
}

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_interval_ms — frame interval in milliseconds. Missing or invalid falls back to 80ms.

Working-status text is owned by zut and reflects the current operation. The legacy spinner_messages field is ignored when present in existing theme files. In this v0 API, the former tui.Theme.SpinnerMessages and tui.ThemeOverrides.SpinnerMessages Go fields are removed; no compatibility alias is provided.

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_interval_ms": 120
109}
110```
111
112Dark-only themes still work on light terminals. If `colors.light` is
113missing, zut applies `colors.dark` overrides on top of the built-in
114light default. The inverse also works.
115
116```json
117{
118  "name": "custom-spinner",
119  "description": "An alternative busy-spinner animation for zut.",
120  "colors": {
121    "dark": {
122      "spinner_frames": ["◢", "◣", "◤", "◥"],
123      "spinner_interval_ms": 120
124    }
125  }
126}
127```
128
129## Full shape
130
131All fields are optional.
132
133```json
134{
135  "name": "my-theme",
136  "description": "Shown in /settings → color theme.",
137  "color_descriptions": {
138    "accent": "Optional documentation for humans. zut ignores this object."
139  },
140  "colors": {
141    "dark": {
142      "fg": 253,
143      "muted": 244,
144      "accent": 111,
145      "background": "#0b1020",
146      "user": 180,
147      "user_bubble_bg": "#42454b",
148      "user_bubble_fg": 248,
149      "assistant": 117,
150      "tool": 114,
151      "tool_out": 245,
152      "error": 203,
153      "warning": 214,
154      "spinner": 183,
155      "selection_bg": 24,
156      "selection_fg": 231,
157      "spinner_frames": ["⠋", "⠙", "⠚", "⠞", "⠖", "⠦", "⠴", "⠲", "⠳", "⠓"],
158      "spinner_interval_ms": 80,
159      "syntax_base_style": "monokai",
160      "syntax": {
161        "keyword": "#81a1c1 bold",
162        "keyword_constant": "#81a1c1",
163        "keyword_declaration": "#81a1c1",
164        "keyword_namespace": "#81a1c1",
165        "keyword_reserved": "#81a1c1 bold",
166        "keyword_type": "#88c0d0",
167        "name_builtin": "#88c0d0",
168        "name_function": "#8fbcbb",
169        "name_class": "#a3be8c bold",
170        "name_decorator": "#b48ead",
171        "literal_string": "#a3be8c",
172        "literal_string_escape": "#bf616a",
173        "literal_number": "#d08770",
174        "comment": "#616e88 italic",
175        "comment_preproc": "#b48ead",
176        "operator": "#eceff4",
177        "punctuation": "#d8dee9",
178        "text": "#e5e9f0"
179      }
180    },
181    "light": {
182      "fg": 236,
183      "muted": 244,
184      "accent": 33
185    }
186  }
187}
188```
189
190You may also put overrides directly at the top level or directly under
191`colors` when they should apply to both modes:
192
193```json
194{
195  "name": "tiny",
196  "accent": 204,
197  "colors": {
198    "spinner_frames": ["◐", "◓", "◑", "◒"]
199  }
200}
201```
202
203## Color fields
204
205Every color field accepts the following JSON forms:
206
207```json
208254
209"#42454b"
210{ "mode": "256", "index": 254 }
211{ "mode": "ansi", "index": 100 }
212{ "mode": "rgb", "r": 66, "g": 69, "b": 75 }
213```
214
215Numeric values are xterm-256 indexes. ANSI values use SGR codes, with
216standard foreground/background ranges normalized to the role where necessary.
217RGB values are emitted directly when truecolor is advertised and quantized to
218xterm-256 otherwise. The built-in inherited theme additionally resolves
219semantic accents through the terminal's ANSI palette and derives muted and
220selected surfaces from the terminal foreground/background.
221
222- `fg` — default foreground text.
223- `muted` — secondary text, dividers, gutters, inactive hints.
224- `accent` — prompt bar, bullets, links, headings, active markers.
225- `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.
226- `user` — user role label color; mostly compatibility.
227- `user_bubble_bg` — background behind user message rows.
228- `user_bubble_fg` — foreground inside user message rows.
229- `assistant` — assistant/zut accent and spinner text.
230- `tool` — tool names, success marks, diff additions.
231- `tool_out` — plain tool-output text.
232- `error` — errors, refused calls, diff deletions.
233- `warning` — warnings and high context-usage state.
234- `spinner` — reserved spinner color slot.
235- `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.
236- `selection_bg` — highlighted row background.
237- `selection_fg` — highlighted row foreground.
238
239The same forms apply to `background`, `user_bubble_bg`, and every other
240semantic color field listed above.
241
242## Spinner fields
243
244Spinner settings can appear at top level, under `colors`, or under
245`colors.dark` / `colors.light`.
246
247- `spinner_frames` — list of frame strings. Single-cell glyphs keep
248  status-bar alignment clean.
249- `spinner_interval_ms` — frame interval in milliseconds. Missing or
250  invalid falls back to 80ms.
251
252Working-status text is owned by zut and reflects the current operation. The
253legacy `spinner_messages` field is ignored when present in existing theme
254files. In this v0 API, the former `tui.Theme.SpinnerMessages` and
255`tui.ThemeOverrides.SpinnerMessages` Go fields are removed; no compatibility
256alias is provided.
257
258## Syntax fields
259
260Syntax highlighting uses Chroma style entries. Values may include
261attributes after the color, such as `bold`, `italic`, or `underline`.
262
263Supported syntax override keys:
264
265```text
266keyword, keyword_constant, keyword_declaration, keyword_namespace,
267keyword_reserved, keyword_type, name_builtin, name_function,
268name_class, name_decorator, literal_string, literal_string_escape,
269literal_number, comment, comment_preproc, operator, punctuation, text
270```
271
272Example:
273
274```json
275{
276  "colors": {
277    "dark": {
278      "syntax_base_style": "monokai",
279      "syntax": {
280        "keyword": "#f05b8d",
281        "name_function": "#b675f1",
282        "literal_string": "#58c760",
283        "comment": "#a1a1a1 italic"
284      }
285    }
286  }
287}
288```
289
290## Theme-only extensions
291
292An extension can exist only to ship a theme. No slash command,
293subprocess, or executable is required when the extension contains a
294valid theme file.
295
296```text
297$ZUT_HOME/extensions/my-theme-extension/
298├── extension.json
299└── theme.json
300```
301
302`extension.json`:
303
304```json
305{
306  "name": "my-theme-extension",
307  "version": "1.0.0",
308  "description": "Ships a zut color theme",
309  "enabled": true
310}
311```
312
313No `exec` is needed when `theme.json` or `themes/theme.json` exists.
314If `exec` is present, zut treats it as a normal extension too.
315
316## Validate
317
318zut theme files are plain JSON, not JSONC. Validate before installing:
319
320```bash
321python3 -m json.tool theme.json >/dev/null
322```

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