turbo-editors/turbo-corepublic Fork 0
main
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-core.git
git clone ssh://git@rickub.com/turbo-editors/turbo-core.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

🛟 Updated. 28d5985 · on main · k33g · 4h ago
README.md · 77 lines · 6.9 KBmarkdown
Blame HistoryOpen raw

theme

Turns TOML colour files into the tcell styles the editor draws with.

A theme is a flat map from a dotted style keysyntax.keyword, menu.selected, editor.text — to a foreground colour, a background colour and a few attributes.

Where themes come from

  1. The user's directory, passed in as userDir — the editor works it out from its profile, which reads $TURBO_GO_THEME_DIR (or $TURBO_RUST_THEME_DIR) and otherwise falls back to <slug>/themes under the platform config directory. Pass "" to offer the embedded themes alone.
  2. Embedded in the binary: turbo-classic, turbo-dark, borland-light, cappuccino, catppuccin-frappe, catppuccin-latte, cobalt, darcula, intellij-light, monochrome-dark, monochrome-light.
  3. A name a theme used to answer to, listed in retiredNames: monochrome loads monochrome-dark, which is what that theme shipped as before monochrome-light joined it.

A user file wins over an embedded theme of the same name, so a shipped theme can be overridden without being replaced. A retired name is resolved after the user's directory, so somebody's own monochrome.toml still wins there too, and it is not listed by Available, so each theme is offered once under the name it has now. The reason the mechanism exists at all is that a theme's name is what somebody wrote in a settings file their whole team shares.

File format

name = "My Theme"
description = "One line, shown in the theme picker."
inherits = "turbo-classic"   # optional: fill in every key you do not set

[colors]
default          = { fg = "silver", bg = "navy" }
"syntax.keyword" = { fg = "white", bold = true }
"syntax.string"  = { fg = "#a8d76f" }
"editor.text"    = { fg = "default", bg = "default" }   # leave it to the terminal

Colours are ANSI names understood by tcell (navy, aqua, silver, fuchsia, …), #rrggbb literals, or default / - / "" for the terminal's own colour. Attributes: bold, underline, italic, reverse, dim, blink — each a boolean, only ever switched on.

An unknown colour name is an error at load time, not a silent fallback, so a typo is pointed at instead of quietly repainting half the screen.

Two kinds of inheritance

Between files, with inherits: the parent's resolved styles are the starting point, and the child overrides what it names. Chains are capped at 16 hops, so a loop is reported rather than hanging.

Between keys, along the dots: Style("syntax.keyword") tries syntax.keyword, then syntax, then default. This happens twice — once at parse time, so an entry setting only fg inherits its bg from its parent key, and once at lookup time, so a theme that never mentions syntax.keyword still colours keywords. Shallower keys are resolved first, whatever order they appear in the file.

Public API

Function What it does
Load(name string) (*Theme, error) User directory first, then embedded; ErrNotFound if neither has it
LoadFile(path string) (*Theme, error) Reads one file, named after it if it sets no name
Parse(data []byte) (*Theme, error) Reads TOML content
Default() *Theme The theme used when the user has not chosen one; never fails
Available() []string Every loadable theme name, sorted, user and embedded merged
This package no longer works out where the user's themes live; profile.Profile.ThemeDir() does, and the answer is passed in.
(*Theme) Style(key string) tcell.Style The style for a key, with dotted fallback
(*Theme) Name() / Description() string What the theme picker shows
(*Theme) Keys() []string Every key the theme sets, sorted
(*Theme) Defines(key string) bool Whether it sets that exact key, without fallback

Constants Key* in keys.go are the complete vocabulary of style keys. A test checks that every embedded theme sets every one of them, so adding a widget without theming it fails the suite.

One consequence of dotted fallback is worth stating, because it surprises people writing a theme: fallback runs along the dots and no further. terminal.text falls back to terminal, then to default — never to editor.text. The same holds for tree.*, which is why the project tree could not simply borrow list.selected: that colour is chosen to stand out against a dialog, and on a window body it can be the very colour underneath it.

Tests

make test
go test ./theme/

A shipped theme states its whole palette

