diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6d509fc..89fcd86 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,9 +31,11 @@ jobs: runs-on: macos-latest steps: - uses: actions/checkout@v4 + - name: Install tmux + run: brew install tmux - name: Test with system Bash 3.2 env: - PATH: /usr/bin:/bin:/usr/sbin:/sbin + PATH: /usr/bin:/bin:/usr/sbin:/sbin:/opt/homebrew/bin:/usr/local/bin run: | /bin/bash --version /bin/bash -n nowplaying.tmux scripts/*.sh diff --git a/nowplaying.tmux b/nowplaying.tmux index 555324d..f80fca3 100755 --- a/nowplaying.tmux +++ b/nowplaying.tmux @@ -10,9 +10,6 @@ else exit 1 fi -# Default options (only set if not already defined) -set_nowplaying_default_options - # Create the interpolation function nowplaying_interpolation() { local string="$1" diff --git a/scripts/helpers.sh b/scripts/helpers.sh index 53b3d2a..29a1ea3 100644 --- a/scripts/helpers.sh +++ b/scripts/helpers.sh @@ -7,14 +7,13 @@ get_tmux_option() { local option="$1" local default_value="$2" local option_value - option_value="$(tmux show-option -qv "$option")" - if [ -z "$option_value" ]; then - option_value="$(tmux show-option -gqv "$option")" - fi - if [ -z "$option_value" ]; then - echo "$default_value" + + if option_value="$(tmux show-option -v "$option" 2>/dev/null)"; then + printf '%s\n' "$option_value" + elif option_value="$(tmux show-option -gv "$option" 2>/dev/null)"; then + printf '%s\n' "$option_value" else - echo "$option_value" + printf '%s\n' "$default_value" fi } @@ -34,26 +33,6 @@ get_nowplaying_default_option() { esac } -# Set every nowplaying tmux option default without overriding user values. -set_nowplaying_default_options() { - local option - for option in \ - "@nowplaying_playing_icon" \ - "@nowplaying_paused_icon" \ - "@nowplaying_stopped_icon" \ - "@nowplaying_scrolling_enabled" \ - "@nowplaying_scrollable_threshold" \ - "@nowplaying_scroll_speed" \ - "@nowplaying_scroll_padding" \ - "@nowplaying_auto_interval" \ - "@nowplaying_playing_interval" - do - if [ -z "$(tmux show-option -gqv "$option")" ]; then - tmux set-option -g "$option" "$(get_nowplaying_default_option "$option")" - fi - done -} - # Get a nowplaying tmux option with its built-in default. get_nowplaying_option() { local option="$1" diff --git a/scripts/test.sh b/scripts/test.sh index 77eadf0..bf20e7d 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -3,7 +3,16 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" TMP_DIR="$(mktemp -d)" -trap 'rm -rf "${TMP_DIR}"' EXIT +REAL_TMUX="" +TMUX_TEST_SERVER="" + +cleanup() { + if [[ -n "$REAL_TMUX" && -n "$TMUX_TEST_SERVER" ]]; then + "$REAL_TMUX" -L "$TMUX_TEST_SERVER" kill-server 2>/dev/null || true + fi + rm -rf "$TMP_DIR" +} +trap cleanup EXIT fail() { printf 'not ok - %s\n' "$1" >&2 @@ -28,19 +37,32 @@ write_tmux_mock() { #!/usr/bin/env bash if [[ "$1" == "show-option" ]]; then option="${@: -1}" - case "$option" in - @nowplaying_playing_icon) printf '♪ ' ;; - @nowplaying_paused_icon) printf '⏸ ' ;; - @nowplaying_stopped_icon) printf '⏹ ' ;; - @nowplaying_scrolling_enabled) printf 'no' ;; - @nowplaying_scrollable_threshold) printf '50' ;; - @nowplaying_scroll_speed) printf '1' ;; - @nowplaying_scroll_padding) printf ' ' ;; - @nowplaying_auto_interval) printf 'no' ;; - @nowplaying_playing_interval) printf '1' ;; - @bad_integer) printf 'abc' ;; - @high_integer) printf '99' ;; - @low_integer) printf '0' ;; + scope="local" + if [[ " $* " == *" -gv "* || " $* " == *" -gqv "* ]]; then + scope="global" + fi + + case "$scope:$option" in + global:@nowplaying_playing_icon) printf '♪ ' ;; + global:@nowplaying_paused_icon) printf '⏸ ' ;; + global:@nowplaying_stopped_icon) printf '⏹ ' ;; + global:@nowplaying_scrolling_enabled) printf 'no' ;; + global:@nowplaying_scrollable_threshold) printf '50' ;; + global:@nowplaying_scroll_speed) printf '1' ;; + global:@nowplaying_scroll_padding) printf ' ' ;; + global:@nowplaying_auto_interval) printf 'no' ;; + global:@nowplaying_playing_interval) printf '1' ;; + global:@bad_integer) printf 'abc' ;; + global:@empty_integer) printf '' ;; + global:@high_integer) printf '99' ;; + global:@low_integer) printf '0' ;; + global:@test_global_empty) printf '' ;; + global:@test_local_empty) printf 'global value' ;; + local:@test_local_empty) printf '' ;; + global:@test_precedence) printf 'global value' ;; + local:@test_precedence) printf 'local value' ;; + global:@test_trailing_spaces) printf 'value ' ;; + *) exit 1 ;; esac elif [[ "$1 $2" == "set-option -g" || "$1 $2" == "set-option -gq" ]]; then : @@ -127,13 +149,115 @@ run_with_mocks() { PATH="${TMP_DIR}:${PATH}" "$@" } +resolve_with_mock() { + PATH="${TMP_DIR}:${PATH}" bash -c 'source "$1"; get_tmux_option "$2" "$3"' \ + _ "${ROOT_DIR}/scripts/helpers.sh" "$1" "$2" +} + +run_real_tmux_tests() { + local real_bin="${TMP_DIR}/real-tmux-bin" + local session="nowplaying-test" + local actual + local global_options + + REAL_TMUX="$(command -v tmux || true)" + if [[ -z "$REAL_TMUX" ]]; then + fail "tmux is required for isolated integration tests" + fi + + TMUX_TEST_SERVER="nowplaying-test-$$-${RANDOM}" + "$REAL_TMUX" -L "$TMUX_TEST_SERVER" -f /dev/null new-session -d -s "$session" + + mkdir -p "$real_bin" + export NOWPLAYING_TEST_REAL_TMUX="$REAL_TMUX" + export NOWPLAYING_TEST_SERVER="$TMUX_TEST_SERVER" + export NOWPLAYING_TEST_SESSION="$session" + cat > "${real_bin}/tmux" <<'WRAPPER' +#!/usr/bin/env bash +if [[ "$1" == "show-option" || "$1" == "set-option" ]]; then + command="$1" + shift + exec "$NOWPLAYING_TEST_REAL_TMUX" -L "$NOWPLAYING_TEST_SERVER" "$command" -t "$NOWPLAYING_TEST_SESSION" "$@" +fi +exec "$NOWPLAYING_TEST_REAL_TMUX" -L "$NOWPLAYING_TEST_SERVER" "$@" +WRAPPER + chmod +x "${real_bin}/tmux" + + real_resolve() { + PATH="${real_bin}:${PATH}" bash -c 'source "$1"; get_tmux_option "$2" "$3"' \ + _ "${ROOT_DIR}/scripts/helpers.sh" "$1" "$2" + } + real_nowplaying_option() { + PATH="${real_bin}:${PATH}" bash -c 'source "$1"; get_nowplaying_option "$2"' \ + _ "${ROOT_DIR}/scripts/helpers.sh" "$1" + } + + assert_eq "fallback" "$(real_resolve @test fallback)" "real tmux absent option uses fallback" + + "$REAL_TMUX" -L "$TMUX_TEST_SERVER" set-option -g @test "" + assert_eq "" "$(real_resolve @test fallback)" "real tmux empty global option wins" + if ! "$REAL_TMUX" -L "$TMUX_TEST_SERVER" show-option -g @test >/dev/null; then + fail "real tmux empty global option remains present" + fi + printf 'ok - real tmux empty global option remains present\n' + + "$REAL_TMUX" -L "$TMUX_TEST_SERVER" set-option -g @test "global value" + assert_eq "global value" "$(real_resolve @test fallback)" "real tmux global option resolves exactly" + + "$REAL_TMUX" -L "$TMUX_TEST_SERVER" set-option -t "$session" @test "local value" + assert_eq "local value" "$(real_resolve @test fallback)" "real tmux local option wins" + + "$REAL_TMUX" -L "$TMUX_TEST_SERVER" set-option -t "$session" @test "" + assert_eq "" "$(real_resolve @test fallback)" "real tmux empty local option wins over global" + + "$REAL_TMUX" -L "$TMUX_TEST_SERVER" set-option -g -u @nowplaying_playing_icon 2>/dev/null || true + "$REAL_TMUX" -L "$TMUX_TEST_SERVER" set-option -t "$session" -u @nowplaying_playing_icon 2>/dev/null || true + assert_eq "♪ " "$(real_nowplaying_option @nowplaying_playing_icon)" "real tmux playing icon fallback preserves trailing space" + + "$REAL_TMUX" -L "$TMUX_TEST_SERVER" set-option -g @nowplaying_playing_icon "" + assert_eq "" "$(real_nowplaying_option @nowplaying_playing_icon)" "real tmux empty playing icon wins" + + "$REAL_TMUX" -L "$TMUX_TEST_SERVER" set-option -g @nowplaying_scroll_padding "" + assert_eq "" "$(real_nowplaying_option @nowplaying_scroll_padding)" "real tmux empty scroll padding wins" + + "$REAL_TMUX" -L "$TMUX_TEST_SERVER" set-option -g -u @nowplaying_playing_icon + "$REAL_TMUX" -L "$TMUX_TEST_SERVER" set-option -g -u @nowplaying_scroll_padding + "$REAL_TMUX" -L "$TMUX_TEST_SERVER" set-option -g status-right 'before#{nowplaying}after' + PATH="${real_bin}:${PATH}" bash "${ROOT_DIR}/nowplaying.tmux" + + global_options="$("$REAL_TMUX" -L "$TMUX_TEST_SERVER" show-options -g)" + if [[ "$global_options" == *"@nowplaying_"* ]]; then + fail "plugin load created nowplaying default options" + fi + printf 'ok - plugin load does not create nowplaying default options\n' + + actual="$("$REAL_TMUX" -L "$TMUX_TEST_SERVER" show-option -gqv status-right)" + assert_eq "before#(${ROOT_DIR}/scripts/nowplaying.sh)after" "$actual" "status interpolation remains active" + + "$REAL_TMUX" -L "$TMUX_TEST_SERVER" set-option -g @nowplaying_playing_icon "" + "$REAL_TMUX" -L "$TMUX_TEST_SERVER" set-option -g @nowplaying_scroll_padding "pad " + PATH="${real_bin}:${PATH}" bash "${ROOT_DIR}/nowplaying.tmux" + + if ! "$REAL_TMUX" -L "$TMUX_TEST_SERVER" show-option -g @nowplaying_playing_icon >/dev/null; then + fail "plugin load removed empty override" + fi + assert_eq "" "$("$REAL_TMUX" -L "$TMUX_TEST_SERVER" show-option -gqv @nowplaying_playing_icon)" "plugin load preserves empty override" + assert_eq "pad " "$("$REAL_TMUX" -L "$TMUX_TEST_SERVER" show-option -gqv @nowplaying_scroll_padding)" "plugin load preserves non-empty override exactly" +} + bash -n "${ROOT_DIR}/nowplaying.tmux" "${ROOT_DIR}"/scripts/*.sh printf 'ok - bash syntax\n' write_tmux_mock -helper_output="$(PATH="${TMP_DIR}:${PATH}" bash -c 'source "$1"; printf "%s %s %s\n" "$(get_tmux_integer_option @bad_integer 50 4)" "$(get_tmux_integer_option @low_integer 50 4)" "$(get_tmux_integer_option @high_integer 1 1 10)"' _ "${ROOT_DIR}/scripts/helpers.sh")" -assert_eq "50 4 10" "$helper_output" "integer option validation" +assert_eq "fallback" "$(resolve_with_mock @test_absent fallback)" "mock absent option uses fallback" +assert_eq "" "$(resolve_with_mock @test_global_empty fallback)" "mock empty global option wins" +assert_eq "" "$(resolve_with_mock @test_local_empty fallback)" "mock empty local option wins over global" +assert_eq "local value" "$(resolve_with_mock @test_precedence fallback)" "mock local option wins over global" +assert_eq "value " "$(resolve_with_mock @test_trailing_spaces fallback)" "mock option preserves trailing spaces" + +helper_output="$(PATH="${TMP_DIR}:${PATH}" bash -c 'source "$1"; printf "%s %s %s %s\n" "$(get_tmux_integer_option @bad_integer 50 4)" "$(get_tmux_integer_option @empty_integer 50 4)" "$(get_tmux_integer_option @low_integer 50 4)" "$(get_tmux_integer_option @high_integer 1 1 10)"' _ "${ROOT_DIR}/scripts/helpers.sh")" +assert_eq "50 50 4 10" "$helper_output" "integer option validation" parse_output="$(PATH="${TMP_DIR}:${PATH}" bash -c 'source "$1"; parse_nowplaying_adapter_output $'"'"'Paused\tArtist\tTitle'"'"'' _ "${ROOT_DIR}/scripts/helpers.sh")" assert_eq $'Paused\tArtist - Title' "$parse_output" "adapter output parsing" @@ -153,4 +277,6 @@ assert_eq "⏹ Done - Song" "$(run_with_mocks "${ROOT_DIR}/scripts/nowplaying.sh write_playerctl_mock empty assert_eq "" "$(run_with_mocks "${ROOT_DIR}/scripts/nowplaying.sh")" "main renders empty output without metadata" +run_real_tmux_tests + printf 'all tests passed\n'