This guide explains the single-source theme workflow in smplOS.
Use one file as the source of truth for core theme behavior:
- colors
- borders
- gaps
- rounding
- blur
- opacity
- app-theme selectors
That file is:
src/shared/themes/<theme>/colors.toml
Everything else is either generated from it, or selected by it.
Each theme lives in:
src/shared/themes/<theme-name>/
colors.toml(edit this first)
btop.themedunstrc.themeeww-colors.scsseww-colors.yuckfish.themefoot.inihyprland.confhyprlock.conflogseq-custom.csssmplos-launcher.rasi
Generated by:
src/generate-theme-configs.sh
These are app-specific preset payloads used by selectors in colors.toml:
neovim.luavscode.json
icons.themelight.mode(optional marker for light themes)tide.themebackgrounds/preview.png
mkdir -p src/shared/themes/my-themecp -r src/shared/themes/tokyo-night/* src/shared/themes/my-theme/Every variable describes what the color does, not what it looks like.
| Key | Purpose |
|---|---|
background |
Main window/app background |
bg_light |
Raised surfaces -- cards, sidebars, code blocks |
bg_lighter |
Further raised surfaces -- scrollbars, tertiary panels |
surface |
Containers, notification backgrounds, selection areas |
| Key | Purpose |
|---|---|
foreground |
Primary text |
fg_dim |
Secondary/dimmed text (EWW widget labels, subtle UI text) |
fg_alt |
Alternative foreground tone (terminal ANSI "white" slot) |
muted |
Tertiary text -- comments, inactive items, dividers, chrome |
autosuggestion |
Fish/shell autosuggestion text (defaults to muted if omitted) |
| Key | Purpose |
|---|---|
accent |
Primary accent -- links, active borders, highlights |
accent_alt |
Secondary accent -- keywords, alternate highlights |
cursor |
Terminal cursor color |
selection_foreground |
Text color inside selections |
selection_background |
Selection highlight background |
| Key | Purpose |
|---|---|
danger |
Errors, critical alerts, destructive actions |
success |
Success indicators, confirmations |
warning |
Warnings, caution states |
info |
Informational indicators |
Used for terminal bright palette slots, btop gradient midpoints, and link hover states.
| Key | Source it pairs with |
|---|---|
danger_bright |
danger |
success_bright |
success |
warning_bright |
warning |
accent_bright |
accent |
accent_alt_bright |
accent_alt |
info_bright |
info |
The terminal palette (ANSI slots 0-15) is auto-derived from semantic names:
| Slot | Default source | Override key |
|---|---|---|
| 0 | surface |
term_0 |
| 1 | danger |
term_1 |
| 2 | success |
term_2 |
| 3 | warning |
term_3 |
| 4 | accent |
term_4 |
| 5 | accent_alt |
term_5 |
| 6 | info |
term_6 |
| 7 | fg_alt |
term_7 |
| 8 | muted |
term_8 |
| 9 | danger_bright |
term_9 |
| 10 | success_bright |
term_10 |
| 11 | warning_bright |
term_11 |
| 12 | accent_bright |
term_12 |
| 13 | accent_alt_bright |
term_13 |
| 14 | info_bright |
term_14 |
| 15 | fg_dim |
term_15 |
Most themes never need overrides. Use term_N only when a terminal slot should differ from its semantic source (e.g. hackerman's accent is green but terminal "blue" slot should still be blue-ish).
These are unchanged from before:
- Decoration:
rounding,blur_size,blur_passes,opacity_active,opacity_inactive,term_opacity_active,term_opacity_inactive,popup_opacity,messenger_opacity,browser_opacity - Layout:
gaps_in,gaps_out,border_size,border_active(optional),border_inactive(optional) - App selectors:
app_theme_nvim,app_theme_vscode,app_theme_logseq
Example:
app_theme_nvim = "tokyo-night"
app_theme_vscode = "tokyo-night"
app_theme_logseq = "tokyo-night"cd src
bash generate-theme-configs.shtheme-set my-themesrc/shared/themes/<theme>/colors.toml
cd src
bash generate-theme-configs.shtheme-set <theme>theme-set reads these keys from the active theme's colors.toml:
app_theme_nvimapp_theme_vscodeapp_theme_logseq
Then it resolves that theme ID from:
~/.config/smplos/themes/<id>(user override)~/.local/share/smplos/themes/<id>(stock)
So one theme can reuse another theme's app presets without duplicating settings.
colors.toml-- canonical theme definition (semantic variable names)hyprland.conf-- generated vars consumed by compositor configeww-colors.scss-- generated EWW token valuesfoot.ini-- generated terminal palette (auto-derived from semantic names)neovim.lua→ app preset payload for LazyVim/Lazy.nvim colorschemevscode.json→ app preset payload (theme name + extension/vsix)logseq-custom.css→ generated Logseq custom css layer
Each key controls a distinct window class. Values are never applied twice — no compounding.
| Key | Default | What it affects |
|---|---|---|
opacity_active |
"1.0" |
Compositor opacity for all regular apps (focused). Set below 1.0 for a theme-wide frosted-glass look. |
opacity_inactive |
"1.0" |
Compositor opacity for all regular apps (unfocused). Must equal opacity_active — see CAUTION below. |
term_opacity_active |
"0.85" |
st-wl background-only alpha (focused). Text is always 100% opaque — only background pixels carry this alpha. Set low (e.g. "0.60") for a heavily frosted glass terminal with perfectly sharp text. |
term_opacity_inactive |
"0.70" |
st-wl background alpha when the terminal loses focus. Creates a gentle fade on the background, not on text. |
browser_opacity |
"1.0" |
Brave, Firefox, Chrome, Chromium, etc. |
messenger_opacity |
"0.85" |
Signal, Telegram, Slack, Discord, Teams, WhatsApp |
popup_opacity |
"0.85" |
smplOS Rust popup apps (start-menu, notif-center, kb-center, disp-center) |
Terminals and the Rust apps own their own alpha channel internally (st ALPHA_PATCH / Slint ARGB surfaces). The compositor passes their pixels through at 1.0 override so values don’t compound. Terminal background transparency is driven entirely by term_opacity_active / term_opacity_inactive — the compositor never touches their surface.
Per-app messenger overrides are available as commented-out lines in windows.conf — uncomment and adjust without touching colors.toml or rebuilding.
Never set
opacity_inactivelower thanopacity_active. Here is exactly what happens when you do:Hyprland applies
opacity_inactiveto every window’s entire composited surface when it loses keyboard focus — pixel-by-pixel, before the window is drawn on screen. This means:
- All app text dims. The compositor multiplies every pixel by the inactive factor. At
opacity_inactive = 0.50withopacity_active = 0.55(the old catppuccin values), every unfocused window dropped to half brightness — including text the app rendered as fully opaque.- Terminal text dims despite the pixel-level fix. st-wl uses
ALPHA_PATCHto draw text cells at alpha0xFF(fully opaque), correctly separating text from background transparency. But this happens inside the buffer. Hyprland then multiplies the whole buffer byopacity_inactiveafter composition. Even a perfectly opaque text pixel becomes1.0 × 0.50 = 0.50opacity on screen.- The terminal’s
1.0 overriderule interminals.confprotects st-wl, but no such override exists for regular GTK/Qt/Electron apps — they have no way to self-protect.term_opacity_active/inactiveare safe because they are implemented inside st-wl at the pixel level — background cells get the alpha, text cells stay at0xFF. They do not interact with compositor opacity at all.Rule: set
opacity_inactive = opacity_active. If you want drama:# Catppuccin-style: strong frosted glass, zero focus-dimming opacity_active = "0.55" opacity_inactive = "0.55" # ← same value — no dimming on focus change blur_size = "14" blur_passes = "3"The transparency is constant. Windows never dim on focus change. Apps that self-manage their surface (terminals, Rust popups) are exempt regardless.
- Edit generated files only for debugging; permanent changes go into:
colors.toml- template files in
src/shared/themes/_templates/
- Keep
colors.tomlcomplete (no missing core keys). - Use semantic names that describe what a color does, never generic indices. The terminal palette is auto-derived -- you never need to think about ANSI slot numbers.
- Prefer selector-based reuse instead of copy-pasting app presets.
- Theme applied but old visuals remain:
- run
theme-set <theme>again - ensure generated files were regenerated after template changes
- run
- App theme did not change:
- verify
app_theme_*keys incolors.toml - verify selected preset theme has
neovim.lua/vscode.json
- verify
- EWW icon colors wrong:
theme-setre-bakes SVG icons fromicons/statustemplates