summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-06-13 10:15:20 +0300
committerPaul Buetow <paul@buetow.org>2026-06-13 10:15:20 +0300
commitd4971998edae804c39797486be7a342d43e61e5c (patch)
tree830c008847704b40b7f45d4c7c620beed03958d6
parente9437367b8fc7a4cb0ffed6c0a8ba89356a3b9ad (diff)
Refactor helper modules for im0
-rw-r--r--Justfile2
-rwxr-xr-xbin/shuriken218
-rw-r--r--src/lib/archive.source.sh52
-rw-r--r--src/lib/config.sync.source.sh23
-rw-r--r--src/lib/image.source.sh213
-rw-r--r--src/lib/imagemagick.source.sh71
-rw-r--r--src/lib/job-pool.source.sh137
-rw-r--r--src/lib/process.source.sh24
-rw-r--r--src/lib/random.source.sh74
-rw-r--r--src/lib/system.source.sh172
-rwxr-xr-xsrc/shuriken.sh12
11 files changed, 503 insertions, 495 deletions
diff --git a/Justfile b/Justfile
index 2c0c0b6..841cfb8 100644
--- a/Justfile
+++ b/Justfile
@@ -7,7 +7,7 @@ PREFIX := env_var_or_default("PREFIX", "/usr")
BINDIR := env_var_or_default("BINDIR", PREFIX + "/bin")
DATADIR := env_var_or_default("DATADIR", PREFIX + "/share")
SYSCONFDIR := env_var_or_default("SYSCONFDIR", "/etc/default")
-LIB_SOURCES := "src/lib/bootstrap.source.sh src/lib/system.source.sh src/lib/template.source.sh src/lib/image.source.sh src/lib/album.source.sh src/lib/config.source.sh src/lib/config.print.source.sh src/lib/config.sync.source.sh src/lib/config.staging.source.sh src/lib/config.validate.source.sh src/lib/config.cli.source.sh src/lib/action.source.sh"
+LIB_SOURCES := "src/lib/bootstrap.source.sh src/lib/imagemagick.source.sh src/lib/process.source.sh src/lib/archive.source.sh src/lib/template.source.sh src/lib/job-pool.source.sh src/lib/image.source.sh src/lib/random.source.sh src/lib/album.source.sh src/lib/config.source.sh src/lib/config.print.source.sh src/lib/config.sync.source.sh src/lib/config.staging.source.sh src/lib/config.validate.source.sh src/lib/config.cli.source.sh src/lib/action.source.sh"
default: build
diff --git a/bin/shuriken b/bin/shuriken
index b37aad3..afad2f4 100755
--- a/bin/shuriken
+++ b/bin/shuriken
@@ -268,7 +268,7 @@ init_config() {
log_info "Created ./$rc_file"
}
-# Inlined from src/lib/system.source.sh
+# Inlined from src/lib/imagemagick.source.sh
# shellcheck disable=SC2034
resolve_imagemagick_command() {
local -r operation="$1"; shift
@@ -341,6 +341,7 @@ imagemagick_identify() {
"${imagemagick_command[@]}" "$@"
}
+# Inlined from src/lib/process.source.sh
run_with_timeout() {
local -r description="$1"; shift
local -r seconds="$1"; shift
@@ -366,6 +367,7 @@ run_with_timeout() {
return "$status"
}
+# Inlined from src/lib/archive.source.sh
tarball() {
local -r tarball_name="$1"; shift
local -r tarball_suffix="$TARBALL_SUFFIX"
@@ -419,29 +421,6 @@ resolve_tar_opts() {
fi
}
-resolve_sync_destinations() {
- local -n destinations_ref="$1"; shift
- local destinations_decl
-
- destinations_ref=()
-
- if ! destinations_decl=$(declare -p SYNC_DESTINATIONS 2>/dev/null); then
- return
- fi
-
- case "$destinations_decl" in
- declare\ -a*\ SYNC_DESTINATIONS=*)
- destinations_ref=("${SYNC_DESTINATIONS[@]}")
- ;;
- *)
- if [ -n "${SYNC_DESTINATIONS:-}" ]; then
- # shellcheck disable=SC2034
- read -r -a destinations_ref <<< "$SYNC_DESTINATIONS"
- fi
- ;;
- esac
-}
-
# Inlined from src/lib/template.source.sh
_html_escape() {
local -r text="$1"; shift
@@ -1017,89 +996,7 @@ template() {
fi
}
-# Inlined from src/lib/image.source.sh
-cleanphotos() {
- local basename
- local photo
- local sub
-
- while IFS= read -r photo; do
- basename=$(basename "$photo")
-
- if [[ -f "$INCOMING_DIR/$basename" ]] \
- && is_supported_image_file "$basename"; then
- continue
- fi
-
- log_info "Cleaning up $(_display_path "$photo")"
- for sub in thumbs blurs photos; do
- if [ -f "$DIST_DIR/$sub/$basename" ]; then
- rm -f "$DIST_DIR/$sub/$basename"
- log_info "removed '$(_display_path "$DIST_DIR/$sub/$basename")'"
- fi
- done
- done < <(find "$DIST_DIR/photos" -maxdepth 1 -type f)
-}
-
-is_supported_image_file() {
- local -r file="$1"; shift
- local extension
-
- if [[ "$file" != *.* ]]; then
- return 1
- fi
-
- extension="${file##*.}"
- extension="${extension,,}"
-
- case "$extension" in
- gif|jpeg|jpg|png|webp)
- return 0
- ;;
- *)
- return 1
- ;;
- esac
-}
-
-incoming_image_files() {
- local file
-
- while IFS= read -r file; do
- if is_supported_image_file "$file"; then
- printf '%s\n' "$file"
- fi
- done < <(find "$INCOMING_DIR" -maxdepth 1 -type f -printf '%f\n') \
- | sort
-}
-
-warn_unsupported_incoming_files() {
- local file
-
- while IFS= read -r file; do
- if ! is_supported_image_file "$file"; then
- log_warning "Ignoring unsupported incoming file: $file"
- fi
- done < <(find "$INCOMING_DIR" -maxdepth 1 -type f -printf '%f\n' | sort)
-}
-
-scalephotos() {
- local -i failed=0
- local -a image_job_pids=()
- local photo
-
- while IFS= read -r photo; do
- wait_for_image_job_slot image_job_pids failed
- scale_photo "$photo" &
- image_job_pids+=("$!")
- done < <(incoming_image_files)
-
- wait_for_image_jobs image_job_pids failed
- if (( failed != 0 )); then
- return 1
- fi
-}
-
+# Inlined from src/lib/job-pool.source.sh
wait_for_parallel_job_pid() {
local -r pid="$1"; shift
local -i status=0
@@ -1238,6 +1135,89 @@ wait_for_template_render_jobs() {
wait_for_parallel_jobs "$render_job_pids_name" "$failed_name"
}
+# Inlined from src/lib/image.source.sh
+cleanphotos() {
+ local basename
+ local photo
+ local sub
+
+ while IFS= read -r photo; do
+ basename=$(basename "$photo")
+
+ if [[ -f "$INCOMING_DIR/$basename" ]] \
+ && is_supported_image_file "$basename"; then
+ continue
+ fi
+
+ log_info "Cleaning up $(_display_path "$photo")"
+ for sub in thumbs blurs photos; do
+ if [ -f "$DIST_DIR/$sub/$basename" ]; then
+ rm -f "$DIST_DIR/$sub/$basename"
+ log_info "removed '$(_display_path "$DIST_DIR/$sub/$basename")'"
+ fi
+ done
+ done < <(find "$DIST_DIR/photos" -maxdepth 1 -type f)
+}
+
+is_supported_image_file() {
+ local -r file="$1"; shift
+ local extension
+
+ if [[ "$file" != *.* ]]; then
+ return 1
+ fi
+
+ extension="${file##*.}"
+ extension="${extension,,}"
+
+ case "$extension" in
+ gif|jpeg|jpg|png|webp)
+ return 0
+ ;;
+ *)
+ return 1
+ ;;
+ esac
+}
+
+incoming_image_files() {
+ local file
+
+ while IFS= read -r file; do
+ if is_supported_image_file "$file"; then
+ printf '%s\n' "$file"
+ fi
+ done < <(find "$INCOMING_DIR" -maxdepth 1 -type f -printf '%f\n') \
+ | sort
+}
+
+warn_unsupported_incoming_files() {
+ local file
+
+ while IFS= read -r file; do
+ if ! is_supported_image_file "$file"; then
+ log_warning "Ignoring unsupported incoming file: $file"
+ fi
+ done < <(find "$INCOMING_DIR" -maxdepth 1 -type f -printf '%f\n' | sort)
+}
+
+scalephotos() {
+ local -i failed=0
+ local -a image_job_pids=()
+ local photo
+
+ while IFS= read -r photo; do
+ wait_for_image_job_slot image_job_pids failed
+ scale_photo "$photo" &
+ image_job_pids+=("$!")
+ done < <(incoming_image_files)
+
+ wait_for_image_jobs image_job_pids failed
+ if (( failed != 0 )); then
+ return 1
+ fi
+}
+
scale_photo() {
local -r photo="$1"; shift
local destphoto
@@ -1269,6 +1249,7 @@ scale_photo() {
fi
}
+# Inlined from src/lib/random.source.sh
random_seed_is_set() {
[ -n "$RANDOM_SEED" ]
}
@@ -2625,6 +2606,29 @@ print_config() {
}
# Inlined from src/lib/config.sync.source.sh
+resolve_sync_destinations() {
+ local -n destinations_ref="$1"; shift
+ local destinations_decl
+
+ destinations_ref=()
+
+ if ! destinations_decl=$(declare -p SYNC_DESTINATIONS 2>/dev/null); then
+ return
+ fi
+
+ case "$destinations_decl" in
+ declare\ -a*\ SYNC_DESTINATIONS=*)
+ destinations_ref=("${SYNC_DESTINATIONS[@]}")
+ ;;
+ *)
+ if [ -n "${SYNC_DESTINATIONS:-}" ]; then
+ # shellcheck disable=SC2034
+ read -r -a destinations_ref <<< "$SYNC_DESTINATIONS"
+ fi
+ ;;
+ esac
+}
+
sync_dist() {
local destination
local -a rsync_args=(-av)
diff --git a/src/lib/archive.source.sh b/src/lib/archive.source.sh
new file mode 100644
index 0000000..127a0c5
--- /dev/null
+++ b/src/lib/archive.source.sh
@@ -0,0 +1,52 @@
+tarball() {
+ local -r tarball_name="$1"; shift
+ local -r tarball_suffix="$TARBALL_SUFFIX"
+ local base
+ local old_tarball
+ local -a tar_opts=()
+
+ # Cleanup tarballs from previous runs for the configured suffix.
+ while IFS= read -r -d '' old_tarball; do
+ if [[ "$old_tarball" == *"$tarball_suffix" ]]; then
+ rm -f "$old_tarball"
+ fi
+ done < <(find "$DIST_DIR" -maxdepth 1 -type f -print0)
+
+ base=$(basename "$INCOMING_DIR")
+ resolve_tar_opts tar_opts
+
+ log_info "Creating tarball $(_display_path "$DIST_DIR/$tarball_name")" \
+ "from $INCOMING_DIR"
+ (
+ cd "$(dirname "$INCOMING_DIR")"
+ run_with_timeout tar "$TAR_TIMEOUT" \
+ tar "${tar_opts[@]}" -f "$DIST_DIR/$tarball_name" "$base"
+ )
+}
+
+resolve_tar_opts() {
+ local -n options_ref="$1"; shift
+ local tar_opts_decl
+
+ options_ref=()
+
+ if ! tar_opts_decl=$(declare -p TAR_OPTS 2>/dev/null); then
+ options_ref=(-c)
+ return
+ fi
+
+ case "$tar_opts_decl" in
+ declare\ -a*\ TAR_OPTS=*)
+ options_ref=("${TAR_OPTS[@]}")
+ ;;
+ *)
+ if [ -n "${TAR_OPTS:-}" ]; then
+ read -r -a options_ref <<< "$TAR_OPTS"
+ fi
+ ;;
+ esac
+
+ if (( ${#options_ref[@]} == 0 )); then
+ options_ref=(-c)
+ fi
+}
diff --git a/src/lib/config.sync.source.sh b/src/lib/config.sync.source.sh
index b9362ac..68bb759 100644
--- a/src/lib/config.sync.source.sh
+++ b/src/lib/config.sync.source.sh
@@ -1,3 +1,26 @@
+resolve_sync_destinations() {
+ local -n destinations_ref="$1"; shift
+ local destinations_decl
+
+ destinations_ref=()
+
+ if ! destinations_decl=$(declare -p SYNC_DESTINATIONS 2>/dev/null); then
+ return
+ fi
+
+ case "$destinations_decl" in
+ declare\ -a*\ SYNC_DESTINATIONS=*)
+ destinations_ref=("${SYNC_DESTINATIONS[@]}")
+ ;;
+ *)
+ if [ -n "${SYNC_DESTINATIONS:-}" ]; then
+ # shellcheck disable=SC2034
+ read -r -a destinations_ref <<< "$SYNC_DESTINATIONS"
+ fi
+ ;;
+ esac
+}
+
sync_dist() {
local destination
local -a rsync_args=(-av)
diff --git a/src/lib/image.source.sh b/src/lib/image.source.sh
index 568ab62..c190b87 100644
--- a/src/lib/image.source.sh
+++ b/src/lib/image.source.sh
@@ -80,144 +80,6 @@ scalephotos() {
fi
}
-wait_for_parallel_job_pid() {
- local -r pid="$1"; shift
- local -i status=0
-
- set +e
- wait "$pid"
- status=$?
- set -e
-
- return "$status"
-}
-
-parallel_job_pid_is_running() {
- local -r pid="$1"; shift
- local running_pid
-
- for running_pid in "$@"; do
- if [ "$running_pid" = "$pid" ]; then
- return 0
- fi
- done
-
- return 1
-}
-
-reap_finished_parallel_jobs() {
- local -r job_pids_name="$1"; shift
- # shellcheck disable=SC2178
- local -n job_pids_ref="$job_pids_name"
- local -n failed_ref="$1"; shift
- local -i reaped_status="$1"; shift
- local -i has_reaped_status=1
- local -i status=0
- local -a remaining_pids=()
- local -a running_pids=()
- local pid
-
- mapfile -t running_pids < <(jobs -rp)
-
- for pid in "${job_pids_ref[@]}"; do
- if parallel_job_pid_is_running "$pid" "${running_pids[@]}"; then
- remaining_pids+=("$pid")
- continue
- fi
-
- if wait_for_parallel_job_pid "$pid"; then
- status=0
- else
- status=$?
- fi
- if (( status == 127 && has_reaped_status != 0 )); then
- status=$reaped_status
- has_reaped_status=0
- fi
-
- if (( status != 0 )); then
- failed_ref=1
- fi
- done
-
- job_pids_ref=("${remaining_pids[@]}")
-}
-
-wait_for_next_parallel_job() {
- local -r job_pids_name="$1"; shift
- local -r failed_name="$1"; shift
- # shellcheck disable=SC2178
- local -n job_pids_ref="$job_pids_name"
- local -i status=0
-
- set +e
- wait -n "${job_pids_ref[@]}"
- status=$?
- set -e
-
- reap_finished_parallel_jobs "$job_pids_name" "$failed_name" "$status"
-}
-
-wait_for_parallel_job_slot() {
- local -r job_pids_name="$1"; shift
- local -r failed_name="$1"; shift
- local -r max_jobs="$1"; shift
- # shellcheck disable=SC2178
- local -n job_pids_ref="$job_pids_name"
-
- while (( ${#job_pids_ref[@]} >= max_jobs )); do
- wait_for_next_parallel_job "$job_pids_name" "$failed_name"
- done
-}
-
-wait_for_parallel_jobs() {
- # shellcheck disable=SC2178
- local -n job_pids_ref="$1"; shift
- local -n failed_ref="$1"; shift
-
- : "$failed_ref"
- while (( ${#job_pids_ref[@]} > 0 )); do
- if ! wait_for_parallel_job_pid "${job_pids_ref[0]}"; then
- failed_ref=1
- fi
- job_pids_ref=("${job_pids_ref[@]:1}")
- done
-}
-
-wait_for_image_job_slot() {
- local -r image_job_pids_name="$1"; shift
- local -r failed_name="$1"; shift
-
- wait_for_parallel_job_slot \
- "$image_job_pids_name" \
- "$failed_name" \
- "$IMAGE_JOBS"
-}
-
-wait_for_image_jobs() {
- local -r image_job_pids_name="$1"; shift
- local -r failed_name="$1"; shift
-
- wait_for_parallel_jobs "$image_job_pids_name" "$failed_name"
-}
-
-wait_for_template_render_job_slot() {
- local -r render_job_pids_name="$1"; shift
- local -r failed_name="$1"; shift
-
- wait_for_parallel_job_slot \
- "$render_job_pids_name" \
- "$failed_name" \
- "$IMAGE_JOBS"
-}
-
-wait_for_template_render_jobs() {
- local -r render_job_pids_name="$1"; shift
- local -r failed_name="$1"; shift
-
- wait_for_parallel_jobs "$render_job_pids_name" "$failed_name"
-}
-
scale_photo() {
local -r photo="$1"; shift
local destphoto
@@ -248,78 +110,3 @@ scale_photo() {
"$destphoto"
fi
}
-
-random_seed_is_set() {
- [ -n "$RANDOM_SEED" ]
-}
-
-deterministic_index() {
- local -r namespace="$1"; shift
- local -r count="$1"; shift
- local checksum
-
- checksum=$(printf '%s' "${RANDOM_SEED}:$namespace" | cksum)
- checksum=${checksum%% *}
-
- printf '%s\n' $(( checksum % count ))
-}
-
-random_index() {
- local -r namespace="$1"; shift
- local -r count="$1"; shift
-
- if random_seed_is_set; then
- deterministic_index "$namespace" "$count"
- else
- printf '%s\n' $(( RANDOM % count ))
- fi
-}
-
-random_animation_css_class() {
- local -r speed="$1"; shift
- local -r context="${1:-$speed}"
- local -i index
- local -a classes=(
- "animate-opacity-$speed"
- "animate-top-$speed"
- "animate-left-$speed"
- "animate-right-$speed"
- "animate-bottom-$speed"
- "animate-zoom-$speed"
- "animate-snap-rotate-$speed"
- "animate-hard-zoom-$speed"
- "animate-slam-left-$speed"
- "animate-slam-right-$speed"
- "animate-flash-in-$speed"
- "animate-invert-pop-$speed"
- "animate-posterize-pop-$speed"
- "animate-skew-snap-$speed"
- "animate-glitch-step-$speed"
- )
-
- index=$(random_index "animation:$speed:$context" "${#classes[@]}")
- printf '%s\n' "${classes[index]}"
-}
-
-deterministic_shuffle() {
- local checksum
- local line
-
- while IFS= read -r line; do
- checksum=$(printf '%s' "${RANDOM_SEED}:shuffle:$line" | cksum)
- checksum=${checksum%% *}
- printf '%010u\t%s\n' "$checksum" "$line"
- done | sort -n -k1,1 -k2,2 | cut -f2-
-}
-
-maybe_shuffle() {
- if [ "$SHUFFLE" = yes ]; then
- if random_seed_is_set; then
- deterministic_shuffle
- else
- sort -R
- fi
- else
- sort
- fi
-}
diff --git a/src/lib/imagemagick.source.sh b/src/lib/imagemagick.source.sh
new file mode 100644
index 0000000..cdc909c
--- /dev/null
+++ b/src/lib/imagemagick.source.sh
@@ -0,0 +1,71 @@
+# shellcheck disable=SC2034
+resolve_imagemagick_command() {
+ local -r operation="$1"; shift
+ local -n command_ref="$1"; shift
+
+ command_ref=()
+
+ case "$operation" in
+ convert)
+ if command -v magick >/dev/null 2>&1; then
+ command_ref=(magick)
+ return
+ fi
+ if command -v convert >/dev/null 2>&1; then
+ command_ref=(convert)
+ return
+ fi
+ ;;
+ identify)
+ if command -v magick >/dev/null 2>&1; then
+ command_ref=(magick identify)
+ return
+ fi
+ if command -v identify >/dev/null 2>&1; then
+ command_ref=(identify)
+ return
+ fi
+ if command -v convert >/dev/null 2>&1; then
+ command_ref=(convert)
+ return
+ fi
+ ;;
+ *)
+ printf 'ERROR: Unknown ImageMagick operation %s\n' \
+ "$operation" >&2
+ return 1
+ ;;
+ esac
+
+ printf 'ERROR: ImageMagick is required; install magick or convert\n' >&2
+ return 127
+}
+
+imagemagick() {
+ local -a imagemagick_command=()
+
+ resolve_imagemagick_command convert imagemagick_command || return
+
+ run_with_timeout ImageMagick "$IMAGEMAGICK_TIMEOUT" \
+ "${imagemagick_command[@]}" "$@"
+}
+
+imagemagick_identify() {
+ local -a imagemagick_command=()
+
+ resolve_imagemagick_command identify imagemagick_command || return
+
+ if [ "${imagemagick_command[0]}" = convert ]; then
+ if [[ "${1:-}" = '-verbose' && $# -eq 2 ]]; then
+ run_with_timeout ImageMagick "$IMAGEMAGICK_TIMEOUT" \
+ "${imagemagick_command[@]}" "$2" -verbose info:
+ else
+ run_with_timeout ImageMagick "$IMAGEMAGICK_TIMEOUT" \
+ "${imagemagick_command[@]}" "$@" info:
+ fi
+ return
+ fi
+
+ run_with_timeout ImageMagick "$IMAGEMAGICK_TIMEOUT" \
+ "${imagemagick_command[@]}" "$@"
+}
diff --git a/src/lib/job-pool.source.sh b/src/lib/job-pool.source.sh
new file mode 100644
index 0000000..5c629aa
--- /dev/null
+++ b/src/lib/job-pool.source.sh
@@ -0,0 +1,137 @@
+wait_for_parallel_job_pid() {
+ local -r pid="$1"; shift
+ local -i status=0
+
+ set +e
+ wait "$pid"
+ status=$?
+ set -e
+
+ return "$status"
+}
+
+parallel_job_pid_is_running() {
+ local -r pid="$1"; shift
+ local running_pid
+
+ for running_pid in "$@"; do
+ if [ "$running_pid" = "$pid" ]; then
+ return 0
+ fi
+ done
+
+ return 1
+}
+
+reap_finished_parallel_jobs() {
+ local -r job_pids_name="$1"; shift
+ # shellcheck disable=SC2178
+ local -n job_pids_ref="$job_pids_name"
+ local -n failed_ref="$1"; shift
+ local -i reaped_status="$1"; shift
+ local -i has_reaped_status=1
+ local -i status=0
+ local -a remaining_pids=()
+ local -a running_pids=()
+ local pid
+
+ mapfile -t running_pids < <(jobs -rp)
+
+ for pid in "${job_pids_ref[@]}"; do
+ if parallel_job_pid_is_running "$pid" "${running_pids[@]}"; then
+ remaining_pids+=("$pid")
+ continue
+ fi
+
+ if wait_for_parallel_job_pid "$pid"; then
+ status=0
+ else
+ status=$?
+ fi
+ if (( status == 127 && has_reaped_status != 0 )); then
+ status=$reaped_status
+ has_reaped_status=0
+ fi
+
+ if (( status != 0 )); then
+ failed_ref=1
+ fi
+ done
+
+ job_pids_ref=("${remaining_pids[@]}")
+}
+
+wait_for_next_parallel_job() {
+ local -r job_pids_name="$1"; shift
+ local -r failed_name="$1"; shift
+ # shellcheck disable=SC2178
+ local -n job_pids_ref="$job_pids_name"
+ local -i status=0
+
+ set +e
+ wait -n "${job_pids_ref[@]}"
+ status=$?
+ set -e
+
+ reap_finished_parallel_jobs "$job_pids_name" "$failed_name" "$status"
+}
+
+wait_for_parallel_job_slot() {
+ local -r job_pids_name="$1"; shift
+ local -r failed_name="$1"; shift
+ local -r max_jobs="$1"; shift
+ # shellcheck disable=SC2178
+ local -n job_pids_ref="$job_pids_name"
+
+ while (( ${#job_pids_ref[@]} >= max_jobs )); do
+ wait_for_next_parallel_job "$job_pids_name" "$failed_name"
+ done
+}
+
+wait_for_parallel_jobs() {
+ # shellcheck disable=SC2178
+ local -n job_pids_ref="$1"; shift
+ local -n failed_ref="$1"; shift
+
+ : "$failed_ref"
+ while (( ${#job_pids_ref[@]} > 0 )); do
+ if ! wait_for_parallel_job_pid "${job_pids_ref[0]}"; then
+ failed_ref=1
+ fi
+ job_pids_ref=("${job_pids_ref[@]:1}")
+ done
+}
+
+wait_for_image_job_slot() {
+ local -r image_job_pids_name="$1"; shift
+ local -r failed_name="$1"; shift
+
+ wait_for_parallel_job_slot \
+ "$image_job_pids_name" \
+ "$failed_name" \
+ "$IMAGE_JOBS"
+}
+
+wait_for_image_jobs() {
+ local -r image_job_pids_name="$1"; shift
+ local -r failed_name="$1"; shift
+
+ wait_for_parallel_jobs "$image_job_pids_name" "$failed_name"
+}
+
+wait_for_template_render_job_slot() {
+ local -r render_job_pids_name="$1"; shift
+ local -r failed_name="$1"; shift
+
+ wait_for_parallel_job_slot \
+ "$render_job_pids_name" \
+ "$failed_name" \
+ "$IMAGE_JOBS"
+}
+
+wait_for_template_render_jobs() {
+ local -r render_job_pids_name="$1"; shift
+ local -r failed_name="$1"; shift
+
+ wait_for_parallel_jobs "$render_job_pids_name" "$failed_name"
+}
diff --git a/src/lib/process.source.sh b/src/lib/process.source.sh
new file mode 100644
index 0000000..5596e03
--- /dev/null
+++ b/src/lib/process.source.sh
@@ -0,0 +1,24 @@
+run_with_timeout() {
+ local -r description="$1"; shift
+ local -r seconds="$1"; shift
+ local -i status=0
+
+ if ! command -v timeout >/dev/null 2>&1; then
+ printf 'ERROR: timeout command is required to run %s\n' \
+ "$description" >&2
+ return 127
+ fi
+
+ if timeout "$seconds" "$@"; then
+ return
+ else
+ status=$?
+ fi
+
+ if (( status == 124 )); then
+ printf 'ERROR: %s timed out after %s seconds\n' \
+ "$description" "$seconds" >&2
+ fi
+
+ return "$status"
+}
diff --git a/src/lib/random.source.sh b/src/lib/random.source.sh
new file mode 100644
index 0000000..5a1ef59
--- /dev/null
+++ b/src/lib/random.source.sh
@@ -0,0 +1,74 @@
+random_seed_is_set() {
+ [ -n "$RANDOM_SEED" ]
+}
+
+deterministic_index() {
+ local -r namespace="$1"; shift
+ local -r count="$1"; shift
+ local checksum
+
+ checksum=$(printf '%s' "${RANDOM_SEED}:$namespace" | cksum)
+ checksum=${checksum%% *}
+
+ printf '%s\n' $(( checksum % count ))
+}
+
+random_index() {
+ local -r namespace="$1"; shift
+ local -r count="$1"; shift
+
+ if random_seed_is_set; then
+ deterministic_index "$namespace" "$count"
+ else
+ printf '%s\n' $(( RANDOM % count ))
+ fi
+}
+
+random_animation_css_class() {
+ local -r speed="$1"; shift
+ local -r context="${1:-$speed}"
+ local -i index
+ local -a classes=(
+ "animate-opacity-$speed"
+ "animate-top-$speed"
+ "animate-left-$speed"
+ "animate-right-$speed"
+ "animate-bottom-$speed"
+ "animate-zoom-$speed"
+ "animate-snap-rotate-$speed"
+ "animate-hard-zoom-$speed"
+ "animate-slam-left-$speed"
+ "animate-slam-right-$speed"
+ "animate-flash-in-$speed"
+ "animate-invert-pop-$speed"
+ "animate-posterize-pop-$speed"
+ "animate-skew-snap-$speed"
+ "animate-glitch-step-$speed"
+ )
+
+ index=$(random_index "animation:$speed:$context" "${#classes[@]}")
+ printf '%s\n' "${classes[index]}"
+}
+
+deterministic_shuffle() {
+ local checksum
+ local line
+
+ while IFS= read -r line; do
+ checksum=$(printf '%s' "${RANDOM_SEED}:shuffle:$line" | cksum)
+ checksum=${checksum%% *}
+ printf '%010u\t%s\n' "$checksum" "$line"
+ done | sort -n -k1,1 -k2,2 | cut -f2-
+}
+
+maybe_shuffle() {
+ if [ "$SHUFFLE" = yes ]; then
+ if random_seed_is_set; then
+ deterministic_shuffle
+ else
+ sort -R
+ fi
+ else
+ sort
+ fi
+}
diff --git a/src/lib/system.source.sh b/src/lib/system.source.sh
deleted file mode 100644
index 5d6f8e7..0000000
--- a/src/lib/system.source.sh
+++ /dev/null
@@ -1,172 +0,0 @@
-# shellcheck disable=SC2034
-resolve_imagemagick_command() {
- local -r operation="$1"; shift
- local -n command_ref="$1"; shift
-
- command_ref=()
-
- case "$operation" in
- convert)
- if command -v magick >/dev/null 2>&1; then
- command_ref=(magick)
- return
- fi
- if command -v convert >/dev/null 2>&1; then
- command_ref=(convert)
- return
- fi
- ;;
- identify)
- if command -v magick >/dev/null 2>&1; then
- command_ref=(magick identify)
- return
- fi
- if command -v identify >/dev/null 2>&1; then
- command_ref=(identify)
- return
- fi
- if command -v convert >/dev/null 2>&1; then
- command_ref=(convert)
- return
- fi
- ;;
- *)
- printf 'ERROR: Unknown ImageMagick operation %s\n' \
- "$operation" >&2
- return 1
- ;;
- esac
-
- printf 'ERROR: ImageMagick is required; install magick or convert\n' >&2
- return 127
-}
-
-imagemagick() {
- local -a imagemagick_command=()
-
- resolve_imagemagick_command convert imagemagick_command || return
-
- run_with_timeout ImageMagick "$IMAGEMAGICK_TIMEOUT" \
- "${imagemagick_command[@]}" "$@"
-}
-
-imagemagick_identify() {
- local -a imagemagick_command=()
-
- resolve_imagemagick_command identify imagemagick_command || return
-
- if [ "${imagemagick_command[0]}" = convert ]; then
- if [[ "${1:-}" = '-verbose' && $# -eq 2 ]]; then
- run_with_timeout ImageMagick "$IMAGEMAGICK_TIMEOUT" \
- "${imagemagick_command[@]}" "$2" -verbose info:
- else
- run_with_timeout ImageMagick "$IMAGEMAGICK_TIMEOUT" \
- "${imagemagick_command[@]}" "$@" info:
- fi
- return
- fi
-
- run_with_timeout ImageMagick "$IMAGEMAGICK_TIMEOUT" \
- "${imagemagick_command[@]}" "$@"
-}
-
-run_with_timeout() {
- local -r description="$1"; shift
- local -r seconds="$1"; shift
- local -i status=0
-
- if ! command -v timeout >/dev/null 2>&1; then
- printf 'ERROR: timeout command is required to run %s\n' \
- "$description" >&2
- return 127
- fi
-
- if timeout "$seconds" "$@"; then
- return
- else
- status=$?
- fi
-
- if (( status == 124 )); then
- printf 'ERROR: %s timed out after %s seconds\n' \
- "$description" "$seconds" >&2
- fi
-
- return "$status"
-}
-
-tarball() {
- local -r tarball_name="$1"; shift
- local -r tarball_suffix="$TARBALL_SUFFIX"
- local base
- local old_tarball
- local -a tar_opts=()
-
- # Cleanup tarballs from previous runs for the configured suffix.
- while IFS= read -r -d '' old_tarball; do
- if [[ "$old_tarball" == *"$tarball_suffix" ]]; then
- rm -f "$old_tarball"
- fi
- done < <(find "$DIST_DIR" -maxdepth 1 -type f -print0)
-
- base=$(basename "$INCOMING_DIR")
- resolve_tar_opts tar_opts
-
- log_info "Creating tarball $(_display_path "$DIST_DIR/$tarball_name")" \
- "from $INCOMING_DIR"
- (
- cd "$(dirname "$INCOMING_DIR")"
- run_with_timeout tar "$TAR_TIMEOUT" \
- tar "${tar_opts[@]}" -f "$DIST_DIR/$tarball_name" "$base"
- )
-}
-
-resolve_tar_opts() {
- local -n options_ref="$1"; shift
- local tar_opts_decl
-
- options_ref=()
-
- if ! tar_opts_decl=$(declare -p TAR_OPTS 2>/dev/null); then
- options_ref=(-c)
- return
- fi
-
- case "$tar_opts_decl" in
- declare\ -a*\ TAR_OPTS=*)
- options_ref=("${TAR_OPTS[@]}")
- ;;
- *)
- if [ -n "${TAR_OPTS:-}" ]; then
- read -r -a options_ref <<< "$TAR_OPTS"
- fi
- ;;
- esac
-
- if (( ${#options_ref[@]} == 0 )); then
- options_ref=(-c)
- fi
-}
-
-resolve_sync_destinations() {
- local -n destinations_ref="$1"; shift
- local destinations_decl
-
- destinations_ref=()
-
- if ! destinations_decl=$(declare -p SYNC_DESTINATIONS 2>/dev/null); then
- return
- fi
-
- case "$destinations_decl" in
- declare\ -a*\ SYNC_DESTINATIONS=*)
- destinations_ref=("${SYNC_DESTINATIONS[@]}")
- ;;
- *)
- if [ -n "${SYNC_DESTINATIONS:-}" ]; then
- # shellcheck disable=SC2034
- read -r -a destinations_ref <<< "$SYNC_DESTINATIONS"
- fi
- ;;
- esac
-}
diff --git a/src/shuriken.sh b/src/