diff --git a/scripts/helpers.sh b/scripts/helpers.sh index 29a1ea3..f441eb2 100644 --- a/scripts/helpers.sh +++ b/scripts/helpers.sh @@ -96,22 +96,46 @@ get_nowplaying_status_icon() { } # Parse adapter output. -# Preferred adapter format: statusartisttitle. -# Plain text remains supported as playing metadata for compatibility. +# Adapter format: statusartisttitle. parse_nowplaying_adapter_output() { - local adapter_output="$1" - local status - local artist - local title - local text - - if [[ "$adapter_output" == *$'\t'* ]]; then - IFS=$'\t' read -r status artist title <<< "$adapter_output" - text="$(format_nowplaying_metadata "$artist" "$title")" - printf '%s\t%s\n' "${status:-Playing}" "$text" - else - printf 'Playing\t%s\n' "$adapter_output" + local adapter_output_value="$1" + local status_output_name="$2" + local artist_output_name="$3" + local title_output_name="$4" + local parsed_status + local adapter_remainder + local parsed_artist + local parsed_title + + printf -v "$status_output_name" '%s' '' + printf -v "$artist_output_name" '%s' '' + printf -v "$title_output_name" '%s' '' + + case "$adapter_output_value" in + *$'\n'*|*$'\r'*) return 1 ;; + esac + + if [[ "$adapter_output_value" != *$'\t'* ]]; then + return 1 + fi + + parsed_status="${adapter_output_value%%$'\t'*}" + adapter_remainder="${adapter_output_value#*$'\t'}" + + if [[ -z "$parsed_status" || "$adapter_remainder" != *$'\t'* ]]; then + return 1 fi + + parsed_artist="${adapter_remainder%%$'\t'*}" + parsed_title="${adapter_remainder#*$'\t'}" + + if [[ "$parsed_title" == *$'\t'* ]]; then + return 1 + fi + + printf -v "$status_output_name" '%s' "$parsed_status" + printf -v "$artist_output_name" '%s' "$parsed_artist" + printf -v "$title_output_name" '%s' "$parsed_title" } # Get the tmux status interval to restore after temporary scrolling changes. diff --git a/scripts/nowplaying.sh b/scripts/nowplaying.sh index b3eadae..f8f7cf2 100755 --- a/scripts/nowplaying.sh +++ b/scripts/nowplaying.sh @@ -32,13 +32,17 @@ case "$(uname -s)" in ;; esac +playback_status="" +track_artist="" +track_title="" +track_text="" + # If we got output, process and display it -if [ -n "$output" ]; then - parsed_output="$(parse_nowplaying_adapter_output "$output")" - IFS=$'\t' read -r playback_status track_text <<< "$parsed_output" +if [ -n "$output" ] && parse_nowplaying_adapter_output "$output" playback_status track_artist track_title; then + track_text="$(format_nowplaying_metadata "$track_artist" "$track_title")" fi -if [ -n "${track_text:-}" ]; then +if [ -n "$track_text" ]; then status_icon="$(get_nowplaying_status_icon "$playback_status")" # Check if scrolling is needed diff --git a/scripts/test.sh b/scripts/test.sh index bf20e7d..faae1bb 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -32,6 +32,41 @@ assert_eq() { printf 'ok - %s\n' "$message" } +assert_adapter_record() { + local adapter_output="$1" + local expected_status="$2" + local expected_artist="$3" + local expected_title="$4" + local message="$5" + local playback_status="stale" + local track_artist="stale" + local track_title="stale" + + if ! parse_nowplaying_adapter_output "$adapter_output" playback_status track_artist track_title; then + fail "${message} was rejected" + fi + + assert_eq "$expected_status" "$playback_status" "${message} status" + assert_eq "$expected_artist" "$track_artist" "${message} artist" + assert_eq "$expected_title" "$track_title" "${message} title" +} + +assert_adapter_rejected() { + local adapter_output="$1" + local message="$2" + local playback_status="stale" + local track_artist="stale" + local track_title="stale" + + if parse_nowplaying_adapter_output "$adapter_output" playback_status track_artist track_title; then + fail "${message} was accepted" + fi + + assert_eq "" "$playback_status" "${message} clears status" + assert_eq "" "$track_artist" "${message} clears artist" + assert_eq "" "$track_title" "${message} clears title" +} + write_tmux_mock() { cat > "${TMP_DIR}/tmux" <<'MOCK' #!/usr/bin/env bash @@ -117,6 +152,17 @@ MOCK ' ;; '-p paused metadata title') printf 'Track ' ;; +MOCK + ;; + title_only) + cat >> "${TMP_DIR}/playerctl" <<'MOCK' + '-l') printf 'title-only +' ;; + '-p title-only status') printf 'Playing +' ;; + '-p title-only metadata artist') printf '' ;; + '-p title-only metadata title') printf 'Title +' ;; MOCK ;; stopped) @@ -259,8 +305,18 @@ assert_eq "value " "$(resolve_with_mock @test_trailing_spaces fallback)" "mock 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" +# shellcheck source=scripts/helpers.sh +source "${ROOT_DIR}/scripts/helpers.sh" +assert_adapter_record $'Paused\tArtist\tTitle' "Paused" "Artist" "Title" "complete adapter record" +assert_adapter_record $'Playing\t\tTitle' "Playing" "" "Title" "title-only adapter record" +assert_adapter_record $'Paused\tArtist\t' "Paused" "Artist" "" "artist-only adapter record" +assert_adapter_record $'Stopped\t\t' "Stopped" "" "" "empty metadata adapter record" +assert_adapter_rejected "plain text" "plain-text adapter output" +assert_adapter_rejected $'Playing\tTitle' "one-tab adapter output" +assert_adapter_rejected $'Playing\tArtist\tTitle\tExtra' "extra-tab adapter output" +assert_adapter_rejected $'\tArtist\tTitle' "empty-status adapter output" +assert_adapter_rejected $'Playing\tArtist\tTitle\nSecond' "multiline adapter output" +assert_adapter_rejected $'Playing\tArtist\tTitle\r' "carriage-return adapter output" write_uname_mock @@ -271,6 +327,10 @@ assert_eq "♪ Artist - Title" "$(run_with_mocks "${ROOT_DIR}/scripts/nowplaying write_playerctl_mock paused assert_eq "⏸ Old - Track" "$(run_with_mocks "${ROOT_DIR}/scripts/nowplaying.sh")" "main renders paused metadata" +write_playerctl_mock title_only +assert_eq $'Playing\t\tTitle' "$(run_with_mocks "${ROOT_DIR}/scripts/nowplaying_linux.sh")" "linux adapter preserves empty artist" +assert_eq "♪ Title" "$(run_with_mocks "${ROOT_DIR}/scripts/nowplaying.sh")" "main renders title-only metadata" + write_playerctl_mock stopped assert_eq "⏹ Done - Song" "$(run_with_mocks "${ROOT_DIR}/scripts/nowplaying.sh")" "main renders stopped metadata"