#!/bin/bash # Description: Switch PulseAudio/PipeWire audio output sinks STATE_FILE="${XDG_CACHE_HOME:-$HOME/.cache}/audio-toggle-prev-sink" usage() { cat < Switch to a sink whose name matches --prev, --previous Switch back to the previously active sink --list List available sinks --current Show the current default sink --move-streams Also move existing audio streams to the new sink --help, -h Show this help message EOF exit 0 } get_current_sink() { pactl info | sed -En 's/Default Sink: (.*)/\1/p' } get_sink_description() { local sink="$1" pactl list sinks | awk -v s="$sink" ' /^[[:space:]]*Sink #[0-9]+/ { in_block=""; name="" } $1 == "Name:" { name = $2 } name == s && /Description:/ { $1=""; sub(/^[[:space:]]+/, ""); print; exit } ' } MOVE_STREAMS=0 MODE="toggle" TARGET_SINK="" while [ $# -gt 0 ]; do case "$1" in --help|-h) usage ;; --list) pactl list short sinks | grep -v easyeffects | while read -r line; do name=$(echo "$line" | awk '{print $2}') desc=$(get_sink_description "$name") echo "$name ($desc)" done exit 0 ;; --current) sink=$(get_current_sink) desc=$(get_sink_description "$sink") echo "$sink ($desc)" exit 0 ;; --prev|--previous) if [ ! -f "$STATE_FILE" ]; then notify-send "Audio Toggle" "No previous sink saved" exit 1 fi prev=$(cat "$STATE_FILE") if ! pactl list short sinks | awk '{print $2}' | grep -qxF "$prev"; then notify-send "Audio Toggle" "Previous sink no longer available" exit 1 fi current=$(get_current_sink) echo "$current" > "$STATE_FILE" TARGET_SINK="$prev" MODE="switch" ;; --to) shift [ $# -eq 0 ] && echo "Error: --to requires a pattern" >&2 && exit 1 matched=$(pactl list short sinks | grep -v easyeffects | awk '{print $2}' | grep -i "$1" | head -1) [ -z "$matched" ] && echo "Error: no sink matching '$1'" >&2 && exit 1 current=$(get_current_sink) echo "$current" > "$STATE_FILE" TARGET_SINK="$matched" MODE="switch" ;; --move-streams) MOVE_STREAMS=1 ;; *) echo "Unknown option: $1" >&2 usage exit 1 ;; esac shift done SINKS=$(pactl list short sinks | grep -v easyeffects) SINK_COUNT=$(echo "$SINKS" | wc -l) if [ "$SINK_COUNT" -eq 0 ]; then notify-send -u critical "Audio Toggle" "No audio sinks available" exit 1 fi CURRENT_SINK=$(get_current_sink) if [ "$MODE" = "switch" ] && [ -n "$TARGET_SINK" ]; then NEW_SINK="$TARGET_SINK" else SINK_INDEX=$(echo "$SINKS" | awk -v s="$CURRENT_SINK" '$2 == s { print NR; exit }') SINK_INDEX=${SINK_INDEX:-0} NEXT_INDEX=$((SINK_INDEX % SINK_COUNT + 1)) NEW_SINK=$(echo "$SINKS" | awk "NR == $NEXT_INDEX { print \$2; exit }") echo "$CURRENT_SINK" > "$STATE_FILE" fi NEW_DESC=$(get_sink_description "$NEW_SINK") notify-send "Audio Output" "Switched to $NEW_DESC" --icon=audio-speakers MAX_RETRIES=6 RETRIES=0 while true; do [ "$RETRIES" -ge "$MAX_RETRIES" ] && notify-send -u critical "Audio Toggle" "Failed after $MAX_RETRIES attempts" && break pactl set-default-sink "$NEW_SINK" [ "$(get_current_sink)" = "$NEW_SINK" ] && break RETRIES=$((RETRIES + 1)) NEXT_INDEX=$(((SINK_INDEX + RETRIES) % SINK_COUNT + 1)) NEW_SINK=$(echo "$SINKS" | awk "NR == $NEXT_INDEX { print \$2; exit }") NEW_DESC=$(get_sink_description "$NEW_SINK") notify-send "Audio Toggle" "Sink unavailable, trying $NEW_DESC..." done if [ "$MOVE_STREAMS" = 1 ]; then pactl list short sink-inputs | grep -Eo '^[0-9]+' | while read -r id; do pactl move-sink-input "$id" "$NEW_SINK" done fi