132 lines
5.4 KiB
Bash
132 lines
5.4 KiB
Bash
# shellcheck shell=bash
|
||
# wallpaper-pick — print a random wallpaper from $wallpapersDir whose palette
|
||
# best matches the given theme variant. Per-image palettes are extracted once
|
||
# (ImageMagick) and cached, keyed by the (immutable) source path so a rebuild
|
||
# that changes the wallpaper set reindexes automatically.
|
||
#
|
||
# Matching uses several key colors on both sides: the variant's base16
|
||
# base00–base05 (the background→foreground neutral range) are each matched to
|
||
# their nearest wallpaper dominant color; the sum of those nearest distances
|
||
# is the score. A random image is picked from the lowest-score tier.
|
||
#
|
||
# Env (injected by the Nix wrapper):
|
||
# wallpapersDir directory of wallpaper images (~/Wallpapers, a git clone)
|
||
# wallpaperRepo git URL cloned into wallpapersDir on first use
|
||
# variantSchemes space-separated `variant:schemeFile` pairs (base16 YAML)
|
||
# MAGICK path to the ImageMagick `magick` binary
|
||
# GIT path to the `git` binary
|
||
# Arg: variant name (e.g. gruvbox-dark)
|
||
|
||
: "${wallpapersDir:?wallpaper-pick: wallpapersDir not set}"
|
||
: "${wallpaperRepo:?wallpaper-pick: wallpaperRepo not set}"
|
||
: "${variantSchemes:?wallpaper-pick: variantSchemes not set}"
|
||
: "${MAGICK:?wallpaper-pick: MAGICK not set}"
|
||
: "${GIT:?wallpaper-pick: GIT not set}"
|
||
set -euo pipefail
|
||
|
||
variant="${1:?usage: wallpaper-pick <variant>}"
|
||
|
||
# Ensure the wallpaper repo is cloned locally. The clone is a one-time cost
|
||
# paid on first use (not at build time), keeping `nix build` fast.
|
||
if [ ! -d "$wallpapersDir/.git" ]; then
|
||
log_info "cloning wallpaper repo (one-time, may take a few minutes)..."
|
||
mkdir -p "$(dirname "$wallpapersDir")"
|
||
"$GIT" clone --depth=1 --single-branch "$wallpaperRepo" "$wallpapersDir" >&2
|
||
log_info "clone ready"
|
||
fi
|
||
|
||
cache_dir="${XDG_CACHE_HOME:-$HOME/.cache}/wallpaper-pick"
|
||
palettes="$cache_dir/palettes.tsv"
|
||
stamp_file="$cache_dir/source"
|
||
|
||
# Cache key: the clone's current commit, so a `git pull` reindexes. Falls back
|
||
# to the resolved path if git fails for some reason.
|
||
src_stamp="$("$GIT" -C "$wallpapersDir" rev-parse HEAD 2>/dev/null || readlink -f "$wallpapersDir")"
|
||
|
||
rebuild_cache() {
|
||
img_count="$(find "$wallpapersDir" -type f \( -iname '*.png' -o -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.webp' \) -printf . 2>/dev/null | wc -c)"
|
||
log_info "indexing ${img_count} wallpapers (one-time, building palette cache)..."
|
||
mkdir -p "$cache_dir"
|
||
tmp="$palettes.tmp"
|
||
: > "$tmp"
|
||
jobs="$(nproc 2>/dev/null || echo 4)"
|
||
|
||
# For each image: top dominant colors (by pixel count) as space-separated #hex.
|
||
find "$wallpapersDir" -type f \( -iname '*.png' -o -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.webp' \) -print0 \
|
||
| xargs -0 -P "$jobs" -n1 sh -c '
|
||
MAGICK="$0" img="$1"
|
||
"$MAGICK" "$img" -resize 128x128^ -gravity center -extent 128x128 +dither -colors 8 -format "%c" histogram:info: 2>/dev/null \
|
||
| grep -E "^[[:space:]]*[0-9]+:" | sort -rn | grep -oE "#[0-9A-Fa-f]{6}" | head -6 | paste -sd" " - \
|
||
| { read -r hex; [ -n "$hex" ] && printf "%s\t%s\n" "$img" "$hex"; }
|
||
' "$MAGICK" >> "$tmp" || true
|
||
|
||
mv "$tmp" "$palettes"
|
||
printf '%s\n' "$src_stamp" > "$stamp_file"
|
||
}
|
||
|
||
if [ ! -f "$palettes" ] || [ ! -f "$stamp_file" ] || [ "$(cat "$stamp_file")" != "$src_stamp" ]; then
|
||
rebuild_cache
|
||
fi
|
||
|
||
# Resolve the base16 scheme file for the requested variant.
|
||
scheme=""
|
||
for pair in $variantSchemes; do
|
||
case "$pair" in
|
||
"${variant}":*) scheme="${pair#*:}"; break ;;
|
||
esac
|
||
done
|
||
[ -n "$scheme" ] || { log_error "no scheme mapped for variant '$variant'"; exit 2; }
|
||
|
||
# Theme key colors: base00–base05 hex (one per line, with leading #).
|
||
theme_colors="$(grep -E '^[[:space:]]*base0[0-5]:' "$scheme" | grep -oE '#[0-9A-Fa-f]{6}')"
|
||
[ -n "$theme_colors" ] || { log_error "could not parse base00–05 from $scheme"; exit 2; }
|
||
|
||
# hex (no #) -> decimal rgb on stdout
|
||
hex2dec() { printf '%d %d %d\n' "$((16#${1:0:2}))" "$((16#${1:2:2}))" "$((16#${1:4:2}))"; }
|
||
|
||
# Theme colors as parallel arrays.
|
||
t_r=(); t_g=(); t_b=()
|
||
while read -r h; do
|
||
read -r r g b < <(hex2dec "${h#\#}")
|
||
t_r+=("$r"); t_g+=("$g"); t_b+=("$b")
|
||
done <<< "$theme_colors"
|
||
|
||
# Score every indexed wallpaper: sum over theme colors of nearest wallpaper color.
|
||
scores="$cache_dir/scores.tmp"
|
||
: > "$scores"
|
||
while IFS=$'\t' read -r path whexes; do
|
||
w_r=(); w_g=(); w_b=()
|
||
for wh in $whexes; do
|
||
read -r wr wg wb < <(hex2dec "${wh#\#}")
|
||
w_r+=("$wr"); w_g+=("$wg"); w_b+=("$wb")
|
||
done
|
||
if [ "${#w_r[@]}" -eq 0 ]; then continue; fi
|
||
total=0
|
||
for i in "${!t_r[@]}"; do
|
||
mind=1000000000
|
||
for j in "${!w_r[@]}"; do
|
||
dr=$(( t_r[i] - w_r[j] )); dg=$(( t_g[i] - w_g[j] )); db=$(( t_b[i] - w_b[j] ))
|
||
cd=$(( dr*dr + dg*dg + db*db ))
|
||
if [ "$cd" -lt "$mind" ]; then mind=$cd; fi
|
||
done
|
||
total=$(( total + mind ))
|
||
done
|
||
printf '%s\t%s\n' "$total" "$path"
|
||
done < "$palettes" | sort -n -k1 > "$scores"
|
||
|
||
[ -s "$scores" ] || { log_error "no wallpapers indexed"; exit 3; }
|
||
|
||
# Matched tier: within 1.4x of the best score (fall back to closest 20).
|
||
best="$(head -n1 "$scores" | cut -f1)"
|
||
threshold=$(( best * 14 / 10 ))
|
||
matched="$(awk -F'\t' -v t="$threshold" '$1 <= t {print $2}' "$scores")"
|
||
if [ "$(printf '%s\n' "$matched" | grep -c .)" -lt 5 ]; then
|
||
matched="$(head -n20 "$scores" | cut -f2)"
|
||
fi
|
||
|
||
# Print the chosen PATH on stdout (data channel for callers) and log the
|
||
# human-readable choice on stderr.
|
||
chosen="$(printf '%s\n' "$matched" | shuf -n1)"
|
||
log_info "picked $(basename "$chosen") for ${variant}"
|
||
printf '%s\n' "$chosen"
|