ThemeSwitcher
ThemeSwitcher applies a chosen color theme across six applications in a single command — using nothing but the Python standard library.
Executive Summary & Problem Statement
A terminal theme is not one setting. It is six independent config files in six
different formats: VS Code's settings.json, Windows Terminal's JSON profiles,
Oh My Posh's theme JSON, Clink's Lua, qBittorrent's Qt settings, and a
color-aware terminal profile. Changing your look by hand means editing each one,
with distinct syntax and palette field names per app.
This project reduces that to a single command:
- One palette, six targets — each theme file defines a 16-color ANSI palette once; per-app templates translate it into each config's dialect.
- Templated resolution — theme variables are substituted into app-specific
templates (
$AnsiColor01→#7AA2F7,accent 3→ its RGB tuple, …) rather than hand-maintained copies. - Defensive writing — JSON is parsed and rewritten where the app expects it; structural hiccups fall back to precise regex, so a config is never clobbered if its format drifts.
Architecture Overview
themes/*.theme ──► resolve_vars() ──► app templates ──► config writers
(16-color ANSI) (runtime tree) (per-app terms) (JSON / regex)
│
▼
VS Code · Windows Terminal · Oh My Posh
Clink · qBittorrent · Terminal palette
| Module | Role |
|---|---|
main.py |
CLI, theme loading, orchestration |
modules/theme_loader.py |
.theme parsing + resolution into a settings tree |
modules/app_writers/ |
One writer per application |
modules/registry.py |
App writer registry + target list |
Theme File Format
A theme is a flat, human-authored file. Each key = value pair declares named
colors, and the loader resolves them at runtime into the settings tree the
writers consume:
# catppuccin theme — accent colors
accent1 = #7287FD
accent2 = #7AA2F7
accent3 = #9ECE6A
# … 16-color ANSI palette …
ansi0 = #1A1B26
Resolution happens once in resolve_vars(), which walks the tree top-down so a
later definition can reference an earlier one. The result is a plain dictionary
of resolved values — the single interface every writer reads:
{
"accent1": "#7287FD",
"ansi0": "#1A1B26",
# writer-specific mappings derived from those
}
The Templating Contract
Every app writer receives the same resolved tree and renders it through its own
templated view. The canonical example is Windows Terminal, whose color scheme
object reads $AnsiColorNN keys:
THEME = {
"ansi_color": {
"$AnsiColor01": {"std": "ansi0"},
"$AnsiColor02": {"std": "ansi1"},
# …
}
}
The writer knows the app's field names ($AnsiColor01), the loader knows the
theme's names (ansi0); the bridge is a per-app mapping. Adding a seventh app
means adding a writer module and registering it:
@classmethod
def register(cls, registry: dict) -> None:
registry["vscode"] = cls
No shared state, no switch statements, no coupling between apps.
Resilient Config Writing
The writers take different routes to the same end state — the file must be correct no matter how malformed the original is:
VS Code (pure JSON)
with codecs.open(path, "r", encoding="utf-8-sig") as f:
data = json.loads(f.read())
# Build the replacement colors object
colors = {}
for k, v in resolve_vars():
colors[k] = v
# Then an itemized repair pass:
removals = [k for k in data.get("workbench.colorCustomizations", {}) if k.startswith("moelectric")]
for k in removals:
del data["workbench.colorCustomizations"][k]
for k, v in colors.items():
data["workbench.colorCustomizations"][k] = v
utf-8-sigstrips the BOM that VS Code helpfully writes, so the file round- trips cleanly.- Outdated keys from a previous theme are removed first, then new keys are written, so a theme switch never leaves stale colors behind.
- Config drift (an app-added field the loader doesn't know) is preserved automatically: the document is diffed from its own JSON tree, and only the keyed values change.
Windows Terminal (JSON with structure)
jsonText = re.sub(
r'"anker2\.something(?:\.|\\x2e)[^"]*":\s*"[^"]*"',
'"{targetProp}": "{resolved}"',
jsonText,
)
Where qBittorrent stores colors in a Qt .ini-style line, Clink uses a Lua
table, and Oh My Posh expects exact component keys, the writer falls back to
anchored regex replacement on the raw text:
anchor + separator + arbitrary-quoted-part → replacement
The anchor (e.g. the Anker2.something key prefix) guarantees only the intended
line changes, even when the value format is unknown ahead of time.
Design Decisions & Tradeoffs
- Standard library only — zero third-party imports across the whole tool;
codecs,json,re, andpathlibare enough. This removes install, venv, and version-drift concerns entirely on a tool that edits system configs. - Resolve-then-render over per-app globals — the single resolved tree means writers are dumb and portable; theme semantics live in one place.
- Precise surgery over wholesale rewrite — every file is patched in place (remove-stale-then-set-new, anchored regex), so unrelated user settings are never lost in a theme transition.
- Registry over hardcoding — app discovery is declarative and self-
documenting:
main.pyasks the registry for targets instead of listing apps.
Engineering Discipline
- Format-aware encodings —
utf-8-sigfor VS Code, explicit reader/writer pairs per format, and consistent newline handling across JSON rewrites. - Idempotent switches — applying theme A then B then A restores byte-equal configs, because each pass first removes the previous theme's keys.
- Safe fallbacks — regex anchors degrade to "no match, no change" rather than risking a destructive write on a shifting format.
- Documented layout — one module per app under
modules/app_writersmirrors the extension path a contributor follows.
Technical Competencies Demonstrated
- Multi-format configuration engineering — safely writing JSON, INI, Lua, and arbitrary text configs from one abstraction.
- Template resolution — a single source of truth (16-color palette) compiled into per-target dialects via declarative mappings.
- In-place config surgery — BOM handling, stale-key removal, and anchored regex replacement with zero collateral edits.
- Zero-dependency design — full standard-library implementation of a tool that touches system state.
- Idempotency — theme switches compose and invert without residue.