#!/bin/bash # omarchy:summary=Apply an Omarchy theme # omarchy:args= # omarchy:examples=omarchy theme list | omarchy theme set "Tokyo Night" if [[ -z $1 ]]; then echo "Usage: omarchy-theme-set " 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-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