#!/bin/sh # Purpose {{{ # This script will allow to change default output sink for PulseAudio # 1. Display the current default and all available outputs. # 2. Set the selected output as default. # 3. Ensure to unmute the new default output. # # 2021-11-27 # }}} # Vars {{{ readonly PROGNAME=$(basename "${0}") readonly PROGDIR=$(readlink -m $(dirname "${0}")) readonly ARGS="${*}" readonly NBARGS="${#}" [ -z "${DEBUG}" ] && DEBUG=1 ## Export DEBUG for sub-script export DEBUG ## Colors readonly PURPLE='\033[1;35m' readonly RED='\033[0;31m' readonly RESET='\033[0m' readonly COLOR_DEBUG="${PURPLE}" # }}} usage() { # {{{ cat <<- EOF usage: $PROGNAME [-d|-h] Select a new default output sink for PulseAudio and unmute it. EXAMPLES : - Choose new default output. ${PROGNAME} OPTIONS : -d,--debug Enable debug messages. -h,--help Print this help message. EOF } # }}} debug_message() { # {{{ local_message="${1}" ## Print message if DEBUG is enable (=0) [ "${DEBUG}" -eq "0" ] && printf '\e[1;35m%-6b\e[m\n' "DEBUG − ${PROGNAME} : ${local_message}" return 0 } # }}} error_message() { # {{{ local_error_message="${1}" local_error_code="${2}" ## Print message if DEBUG is enable (=0) [ "${DEBUG}" -eq "0" ] && printf '%b\n' "ERROR − ${PROGNAME} : ${RED}${local_error_message}${RESET}" exit "${local_error_code:=66}" } # }}} define_vars() { # {{{ ## Temp file vars {{{ readonly pa_sink_list_path="/tmp/${PROGNAME}.list" ## }}} } # }}} is_command_absent() { # {{{ local_command_absent_cmd="${1}" ## A command is absent by default return_command_absent="0" if [ "$(command -v ${local_command_absent_cmd})" ]; then debug_message "is_command_absent − \ ${RED}${local_command_absent_cmd}${COLOR_DEBUG} seems present on this host." return_command_absent="1" else debug_message "is_command_absent − \ ${RED}${local_command_absent_cmd}${COLOR_DEBUG} is not available on this host." return_command_absent="0" fi return "${return_command_absent}" } # }}} get_pulseaudio_sink_list() { # {{{ ## Return False by default return_get_pulseaudio_sink_list="1" debug_message "get_pulseaudio_sink_list − \ Create or empty ${RED}${pa_sink_list_path}${COLOR_DEBUG} file to store pulseaudio sinks." true > "${pa_sink_list_path}" ## Get sinks list with pactl (index and description) ## Merge every two lines if pactl list sinks | \ grep --only-matching --perl-regexp '((?<=Sink #)[[:digit:].]*|(?<=Description: ).*)' | \ paste --delimiters=" " - - > "${pa_sink_list_path}"; then if [ -s "${pa_sink_list_path}" ]; then debug_message "get_pulseaudio_sink_list − \ PulseAudio sinks list successfully created (see ${pa_sink_list_path} file)." return_get_pulseaudio_sink_list="0" else debug_message "get_pulseaudio_sink_list − \ Error, the PulseAudio sinks list is empty (${pa_sink_list_path} file)." return_get_pulseaudio_sink_list="1" fi else debug_message "get_pulseaudio_sink_list − \ Error in ${RED}pacmd list-sinks${COLOR_DEBUG} command." return_get_pulseaudio_sink_list="1" fi return "${return_get_pulseaudio_sink_list}" } # }}} get_pulseaudio_current_sink() { # {{{ ## Return False by default return_get_pulseaudio_current_sink="1" pa_current_sink_name=$(pactl info | awk '/Default Sink:/ { print $NF }') ## If variable for current default sink name contains something if printf '%b' "${pa_current_sink_name}" | grep --silent -- "."; then debug_message "get_pulseaudio_current_sink − \ PulseAudio current default sink name: ${RED}${pa_current_sink_name}${COLOR_DEBUG}." pa_current_sink_description=$(pactl list sinks \ | grep --after-context=1 -- "Name: ${pa_current_sink_name}" \ | sed --silent 's/^.*Description: \(.*\)/\1/p') ### If variable for current default sink description contains something if printf '%b' "${pa_current_sink_description}" | grep --silent -- "."; then debug_message "get_pulseaudio_current_sink − \ PulseAudio current deault sink description: ${RED}${pa_current_sink_description}${COLOR_DEBUG}." return_get_pulseaudio_current_sink="0" else debug_message "get_pulseaudio_current_sink − \ Can't get PulseAudio current deault sink description (got: ${pa_current_sink_description})." return_get_pulseaudio_current_sink="1" fi else debug_message "get_pulseaudio_current_sink − \ Can't get PulseAudio current sink name (got: ${pa_current_sink_name})." return_get_pulseaudio_current_sink="1" fi return "${return_get_pulseaudio_current_sink}" } # }}} choose_pulseaudio_sink() { # {{{ ## Return False by default return_choose_pulseaudio_sink="1" debug_message "choose_pulseaudio_sink − \ Display PulseAudio sinks list with rofi to select one." choosen_sink=$(rofi -theme solarized_alternate -location 2 -l 4 -no-auto-select -i -dmenu -p "New output sink for PulseAudio (current: ${pa_current_sink_description:-Not available})" < "${pa_sink_list_path}") choosen_sink_index=$(printf -- '%s' "${choosen_sink}" | cut --delimiter=" " --field=1) choosen_sink_description=$(printf -- '%s' "${choosen_sink}" | cut --delimiter=" " --field=2-) ## If no sink was selected (empty var) if [ -z "${choosen_sink_index}" ]; then debug_message "choose_pulseaudio_sink − \ Choosen sink var is ${RED}empty${COLOR_DEBUG}." return_choose_pulseaudio_sink="1" else debug_message "choose_pulseaudio_sink − \ PulseAudio choosen sink is ${RED}${choosen_sink_description}${COLOR_DEBUG} (index: ${RED}${choosen_sink_index}${COLOR_DEBUG})." return_choose_pulseaudio_sink="0" fi return "${return_choose_pulseaudio_sink}" } # }}} set_default_pulseaudio_sink() { # {{{ ## Return False by default return_set_default_pulseaudio_sink="1" debug_message "set_default_pulseaudio_sink − \ Try to set ${RED}${choosen_sink_description}${COLOR_DEBUG} (index: ${RED}${choosen_sink_index}${COLOR_DEBUG}) default PulseAudio sink." if pactl set-default-sink "${choosen_sink_index}"; then debug_message "set_default_pulseaudio_sink − \ ${RED}${choosen_sink_description}${COLOR_DEBUG} is now the default output device." return_set_default_pulseaudio_sink="0" else debug_message "set_default_pulseaudio_sink − \ Error with ${RED}pacmd set-default-sink${COLOR_DEBUG} command." return_set_default_pulseaudio_sink="1" fi return "${return_set_default_pulseaudio_sink}" } # }}} unmute_default_pulseaudio_sink() { # {{{ ## Return False by default return_unmute_default_pulseaudio_sink="1" debug_message "unmute_default_pulseaudio_sink − \ Try to unmute ${RED}@DEFAULT_SINK@${COLOR_DEBUG} for PulseAudio." if pactl set-sink-mute @DEFAULT_SINK@ false; then debug_message "unmute_default_pulseaudio_sink − \ ${RED}@DEFAULT_SINK@${COLOR_DEBUG} for PulseAudio is now unmute." return_unmute_default_pulseaudio_sink="0" else debug_message "unmute_default_pulseaudio_sink − \ Error with ${RED}pacmd set-sink-mute${COLOR_DEBUG} command." return_unmute_default_pulseaudio_sink="1" fi return "${return_unmute_default_pulseaudio_sink}" } # }}} main() { # {{{ ## If rofi command is absent from the system {{{ ### Exit is_command_absent "rofi" \ && exit 0 ## }}} ## If pacmd command is absent from the system {{{ ### Exit is_command_absent "pacmd" \ && exit 0 ## }}} ## Define all vars define_vars ## Try to get the sinks list {{{ ### OR Exit with error message get_pulseaudio_sink_list \ || error_message "Can't get the PulseAudio sinks list. Please use --debug option." 1 ## }}} ## Try to get current default sink {{{ ### OR just display debug message get_pulseaudio_current_sink \ || debug_message "Can't get the PulseAudio current default sink." ## }}} ## Choose a sink with rofi {{{ ### OR Exit with error message choose_pulseaudio_sink \ || error_message "Can't get the PulseAudio sinks list. Please use --debug option." 2 ## }}} ## Set default output sink/device for PulseAudio {{{ ### OR Exit with error message set_default_pulseaudio_sink \ || error_message "Can't set the selected sink (${choosen_sink_description}) as default output sink/device. Please use --debug option." 3 ## }}} ## Try to unmute the default output sink {{{ ### OR Exit with error message unmute_default_pulseaudio_sink \ || error_message "Can't unmute the selected sink (${choosen_sink_description}). Please use --debug option." 4 ## }}} } # }}} # Manage arguments # {{{ # This code can't be in a function due to argument management if [ ! "${NBARGS}" -eq "0" ]; then manage_arg="0" ## If the first argument is not an option if ! printf -- '%s' "${1}" | grep -q -E -- "^-+"; then ## Print help message and exit printf '%b\n' "${RED}Invalid option: ${1}${RESET}" printf '%b\n' "---" usage exit 1 fi # Parse all options (start with a "-") one by one while printf -- '%s' "${1}" | grep -q -E -- "^-+"; do case "${1}" in -d|--debug ) ## debug DEBUG=0 ;; -h|--help ) ## help usage ## Exit after help informations exit 0 ;; * ) ## unknow option printf '%b\n' "${RED}Invalid option: ${1}${RESET}" printf '%b\n' "---" usage exit 1 ;; esac debug_message "Arguments management − \ ${RED}${1}${COLOR_DEBUG} option managed." ## Move to the next argument shift manage_arg=$((manage_arg+1)) done debug_message "Arguments management − \ ${RED}${manage_arg}${COLOR_DEBUG} argument(s) successfully managed." else debug_message "Arguments management − \ No arguments/options to manage." fi # }}} main exit 0