| 📦 Turbo Go 3d7798b k33g 13h ago | 1 | # How to write your own theme |
| 2 | |
| 3 | This guide shows how to add a colour theme of your own. It assumes you know where your configuration directory is and can edit a TOML file. |
| 4 | |
| 5 | ## 1. Find where themes go |
| 6 | |
| 7 | ```bash |
| 8 | turbo-go -list-themes |
| 9 | ``` |
| 10 | |
| 11 | The last line tells you the directory — `~/.config/turbo-go/themes` on Linux, `~/Library/Application Support/turbo-go/themes` on macOS. Create it: |
| 12 | |
| 13 | ```bash |
| 14 | mkdir -p ~/.config/turbo-go/themes |
| 15 | ``` |
| 16 | |
| 17 | ## 2. Start from an existing theme |
| 18 | |
| 19 | The quickest start is to inherit from one that already works and override only what you want: |
| 20 | |
| 21 | ```toml |
| 22 | # ~/.config/turbo-go/themes/mine.toml |
| 23 | name = "Mine" |
| 24 | description = "Turbo Classic, but the comments are readable." |
| 25 | inherits = "turbo-classic" |
| 26 | |
| 27 | [colors] |
| 28 | "syntax.comment" = { fg = "#8a8a8a", italic = true } |
| 29 | "syntax.string" = { fg = "#87d7af" } |
| 30 | ``` |
| 31 | |
| 32 | Everything you do not set is taken from `turbo-classic`. |
| 33 | |
| 34 | **Inherit from a theme whose ground is the same as yours.** The colours you leave out were chosen against the background of the theme you inherit from, so a dark theme built on `turbo-classic` will show, here and there, a colour picked for Borland navy. If you are writing a dark theme, inherit from `turbo-dark`, `cappuccino`, `catppuccin-frappe`, `cobalt`, `darcula` or `monochrome-dark`; if a light one, from `borland-light`, `catppuccin-latte`, `intellij-light` or `monochrome-light`. That is also why the eleven themes that ship in the binary each state their palette in full rather than inheriting most of it — a test holds them to it, because a shipped theme is one the project is answerable for. |
| 35 | |
| 36 | ## 3. Use it |
| 37 | |
| 38 | ```bash |
| 39 | turbo-go -theme mine main.go |
| 40 | ``` |
| 41 | |
| 42 | Or from inside the editor: `Options ▸ Theme…`, which lists every theme it can find. |
| 43 | |
| 44 | ## 4. Iterate |
| 45 | |
| 46 | Edit the file, then restart the editor. There is no live reload. |
| 47 | |
| 48 | If the theme fails to load, Turbo Go falls back to the default rather than refusing to start. To see *why* it failed: |
| 49 | |
| 50 | ```bash |
| 51 | turbo-go -list-themes |
| 52 | ``` |
| 53 | |
| 54 | A broken theme is listed with the parse error beside it — an unknown colour name is an error, not a silent fallback, so a typo is pointed at rather than quietly repainting half the screen. |
| 55 | |
| 56 | ## 5. Check it stays readable |
| 57 | |
| 58 | The project holds every theme it ships to five measured rules, and they are worth applying to your own. `make test` runs them. |
| 59 | |
| 60 | | Rule | Why | |
| 61 | | --- | --- | |
| 62 | | The cursor is at least 64 apart from the line it sits on, in its strongest channel | A terminal draws its cursor over the cell; one that blends in cannot be found | |
| 63 | | The cursor is never a plain reversal of that line | A terminal that draws its cursor by inverting the cell would invert it back into invisibility | |
| 64 | | The current line is at least 16 from the page | `turbo-dark` once used ten, which is no highlight at all | |
| 65 | | Text you have to read is at least 64 from its background | Furniture — the desktop, a shadow, a scrollbar trough, a disabled entry — is exempt: it exists to recede | |
| 66 | | Comments read at 4.5:1 or better against their background, by WCAG relative luminance | A comment is prose, read word by word. `turbo-classic` drew them in `#808080` on its navy: 128 channel values apart, so the rule above waved it through, and 4.05:1 to read, which is below the W3C's floor for body text. Comments were the dimmest colour in six of the eight shipped themes. | |
| 67 | |
| 68 | |
| 69 | The contrast rule is applied to the six themes this project authors. The two Catppuccin themes are exempt, and the exemption is written where it is made: their colours are somebody else's published palette, faithfully copied, and Catppuccin puts comments at 2.87:1 in Frappé and 2.83:1 in Latte. A theme called Catppuccin that is not those exact values is a different theme wearing a borrowed name, so the fix — if anyone wants one — is upstream. |
| 70 | |
| 71 | It is applied to comments and to nothing else. `syntax.punctuation` is quieter still in several themes and stays that way: punctuation is recognised by shape, not read. |
| 72 | |
| 73 | A sixth rule catches the mistake 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 — every colour readable, every key set, and the two simply equal. Both monochromes pass this rule with no hue at all, by using bold, italic and underline instead. |
| 74 | |
| 75 | ## Variants |
| 76 | |
| 77 | **Override a shipped theme rather than adding one.** Name your file after it — `turbo-classic.toml` — and yours wins. The embedded one is not replaced, so deleting your file brings it back. |
| 78 | |
| 79 | **Start from scratch.** Leave `inherits` out. Set at least `default`; every key you do not set falls back along the dots to it, so a theme with one line is still a usable theme. |
| 80 | |
| 81 | **Colour only the syntax.** One key does it: |
| 82 | |
| 83 | ```toml |
| 84 | [colors] |
| 85 | syntax = { fg = "silver" } |
| 86 | ``` |
| 87 | |
| 88 | `syntax.keyword`, `syntax.string` and the rest all fall back to it. |
| 89 | |
| 90 | **Keep the terminal's own colours.** Use `default` as a colour value: |
| 91 | |
| 92 | ```toml |
| 93 | [colors] |
| 94 | "editor.text" = { fg = "default", bg = "default" } |
| 95 | ``` |
| 96 | |
| 97 | **Test it in a checkout without installing it.** Point the editor at any directory: |
| 98 | |
| 99 | ```bash |
| 100 | TURBO_GO_THEME_DIR=./my-themes turbo-go -theme mine main.go |
| 101 | ``` |
| 102 | |
| 103 | **The cursor is hard to see.** `editor.cursor` does two things: its **background** is sent to the terminal as the cursor's own colour, and it also paints the cell underneath as a fallback for terminals that ignore that. Set it to something loud: |
| 104 | |
| 105 | ```toml |
| 106 | [colors] |
| 107 | "editor.cursor" = { fg = "#000000", bg = "#ff8700" } |
| 108 | ``` |
| 109 | |
| 110 | Two things make a cursor colour a bad one, and the test suite rejects both: a plain reversal of the line — which terminals that draw their cursor by inverting the cell turn back into invisibility — and anything less than 64 channel values away from the line it sits on. |
| 111 | |
| 112 | **The cursor's line is hard to find.** That is `editor.currentline`, and it is a different key. It has to differ from `editor.text` by at least 16 channel values to count as a highlight at all. |
| 113 | |
| 114 | **Markdown and HTML look plain.** Five keys belong to the markup languages and have no equivalent in Go, so a theme written before they existed does not set them: |
| 115 | |
| 116 | ```toml |
| 117 | [colors] |
| 118 | "syntax.heading" = { fg = "white", bold = true } |
| 119 | "syntax.tag" = { fg = "aqua" } |
| 120 | "syntax.attribute" = { fg = "yellow" } |
| 121 | "syntax.emphasis" = { fg = "fuchsia", bold = true } |
| 122 | "syntax.link" = { fg = "aqua", underline = true } |
| 123 | ``` |
| 124 | |
| 125 | Give `syntax.link` a different colour from `syntax.string`: a link and an inline `code` span sit side by side in most prose, and sharing a colour makes them one blur. The shipped `turbo-classic` had exactly that fault until it was looked at on a real terminal. |
| 126 | |
| 127 | **The project tree looks flat.** It has four keys of its own, and none of them falls back to `list`: |
| 128 | |
| 129 | ```toml |
| 130 | [colors] |
| 131 | "tree.text" = { fg = "silver", bg = "navy" } |
| 132 | "tree.directory" = { fg = "white", bg = "navy", bold = true } |
| 133 | "tree.selected" = { fg = "black", bg = "aqua" } |
| 134 | "tree.unfocused" = { fg = "black", bg = "gray" } |
| 135 | ``` |
| 136 | |
| 137 | Give `tree.text` the same background as `window.body`, so the tree looks like part of its window, and make `tree.selected` clearly different from it — the test suite holds every shipped theme to at least 64 channel values between the two, because a highlight the same colour as the page is no highlight. |
| 138 | |
| 139 | **Terminal windows look wrong.** They have two keys of their own, and neither falls back to `editor`: |
| 140 | |
| 141 | ```toml |
| 142 | [colors] |
| 143 | "terminal.text" = { fg = "silver", bg = "black" } |
| 144 | "terminal.cursor" = { fg = "black", bg = "aqua" } |
| 145 | ``` |
| 146 | |
| 147 | `terminal.text` is what a shell's output gets when it names no colour of its own — set it to something close to a real terminal rather than to your editor background, or `less` and `htop` will look out of place. A program that does name its colours keeps them either way. |
| 148 | |
| 149 | ## See also |
| 150 | |
| 151 | - Every key you may set, and every colour name: [theme file reference](../reference/themes.md) |
| 152 | - Why the format is TOML with two kinds of inheritance: [Design decisions](../explanation/design-decisions.md) |