Add theme customizer.

This commit is contained in:
Greyson Parrelli
2026-08-13 15:55:23 -04:00
parent 2daeb6125f
commit 695417a601
12 changed files with 1661 additions and 244 deletions
+75 -8
View File
@@ -123,9 +123,11 @@ Session.zig libghostty-vt Terminal + parser, fed by the PTY
Pty.zig openpt/fork/exec, controlling terminal setup
script.zig runs a command for `$(...)` in a layout's directory
key.zig GDK keyval -> libghostty-vt key mapping
theme.zig colors libghostty-vt has no opinion about, per scheme
Settings.zig preferences: theme and startup tabs, JSON on disk
palette.zig every colour by name, its default per scheme, and the CSS for it
theme.zig the palette resolved for the terminal renderer, which reads numbers
Settings.zig preferences: theme, palette overrides, startup tabs, JSON on disk
SettingsDialog.zig the settings page, including the startup list editor
PaletteEditor.zig the colour editor inside it: a swatch per palette entry
TabSettingsDialog.zig one tab's own settings, and the emoji picker
emoji.zig generated: every emoji the picker offers, and the search over them
```
@@ -392,6 +394,11 @@ in principle, but a terminal grid is small.
- **Light and dark schemes**, following the desktop by default and pinnable
from the settings page, applying to open tabs immediately — terminal palette
included. See [Theme](#theme)
- **Every colour in the palette editable**, per scheme, from **Settings →
Appearance → Customise**: the surfaces, the text on them, the accents, the
status colours, and the terminal's own background, cursor, selection and 16
ANSI colours. Each change repaints as you make it. See
[Customising the palette](#customising-the-palette)
### Shortcuts
@@ -669,6 +676,9 @@ light and a dark scheme. `Ctrl+,` or the gear at the foot of the sidebar opens
**Settings**, whose Appearance section holds the choice: light, dark, or
**system**, which is the default and follows the desktop.
Every colour it resolves to can be changed — see
[Customising the palette](#customising-the-palette).
The preference lives in `~/.config/playpen/settings.json` (or
`$XDG_CONFIG_HOME`), beside `layouts.json`, alongside the
[startup tabs](#startup-tabs):
@@ -677,6 +687,7 @@ The preference lives in `~/.config/playpen/settings.json` (or
{
"version": 1,
"theme": "system",
"colors": {},
"startup": []
}
```
@@ -691,15 +702,18 @@ Three separate colour systems have to agree for that to be true, which is what
dialog chrome. It is told to force a scheme, or left on `default` to follow
the desktop.
- **`style.css`** colours everything the app draws itself. It is written
entirely against named colours, with one palette file per scheme
(`palette-dark.css`, `palette-light.css`); the matching palette is prepended
and the pair loaded as a single `GtkCssProvider`. No rule in `style.css` may
hardcode a colour — a literal hex is a rule that looks right in whichever
scheme you happened to be testing in.
entirely against named colours; `palette.zig` writes the `@define-color`
block that defines them for the scheme in force, and the two are loaded as a
single `GtkCssProvider`. No rule in `style.css` may hardcode a colour — a
literal hex is a rule that looks right in whichever scheme you happened to be
testing in, and one the colour editor cannot reach.
- **`theme.zig`** holds what Cairo draws the terminal grid from: the default
background, foreground and cursor, plus the 16 ANSI colours. The style tree
is never consulted there, so a CSS reload alone would leave every terminal
painted in the scheme it started in.
painted in the scheme it started in. It resolves the same table `style.css`
is fed from, and caches the result — the renderer asks for the background
once a frame and for the palette once a cell, and neither should be going
through the settings for an answer.
The ANSI palette is the part that is easy to skip and shouldn't be. The
standard xterm yellow is `#cdcd00`, which on a white background is close to
@@ -718,6 +732,59 @@ its `dark` property rather than the stored preference, and the preference only
decides what the style manager is told. A desktop that switches at sunset takes
this app with it, with no extra machinery and no second source of truth.
### Customising the palette
**Settings → Appearance → Customise** opens the palette itself: a swatch for
every colour named above, in sections — surfaces, text, tab rows, accent,
status, then the terminal's own background, foreground, cursor and selection,
and the 16 ANSI colours as a strip. Beside each row is the hex it currently
resolves to, so a palette can be read off as text rather than only picked at,
and a button that puts that one colour back. Each section heading has the same
button for the section, and the foot of the list has one for the whole scheme.
Three things about how it behaves, all following from the same decision — that
the editor edits **the palette you can see**:
- **A change applies as you make it.** Every colour in the list paints something
behind the dialog, so the swatch is its own preview; there is no OK button
because there is nothing pending to confirm.
- **It edits the scheme in force.** Which is why the way to edit the other one is
the theme picker directly above: switch to light, and the section switches with
it. Both palettes are kept, so an afternoon spent pinned to light doesn't cost
you the dark palette you built.
- **The terminal follows the window.** Left alone, the terminal's background *is*
the pane surface, its foreground *is* the body text, and its selection *is* the
muted accent — so recolouring the app recolours the terminal drawn inside it,
and the frame around a terminal keeps matching its contents. Setting one of
those explicitly breaks the link for that colour and only that colour.
Only what has been changed is stored, per scheme, under `colors` in
`settings.json`:
```json
{
"version": 1,
"theme": "dark",
"colors": {
"dark": { "accent": "#ff8800", "sidebar": "#3a0d0d" }
}
}
```
That is a deliberate choice over writing whole palettes out. It keeps the file
short and hand-editable, it is what lets a row know whether it has been changed,
and it means the shipped defaults can be retuned in a later version and still
reach everyone who never touched them — rather than only the people who had
never opened the editor. A name this build doesn't know, or a value that isn't
`#rrggbb`, costs that one colour and is logged; the rest of the file is read
normally.
`palette.zig` is where the names, the defaults and the grouping live, which is
why there is no longer a `palette-dark.css`. An editor needs to know what the
colours are called, what they started as, and which ones belong together, and
none of that can be read back out of a stylesheet without parsing it — so the
table answers all of it and the CSS is generated from the table.
## Not implemented
This is a proof of concept, and the following are deliberately absent: