#!/bin/sh # # DESCRIPTION: # dmenu-based pulseaudio helper # set -e [ -n "$DEBUG" ] && set -x # check requirements for tool in jq swq pactl perl; do if ! command -v "$tool" >/dev/null; then printf "%s: %s not installed but required\n" "$(basename "$0")" "$tool" >&2 exit 1 fi done DEFAULT_PROFILE="output:analog-stereo+input:analog-stereo" HDMI_PROFILE="output:hdmi-stereo" export MENU_PROMPT="soundswitch" _menu() { menu -l 8 } mode= output= input= mute= volume_down= volume_up= usage() { printf "usage: %s [-hIOgfpHRmMvV]\n" "$(basename "$0")" printf "\n" printf " -O \t(manipulate pulseaudio sinks)\n" printf " -I \t(manipulate pulseaudio sources)\n" printf " -g \t(set default sink and switch global output)\n" printf " -f \t(switch focused container output)\n" printf " -p \t(switch card profile)\n" printf " -H \t(switch card to HDMI profile)\n" printf " -R \t(switch card to default profile)\n" printf "\n" printf " -m \t(mute default sink)\n" printf " -M \t(mute default source)\n" printf " -v \t(reduce sink volume by 5%%)\n" printf " -V \t(increase sink volume by 5%%)\n" printf "\n" printf "Needs swq(1) to work.\n" exit 2 } _args="$*" while getopts hIOgfpHRmMvV name; do case ${name} in g) mode=global ;; f) mode=focused ;; p) mode=profile ;; H) mode=hdmi ;; R) mode=reset ;; O) output=1 ;; I) input=1 ;; # quick actions, to make move to pipewire easier m) mode=none ; output=1 ; mute=1 ;; M) mode=none ; input=1 ; mute=1 ;; v) mode=none ; output=1 ; volume_down=1 ;; V) mode=none ; output=1 ; volume_up=1 ;; h|?) echo "$*" ; usage ;; esac done shift $((OPTIND-1)) # ask mode _mode_global="󰣨 global" _mode_focused="󰻿 focused" _mode_profile_card="󱂇 card profile" _mode_profile_hdmi="󰡁 hdmi profile" _mode_profile_reset="󰕍 reset profile" if [ -z "$mode" ]; then printf "$_mode_global\n$_mode_focused\n$_mode_profile_card\n$_mode_profile_hdmi\n$_mode_profile_reset\n" | _menu | { read -r line case ${line} in $_mode_global) exec "$0" -g "$*" ;; $_mode_focused) exec "$0" -f "$*" ;; $_mode_profile_card) exec "$0" -p "$*" ;; $_mode_profile_hdmi) exec "$0" -H "$*" ;; $_mode_profile_reset) exec "$0" -R "$*" ;; *) usage ;; esac } exit $? fi _mode_output="󰴸 output" _mode_input="󰍬 input" if [ -n "$mode" ]; then # ask target? if [ -z "$output" ] && [ -z "$input" ]; then printf "$_mode_output\n$_mode_input\n" | _menu | { read -r line case ${line} in $_mode_output) exec "$0" -O $_args ;; $_mode_input) exec "$0" -I $_args ;; *) usage ;; esac } fi fi # process quick actions first [ -n "$output" ] && [ -n "$mute" ] && exec pactl set-sink-mute '@DEFAULT_SINK@' toggle [ -n "$input" ] && [ -n "$mute" ] && exec pactl set-source-mute '@DEFAULT_SINK@' toggle [ -n "$volume_down" ] && exec pactl set-sink-volume '@DEFAULT_SINK@' '-5%' [ -n "$volume_up" ] && exec pactl set-sink-volume '@DEFAULT_SINK@' '+5%' [ -n "$output" ] && keyword="sink" && what="sink-input" [ -n "$input" ] && keyword="source" && what="source-output" # returns the sink_id selects() { pactl list short "${1}s" | cut -f2 | menu } notif() { topic=$1 mesg=$2 urgency=normal [ -n "$3" ] && urgency=critical notify-send -u ${urgency} -a soundswitch "soundswitch: ${topic}" "${mesg}" } case ${mode} in global) target=$(selects "${keyword}") if [ -z "${target}" ]; then notif "focused" "no target ${what}" err exit 1 fi notif "global" "switch to ${target}" # set new default pactl "set-default-${keyword}" "${target}" # move all attached to the new target pactl list "${what}s" short | cut -f1 \ | xargs -i pactl "move-${what}" "{}" "${target}" ;; focused) # parse pid of currently focused container # then get the sink-inputs for it and parse the sink-input-id focused_binary=$(swq current con | jq -r '.pid' | xargs ps -o comm=) what_id=$(printf "%s\n" "${focused_binary}" \ | xargs pactl list "${what}s" \ | tr '\n' '\r' \ | perl -pe 's/[^\r]*index: ([0-9]+).+?application\.process\.binary = "([^\r]+)"\r.+?(?=index:|$)/\2=\1\r/g' \ | tr '\r' '\n' \ | grep "^{}" \ | cut -d'=' -f2) if [ -z "${what_id}" ]; then notif "focused" "no ${what}" err exit 1 fi # select where to switch to target=$(selects "${keyword}") if [ -z "${target}" ]; then exit 1 fi notif "${focused_binary}" "switch to ${target}" pactl move-sink-input "${what_id}" "${target}" ;; profile) target_card=$(selects "card") if [ -z "${target_card}" ]; then exit 1 fi # [^\r]#([0-9]+).*?Name: ([^\r]+).*?Profiles:\r(.*?)Active.*?(?=Card #|$) target_profile=$(pactl list cards \ | tr '\n' '\r' \ | perl -pe 's/[^\r]#([0-9]+).*?Name: ([^\r]+).*?Profiles:\r(.*?)Active.*?(?=Card #|$)/\2~\3`/g' \ | tr -d '\t' \ | tr '`' '\n' \ | grep "${target_card}" \ | cut -d'~' -f2- \ | tr '\r' '\n' \ | xargs -L1 \ | _menu \ | perl -pe 's/^\s*([^\s]+):\s.*$/\1/') if [ -z "${target_profile}" ]; then exit 1 fi notif "profile" "switch ${target_card} to ${target_profile} profile" pactl set-card-profile "${target_card}" "${target_profile}" ;; hdmi) target_card=$(selects "card") if [ -z "${target_card}" ]; then exit 1 fi notif "hdmi" "switch ${target_card} to HDMI profile" pactl set-card-profile "${target_card}" ${HDMI_PROFILE} exit $? ;; reset) target_card=$(selects "card") if [ -z "${target_card}" ]; then exit 1 fi notif "reset" "switch ${target_card} to default profile" pactl set-card-profile "${target_card}" ${DEFAULT_PROFILE} ;; esac