Defines is satisfied by inheritance, so a theme that omits a key still passes the completeness test — while quietly showing a colour Turbo Classic chose for its navy background. On espresso, on cream or on black that colour can be unreadable, and nothing says so.

TestEveryEmbeddedThemeSetsEveryKeyItself closes that. A theme you write may still inherit — that is what inherits is for, and the documentation recommends it. The rule is narrower: a theme shipped inside the binary is one the project is answerable for, so it states its palette in full.

Three more rules, in editor where the colour arithmetic already lives: the cursor is at least 64 from its line and never a plain reversal of it, the current line is at least 16 from the page, and text meant to be read is at least 64 from its background. That last one exempts the furniture — desktop, shadow, scrollbar trough, inactive frame, disabled entry, line-number gutter — which sits between 20 and 70 in every theme because it exists to recede.

Channel distance is the wrong measure for prose. The rule above compares the strongest channel, which is right for a cursor and wrong for something read word by word: Turbo Classic drew comments in #808080 on #000080 — 128 apart, waved straight through, and 4.05:1 to read, below the W3C's floor for body text. TestEveryThemeWeAuthorKeepsItsCommentsReadable measures WCAG relative luminance instead and holds syntax.comment to 4.5:1. Comments were the dimmest reading colour in six of the eight shipped themes when it was written.

It covers comments and nothing else, deliberately. syntax.punctuation is quieter still in several themes and stays that way — punctuation is recognised by shape, not read. And the two Catppuccin themes are exempt, because their colours are somebody else's published palette copied faithfully, and Catppuccin puts comments at 2.87:1 and 2.83:1. A second test fails if that exemption ever names a theme this package does not ship, so it cannot quietly outlive its reason.

And one rule no measurement finds: two syntax classes a reader meets side by side must not be drawn identically. turbo-classic once painted syntax.link the same lime as syntax.string, so a Markdown link and an inline code span were the same thing on screen. Classes deliberately alike — string and char, constant and number — are not grouped together, so the test says nothing about them.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# theme

Turns TOML colour files into the `tcell` styles the editor draws with.

A theme is a flat map from a **dotted style key**`syntax.keyword`, `menu.selected`, `editor.text` — to a foreground colour, a background colour and a few attributes.

## Where themes come from

1. **The user's directory**, passed in as `userDir` — the editor works it out from its profile, which reads `$TURBO_GO_THEME_DIR` (or `$TURBO_RUST_THEME_DIR`) and otherwise falls back to `<slug>/themes` under the platform config directory. Pass `""` to offer the embedded themes alone.
2. **Embedded in the binary**: `turbo-classic`, `turbo-dark`, `borland-light`, `cappuccino`, `catppuccin-frappe`, `catppuccin-latte`, `cobalt`, `darcula`, `intellij-light`, `monochrome-dark`, `monochrome-light`.
3. **A name a theme used to answer to**, listed in `retiredNames`: `monochrome` loads `monochrome-dark`, which is what that theme shipped as before `monochrome-light` joined it.

A user file wins over an embedded theme of the same name, so a shipped theme can be overridden without being replaced. A retired name is resolved *after* the user's directory, so somebody's own `monochrome.toml` still wins there too, and it is not listed by `Available`, so each theme is offered once under the name it has now. The reason the mechanism exists at all is that a theme's name is what somebody wrote in a settings file their whole team shares.

## File format

```toml
name = "My Theme"
description = "One line, shown in the theme picker."
inherits = "turbo-classic"   # optional: fill in every key you do not set

[colors]
default          = { fg = "silver", bg = "navy" }
"syntax.keyword" = { fg = "white", bold = true }
"syntax.string"  = { fg = "#a8d76f" }
"editor.text"    = { fg = "default", bg = "default" }   # leave it to the terminal
```

Colours are ANSI names understood by tcell (`navy`, `aqua`, `silver`, `fuchsia`, …), `#rrggbb` literals, or `default` / `-` / `""` for the terminal's own colour. Attributes: `bold`, `underline`, `italic`, `reverse`, `dim`, `blink` — each a boolean, only ever switched **on**.

An unknown colour name is an **error at load time**, not a silent fallback, so a typo is pointed at instead of quietly repainting half the screen.

## Two kinds of inheritance

