#!/bin/bash # pw-mute-active-window 0.3: 2025 Christian Birchinger # # This script opens a rofi (dmenu) with available Pipewire Sinks for the currently # active (X11) window. # # It uses "xdotool getactivewindow getwindowpid" to get the PID of the active window, # obtains all child PIDs and then tries to find "Playback" stremas in PipeWire. # # So it not only works for GUI Players like DeaDBeeF, MPV, Fooyin but also players running # in a terminal like openmpt123, Sidplay2, MPV (cli mode) # # Of course with such a hack, there are limits and restrictions. Some knowns are: # - The player has to be activly playing something to be found. # - Terminals that use one multiplexing main process for all windows fail() { echo "ERROR: $*" >&2 notify-send -i audio-speakers-symbolic "${0//*\/}" "ERROR: $*\n(Playback must be active to be detected)" exit 1 } getchilds() { cpids=$(pgrep -P "$1" | xargs) for cpid in ${cpids}; do echo "$cpid" ${FUNCNAME[0]} "${cpid}" done } get_objs_from_pids() { local p for p in "$@"; do jq -r ".[].info.props | select(.\"application.process.id\"==${p}) | .\"object.id\"" <<< "${pw_json}" done } get_pbs_from_objs() { local o for o in "$@"; do jq -r ".[].info.props | select(.\"media.class\"==\"Stream/Output/Audio\" and .\"client.id\"==${o} ) | .\"object.serial\"" <<< "${pw_json}" done } get_names_from_pbs() { local o local j local l=() for o in "$@"; do l+=( "$(jq -r ".[].info.props | select(.\"object.serial\"==${o}) | .\"application.name\"" <<< "${pw_json}")" ) done printf -v j '%s,' "${l[@]}" echo "${j%,}" } get_sink_from_obj() { local o=$1 local oid local sids=() oid=$(jq -r ".[] | select (.type==\"PipeWire:Interface:Node\") | .info.props | select(.\"media.class\"==\"Stream/Output/Audio\" and .\"client.id\"==${o} ) | .\"object.id\"" <<< "${pw_json}") readarray -t sids < <(jq -r ".[].info.props | select(.\"link.output.node\"==${oid}) | .\"link.input.node\"" <<< "${pw_json}") jq -r ".[].info.props | select(.\"object.id\"==${sids[0]}) | .\"node.description\"" <<< "${pw_json}" } wid=$(xdotool getwindowfocus) wpid=$(xdotool getactivewindow getwindowpid) [[ -z $wpid ]] && fail "Unable to get PID from active window" readarray -t cpids <<< "$(getchilds "${wpid}")" pids=( "${wpid}" ) [[ -n ${cpids[*]} ]] && pids+=( "${cpids[@]}" ) pw_json=$(pw-dump) readarray -t objs < <(get_objs_from_pids "${pids[@]}") [[ -z ${objs[*]} ]] && fail "Unable to get Pipewire objects from PIDs" readarray -t pbs < <(get_pbs_from_objs "${objs[@]}") [[ -z ${pbs[*]} ]] && fail "Unable to get Pipewire playbacks from PIDs" current_sink=$(get_sink_from_obj "${objs[0]}") players=$(get_names_from_pbs "${pbs[@]}") sinks=$(jq -r '.[].info.props | select(."media.class"=="Audio/Sink") | "\(."node.description")\t\(."object.serial")"' <<< "${pw_json}") [[ -z $sinks ]] && fail "Unable to get Pipewire sinks" selected_sink=$(rofi -select "${current_sink}" -display-columns 1 -w "${wid}" -dmenu -i -p "Move ${players} to output device" <<< "${sinks}") [[ -z $selected_sink ]] && exit selected_sink=${selected_sink/*$'\t'} for p in "${pbs[@]}"; do pactl move-sink-input "$p" "${selected_sink}" done