Files
Nixos/hm-modules/theme/wallpaper-pick.sh
T
2026-07-03 19:14:45 +02:00

132 lines
5.4 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
# base00base05 (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: base00base05 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 base0005 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"