summary refs log tree commit diff
diff options
context:
space:
mode:
-rwxr-xr-xbin/soundswitch379
1 files changed, 234 insertions, 145 deletions
diff --git a/bin/soundswitch b/bin/soundswitch
index e39a292..78021b6 100755
--- a/bin/soundswitch
+++ b/bin/soundswitch
@@ -19,11 +19,13 @@ for tool in jq swq pactl perl pw-dump; do
 done
 
 DEFAULT_PROFILE="output:analog-stereo+input:analog-stereo"
+# DEFAULT_PROFILE="pro-audio"
 HDMI_PROFILE="output:hdmi-stereo"
 
 _menu() {
-	export MENU_PROMPT="soundswitch"
-	menu -l 8
+	[ -n "${mode}" ] && prompt="soundswitch (${mode})" || prompt="soundswitch"
+	export MENU_PROMPT=${MENU_PROMPT:-${prompt}}
+	menu -l10 --ifne --autoselect
 }
 
 mode=
@@ -52,41 +54,42 @@ usage() {
 	exit 2
 }
 _args="$*"
-while getopts hIOgfpHRmMvVB name; do
+while getopts hIOgfpHRmMvVBj 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
-			;;
+	j) mode=json ;;
+	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 ;;
+	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))
@@ -102,15 +105,15 @@ 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
-				;;
+		"$_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 $?
@@ -124,11 +127,12 @@ if [ -n "$mode" ]; 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 ;;
+			"$_mode_output") exec "$0" -O $_args ;;
+			"$_mode_input") exec "$0" -I $_args ;;
+			*) usage ;;
 			esac
 		}
+		exit $?
 	fi
 fi
 
@@ -142,8 +146,53 @@ fi
 [ -n "$input" ] && keyword="source" && what="source-output"
 
 # returns the sink_id
+default() {
+	key=$1
+	case "$1" in
+	sink) key="audio.default.sink" ;;
+	source) key="audio.default.source" ;;
+	esac
+	pw-dump | jq -r --arg key "$key" '.[]|select(.type=="PipeWire:Interface:Metadata")|.metadata[]|select(.key==$key)|.value.name'
+}
+
+# __identifier: compacted json containing index and description
+#               e.g. {"i":60,"d":"Built-in Audio Analog Stereo"}
+__identifier_list() {
+	if [ "$1" = "card" ]; then
+		pactl --format=json list "${1}s" | jq -c '.[]|{i:.index,d:.properties["api.alsa.card.name"]}'
+	else
+		pactl --format=json list "${1}s" | jq -c '.[]|{i:.index,d:.description}'
+	fi
+}
+__identifier_index() {
+	if [ -z "$1" ]; then
+		jq -r '.i'
+	else
+		printf "%s" "$1" | __identifier_index
+	fi
+}
+__identifier_desc() {
+	if [ -z "$1" ]; then
+		jq -r '.d'
+	else
+		printf "%s" "$1" | __identifier_desc
+	fi
+}
+__identifier_fmt() {
+	jq -r '[.i,.d]|@csv' | tr -d '"' | column -s ',' -t
+}
+__identifier_fmt_parse() {
+	fmt=$(cat)
+	i=$(printf "%s" "$fmt" | tr -s ' ' | cut -d' ' -f1)
+	d=$(printf "%s" "$fmt" | tr -s ' ' | cut -d' ' -f2-)
+	jq -cn --arg i "$i" --arg d "$d" '{i:$i,d:$d}'
+}
+
 selects() {
-	pactl list short "${1}s" | cut -f2 | _menu
+	__identifier_list "$1" |
+		__identifier_fmt |
+		_menu |
+		__identifier_fmt_parse
 }
 notif() {
 	topic=$1
@@ -153,108 +202,148 @@ notif() {
 	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 <i>${target}</i>"
-		# 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}"
-		;;
+case "${mode}" in
+json)
+	selects "${keyword}"
+	;;
+global)
+	target=$(selects "${keyword}")
+	if [ -z "${target}" ]; then
+		notif "focused" "no target ${what}" error
+		exit 1
+	fi
 
-	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 <i>${target}</i>"
-		pactl move-sink-input "${what_id}" "${target}"
-		;;
+	notif "global" "switch to <i>$(__identifier_desc "${target}")</i>"
+	# set new default
+	pactl "set-default-${keyword}" "$(__identifier_index "${target}")"
+	# move all attached to the new target
+	pactl list "${what}s" short | cut -f1 |
+		xargs -i pactl "move-${what}" "{}" "$(__identifier_index "${target}")"
+	exit $?
+	;;
 
-	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 <i>${target_card}</i> to ${target_profile} profile"
-		pactl set-card-profile "${target_card}" "${target_profile}"
-		;;
+focused)
+	# parse pid of currently focused container
+	# then get the sink-inputs for it and parse the sink-input-id
+	notif "not implemented, do we need it?" error
+	exit 1
 
-	hdmi)
-		target_card=$(selects "card")
-		if [ -z "${target_card}" ]; then
-			exit 1
-		fi
+	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}" error
+		exit 1
+	fi
 
-		notif "hdmi" "switch <i>${target_card}</i> to HDMI profile"
-		pactl set-card-profile "${target_card}" ${HDMI_PROFILE}
-		exit $?
-		;;
+	# select where to switch to
+	target=$(selects "${keyword}")
+	if [ -z "${target}" ]; then
+		exit 1
+	fi
 
-	reset)
-		target_card=$(selects "card")
-		if [ -z "${target_card}" ]; then
-			exit 1
-		fi
+	notif "${focused_binary}" "switch to <i>${target}</i>"
+	pactl move-sink-input "${what_id}" "${target}"
+	exit $?
+	;;
 
-		notif "reset" "switch <i>${target_card}</i> to default profile"
-		pactl set-card-profile "${target_card}" ${DEFAULT_PROFILE}
-		;;
+profile)
+	notif "not implemented, do we need it?" "" error
+	exit 1
 
-	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}"
-		;;
+	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 <i>${target_card}</i> to ${target_profile} profile"
+	# pactl set-card-profile "${target_card}" "${target_profile}"
+	# exit $?
+	;;
+
+hdmi)
+	notif "not implemented, do we need it?" "" error
+	exit 1
+
+	target_card=$(selects "card")
+	if [ -z "${target_card}" ]; then
+		exit 1
+	fi
+
+	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' |
+		grep -i "hdmi" |
+		grep -i -v "input" |
+		xargs -L1 |
+		_menu |
+		perl -pe 's/^\s*([^\s]+):\s.*$/\1/')
+	if [ -z "${target_profile}" ]; then
+		target_profile=${HDMI_PROFILE}
+	fi
+
+	notif "hdmi" "switch <i>${target_card}</i> to HDMI profile\n<i>${target_profile}</i>"
+	pactl set-card-profile "${target_card}" "${target_profile}"
+	exit $?
+	;;
+
+reset)
+	notif "not implemented, do we need it?" "" error
+	exit 1
+
+	target_card=$(selects "card")
+	if [ -z "${target_card}" ]; then
+		exit 1
+	fi
+
+	notif "reset" "switch <i>${target_card}</i> to default profile"
+	pactl set-card-profile "${target_card}" ${DEFAULT_PROFILE}
+	exit $?
+	;;
+
+mute)
+	notif "not implemented, do we need it?" "" error
+	exit 1
+
+	_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}"
+	exit $?
+	;;
 
 esac