Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
452 lines
15 KiB
Bash
Executable File
452 lines
15 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"
|
|
CURRENT_THEME_NAME_PATH="$HOME/.local/state/omarchy/current/theme.name"
|
|
NEXT_THEME_PATH="$HOME/.local/state/omarchy/current/next-theme"
|
|
CURRENT_BACKGROUND_LINK="$HOME/.local/state/omarchy/current/background"
|
|
THEME_BACKGROUND_STATE_PATH="$HOME/.local/state/omarchy/theme-backgrounds"
|
|
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
|
|
}
|
|
|
|
is_video_path() {
|
|
[[ ${1,,} =~ \.(mp4|m4v|mov|webm|mkv|avi)$ ]]
|
|
}
|
|
|
|
snapshot_background_path() {
|
|
local background="$1"
|
|
local name="$2"
|
|
local snapshot extension
|
|
|
|
[[ -f $background ]] || return
|
|
is_video_path "$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"
|
|
}
|
|
|
|
theme_background_state_file() {
|
|
local theme="$1"
|
|
|
|
[[ -n $theme && ${theme##*/} == $theme && $theme != "." && $theme != ".." ]] || return 1
|
|
printf '%s/%s\n' "$THEME_BACKGROUND_STATE_PATH" "$theme"
|
|
}
|
|
|
|
remember_current_theme_background() {
|
|
local current_background state_file
|
|
|
|
state_file=$(theme_background_state_file "$PREVIOUS_THEME_NAME") || return
|
|
current_background=$(readlink -f "$CURRENT_BACKGROUND_LINK" 2>/dev/null || true)
|
|
[[ -f $current_background ]] || return
|
|
|
|
mkdir -p "$THEME_BACKGROUND_STATE_PATH"
|
|
printf '%s\n' "$current_background" >"$state_file"
|
|
}
|
|
|
|
background_transition_uses_snapshots() {
|
|
local next_background="$1"
|
|
local current_background
|
|
|
|
current_background=$(readlink -f "$CURRENT_BACKGROUND_LINK" 2>/dev/null || true)
|
|
! is_video_path "$current_background" && ! is_video_path "$next_background"
|
|
}
|
|
|
|
choose_theme_background() {
|
|
local theme_path="${1:-$CURRENT_THEME_PATH}"
|
|
local current_theme_backgrounds="$CURRENT_THEME_PATH/backgrounds"
|
|
local backgrounds=()
|
|
local current_background remembered_background state_file index next_index i
|
|
|
|
CHOSEN_THEME_BACKGROUND=""
|
|
mapfile -d '' -t backgrounds < <(
|
|
find -L "$HOME/.config/omarchy/backgrounds/$THEME_NAME/" "$theme_path/backgrounds/" -maxdepth 1 -type f \
|
|
\( -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.png' -o -iname '*.gif' -o -iname '*.bmp' -o -iname '*.webp' \
|
|
-o -iname '*.mp4' -o -iname '*.m4v' -o -iname '*.mov' -o -iname '*.webm' -o -iname '*.mkv' -o -iname '*.avi' \) \
|
|
-print0 2>/dev/null | sort -z
|
|
)
|
|
|
|
(( ${#backgrounds[@]} > 0 )) || return 1
|
|
|
|
if [[ $PREVIOUS_THEME_NAME == "$THEME_NAME" ]]; then
|
|
current_background=$(readlink "$CURRENT_BACKGROUND_LINK" 2>/dev/null || true)
|
|
if [[ $theme_path != $CURRENT_THEME_PATH && ${current_background%/*} == $current_theme_backgrounds ]]; then
|
|
current_background="$theme_path/backgrounds/${current_background##*/}"
|
|
fi
|
|
|
|
index=-1
|
|
for i in "${!backgrounds[@]}"; do
|
|
if [[ ${backgrounds[$i]} == $current_background ]]; then
|
|
index=$i
|
|
break
|
|
fi
|
|
done
|
|
|
|
if (( index != -1 )); then
|
|
next_index=$(((index + 1) % ${#backgrounds[@]}))
|
|
CHOSEN_THEME_BACKGROUND="${backgrounds[$next_index]}"
|
|
else
|
|
CHOSEN_THEME_BACKGROUND="${backgrounds[0]}"
|
|
fi
|
|
return
|
|
fi
|
|
|
|
state_file=$(theme_background_state_file "$THEME_NAME" 2>/dev/null || true)
|
|
remembered_background=$(cat "$state_file" 2>/dev/null || true)
|
|
if [[ $theme_path != $CURRENT_THEME_PATH && ${remembered_background%/*} == $current_theme_backgrounds ]]; then
|
|
remembered_background="$theme_path/backgrounds/${remembered_background##*/}"
|
|
fi
|
|
if [[ -f $remembered_background ]]; then
|
|
CHOSEN_THEME_BACKGROUND="$remembered_background"
|
|
return
|
|
fi
|
|
|
|
CHOSEN_THEME_BACKGROUND="${backgrounds[0]}"
|
|
}
|
|
|
|
choose_staged_theme_background() {
|
|
local next_theme_backgrounds="$NEXT_THEME_PATH/backgrounds"
|
|
|
|
choose_theme_background "$NEXT_THEME_PATH" || return 1
|
|
STAGED_THEME_BACKGROUND="$CHOSEN_THEME_BACKGROUND"
|
|
|
|
if [[ ${CHOSEN_THEME_BACKGROUND%/*} == $next_theme_backgrounds ]]; then
|
|
CHOSEN_THEME_BACKGROUND="$CURRENT_THEME_PATH/backgrounds/${CHOSEN_THEME_BACKGROUND##*/}"
|
|
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 [[ -z $CHOSEN_THEME_BACKGROUND || ! -f $CHOSEN_THEME_BACKGROUND ]]; then
|
|
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
|
|
fi
|
|
|
|
new_background="$CHOSEN_THEME_BACKGROUND"
|
|
new_background_snapshot=""
|
|
if [[ -n $PREPARED_BACKGROUND_SNAPSHOT && $PREPARED_BACKGROUND == "$new_background" ]]; then
|
|
new_background_snapshot="$PREPARED_BACKGROUND_SNAPSHOT"
|
|
else
|
|
[[ -z $PREPARED_BACKGROUND_SNAPSHOT ]] || rm -f "$PREPARED_BACKGROUND_SNAPSHOT"
|
|
if [[ $BACKGROUND_TRANSITION_SNAPSHOTS == "true" ]]; then
|
|
new_background_snapshot=$(snapshot_background_path "$new_background" "next")
|
|
fi
|
|
fi
|
|
|
|
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
|
|
|
|
PREVIOUS_THEME_NAME=$(cat "$CURRENT_THEME_NAME_PATH" 2>/dev/null || true)
|
|
remember_current_theme_background
|
|
|
|
# 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
|
|
|
|
CHOSEN_THEME_BACKGROUND=""
|
|
STAGED_THEME_BACKGROUND=""
|
|
OLD_BACKGROUND_SNAPSHOT=""
|
|
PREPARED_BACKGROUND=""
|
|
PREPARED_BACKGROUND_SNAPSHOT=""
|
|
BACKGROUND_TRANSITION_SNAPSHOTS=true
|
|
if [[ $THEME_HEADLESS != "1" && $OMARCHY_THEME_SKIP_BACKGROUND != "1" ]]; then
|
|
# Resolve the staged choice while the old theme still exists. Video changes
|
|
# switch directly to the durable path in QML, so neither side needs a copy.
|
|
if choose_staged_theme_background; then
|
|
if background_transition_uses_snapshots "$CHOSEN_THEME_BACKGROUND"; then
|
|
OLD_BACKGROUND_SNAPSHOT=$(snapshot_current_background)
|
|
|
|
# Hand the shell the next background now, so it decodes while the
|
|
# templates render and the theme swaps in. The staged file is the same
|
|
# inode the swap moves into place, so its snapshot is the final image.
|
|
PREPARED_BACKGROUND="$CHOSEN_THEME_BACKGROUND"
|
|
PREPARED_BACKGROUND_SNAPSHOT=$(snapshot_background_path "$STAGED_THEME_BACKGROUND" "next")
|
|
if [[ -n $PREPARED_BACKGROUND_SNAPSHOT ]]; then
|
|
shell_ipc background prepare "$PREPARED_BACKGROUND_SNAPSHOT" 9>&- &
|
|
fi
|
|
else
|
|
BACKGROUND_TRANSITION_SNAPSHOTS=false
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# Generate dynamic configs
|
|
omarchy-theme-set-templates
|
|
|
|
# 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" >"$CURRENT_THEME_NAME_PATH"
|
|
|
|
# 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-hunk
|
|
omarchy-theme-set-gnome
|
|
omarchy-theme-set-pi
|
|
omarchy-theme-set-claude
|
|
omarchy-theme-set-hermes
|
|
omarchy-theme-set-t3code
|
|
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
|
|
|
|
# Mirror the theme to herdr machines. SSH can take a while, so this never holds up the theme change.
|
|
setsid omarchy-theme-set-herdr-machines >>"$HOME/.local/state/omarchy/theme-set-herdr-machines.log" 2>&1 </dev/null &
|
|
|
|
# Warm the background selector's thumbnails. The theme picker keeps its own
|
|
# rows in the shell and refreshes them each time it opens.
|
|
omarchy-theme-bg-cache >/dev/null 2>&1 &
|
|
fi
|