Running OpenCode inside Ghostty on Omarchy produced a near-unreadable light-gray-on-light-gray interface. The logo, prompt text, and status bar were all washed out. Switching Omarchy themes didn't help — the TUI stayed stuck in a light palette regardless of whether the system was using Catppuccin (dark), Tokyo Night, or anything else.
First I checked what OpenCode thought the system theme was. Its tui.json was set to "theme": "system", which means it tries to auto-detect light vs dark mode.
I checked the GNOME/XDG desktop portal:
gsettings get org.gnome.desktop.interface color-scheme
# -> 'prefer-dark'
busctl call --user org.freedesktop.portal.Desktop /org/freedesktop/portal/desktop org.freedesktop.portal.Settings.Read ss org.freedesktop.appearance color-scheme
# -> 1 (dark)So the system was reporting dark mode correctly. But OpenCode was still rendering a light theme. The problem: "system" theme detection inside a terminal TUI isn't as simple as reading the desktop portal.
OpenCode uses Ratatui (a Rust TUI framework) which typically detects dark/light mode by checking the terminal's reported background color via the OSC 11 escape sequence. Ghostty can report this, but:
- Ghostty's
window-theme = autowas falling back to GTK/system theme - Even with
window-theme = ghostty, the terminal surface inside could still present ambiguous signals - OpenCode's auto-detection appeared to misread the terminal as having a light background
The result: it picked the light variant of whichever theme it resolved (when system failed, it appeared to fall through to a default light palette).
I added window-theme = ghostty to ~/.config/ghostty/config to prevent GTK from influencing the terminal chrome. This changed Ghostty's title bar and frame, but the OpenCode TUI inside still rendered light.
I changed ~/.config/opencode/tui.json from "theme": "system" to "theme": "catppuccin-mocha". This immediately broke OpenCode entirely — it refused to start. Reverting to "system" restored functionality, confirming that OpenCode validates theme names against its bundled theme database and catppuccin-mocha wasn't recognized in the version installed. (Later investigation showed the binary does contain catppuccin-mocha, so this might have been a config parsing issue or version mismatch — but the key lesson is that hardcoding breaks the dynamic behavior we want.)
The root issue is that "system" auto-detection is unreliable in a terminal that doesn't cleanly report its background color through the standard OSC sequences, especially on a Wayland/Hyprland desktop where xdg-desktop-portal-hyprland is running but may not expose appearance settings in the way the TUI library expects.
More importantly, even if auto-detection did work, it would only tell OpenCode "dark" or "light" — not which of Omarchy's 20+ themes is currently active. Catppuccin Mocha, Tokyo Night, Nord, and Dracula are all dark, but they have completely different color palettes. Auto-detection would at best give us a generic dark theme, not the matching one.
Omarchy has a clean hook system at ~/.config/omarchy/hooks/. The theme-set hook runs automatically every time omarchy-theme-set is called. It receives the theme name (snake-cased) as $1.
This is the exact same mechanism that omazed (the Zed theme sync tool for Omarchy) uses.
After extracting OpenCode's bundled theme IDs from its binary, I built a mapping between Omarchy's theme names and OpenCode's internal theme IDs:
| Omarchy theme | OpenCode theme ID |
|---|---|
| catppuccin | catppuccin-mocha |
| catppuccin-latte | catppuccin-latte |
| everforest | everforest-dark |
| flexoki-light | catppuccin-latte |
| gruvbox | gruvbox |
| kanagawa | kanagawa |
| nord | nord |
| rose-pine | rose-pine |
| tokyo-night | tokyo-night |
| Ethereal, Hackerman, Lumon, Matte Black, Miasma, Osaka Jade, Retro 82, Ristretto, Vantablack | catppuccin-mocha (safe dark fallback) |
| White | catppuccin-latte (safe light fallback) |
| Unknown / custom | system (let it auto-detect) |
I updated ~/.config/omarchy/hooks/theme-set (which already existed for omazed/Zed integration) to also update OpenCode's TUI config:
#!/bin/bash
# This hook is called with the snake-cased name of the theme that has just been set.
# >>> omazed hook - do not edit >>>
omazed set "$1"
# <<< omazed hook - do not edit <<<
# Sync OpenCode TUI theme with Omarchy theme
THEME_NAME="$1"
# Map Omarchy themes to OpenCode theme IDs
case "$THEME_NAME" in
catppuccin) opencode_theme="catppuccin-mocha" ;;
catppuccin-latte) opencode_theme="catppuccin-latte" ;;
everforest) opencode_theme="everforest-dark" ;;
flexoki-light) opencode_theme="catppuccin-latte" ;;
gruvbox) opencode_theme="gruvbox" ;;
kanagawa) opencode_theme="kanagawa" ;;
nord) opencode_theme="nord" ;;
rose-pine) opencode_theme="rose-pine" ;;
tokyo-night) opencode_theme="tokyo-night" ;;
# Dark themes without exact match -> safe dark fallback
ethereal|hackerman|lumon|matte-black|miasma|osaka-jade|retro-82|ristretto|vantablack)
opencode_theme="catppuccin-mocha" ;;
# Light themes without exact match -> safe light fallback
white)
opencode_theme="catppuccin-latte" ;;
# Unknown -> system (will use terminal detection)
*) opencode_theme="system" ;;
esac
# Update OpenCode TUI config atomically
if command -v python3 &>/dev/null; then
tui_config="$HOME/.config/opencode/tui.json"
tui_tmp="$HOME/.config/opencode/tui.json.tmp.$$"
python3 << PYEOF
import json
import os
tui_config = os.path.expanduser("$tui_config")
tui_tmp = os.path.expanduser("$tui_tmp")
try:
with open(tui_config, 'r') as f:
d = json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
d = {"\$schema": "https://opencode.ai/tui.json", "theme": "system"}
d["theme"] = "$opencode_theme"
with open(tui_tmp, 'w') as f:
json.dump(d, f, indent=2)
f.write("\n")
os.replace(tui_tmp, tui_config)
PYEOF
fiThe Python block is used instead of sed because it safely handles JSON parsing, preserves the schema field, and avoids escaping nightmares in heredocs.
~/.config/omarchy/hooks/theme-set catppuccin-latte
cat ~/.config/opencode/tui.json
# -> { "theme": "catppuccin-latte" }
~/.config/omarchy/hooks/theme-set catppuccin
cat ~/.config/opencode/tui.json
# -> { "theme": "catppuccin-mocha" }After restarting OpenCode, the Catppuccin Mocha palette rendered correctly — dark background with high-contrast text. Switching Omarchy themes via omarchy-theme-set "Tokyo Night" or omarchy-theme-next now updates OpenCode's config automatically. The next time OpenCode launches, it uses the matching theme.
- Don't trust "system" theme detection in TUI apps running inside terminals. Desktop portals, GTK settings, and terminal OSC sequences often don't agree. Wayland/Hyprland makes this even more complex.
- Omarchy's hook system is the right integration point. Rather than patching Ghostty or OpenCode individually, hook into the theme switch event and push the correct configuration downstream.
- This is the same pattern omazed uses for Zed. Any third-party app that has its own theme system but supports explicit theme names can be synchronized this way.
- Use atomic file writes. Writing to a temp file and
os.replace()prevents races if OpenCode reads the config while it's being updated. - Bundle a fallback mapping. Not every Omarchy theme has a 1:1 match in OpenCode. Having safe dark/light fallbacks prevents breaking the app when switching to an esoteric theme.