Files
omarchy/bin/omarchy-theme-set
T
Spencer Bull 6baae0f556 Make Hermes follow the Omarchy theme as a skin
Hermes Desktop installed under Install > AI kept its own palette while every other agent app retinted with the theme. Hermes' skin is its one theme unit for the desktop app, the TUI and the CLI, and its gateway watches the active skin file and broadcasts changes to every surface, so Omarchy publishes a skin named omarchy from a template on every theme switch and nothing Omarchy-specific goes upstream.

Activation goes through hermes config set, which writes the active profile's config and touches the skin so a running gateway repaints at once, and it only replaces Hermes' default skin so a choice made in Hermes stays. A theme switch runs that activation too when the desktop package is present and Hermes is still on its default, so a hand-over the installer missed is finished by the next switch; once the config names the skin a switch never starts Hermes. The desktop adopts a skin from a change broadcast rather than from the config it finds at connect time, and its first launch builds the runtime over minutes, so the installer starts --wait as a transient user unit that outlives the install terminal, activates once the runtime marker appears, republishes after the gateway is up, and reports to the journal. A migration hands the skin to existing Hermes Desktop installs through --activate, which also renders the skin for a theme applied before the template existed.

The generated file is validated before it is published, because Hermes parses it as YAML: only the name, a plain description and #rrggbb colours pass, so an unresolved palette key or a cloned theme's own hermes.yaml leaves the previous skin in place.

🤖 Generated by Fable 5.1 in Claude Code. Reviewed by Fable 5.1 code-review at high.
2026-09-05 23:05:10 -05:00

348 lines
11 KiB
Bash
Executable File

