From 72d24e3b13560f94bdcfd986fda47b20f0ad4227 Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Mon, 15 Jun 2026 23:36:14 +0300 Subject: Make the favicon configurable via FAVICON config / --favicon flag The generated pages link a favicon.ico that was always the bundled shuriken favicon (copy_site_favicon hard-copied share/shuriken/assets/favicon.ico). Add a FAVICON config variable and a --favicon PATH CLI flag: when set, that file is published as favicon.ico instead of the bundled default; when empty, the bundled favicon is used as before. Plumbed through apply_config_defaults, CLI_OPTION_SPEC + override allowlist, usage, print_config, the action config list and effective-setting log, and validated (a non-empty FAVICON must be a readable file) before generation. shuriken.default.conf and the README document it; a test covers a custom favicon, its appearance in --print-config, and rejection of a missing file. Co-Authored-By: Claude Opus 4.8 --- README.md | 14 +++++++---- bin/shuriken | 32 ++++++++++++++++++++++-- src/lib/action.source.sh | 2 ++ src/lib/album.source.sh | 10 ++++++-- src/lib/bootstrap.source.sh | 1 + src/lib/config.print.source.sh | 1 + src/lib/config.source.sh | 3 +++ src/lib/config.validate.source.sh | 13 ++++++++++ src/shuriken.default.conf | 4 +++ src/shuriken.sh | 2 ++ tests/cli.sh | 51 +++++++++++++++++++++++++++++++++++++++ 11 files changed, 124 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 219d051..9e9dc81 100644 --- a/README.md +++ b/README.md @@ -74,9 +74,9 @@ When `--config PATH` is not provided, `--generate`, `--dry-run`, If the file is missing, run `shuriken --init` first. The config file is a Bash file with assignments such as `INCOMING_DIR`, -`DIST_DIR`, `TEMPLATE_DIR`, `TITLE`, `HEIGHT`, `THUMBHEIGHT`, `MAXPREVIEWS`, -`IMAGE_JOBS`, `IMAGEMAGICK_TIMEOUT`, `RANDOM_SEED`, `SHUFFLE`, `SPLASH_PAGE`, -`STATS_PAGE`, `TARBALL_INCLUDE`, `TAR_TIMEOUT`, `SYNC_DELETE`, and +`DIST_DIR`, `TEMPLATE_DIR`, `FAVICON`, `TITLE`, `HEIGHT`, `THUMBHEIGHT`, +`MAXPREVIEWS`, `IMAGE_JOBS`, `IMAGEMAGICK_TIMEOUT`, `RANDOM_SEED`, `SHUFFLE`, +`SPLASH_PAGE`, `STATS_PAGE`, `TARBALL_INCLUDE`, `TAR_TIMEOUT`, `SYNC_DELETE`, and `SYNC_DESTINATIONS`. Before generating, `shuriken` validates the loaded config and command-line @@ -95,7 +95,8 @@ values that generation would use after applying command-line overrides. Its tarball filename uses `` as a placeholder so the output is stable. `--print-config` writes stable shell-style assignments to stdout in this order: -`CONFIG_SOURCE`, `INCOMING_DIR`, `DIST_DIR`, `TEMPLATE_DIR`, `TITLE`, `HEIGHT`, +`CONFIG_SOURCE`, `INCOMING_DIR`, `DIST_DIR`, `TEMPLATE_DIR`, `FAVICON`, `TITLE`, +`HEIGHT`, `THUMBHEIGHT`, `MAXPREVIEWS`, `IMAGE_JOBS`, `IMAGEMAGICK_TIMEOUT`, `RANDOM_SEED`, `SHUFFLE`, `SPLASH_PAGE`, `STATS_PAGE`, `TARBALL_INCLUDE`, `TARBALL_SUFFIX`, `TAR_TIMEOUT`, `TAR_OPTS`, `SYNC_DELETE`, @@ -110,7 +111,9 @@ metadata records the generator version and timestamp, config source, template directory, supported source image and generated file counts, tarball status, and effective settings useful for debugging a published album. Generation also writes `favicon.ico` into the output directory and the default -templates link to it. +templates link to it. By default this is the bundled shuriken favicon; set +`FAVICON` in the config or pass `--favicon PATH` to publish your own favicon +file instead (it is copied in as `favicon.ico`). Normal generation preserves reusable generated artifacts from the previous `DIST_DIR` while still rerendering HTML, random splash/background choices, @@ -139,6 +142,7 @@ The following long options override config values: | `--incoming PATH` | `INCOMING_DIR` | | `--dist PATH` | `DIST_DIR` | | `--template PATH` | `TEMPLATE_DIR` | +| `--favicon PATH` | `FAVICON` | | `--title TEXT` | `TITLE` | | `--height VALUE` | `HEIGHT` | | `--thumbheight VALUE` | `THUMBHEIGHT` | diff --git a/bin/shuriken b/bin/shuriken index 71c2407..84fae84 100755 --- a/bin/shuriken +++ b/bin/shuriken @@ -32,6 +32,7 @@ declare -ra CLI_CONFIG_OVERRIDE_TARGETS=( INCOMING_DIR DIST_DIR TEMPLATE_DIR + FAVICON TITLE HEIGHT THUMBHEIGHT @@ -46,6 +47,7 @@ declare -ra CLI_CONFIG_OVERRIDE_TARGETS=( ) declare -Ar CLI_OPTION_SPEC=( [--config]='kind=value target=SHURIKEN_CLI_CONFIG_FILE argument=path' + [--favicon]='kind=value config=FAVICON argument=path' [--incoming]='kind=value config=INCOMING_DIR' [--dist]='kind=value config=DIST_DIR' [--template]='kind=value config=TEMPLATE_DIR' @@ -102,6 +104,7 @@ usage() { --incoming PATH --dist PATH --template PATH + --favicon PATH --title TEXT --height VALUE --thumbheight VALUE @@ -2562,8 +2565,14 @@ copy_site_favicon() { local asset_dir local favicon_src - asset_dir=$(resolve_default_asset_dir) - favicon_src="$asset_dir/favicon.ico" + # Use the configured favicon when set, otherwise the bundled default. Either + # way it is published as favicon.ico (the name the templates link to). + if [ -n "${FAVICON:-}" ]; then + favicon_src="$FAVICON" + else + asset_dir=$(resolve_default_asset_dir) + favicon_src="$asset_dir/favicon.ico" + fi if [ ! -r "$favicon_src" ]; then config_error "favicon file $favicon_src must be readable" @@ -4098,6 +4107,9 @@ missing_config() { } apply_config_defaults() { + # Empty FAVICON means use the bundled default favicon; otherwise it is a path + # to a custom favicon file copied into the album as favicon.ico. + FAVICON="${FAVICON:-}" HEIGHT="${HEIGHT:-}" IMAGE_JOBS="${IMAGE_JOBS:-3}" IMAGEMAGICK_TIMEOUT="${IMAGEMAGICK_TIMEOUT:-60}" @@ -4148,6 +4160,7 @@ print_config() { print_shell_assignment INCOMING_DIR "$INCOMING_DIR" print_shell_assignment DIST_DIR "$DIST_DIR" print_shell_assignment TEMPLATE_DIR "$TEMPLATE_DIR" + print_shell_assignment FAVICON "$FAVICON" print_shell_assignment TITLE "$TITLE" print_shell_assignment HEIGHT "$HEIGHT" print_shell_assignment THUMBHEIGHT "$THUMBHEIGHT" @@ -4625,6 +4638,19 @@ validate_common_config() { validate_yes_no_config_var SPLASH_PAGE || return validate_yes_no_config_var STATS_PAGE || return validate_yes_no_config_var TARBALL_INCLUDE || return + validate_favicon_config || return +} + +# A custom FAVICON (when set) must be a readable file; empty means the bundled +# default favicon is used. +validate_favicon_config() { + if [ -z "${FAVICON:-}" ]; then + return + fi + if [ ! -f "$FAVICON" ] || [ ! -r "$FAVICON" ]; then + config_error "FAVICON file $FAVICON must be a readable file" + return 1 + fi } validate_generation_config() { @@ -4882,6 +4908,7 @@ run_action_body_context() { INCOMING_DIR DIST_DIR TEMPLATE_DIR + FAVICON TITLE HEIGHT THUMBHEIGHT @@ -4995,6 +5022,7 @@ log_configured_action() { log_verbose "Effective incoming directory: ${INCOMING_DIR:-}" log_verbose "Effective output directory: ${DIST_DIR:-}" log_verbose "Effective template directory: ${TEMPLATE_DIR:-}" + log_verbose "Effective favicon: ${FAVICON:-(bundled default)}" log_verbose "Effective image jobs: $IMAGE_JOBS" log_verbose "Effective ImageMagick timeout: ${IMAGEMAGICK_TIMEOUT}s" log_verbose "Effective tar timeout: ${TAR_TIMEOUT}s" diff --git a/src/lib/action.source.sh b/src/lib/action.source.sh index e2649d7..12be053 100644 --- a/src/lib/action.source.sh +++ b/src/lib/action.source.sh @@ -42,6 +42,7 @@ run_action_body_context() { INCOMING_DIR DIST_DIR TEMPLATE_DIR + FAVICON TITLE HEIGHT THUMBHEIGHT @@ -155,6 +156,7 @@ log_configured_action() { log_verbose "Effective incoming directory: ${INCOMING_DIR:-}" log_verbose "Effective output directory: ${DIST_DIR:-}" log_verbose "Effective template directory: ${TEMPLATE_DIR:-}" + log_verbose "Effective favicon: ${FAVICON:-(bundled default)}" log_verbose "Effective image jobs: $IMAGE_JOBS" log_verbose "Effective ImageMagick timeout: ${IMAGEMAGICK_TIMEOUT}s" log_verbose "Effective tar timeout: ${TAR_TIMEOUT}s" diff --git a/src/lib/album.source.sh b/src/lib/album.source.sh index 532c218..08509fb 100644 --- a/src/lib/album.source.sh +++ b/src/lib/album.source.sh @@ -1047,8 +1047,14 @@ copy_site_favicon() { local asset_dir local favicon_src - asset_dir=$(resolve_default_asset_dir) - favicon_src="$asset_dir/favicon.ico" + # Use the configured favicon when set, otherwise the bundled default. Either + # way it is published as favicon.ico (the name the templates link to). + if [ -n "${FAVICON:-}" ]; then + favicon_src="$FAVICON" + else + asset_dir=$(resolve_default_asset_dir) + favicon_src="$asset_dir/favicon.ico" + fi if [ ! -r "$favicon_src" ]; then config_error "favicon file $favicon_src must be readable" diff --git a/src/lib/bootstrap.source.sh b/src/lib/bootstrap.source.sh index a047f5d..2b19e58 100644 --- a/src/lib/bootstrap.source.sh +++ b/src/lib/bootstrap.source.sh @@ -15,6 +15,7 @@ usage() { --incoming PATH --dist PATH --template PATH + --favicon PATH --title TEXT --height VALUE --thumbheight VALUE diff --git a/src/lib/config.print.source.sh b/src/lib/config.print.source.sh index 7b91b60..c5fe2b4 100644 --- a/src/lib/config.print.source.sh +++ b/src/lib/config.print.source.sh @@ -27,6 +27,7 @@ print_config() { print_shell_assignment INCOMING_DIR "$INCOMING_DIR" print_shell_assignment DIST_DIR "$DIST_DIR" print_shell_assignment TEMPLATE_DIR "$TEMPLATE_DIR" + print_shell_assignment FAVICON "$FAVICON" print_shell_assignment TITLE "$TITLE" print_shell_assignment HEIGHT "$HEIGHT" print_shell_assignment THUMBHEIGHT "$THUMBHEIGHT" diff --git a/src/lib/config.source.sh b/src/lib/config.source.sh index 50c331a..c13e807 100644 --- a/src/lib/config.source.sh +++ b/src/lib/config.source.sh @@ -30,6 +30,9 @@ missing_config() { } apply_config_defaults() { + # Empty FAVICON means use the bundled default favicon; otherwise it is a path + # to a custom favicon file copied into the album as favicon.ico. + FAVICON="${FAVICON:-}" HEIGHT="${HEIGHT:-}" IMAGE_JOBS="${IMAGE_JOBS:-3}" IMAGEMAGICK_TIMEOUT="${IMAGEMAGICK_TIMEOUT:-60}" diff --git a/src/lib/config.validate.source.sh b/src/lib/config.validate.source.sh index bd2fccf..fda20f0 100644 --- a/src/lib/config.validate.source.sh +++ b/src/lib/config.validate.source.sh @@ -195,6 +195,19 @@ validate_common_config() { validate_yes_no_config_var SPLASH_PAGE || return validate_yes_no_config_var STATS_PAGE || return validate_yes_no_config_var TARBALL_INCLUDE || return + validate_favicon_config || return +} + +# A custom FAVICON (when set) must be a readable file; empty means the bundled +# default favicon is used. +validate_favicon_config() { + if [ -z "${FAVICON:-}" ]; then + return + fi + if [ ! -f "$FAVICON" ] || [ ! -r "$FAVICON" ]; then + config_error "FAVICON file $FAVICON must be a readable file" + return 1 + fi } validate_generation_config() { diff --git a/src/shuriken.default.conf b/src/shuriken.default.conf index c0fe41a..9d0b1a8 100644 --- a/src/shuriken.default.conf +++ b/src/shuriken.default.conf @@ -28,6 +28,10 @@ DIST_DIR=$(pwd)/dist TEMPLATE_DIR=/usr/share/shuriken/templates/default #TEMPLATE_DIR=/usr/share/shuriken/templates/minimal +# Custom favicon, published as favicon.ico. Leave unset/empty to use the bundled +# shuriken favicon. Can also be set with --favicon PATH. +#FAVICON=$(pwd)/my-favicon.ico + # Includes a .tar of the incoming dir in the dist, can be yes or no TARBALL_INCLUDE=yes TARBALL_SUFFIX=.tar diff --git a/src/shuriken.sh b/src/shuriken.sh index b6a6cc5..36f3b57 100755 --- a/src/shuriken.sh +++ b/src/shuriken.sh @@ -32,6 +32,7 @@ declare -ra CLI_CONFIG_OVERRIDE_TARGETS=( INCOMING_DIR DIST_DIR TEMPLATE_DIR + FAVICON TITLE HEIGHT THUMBHEIGHT @@ -46,6 +47,7 @@ declare -ra CLI_CONFIG_OVERRIDE_TARGETS=( ) declare -Ar CLI_OPTION_SPEC=( [--config]='kind=value target=SHURIKEN_CLI_CONFIG_FILE argument=path' + [--favicon]='kind=value config=FAVICON argument=path' [--incoming]='kind=value config=INCOMING_DIR' [--dist]='kind=value config=DIST_DIR' [--template]='kind=value config=TEMPLATE_DIR' diff --git a/tests/cli.sh b/tests/cli.sh index aa869cc..1dac04a 100755 --- a/tests/cli.sh +++ b/tests/cli.sh @@ -1300,6 +1300,7 @@ CONFIG_SOURCE=$TEST_REPO_ROOT/src/shuriken.default.conf INCOMING_DIR=$TEST_TMPDIR/incoming DIST_DIR=$TEST_TMPDIR/dist TEMPLATE_DIR=$TEST_REPO_ROOT/share/templates/default +FAVICON='' TITLE=A\\ simple\\ Shuriken HEIGHT=1200 THUMBHEIGHT=300 @@ -1347,6 +1348,7 @@ CONFIG_SOURCE=$config_file INCOMING_DIR=$TEST_TMPDIR/incoming DIST_DIR=$TEST_TMPDIR/dist TEMPLATE_DIR=$TEST_REPO_ROOT/share/templates/default +FAVICON='' TITLE=Minimal\\ defaults HEIGHT='' THUMBHEIGHT=30 @@ -1487,6 +1489,7 @@ CONFIG_SOURCE=$config_file INCOMING_DIR=$TEST_TMPDIR/custom-incoming DIST_DIR=$TEST_TMPDIR/custom-dist TEMPLATE_DIR=$TEST_REPO_ROOT/share/templates/default +FAVICON='' TITLE=Selected\\ config HEIGHT=120 THUMBHEIGHT=30 @@ -1529,6 +1532,7 @@ CONFIG_SOURCE=./shuriken.conf INCOMING_DIR=$TEST_TMPDIR/incoming DIST_DIR=$TEST_TMPDIR/dist TEMPLATE_DIR=$TEST_REPO_ROOT/share/templates/default +FAVICON='' TITLE=Current\\ directory\\ config HEIGHT=120 THUMBHEIGHT=30 @@ -1596,6 +1600,7 @@ CONFIG_SOURCE=./shuriken.conf INCOMING_DIR=$TEST_TMPDIR/cli-incoming DIST_DIR=$dist_dir TEMPLATE_DIR=$TEST_TMPDIR/cli-template +FAVICON='' TITLE=CLI\\ title HEIGHT=456 THUMBHEIGHT=45 @@ -3052,6 +3057,49 @@ test_generate_cli_no_splash_overrides_config() { test::teardown } +test_generate_uses_custom_favicon() { + local config_file + local fake_bin + local favicon_src + local output + + test::setup + fake_bin="$TEST_TMPDIR/bin" + config_file="$TEST_TMPDIR/shuriken.conf" + favicon_src="$TEST_TMPDIR/my-favicon.ico" + + test::install_fake_imagemagick "$fake_bin" + PATH="$fake_bin:$PATH" \ + test::generate_fixture_images "$TEST_TMPDIR/incoming" + printf 'CUSTOM-FAVICON-MARKER\n' > "$favicon_src" + test::write_album_config \ + "$config_file" "$TEST_TMPDIR/incoming" "$TEST_TMPDIR/dist" \ + 'Favicon album' 40 + + ( + cd "$TEST_TMPDIR" + PATH="$fake_bin:$PATH" "$TEST_SHURIKEN" --generate --favicon "$favicon_src" + ) + + # The published favicon.ico is the custom file, not the bundled default. + test::assert_file_exists "$TEST_TMPDIR/dist/favicon.ico" + test "$(<"$TEST_TMPDIR/dist/favicon.ico")" = 'CUSTOM-FAVICON-MARKER' + # --print-config reports the configured favicon path. + test::assert_contains "FAVICON=$favicon_src" \ + "$(cd "$TEST_TMPDIR" && "$TEST_SHURIKEN" --print-config --favicon "$favicon_src")" + + # A missing favicon is rejected before any output is written. + rm -rf "$TEST_TMPDIR/dist" + output=$( + cd "$TEST_TMPDIR" + test::capture_failure_output "$TEST_SHURIKEN" \ + --generate --favicon "$TEST_TMPDIR/nope.ico" + ) + test::assert_contains 'FAVICON file' "$output" + test::assert_path_absent "$TEST_TMPDIR/dist" + test::teardown +} + # Synthetic `identify -verbose` output with EXIF the stats aggregation can parse, # so a full --generate produces a real camera leaderboard + per-camera page. test::stats_identify_output() { @@ -5701,6 +5749,9 @@ main() { test::run_case \ '--generate --no-splash keeps root index redirect' \ test_generate_cli_no_splash_overrides_config + test::run_case \ + '--generate --favicon uses a custom favicon' \ + test_generate_uses_custom_favicon test::run_case \ '--generate creates stats and per-camera pages with nav link' \ test_generate_stats_pages_created_and_nav_linked -- cgit v1.2.3