One Theme, Every Tool: monotheme

I change color themes the way other people change desktop wallpapers. The problem: my "environment" isn't one app, it's twenty. Ghostty, tmux, Neovim, VSCode, Cursor, Zed, btop, lazygit, yazi, fzf, bat, opencode, Claude Code, even the macOS accent color and Raycast. Switching themes meant hand-porting the same six colors into twenty config formats, getting bored halfway, and living with a half-themed setup forever.

So I built monotheme: one source of truth, one command, everything reskins at once.

theme set <name>

Repo: github.com/eduwass/monotheme.

The canonical format problem

The naive approach is to invent your own theme schema — a little YAML file with background, foreground, eight ANSI colors — and generate everything from that. I tried. It falls apart the moment a real editor wants syntax highlighting, because "the color of a comment" isn't one color. It's the resolution of a TextMate scope like comment.line.double-slash.ts against ~140 scope rules with fallbacks.

A 16-color palette can't represent that. So I flipped it: the canonical format is the full VSCode theme — the fat one, all 140-odd TextMate scopes intact. That's the source of truth. Everything else projects down from it.

  • A dumb tool (fzf, btop) gets the normalized 16-ANSI palette.
  • A smart tool (Neovim, bat) gets the full scope resolution.

Keep the richest representation as canonical, lose nothing, throw away detail only at the edges where the target can't use it anyway.

The pipeline

Four stages, each boring on purpose:

load → project → adapter → target

load parses the VSCode theme JSON. project derives the normalized palette — bg, fg, ANSI 16, accents — and exposes resolveToken, which maps any TextMate scope to its color through a matcher algorithm I ported from VSCode's own resolver. Then each adapter renders that into one tool's native format, and each target writes the file to the right place and reloads the running app.

The split between adapter (what to write) and target (where to put it and how to reload) is the part that made the whole thing extensible. Adding a new tool is one adapter + one target, and it never touches the core.

Live reload

theme set doesn't just write configs — it reloads the apps that are already running. tmux gets a source-file, Ghostty re-reads, Neovim instances get poked. The win isn't really the command, it's that there's no follow-up: no "now restart your terminal," no stale half-themed windows. You run one command and the whole screen flips.

Install

Needs Bun.

git clone https://github.com/eduwass/monotheme
cd monotheme && bun install && bun link

That puts a theme command on your PATH:

theme list          # installed + bundled themes
theme set <name>    # project to every tool + live-reload
theme current       # active theme
theme check         # self-check, no writes

Drop theme init in your shell rc so a fresh shell re-applies the active theme, and you're done.

It won't touch tools you don't have — if Zed isn't installed, that target is skipped. Start with one or two tools, add adapters as you go.

Repo: github.com/eduwass/monotheme.

Read other posts →