Files
omarchy/bin/omarchy-menu-images
T
David Heinemeier HanssonandClaude Opus 5.5 24f1243120 Make the theme picker open instantly (#13403)
* Run menu summon actions in-process

A menu action that only summons another shell plugin spawned bash and a
qs ipc client to ask this same shell to do it, about 60ms of the path.
Call shell.summon directly instead, and fall back to bash when the call
is refused or the action is anything more than a bare summon.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Open the theme picker from rows held in the shell

Opening the theme picker ran omarchy-theme-switcher to rebuild its index
and then made a second IPC call, about 170ms before the picker mapped.
The picker now holds the theme rows itself, opens from them at once, and
refreshes them behind the open via omarchy-theme-switcher --print-rows.
It applies the chosen theme with omarchy-theme-set directly, so
omarchy-theme-set no longer preloads the picker.

From the keybinding to the overlay mapped drops from ~245ms to ~83ms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Keep the image picker surface mapped between opens

Each open mapped a fresh surface, which rendered its first frames before
Hyprland sent its fractional scale: the pixel ratio stepped 2, 1, then
1.6, so the picker flashed blurry for ~130ms and re-uploaded every
thumbnail texture. Keep the surface and park it transparent and
input-less on the bottom layer while closed, since anything on the
overlay layer blocks direct scanout for fullscreen apps. It follows the
focused monitor on each open.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 15:10:49 +02:00

383 lines
12 KiB
Bash
Executable File

#!/bin/bash
# omarchy:summary=Open a generic image selector menu
# omarchy:args=[--selected <image>] [--print-name] [--show-labels] [--filterable] [--lazy-thumbnails] [--preload] [--cache-only] [--print-rows] <image-dir>...
selected_image=""
print_name=false
show_labels=false
filterable=false
lazy_thumbnails=false
prepare_only=false
preload=false
cache_only=false
print_rows=false
image_dirs=()
usage() {
echo "Usage: omarchy-menu-images [--selected <image>] [--print-name] [--show-labels] [--filterable] [--lazy-thumbnails] [--preload] [--cache-only] [--print-rows] <image-dir>..."
}
while (( $# > 0 )); do
case "$1" in
--selected)
if (( $# < 2 )); then
usage >&2
exit 1
fi
selected_image="$2"
shift 2
;;
--print-name)
print_name=true
shift
;;
--show-labels)
show_labels=true
shift
;;
--filterable)
filterable=true
shift
;;
--lazy-thumbnails)
lazy_thumbnails=true
shift
;;
--prepare-only)
prepare_only=true
shift
;;
--preload)
preload=true
shift
;;
--cache-only)
cache_only=true
shift
;;
--print-rows)
print_rows=true
shift
;;
--help|-h)
usage
exit 0
;;
*)
image_dirs+=("$1")
shift
;;
esac
done
if (( ${#image_dirs[@]} == 0 )); then
usage >&2
exit 1
fi
selection_file=$(mktemp)
done_file=$(mktemp)
pending_file=$(mktemp)
pending_video_file=$(mktemp)
rm -f "$done_file"
trap 'rm -f "$selection_file" "$done_file" "$pending_file" "$pending_video_file"' EXIT
image_dirs_env=""
for dir in "${image_dirs[@]}"; do
if [[ -z $image_dirs_env ]]; then
image_dirs_env="$dir"
else
image_dirs_env+=$'\n'"$dir"
fi
done
current_image=$(readlink -f "$selected_image" 2>/dev/null)
selected_list_image=""
if [[ -n $current_image ]]; then
for dir in "${image_dirs[@]}"; do
if [[ -d $dir && -f $selected_image && ${selected_image%/*} == "$dir" ]]; then
selected_list_image="$selected_image"
break
elif [[ -d $dir ]]; then
selected_list_image=$(find -L "$dir" -maxdepth 1 -type f -samefile "$current_image" -print -quit 2>/dev/null)
[[ -n $selected_list_image ]] && break
fi
done
fi
cache_dir=${XDG_CACHE_HOME:-$HOME/.cache}/omarchy/image-selector
index_file="$cache_dir/index.tsv"
rows=""
mkdir -p "$cache_dir"
cache_key=$(printf '%s' "$image_dirs_env" | md5sum | cut -d ' ' -f 1)
rows_cache_file="$cache_dir/$cache_key.rows"
rows_signature_file="$cache_dir/$cache_key.signature"
rows_fast_signature_file="$cache_dir/$cache_key.fast-signature"
rows_signature="v4"$'\n'
rows_fast_signature="v3"$'\n'
rows_cacheable=true
rows_cache_hit=false
image_files=()
for dir in "${image_dirs[@]}"; do
[[ -d $dir ]] && rows_fast_signature+="$dir:$(stat -Lc '%Y' "$dir")"$'\n'
done
if [[ -f $rows_cache_file && -f $rows_fast_signature_file ]] && cmp -s "$rows_fast_signature_file" <(printf '%s' "$rows_fast_signature"); then
rows=$(<"$rows_cache_file")
rows_cache_hit=true
else
for dir in "${image_dirs[@]}"; do
if [[ -d $dir ]]; then
rows_signature+="$dir:$(stat -Lc '%Y' "$dir")"$'\n'
while IFS= read -r -d '' image; do
image_files+=("$image")
image_signature=$(stat -Lc '%s:%Y' "$image") || continue
rows_signature+="$image:$image_signature"$'\n'
done < <(find -L "$dir" -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)
fi
done
fi
is_video_path() {
[[ ${1,,} =~ \.(mp4|m4v|mov|webm|mkv|avi)$ ]]
}
generate_thumbnail() {
local image="$1"
local thumbnail="$2"
local lock="$thumbnail.lock"
local lock_fd
local tmp="$thumbnail.$$.jpg"
local thumbnail_command
# Older releases used directories as locks, which could survive a killed
# generator and block this thumbnail forever. Only reap aged ones, so a
# legacy generator still running through an upgrade keeps its lock.
if [[ -d $lock ]] && (( $(date +%s) - $(stat -c '%Y' "$lock" 2>/dev/null || date +%s) > 120 )); then
rmdir "$lock" 2>/dev/null
fi
exec {lock_fd}>"$lock" || return
flock -w 30 "$lock_fd" || return
# A generator killed mid-write leaves its partial $thumbnail.<pid>.jpg
# behind. Only the lock holder writes these, so any found now are stale.
rm -f "$thumbnail".*.jpg
[[ -f $thumbnail ]] && return
# Callers fan out one generator per file, so keep each image conversion
# single-threaded. ffmpegthumbnailer provides a still preview for videos.
if is_video_path "$image"; then
# Videos are generated before the picker opens, so one unreadable or
# stalled file must not hold it shut. A failed run drops the row.
thumbnail_command=(timeout -k 5 10 ffmpegthumbnailer -i "$image" -o "$tmp" -s 1536 -q 8)
else
thumbnail_command=(env VIPS_CONCURRENCY=1 vipsthumbnail "$image" --size 1536x864 --smartcrop=centre --path "$tmp[Q=82,strip]")
fi
# Close the lock fd for the converter: an orphaned or hung child must not
# keep holding the lock after this shell is killed.
if "${thumbnail_command[@]}" {lock_fd}>&-; then
mv -f "$tmp" "$thumbnail"
else
status=$?
rm -f "$tmp" "$thumbnail"
# Remember a video the converter rejected, so it costs nothing on the next
# open. The key covers size and mtime, so a repaired file starts clean. A
# timeout is left to retry: the machine may only have been busy.
if is_video_path "$image" && (( status != 124 && status != 137 )); then
: >"$thumbnail.failed"
fi
fi
}
thumbnail_for() {
local image="$1"
local signature hash thumbnail
signature=$(stat -Lc '%s:%Y' "$image") || return
hash=$(awk -F '\t' -v path="$image" -v sig="$signature" '$1 == path && $2 == sig { print $3; exit }' "$index_file" 2>/dev/null)
if [[ -z $hash ]]; then
hash=$(printf '%s\t%s' "$image" "$signature" | md5sum | cut -d ' ' -f 1)
printf '%s\t%s\t%s\n' "$image" "$signature" "$hash" >>"$index_file"
fi
thumbnail="$cache_dir/$hash.jpg"
if [[ ! -f $thumbnail ]]; then
# A video that already failed to convert has no row to offer. Hand the
# marker back so the caller can keep the rows uncached over its absence.
if is_video_path "$image" && [[ -f $thumbnail.failed ]]; then
printf '%s' "$thumbnail.failed"
return
fi
# A lazy row stands in with the media file itself, which the picker draws
# with an Image -- fine for a picture, blank for a video. Videos take the
# queue instead, which also keeps them under its narrower fan out.
if [[ $lazy_thumbnails == true && $cache_only != true ]] && ! is_video_path "$image"; then
rows_cacheable=false
if [[ $prepare_only != true ]]; then
generate_thumbnail "$image" "$thumbnail" >/dev/null 2>&1 &
fi
printf '%s' "$image"
return
fi
if is_video_path "$image"; then
printf '%s\0%s\0' "$image" "$thumbnail" >>"$pending_video_file"
else
printf '%s\0%s\0' "$image" "$thumbnail" >>"$pending_file"
fi
fi
printf '%s' "$thumbnail"
}
# Each vips run is single-threaded, so still images can fill every core.
# ffmpegthumbnailer leaves FFmpeg's automatic threading on, so a full-width fan
# out of those would put a codec thread pool on every core at once.
drain_pending_thumbnails() {
local video_jobs
export -f generate_thumbnail is_video_path
if [[ -s $pending_file ]]; then
xargs -a "$pending_file" -0 -n 2 -P "$(nproc)" \
bash -c 'generate_thumbnail "$1" "$2"' _ >/dev/null 2>&1 || true
fi
if [[ -s $pending_video_file ]]; then
video_jobs=$(( $(nproc) / 4 ))
(( video_jobs > 0 )) || video_jobs=1
xargs -a "$pending_video_file" -0 -n 2 -P "$video_jobs" \
bash -c 'generate_thumbnail "$1" "$2"' _ >/dev/null 2>&1 || true
fi
}
if [[ $rows_cache_hit != true && -f $rows_cache_file && -f $rows_signature_file ]] && cmp -s "$rows_signature_file" <(printf '%s' "$rows_signature"); then
rows=$(<"$rows_cache_file")
printf '%s' "$rows_fast_signature" >"$rows_fast_signature_file"
elif [[ $rows_cache_hit != true ]]; then
for image in "${image_files[@]}"; do
thumbnail=$(thumbnail_for "$image")
[[ -n $thumbnail ]] || continue
# Cached rows are trusted on the directory's mtime alone, which a file
# repaired in place never changes. Leave them uncached instead.
if [[ $thumbnail == *.failed ]]; then
rows_cacheable=false
continue
fi
if [[ $lazy_thumbnails == true && $cache_only != true && $thumbnail == $image ]]; then
rows_cacheable=false
fi
if [[ -z $rows ]]; then
rows="$image"$'\t'"$thumbnail"
else
rows+=$'\n'"$image"$'\t'"$thumbnail"
fi
done
drain_pending_thumbnails
if [[ -s $pending_file || -s $pending_video_file ]]; then
pruned=""
while IFS=$'\t' read -r row_image row_thumbnail; do
if [[ ! -e $row_thumbnail ]]; then
rows_cacheable=false
continue
fi
if [[ -z $pruned ]]; then
pruned="$row_image"$'\t'"$row_thumbnail"
else
pruned+=$'\n'"$row_image"$'\t'"$row_thumbnail"
fi
done <<<"$rows"
rows="$pruned"
fi
# Publish the cache under a lock and via renames: a picker killed mid-write,
# or two pickers interleaving, must never leave truncated or mismatched rows
# behind signatures that still validate. Rows go first so a kill between
# renames leaves signatures that are either older (a harmless miss) or
# describe the same directory state.
if exec {rows_lock_fd}>"$rows_cache_file.lock" && flock -w 30 "$rows_lock_fd"; then
if [[ $rows_cacheable == true ]]; then
rm -f "$cache_dir/$cache_key".*.tmp
printf '%s' "$rows" >"$rows_cache_file.$$.tmp" && mv -f "$rows_cache_file.$$.tmp" "$rows_cache_file"
printf '%s' "$rows_signature" >"$rows_signature_file.$$.tmp" && mv -f "$rows_signature_file.$$.tmp" "$rows_signature_file"
printf '%s' "$rows_fast_signature" >"$rows_fast_signature_file.$$.tmp" && mv -f "$rows_fast_signature_file.$$.tmp" "$rows_fast_signature_file"
else
rm -f "$rows_cache_file" "$rows_signature_file" "$rows_fast_signature_file"
fi
exec {rows_lock_fd}>&-
fi
fi
if [[ $cache_only == true || $prepare_only == true ]]; then
exit 0
fi
# The shell keeps a copy of these rows so its picker can open without waiting
# on this script, and reads them back from stdout to refresh that copy.
if [[ $print_rows == true ]]; then
printf '%s\n' "$rows"
exit 0
fi
# Image rows can contain newlines and tabs, which don't survive positional
# shell IPC arguments. Base64-encode for transit; the ImagePicker plugin
# Qt.atob()s on the other side.
rows_b64=$(printf '%s' "$rows" | base64 -w 0)
if [[ $preload == true ]]; then
omarchy-shell image-selector preload "$rows_b64" "$selected_list_image" "$show_labels" "$filterable" >/dev/null || true
exit 0
fi
if ! open_result=$(omarchy-shell image-selector open \
"" \
"$rows_b64" \
"$selected_list_image" \
"$selection_file" \
"$done_file" \
"$show_labels" \
"$filterable"); then
echo "Image selector failed to accept request" >&2
exit 1
fi
if [[ $open_result != "ok" ]]; then
echo "Image selector failed to accept request" >&2
exit 1
fi
while [[ ! -e $done_file ]]; do
sleep 0.01
done
if [[ -s $selection_file ]]; then
if [[ $print_name == true ]]; then
selection=$(<"$selection_file")
selection=${selection##*/}
printf '%s\n' "${selection%.*}"
else
cat "$selection_file"
fi
fi