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
- 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>/themesunder the platform config directory. Pass""to offer the embedded themes alone. - Embedded in the binary:
turbo-classic,turbo-dark,borland-light,cappuccino,catppuccin-frappe,catppuccin-latte,cobalt,darcula,intellij-light,monochrome-dark,monochrome-light. - A name a theme used to answer to, listed in
retiredNames:monochromeloadsmonochrome-dark, which is what that theme shipped as beforemonochrome-lightjoined 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 |
|