Skip to content

Instantly share code, notes, and snippets.

@peterberkenbosch
Created April 21, 2026 19:19
Show Gist options
  • Select an option

  • Save peterberkenbosch/1ca2fcf7adab6efe7fc05c416735d613 to your computer and use it in GitHub Desktop.

Select an option

Save peterberkenbosch/1ca2fcf7adab6efe7fc05c416735d613 to your computer and use it in GitHub Desktop.
Omarchy + OpenCode theme sync: fixing light-on-light rendering in Ghostty

Fixing OpenCode's broken colors in Ghostty on Omarchy

The symptom

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.

Diagnosis

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:

  1. Ghostty's window-theme = auto was falling back to GTK/system theme
  2. Even with window-theme = ghostty, the terminal surface inside could still present ambiguous signals
  3. 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).

False starts

Attempt 1: Force Ghostty's internal theme

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.

Attempt 2: Hardcode catppuccin-mocha in tui.json

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 real problem

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.

The proper fix: sync via Omarchy's theme-set hook

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.

The mapping

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)

The hook

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
fi

The Python block is used instead of sed because it safely handles JSON parsing, preserves the schema field, and avoids escaping nightmares in heredocs.

Testing the hook

~/.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" }

Result

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.

Key takeaways

  1. 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.
  2. 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.
  3. 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.
  4. Use atomic file writes. Writing to a temp file and os.replace() prevents races if OpenCode reads the config while it's being updated.
  5. 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment