#!/bin/sh
#
# DESCRIPTION:
# dmenu-based audio helper
#
# TODO
# - auto-select the currently active profile
set -e
[ -n "$DEBUG" ] && set -x
# check requirements
for tool in jq swq pactl perl pw-dump; 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"
_menu() {
export MENU_PROMPT="soundswitch"
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 hIOgfpHRmMvVB name; do
case ${name} in
g) mode=global ;;
f) mode=focused ;;
p) mode=profile output=1 input=1 ;;
H) mode=hdmi output=1 input=1 ;;
R) mode=reset ;;
B) mode=mute output=1 input=1 ;;
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"
_mode_mute=" mute"
if [ -z "$mode" ]; then
printf "$_mode_global\n$_mode_focused\n$_mode_profile_card\n$_mode_profile_hdmi\n$_mode_profile_reset\n$_mode_mute\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 "$*" ;;
$_mode_mute) exec "$0" -B "$*" ;;
*)
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 -i /usr/share/icons/mdi/scalable/cast-audio.svg -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}
;;
mute)
_data=$(pw-dump -N | jq -r '.. | select(.type?=="PipeWire:Interface:Node" and .info.props."media.class"?=="Stream/Output/Audio" and .info.props."stream.is-live"?==true) | @text')
_id=$(printf "${_data}" | jq -r '. | [ .info.props."application.name", .info.props."application.process.id", .id ] | @tsv' \
| awk '{ system("swaymsg -t get_tree | jq -r \".. | select(.pid?==" $2 ") | .name\" | grep -v Picture-in-Picture | tr -d \"\\n\""); printf " [%s]", $3 }' \
| _menu \
| perl -pe 's/^.*\[([0-9]+)\]$/\1/')
if [ -z "${_id}" ]; then
exit 1
fi
_oid=$(printf "${_data}" | jq -r --argjson id "${_id}" '.. | select(.id?==$id) | .info.props."object.id"')
_target_state=$(printf "${_data}" | jq -r --argjson id "${_id}" '.. | select(.id?==$id) | .info.params.Props[0] | .mute = (.mute | not) | @text')
pw-cli set-param "${_oid}" Props "${_target_state}"
;;
esac