Guides
Theming Guide
The dotfiles ship a wallpaper-driven theme system that generates terminal color palettes directly from wallpaper images using K-Means clustering in CIELAB color space. One command changes the terminal, editor, window manager, GTK, desktop environment, wallpaper, and browser-facing color mode in under a second.
Themes are not hand-crafted: they are extracted from wallpapers. A prebuilt set ships in defaults/.theme-manifest/themes.toml, and dot theme rebuild regenerates it from the wallpapers available on your system.
How Themes Work
Wallpapers are the source of truth. The system discovers wallpapers from two locations:
- System wallpapers — platform-native (macOS
/System/Library/Desktop Pictures/, Linux/usr/share/backgrounds/) - Custom wallpapers — user-provided in
~/Pictures/Wallpapers/(custom overrides system)
extract-theme.py extracts dominant colors from each wallpaper using K-Means clustering in CIELAB color space, then generates a full terminal palette (16 ANSI colors, accent, bg/fg, panel, border) with WCAG contrast enforcement. Dark variants map wallpaper hues to luminous color blocks with black text; light variants map them to deep blocks with white text, so both modes remain vivid and readable.
The mode-specific tuning follows Apple's semantic-color model: light and dark are generated independently, backgrounds use a primary/secondary/tertiary surface hierarchy, and chromatic roles are calibrated against Apple's increased-contrast system colors while retaining the wallpaper hue. Terminal windows are opaque so translucency cannot invalidate the generated sRGB contrast. This applies consistently to Ghostty, Kitty, Alacritty, WezTerm, Foot, iTerm2, Warp, tmux, and provider TUIs.
rebuild-themes.sh orchestrates discovery → extraction → assembly into defaults/.theme-manifest/themes.toml. Themes are cached in ~/.cache/dotfiles/themes/ and only regenerated when wallpapers change.
The theme, theme_family and theme_mode keys control the active theme. defaults/.chezmoidata.toml holds the fresh-install defaults; a theme switch writes the machine's choice to ~/.config/chezmoi/chezmoi.toml, which takes precedence. Templates load the manifest and resolve the active theme like this:
{{- $themes := (include ".theme-manifest/themes.toml" | fromToml).themes -}}
{{- $themeName := includeTemplate "theme-name" (dict "theme" .theme "themes" $themes) -}}
{{- $t := index $themes $themeName -}}
The palette contract (palette.v1)
Code outside the templates does not parse the manifest. Chezmoi renders the active theme's palette to ~/.config/dotfiles/palette.env (from defaults/dot_config/dotfiles/palette.env.tmpl, with the same theme-name resolution), and readers take the colours from there: lib/dot/ui.sh exports them to dot-ui, and the AI CLI theme adapter reads the mode. Every dot apply and every theme switch rewrites it.
DOT_PALETTE=palette.v1
THEME=maui-dark
MODE=dark
ACCENT=#00d2e0
ERROR=#ff8ac4
WARNING=#ff9230
SUCCESS=#30d158
INFO=#5cb8ff
PANEL=#222f31
BORDER=#323c3d
FG=#f1f6f7
BG=#0f1f21
DOT_PALETTE, THEME and MODE (dark or light) are always present; a colour is #rrggbb and absent when the theme has none. Readers parse the lines rather than sourcing the file, and ignore keys they do not know.
Switching Themes
Interactive Picker
Opens an fzf picker (a numbered menu without fzf) listing every paired theme family (families that have both -dark and -light variants), with its wallpaper source and a live palette preview. The current family is marked with ✓ and its mode. Select one and press Enter: a family picked here follows the system appearance (auto mode).
Direct Switch
Sets the theme immediately. Regenerates configs and reloads running applications.
Preview the Transaction
Planning is pure: it does not acquire the apply lock, create state, render a template, reload an application, or use the network. JSON plans conform to schemas/theme-plan.schema.json and include the operation identity, previous and desired theme identities, installed targets, required/optional status, and pre-change hashes.
Rebuild Themes
Discovers wallpapers from system + custom paths, runs K-Means extraction in parallel (4 jobs), caches generated themes in ~/.cache/dotfiles/themes/, and writes .theme-manifest/themes.toml. Custom wallpapers override system wallpapers on name collision.
Under the Hood: dot-theme-sync
dot-theme-sync handles the switching pipeline as a per-user transaction:
- Acquires an exclusive, portable user lock and records its PID and operation ID.
- Snapshots installed file targets, including symlink identity, and chezmoi's records of what it last wrote to them, into a private operation directory.
- Writes machine-local runtime state to
~/.config/chezmoi/chezmoi.toml; the tracked.chezmoidata.tomlremains the fresh-install default. - Runs a targeted
chezmoi applyand treats renderer failures as required transaction failures. - Updates optional AI-provider fragments and reloads installed applications.
- Reads back the active theme identity, writes a versioned JSON journal, and releases the lock.
- On a required failure or signal, restores files in reverse order, puts chezmoi's records back so the next
chezmoi applydoes not stop to ask about them, and recordsrolled_backorrollback_failed.
Journals and snapshots are retained under $XDG_STATE_HOME/dot/theme-transactions/ (default ~/.local/state/dot/theme-transactions/) for the latest 20 operations. File mutations are reversible in this milestone; desktop IPC and system appearance reloads remain best-effort adapters and are explicitly reported as optional results.
AI CLI provider files remain user-owned. The adapter validates JSON or TOML, checks that the file hash has not changed immediately before its atomic rename, and reports invalid_config, conflict, or write_failed without replacing the file. Disable all provider updates, or one provider, in the machine-local chezmoi configuration:
[data.features]
ai_theme_sync = false
[data.features.ai_theme_providers]
gemini = false
What Changes
Each theme switch touches these applications:
| Application | Mechanism | What Changes |
|---|---|---|
| Ghostty | chezmoi apply + macOS app-support sync + DBus reload-config or runtime signal fallback | Background, foreground, all 16 ANSI colors, cursor |
| Tmux | chezmoi apply + source-file | Wallpaper colors, deterministic session identity, focus/prefix state, pane borders, mode indicators |
| AI CLIs | terminal ANSI + COLORFGBG + provider adapters | Codex custom syntax theme; Claude auto; Gemini/Qwen ANSI light/dark; agy terminal; OpenCode system; Aider mode and semantic colors |
| Niri | chezmoi apply + load-config-file IPC | Window borders, focus ring, inactive tint |
| Desktop (macOS) | osascript + defaults write + killall | System appearance (Light/Dark), accent color, highlight color; forces SystemUIServer/Dock/cfprefsd refresh |
| Wallpaper (macOS) | osascript System Events | Desktop wallpaper set across all displays |
| Wallpaper (Linux) | gsettings / dms / swaybg / feh | HEIC auto-converted to PNG; picture-uri and picture-uri-dark set separately |
| Desktop (Linux/GNOME) | chezmoi apply + gsettings | Theme name, icon theme, color scheme preference |
| Safari / Chrome / Edge | Native browser appearance follows desktop theme | Browser chrome stays aligned when using the default/native browser theme |
| Firefox | chezmoi apply on ~/.config/firefox/user.js | Website color scheme preference follows the active dot theme; link that file into a Firefox profile to enforce it |
| DMS | sed -i on settings.json + IPC | Stock theme mapped to accent family, dark/light mode |
| Neovim | --remote-expr Lua eval over socket | Colorscheme, style variant, background mode |
| VS Code | chezmoi apply on settings.json | workbench.colorTheme value |
| dot UI | chezmoi apply on ~/.config/dotfiles/palette.env | Accent, status, panel and border colours of dot pickers, tables and step views |
| Alacritty | chezmoi apply | Full color block regeneration |
| Kitty | chezmoi apply | Full color block regeneration |
| WezTerm | chezmoi apply | Color scheme in Lua config |
Dark/Light Toggle
Toggles between the dark and light variant of the current theme family. A theme named tahoe-dark toggles to tahoe-light, and vice versa.
Theme Families
Run dot theme list to see the available themes. The shipped manifest has a fixed set of families plus a fallback pair; dot theme rebuild replaces it with what your system wallpapers and ~/Pictures/Wallpapers/ provide.
Rebuilding themes
When wallpapers change (new system update, new custom wallpapers), regenerate:
What works without wallpapers
Theme switching is a two-tier system:
Core (always works) — ships in the repo, no setup needed:
- Terminal colors (Ghostty, Alacritty, Kitty, WezTerm, tmux)
- Editor themes (Neovim colorscheme, VS Code)
- macOS dark/light mode and accent color
- Linux GNOME color-scheme, GTK theme, icon theme
- Browser color mode (Safari, Chrome, Firefox)
Wallpapers (optional) — user-provided, enhances the theme:
- Desktop wallpaper matched to the active theme
- Requires
~/Pictures/Wallpapers/with files named after the theme family (see below)
If no wallpapers are present, dot theme applies all core changes and skips the wallpaper step. No errors, no manual config.
Wallpapers (optional)
Wallpapers are not shipped in the repo. Each user sources their own and places them in ~/Pictures/Wallpapers/:
tahoe-dark.heic
tahoe-light.heic
The naming convention is FAMILY-dark and FAMILY-light (.heic, .jpg or .png), or a single dynamic FAMILY.heic. The theme picker shows a family with a matching file in ~/Pictures/Wallpapers/ (override with DOTFILES_WALLPAPER_DIR) as a Custom source.
Wallpaper guidelines
- Resolution: 6016x6016 recommended (matches Apple's native resolution)
- Format:
.heicpreferred on macOS,.png/.jpgalso supported - Brightness: dark/light pairs targeting a golden ratio (1.618) relationship give balanced contrast across displays
Platform behavior
| Platform | Wallpaper support | Mechanism |
|---|---|---|
| macOS | .heic, .jpg, .png | osascript (all desktops) |
| Linux (GNOME) | .png, .jpg (.heic auto-converted) | gsettings picture-uri + picture-uri-dark |
| Linux (Wayland) | .png, .jpg (.heic auto-converted) | swaybg, feh, or Niri/DMS IPC |
| WSL | Not applicable | No compositor; terminal colors still apply |
On Linux, .heic files are automatically converted to .png using magick, heif-convert, or convert (whichever is available). The .png is cached and only regenerated when the source .heic changes.
Using your OS default wallpapers
If you don't provide custom wallpapers, your OS keeps its current desktop wallpaper. The theme still applies all color changes (terminal, editor, accent, dark/light mode). This is the expected default for most users.
Build Artifacts
Build caches (Cargo, Go, pip, uv, Zig) are redirected below the private per-user $DOT_BUILD_ROOT, which defaults to $XDG_CACHE_HOME/dot/builds. mise exports the root for managed commands, while Fish and Zsh create it with owner-only permissions during shell initialization. The cache is disposable and can be removed when no build is running.
Troubleshooting
Theme switch did not apply
Run a full apply to force all configs:
Ghostty did not reload
Ghostty reloads via DBus (com.mitchellh.ghostty / reload-config). If DBus is unavailable, the fallback sends SIGUSR2 to the main process and also matches the macOS app bundle path when needed. Verify Ghostty is running:
On macOS, Ghostty may also read ~/Library/Application Support/com.mitchellh.ghostty/config. dot-theme-sync now mirrors the regenerated XDG config into that location before reloading so the app-support override cannot keep an older palette active.
Neovim did not change colors
dot-theme-sync finds Neovim server sockets at /tmp/nvim*/0 and $XDG_RUNTIME_DIR/nvim.*.0. If Neovim runs with a custom --listen path, the auto-discovery misses it. Restart Neovim to pick up the new theme from the regenerated config.
GTK theme looks wrong
GTK theme names must match installed themes exactly. Catppuccin themes use names like catppuccin-mocha-blue-standard+default. Install the matching GTK theme package or fall back to Adwaita-dark / Adwaita.
macOS accent or appearance did not update
dot-theme-sync applies macOS appearance using osascript and accent via:
dot-theme-sync now kills cfprefsd, SystemUIServer, Dock, and System Settings after writing accent/highlight defaults to force an immediate refresh. If the UI still does not update, close and reopen System Settings.
Browser theme did not change
Safari, Chrome, and Edge are coordinated through the desktop theme, so custom browser themes can override what dot-theme-sync is trying to align. Switch those browsers back to their native/default theme if you want them to track macOS or GTK automatically.
Firefox uses the managed file at ~/.config/firefox/user.js. Link that file into your active Firefox profile as user.js if you want dot-theme-sync to control website prefers-color-scheme behavior:
tmux shows old colors
Tmux reloads via source-file. If TPM plugins override colors, run:
Each session hashes its name onto one of ten Apple system colours, collision probing keeps the active sessions visually distinct, and renaming a session updates its colour. Every bar block carries a white label, so its colour is Apple's Increased contrast (light) value, the deep variant Apple defines for white text (exact at 18pt and up, WCAG AAA large text; deepened further to 7:1 for smaller fonts; see .chezmoidata/apple.toml). The frame is fixed: the top bar (tmux's pane border line) shows the current window in white on black from the left edge and the CPU, memory, and battery indicators on Apple cyan. The bottom bar is transparent between two badges: on the left a mode badge (the session on its colour, PREFIX while the prefix is armed, COPY in copy mode), on the right the time on the same cyan; the other windows and the directory sit between them in the theme's text colour. Cyan, and teal beside it, are never session colours. Nothing appears in both bars, and no element depends on the window width, so a half-screen window shows exactly what a full-screen one does. The indicators come from one lightweight status helper on macOS, Linux/Arch, WSL, and Windows. Set @dot_status_show_path or @dot_status_show_system to off before reloading tmux to hide either module.
AI provider TUIs receive the same mode and palette through the most native interface each exposes. COLORFGBG is injected into new shells and tmux panes for provider-neutral detection. dot-theme-sync also preserves and updates installed provider configs for Codex, Claude, Gemini, Antigravity (agy), Qwen, and OpenCode; Aider reads generated mode and color environment variables. Restart an already-running provider TUI after a theme change because most cache their appearance at startup.
Checking the active theme
This prints the current theme, its family and mode. Cross-reference with dot theme list for available options.