#!/bin/bash
# omarchy:summary=Apply an Omarchy theme
# omarchy:args=<theme-name>
# omarchy:examples=omarchy theme list | omarchy theme set "Tokyo Night"
if [[ -z $1 ]]; then
echo "Usage: omarchy-theme-set <theme-name>"
exit 1
fi
CURRENT_THEME_PATH="$HOME/.local/state/omarchy/current/theme"
NEXT_THEME_PATH="$HOME/.local/state/omarchy/current/next-theme"
CURRENT_BACKGROUND_LINK="$HOME/.local/state/omarchy/current/background"
BACKGROUND_TRANSITION_CACHE="$HOME/.cache/omarchy/background-transitions"
THEME_SET_LOCK="${XDG_RUNTIME_DIR:-/tmp}/omarchy-theme-set.lock"
USER_THEMES_PATH="$HOME/.config/omarchy/themes"
OMARCHY_THEMES_PATH="$OMARCHY_PATH/themes"
# What a theme installed from a git repo may not ship, because these run code.
# Hyprland requires a theme's hyprland.lua and gum_env.lua at login and Neovim
# loads its neovim.lua at startup, so no .lua from such a theme is staged at all.
# Each terminal config names the program the terminal launches, and vscode.json
# names an extension omarchy-theme-set-vscode installs, a VS Code extension being
# arbitrary JavaScript. Everything else a theme ships is colour and is kept.
#
# Adding a template for another terminal, or for another editor that loads Lua,
# means adding it here. test/shell.d/theme-staging-test.sh fails on a generated
# theme file that is neither denied here nor recorded there as colour-only.
INSTALLED_THEME_DENIED=(alacritty.toml foot.ini ghostty.conf kitty.conf vscode.json)
IGNORED_THEME_FILES=()
run_parallel() {
local pid
local pids=()
for command in "$@"; do
bash -lc "$command" &
pids+=("$!")
done
for pid in "${pids[@]}"; do
wait "$pid"
done
}
shell_ipc() {
timeout 2 omarchy-shell "$@" >/dev/null 2>&1
}
snapshot_background_path() {
local background="$1"
local name="$2"
local snapshot extension
[[ -f $background ]] || return
mkdir -p "$BACKGROUND_TRANSITION_CACHE"
extension=${background##*.}
snapshot="$BACKGROUND_TRANSITION_CACHE/$name-$$.$extension"
ln "$background" "$snapshot" 2>/dev/null || cp "$background" "$snapshot"
echo "$snapshot"
}
snapshot_current_background() {
local current_background
current_background=$(readlink -f "$CURRENT_BACKGROUND_LINK" 2>/dev/null || true)
snapshot_background_path "$current_background" "previous"
}
choose_theme_background() {
local backgrounds=()
local current_background index next_index i
CHOSEN_THEME_BACKGROUND=""
mapfile -d '' -t backgrounds < <(
find -L "$HOME/.config/omarchy/backgrounds/$THEME_NAME/" "$CURRENT_THEME_PATH/backgrounds/" -maxdepth 1 -type f \
\( -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.png' -o -iname '*.gif' -o -iname '*.bmp' -o -iname '*.webp' \) \
-print0 2>/dev/null | sort -z
)
(( ${#backgrounds[@]} > 0 )) || return 1
current_background=$(readlink "$CURRENT_BACKGROUND_LINK" 2>/dev/null || true)
index=-1
for i in "${!backgrounds[@]}"; do
if [[ ${backgrounds[$i]} == $current_background ]]; then
index=$i
break
fi
done
if (( index == -1 )); then
CHOSEN_THEME_BACKGROUND="${backgrounds[0]}"
else
next_index=$(((index + 1) % ${#backgrounds[@]}))
CHOSEN_THEME_BACKGROUND="${backgrounds[$next_index]}"
fi
}
set_theme_background_link() {
choose_theme_background || return 1
ln -nsf "$CHOSEN_THEME_BACKGROUND" "$CURRENT_BACKGROUND_LINK"
}
set_theme_background() {
local new_background new_background_snapshot
if ! choose_theme_background; then
omarchy-notification-send "No background was found for theme" -t 2000
shell_ipc shell applyTheme "$colors_payload" "$shell_payload" || true
return
fi
new_background="$CHOSEN_THEME_BACKGROUND"
new_background_snapshot=$(snapshot_background_path "$new_background" "next")
if [[ -f $OLD_BACKGROUND_SNAPSHOT && -f $new_background_snapshot ]]; then
shell_ipc background themeTransition "$OLD_BACKGROUND_SNAPSHOT" "$new_background_snapshot" "$new_background" "$colors_payload" "$shell_payload" || \
shell_ipc shell applyTheme "$colors_payload" "$shell_payload" || true
(sleep 3; rm -f "$OLD_BACKGROUND_SNAPSHOT" "$new_background_snapshot") &
elif [[ -f $new_background_snapshot ]]; then
shell_ipc background themeTransition "" "$new_background_snapshot" "$new_background" "$colors_payload" "$shell_payload" || \
shell_ipc shell applyTheme "$colors_payload" "$shell_payload" || true
(sleep 3; rm -f "$new_background_snapshot") &
else
shell_ipc background themeTransition "$OLD_BACKGROUND_SNAPSHOT" "$new_background" "$new_background" "$colors_payload" "$shell_payload" || \
shell_ipc shell applyTheme "$colors_payload" "$shell_payload" || true
if [[ -f $OLD_BACKGROUND_SNAPSHOT ]]; then
(sleep 3; rm -f "$OLD_BACKGROUND_SNAPSHOT") &
fi
fi
ln -nsf "$new_background" "$CURRENT_BACKGROUND_LINK"
}
is_denied_installed_file() {
local name="$1"
local denied
[[ $name == *.lua ]] && return 0
for denied in "${INSTALLED_THEME_DENIED[@]}"; do
[[ $name == "$denied" ]] && return 0
done
return 1
}
# Themes older than colors.toml still get their palette, but their
# alacritty.toml never reaches the staged theme: an Alacritty config names the
# program the terminal launches.
stage_installed_colors_from_alacritty() {
local source="$1"
local scratch
if [[ -f $NEXT_THEME_PATH/colors.toml ]]; then
return
fi
if [[ ! -f $source/alacritty.toml || -L $source/alacritty.toml ]]; then
return
fi
scratch=$(mktemp -d)
cp "$source/alacritty.toml" "$scratch/alacritty.toml"
omarchy-theme-colors-from-alacritty "$scratch"
if [[ -f $scratch/colors.toml ]]; then
cp "$scratch/colors.toml" "$NEXT_THEME_PATH/colors.toml"
fi
rm -rf "$scratch"
}
# Copies a directory without ever following a symlink: in an installed theme one
# points wherever the theme author chose, which is how an unlock.png becomes a
# copy of any file the session can read.
stage_installed_dir() {
local source="$1"
local dest="$2"
local entry name
mkdir -p "$dest"
for entry in "$source"/*; do
[[ -e $entry && ! -L $entry ]] || continue
name=${entry##*/}
if [[ -d $entry ]]; then
stage_installed_dir "$entry" "$dest/$name"
else
cp "$entry" "$dest/$name"
fi
done
}
# `omarchy theme install` clones into ~/.config/omarchy/themes, so a .git
# directory there means the contents came from a stranger and are held to the
# list above. A directory the user wrote themselves, and a symlink to their own
# working copy, are theirs to fill however they like -- the same distinction
# omarchy-theme-extras draws when it decides which themes it may pull.
theme_came_from_a_repo() {
local source="$1"
[[ ! -L $source && -d $source/.git ]]
}
stage_installed_theme() {
local source="$1"
local entry name
[[ -d $source ]] || return 0
for entry in "$source"/*; do
[[ -e $entry ]] || continue
name=${entry##*/}
if [[ -L $entry ]] || is_denied_installed_file "$name"; then
case "${name,,}" in
readme* | license* | changelog* | *.md | *.txt) ;;
*) IGNORED_THEME_FILES+=("$name") ;;
esac
elif [[ -d $entry ]]; then
stage_installed_dir "$entry" "$NEXT_THEME_PATH/$name"
else
cp "$entry" "$NEXT_THEME_PATH/$name"
fi
done
stage_installed_colors_from_alacritty "$source"
}
report_ignored_theme_files() {
(( ${#IGNORED_THEME_FILES[@]} > 0 )) || return 0
echo "Ignored in $USER_THEMES_PATH/$THEME_NAME: ${IGNORED_THEME_FILES[*]}" >&2
echo "A theme installed from a git repo cannot supply Lua, a terminal config, or vscode.json." >&2
}
THEME_NAME=$(echo "$1" | sed -E 's/<[^>]+>//g' | tr '[:upper:]' '[:lower:]' | tr ' ' '-')
THEME_HEADLESS=0
if [[ ${OMARCHY_THEME_HEADLESS:-} == "1" || ${OMARCHY_THEME_OFFLINE:-} == "1" ]]; then
THEME_HEADLESS=1
fi
if [[ -z $THEME_NAME || $THEME_NAME == .* || $THEME_NAME == */* ]]; then
echo "Invalid theme name: $1"
exit 1
fi
if [[ ! -d $OMARCHY_THEMES_PATH/$THEME_NAME ]] && [[ ! -d $USER_THEMES_PATH/$THEME_NAME ]]; then
echo "Theme '$THEME_NAME' does not exist"
exit 1
fi
# Serialize theme changes. Theme switching rebuilds a shared next-theme staging
# directory and updates current theme/background symlinks; concurrent calls can
# otherwise race and make one selection appear to be ignored.
exec 9>"$THEME_SET_LOCK"
flock 9
# Setup clean next theme directory (for atomic theme config swapping)
rm -rf "$NEXT_THEME_PATH"
mkdir -p "$NEXT_THEME_PATH"
# Copy official theme first, then overlay the user's theme on top
cp -r "$OMARCHY_THEMES_PATH/$THEME_NAME/"* "$NEXT_THEME_PATH/" 2>/dev/null
if theme_came_from_a_repo "$USER_THEMES_PATH/$THEME_NAME"; then
stage_installed_theme "$USER_THEMES_PATH/$THEME_NAME"
report_ignored_theme_files
else
cp -r "$USER_THEMES_PATH/$THEME_NAME/"* "$NEXT_THEME_PATH/" 2>/dev/null
fi
# Generate colors.toml from alacritty.toml if theme is missing colors.toml
if [[ ! -f $NEXT_THEME_PATH/colors.toml && -f $NEXT_THEME_PATH/alacritty.toml ]]; then
omarchy-theme-colors-from-alacritty "$NEXT_THEME_PATH"
fi
# Generate dynamic configs
omarchy-theme-set-templates
OLD_BACKGROUND_SNAPSHOT=""
if [[ $THEME_HEADLESS != "1" && $OMARCHY_THEME_SKIP_BACKGROUND != "1" ]]; then
OLD_BACKGROUND_SNAPSHOT=$(snapshot_current_background)
fi
# Swap next theme in as current
rm -rf "$CURRENT_THEME_PATH"
mv "$NEXT_THEME_PATH" "$CURRENT_THEME_PATH"
# Store theme name for reference
echo "$THEME_NAME" >"$HOME/.local/state/omarchy/current/theme.name"
# Make the running shell pick up the new palette immediately while the rest of
# the theme hooks run.
colors_payload=$([[ -f $CURRENT_THEME_PATH/colors.toml ]] && base64 -w 0 "$CURRENT_THEME_PATH/colors.toml")
shell_payload=$([[ -f $CURRENT_THEME_PATH/shell.toml ]] && base64 -w 0 "$CURRENT_THEME_PATH/shell.toml")
if [[ $THEME_HEADLESS == "1" ]]; then
# No shell/session bus exists during ISO chroot finalization, but the first
# real login still needs a current background symlink for omarchy-shell to
# render.
[[ $OMARCHY_THEME_SKIP_BACKGROUND == "1" ]] || set_theme_background_link || true
elif [[ $OMARCHY_THEME_SKIP_BACKGROUND == "1" ]]; then
shell_ipc shell applyTheme "$colors_payload" "$shell_payload" || true
else
set_theme_background
fi
# The shared staging/current symlinks are updated and the shell has accepted
# the transition. Let another theme selection queue only behind that critical
# section, not behind slower app-retint hooks and selector cache warmups.
flock -u 9
post_theme_commands=(
omarchy-restart-terminal
omarchy-restart-hyprctl
omarchy-restart-btop
omarchy-restart-opencode
omarchy-restart-helix
omarchy-theme-set-foot
omarchy-theme-set-tmux
omarchy-theme-set-gnome
omarchy-theme-set-pi
omarchy-theme-set-claude
omarchy-theme-set-hermes
omarchy-theme-set-browser
omarchy-theme-set-vscode
omarchy-theme-set-obsidian
omarchy-theme-set-keyboard
)
if [[ $THEME_HEADLESS != "1" ]]; then
run_parallel "${post_theme_commands[@]}"
# Call hook on theme set
omarchy-hook theme-set "$THEME_NAME" >/dev/null
# Warm selector caches after the theme is applied. The shell hot-reloads theme
# colors/backgrounds, so keep the running instance alive and preload the picker
# rows/selection to avoid first-open carousel settling after a theme change.
omarchy-theme-switcher --preload >/dev/null 2>&1
omarchy-theme-bg-cache >/dev/null 2>&1 &
fi