Files
arthur-os/bin/omarchy-menu-images
T

311 lines
8.7 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] <image-dir>...
selected_image=""
print_name=false
show_labels=false
filterable=false
lazy_thumbnails=false
prepare_only=false
preload=false
cache_only=false
image_dirs=()
usage() {
echo "Usage: omarchy-menu-images [--selected <image>] [--print-name] [--show-labels] [--filterable] [--lazy-thumbnails] [--preload] [--cache-only] <image-dir>..."
}
while [[ $# -gt 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
;;
--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)
rm -f "$done_file"
trap 'rm -f "$selection_file" "$done_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="v2"$'\n'
rows_fast_signature="v1"$'\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' \) -print0 2>/dev/null | sort -z)
fi
done
fi
generate_thumbnail() {
local image="$1"
local thumbnail="$2"
local lock="$thumbnail.lock"
local tmp="$thumbnail.$$.jpg"
if mkdir "$lock" 2>/dev/null; then
if magick "${image}[0]" -auto-orient -resize '1536x864^' -gravity center -extent '1536x864' -strip -quality 82 "$tmp"; then
mv -f "$tmp" "$thumbnail"
else
rm -f "$tmp" "$thumbnail"
fi
rmdir "$lock" 2>/dev/null || true
else
for ((i = 0; i < 3000; i++)); do
[[ -f $thumbnail ]] && return
[[ -d $lock ]] || break
sleep 0.01
done
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
if [[ $lazy_thumbnails == true && $cache_only != true ]]; then
rows_cacheable=false
if [[ $prepare_only != true ]]; then
generate_thumbnail "$image" "$thumbnail" >/dev/null 2>&1 &
fi
printf '%s' "$image"
return
fi
generate_thumbnail "$image" "$thumbnail"
fi
[[ -f $thumbnail ]] && printf '%s' "$thumbnail"
}
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
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
if [[ $rows_cacheable == true ]]; then
printf '%s' "$rows" >"$rows_cache_file"
printf '%s' "$rows_signature" >"$rows_signature_file"
printf '%s' "$rows_fast_signature" >"$rows_fast_signature_file"
else
rm -f "$rows_cache_file" "$rows_signature_file" "$rows_fast_signature_file"
fi
fi
if [[ $cache_only == true || $prepare_only == true ]]; then
exit 0
fi
# Image rows can contain newlines and tabs, which don't survive a positional
# bash argv into `quickshell ipc call`. 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-ipc image-selector preload "$rows_b64" "$selected_list_image" "$show_labels" "$filterable" >/dev/null || true
exit 0
fi
send_builtin_ipc_request() {
perl -MCwd=abs_path -MEncode=encode,decode -MSocket \
-e '
sub read_qstring {
my ($data, $offset) = @_;
return ("", $offset) if $offset + 4 > length($data);
my $length = unpack("N", substr($data, $offset, 4));
$offset += 4;
return ("", $offset) if $length == 0xffffffff || $offset + $length > length($data);
return (decode("UTF-16BE", substr($data, $offset, $length)), $offset + $length);
}
sub qstring {
my $encoded = encode("UTF-16BE", $_[0] // "");
return pack("N", length($encoded)) . $encoded;
}
my $shell_qml = abs_path(shift @ARGV);
my @open_args = @ARGV;
my $runtime_dir = $ENV{"XDG_RUNTIME_DIR"} || "/run/user/$<";
my $payload = chr(3) . qstring("image-selector") . qstring("open") . pack("N", scalar @open_args) . join("", map { qstring($_) } @open_args);
my @candidates;
for my $lock_path (glob("$runtime_dir/quickshell/by-id/*/instance.lock")) {
my $data;
next unless open(my $lock, "<:raw", $lock_path);
{ local $/; $data = <$lock>; }
close($lock);
my (undef, $offset) = read_qstring($data, 0);
my ($path) = read_qstring($data, $offset);
next unless $path && abs_path($path) eq $shell_qml;
(my $socket_path = $lock_path) =~ s{/instance\.lock$}{/ipc.sock};
push @candidates, [(stat($lock_path))[9] || 0, $socket_path];
}
for my $candidate (sort { $b->[0] <=> $a->[0] } @candidates) {
socket(my $client, AF_UNIX, SOCK_STREAM, 0) || next;
if (connect($client, sockaddr_un($candidate->[1]))) {
syswrite($client, $payload);
close($client);
exit 0;
}
close($client);
}
exit 1;
' "$OMARCHY_PATH/default/quickshell/omarchy-shell/shell.qml" \
"" "$rows_b64" "$selected_list_image" "$selection_file" "$done_file" "$show_labels" "$filterable"
}
if ! send_builtin_ipc_request && ! omarchy-shell-ipc image-selector open \
"" \
"$rows_b64" \
"$selected_list_image" \
"$selection_file" \
"$done_file" \
"$show_labels" \
"$filterable" >/dev/null; 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