* Decode the next theme background while the theme stages The wipe waited 125-290ms after the transition arrived, decoding the new wallpaper. Most stock wallpapers are WebP, which Qt decodes at full size and scales afterwards, so a screen-sized sourceSize does not shorten it. Start the decode earlier instead. omarchy-theme-set chooses the next background and snapshots it before rendering templates, then sends a new background prepare call in the background. The shell loads it into the hidden incoming frame, so the transition finds it decoded. A prepare that arrives after its transition is ignored, and one no transition claims is dropped after five seconds. The wipe now starts ~255ms after omarchy-theme-set begins instead of ~345-490ms. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Decode the wallpaper at screen size instead of shipped size Wallpapers decoded at the resolution they were shipped at: a 5120x2880 stock wallpaper took 5120x2880 of RGBA on a 1920x1200 panel, and a transition held up to three such frames. Bind sourceSize on the displayed wallpaper and both transition frames to the screen's physical size. PreserveAspectCrop treats it as the area to cover, so the image still fills the screen. Qt scales a decode up as well as down to cover sourceSize, so the native size is read from the file header first with magick identify, and a wallpaper smaller than the screen decodes at its own size. The images wait for both sizes, so nothing decodes at native size first. Ported from #8324 onto BackgroundMedia and the prepared incoming frame. Measured with a 5120x2880 wallpaper, the shell's GPU memory at rest drops from 264 MiB to ~148 MiB. The size probe delays the reveal by ~30ms, which the earlier prepare still more than covers. Co-authored-by: Ryan Yogan <ryanyogan@gmail.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Ryan Yogan <ryanyogan@gmail.com>
448 lines
15 KiB
Bash
Executable File
448 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-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
|
|
|
|
# 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
|