#!/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