**Between files**, with `inherits`: the parent's resolved styles are the starting point, and the child overrides what it names. Chains are capped at 16 hops, so a loop is reported rather than hanging.

**Between keys**, along the dots: `Style("syntax.keyword")` tries `syntax.keyword`, then `syntax`, then `default`. This happens twice — once at parse time, so an entry setting only `fg` inherits its `bg` from its parent key, and once at lookup time, so a theme that never mentions `syntax.keyword` still colours keywords. Shallower keys are resolved first, whatever order they appear in the file.

## Public API

| Function | What it does |
| --- | --- |
| `Load(name string) (*Theme, error)` | User directory first, then embedded; `ErrNotFound` if neither has it |
| `LoadFile(path string) (*Theme, error)` | Reads one file, named after it if it sets no `name` |
| `Parse(data []byte) (*Theme, error)` | Reads TOML content |
| `Default() *Theme` | The theme used when the user has not chosen one; never fails |
| `Available() []string` | Every loadable theme name, sorted, user and embedded merged |
| — | This package no longer works out where the user's themes live; `profile.Profile.ThemeDir()` does, and the answer is passed in. |
| `(*Theme) Style(key string) tcell.Style` | The style for a key, with dotted fallback |
| `(*Theme) Name() / Description() string` | What the theme picker shows |
| `(*Theme) Keys() []string` | Every key the theme sets, sorted |
| `(*Theme) Defines(key string) bool` | Whether it sets that exact key, without fallback |

Constants `Key*` in `keys.go` are the complete vocabulary of style keys. A test checks that every embedded theme sets every one of them, so adding a widget without theming it fails the suite.

One consequence of dotted fallback is worth stating, because it surprises people writing a theme: fallback runs along the dots and no further. `terminal.text` falls back to `terminal`, then to `default` — never to `editor.text`. The same holds for `tree.*`, which is why the project tree could not simply borrow `list.selected`: that colour is chosen to stand out against a dialog, and on a window body it can be the very colour underneath it.

## Tests

```sh
make test
go test ./theme/
```

## A shipped theme states its whole palette

`Defines` is satisfied by inheritance, so a theme that omits a key still passes the completeness test — while quietly showing a colour Turbo Classic chose for its navy background. On espresso, on cream or on black that colour can be unreadable, and nothing says so.

`TestEveryEmbeddedThemeSetsEveryKeyItself` closes that. A theme *you* write may still inherit — that is what `inherits` is for, and the documentation recommends it. The rule is narrower: a theme shipped inside the binary is one the project is answerable for, so it states its palette in full.

Three more rules, in `editor` where the colour arithmetic already lives: the cursor is at least 64 from its line and never a plain reversal of it, the current line is at least 16 from the page, and **text meant to be read is at least 64 from its background**. That last one exempts the furniture — desktop, shadow, scrollbar trough, inactive frame, disabled entry, line-number gutter — which sits between 20 and 70 in every theme because it exists to recede.

**Channel distance is the wrong measure for prose.** The rule above compares the strongest channel, which is right for a cursor and wrong for something read word by word: Turbo Classic drew comments in `#808080` on `#000080` — 128 apart, waved straight through, and 4.05:1 to read, below the W3C's floor for body text. `TestEveryThemeWeAuthorKeepsItsCommentsReadable` measures WCAG relative luminance instead and holds `syntax.comment` to **4.5:1**. Comments were the dimmest reading colour in six of the eight shipped themes when it was written.

It covers comments and nothing else, deliberately. `syntax.punctuation` is quieter still in several themes and stays that way — punctuation is recognised by shape, not read. And the two Catppuccin themes are exempt, because their colours are somebody else's published palette copied faithfully, and Catppuccin puts comments at 2.87:1 and 2.83:1. A second test fails if that exemption ever names a theme this package does not ship, so it cannot quietly outlive its reason.

And one rule no measurement finds: **two syntax classes a reader meets side by side must not be drawn identically**. `turbo-classic` once painted `syntax.link` the same lime as `syntax.string`, so a Markdown link and an inline code span were the same thing on screen. Classes deliberately alike — string and char, constant and number — are not grouped together, so the test says nothing about them.