add support for GUI dialogs using zenity

This commit is contained in:
Tilman Kranz 2025-03-30 15:04:21 +02:00
parent 3a491e398d
commit f02ec71a3b
2 changed files with 214 additions and 152 deletions

View File

@ -4,6 +4,7 @@
### Dependencies ### Dependencies
* zenity (unless called with option `--no-gui`)
* jq * jq
* Audio subsystem: * Audio subsystem:
- PulseAudio or - PulseAudio or

View File

@ -17,81 +17,144 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
##
# Arguments
operation=$1
##
# Configuration
config_dir="$HOME"/.config/pulseaudio-tcp
config="$config_dir"/config.inc.sh
## ##
# Functions # Functions
usage() {
cat >&2 <<EOF
Usage: $0 [OPTIONS] OPERATION
Options:
--debug Enable additional debug output for start and stop operations.
--no-gui Do not display GUI dialogs, use terminal for input and output.
Operations:
setup Interactively gather settings.
start Start redirection to remote host.
stop Stop redirection to remote host.
restart Stop and then start redirection.
status Check if redirection is set up and enabled.
EOF
}
log() {
level=$1
shift
msg=$*
echo "$level: $msg" >&2
if [[ -t 1 ]] && $gui && [[ -n $(type -p zenity) ]] ; then
case "$level" in
ERROR)
zenity --error --text="$msg"
;;
*)
zenity --info --text="$msg"
;;
esac
fi
}
error() {
log ERROR "$@"
}
info() {
log INFO "$@"
}
debug() {
if "$debug" ; then
log DEBUG "$@"
else
gui=false log DEBUG "$@"
fi
}
_ssh() { _ssh() {
if ssh -S "$USER"-pulseaudio "$remote_user"@"$remote_ip" "$@" ; then if ssh -S "$USER"-pulseaudio "$remote_user"@"$remote_ip" "$@" ; then
return 0 return 0
else else
echo "ERROR: SSH remote_ip=$remote_ip failed." >&2 error "SSH remote_ip=$remote_ip failed."
return 1 return 1
fi fi
} }
question_yesno() {
question=$*
if "$gui" ; then
if zenity --question --text="$question" ; then
return 0
else
return 1
fi
else
while read -r -p "$question [y|n] " answer ; do
case $answer in
[yY])
return 0
;;
[nN])
return 1
;;
esac
done
fi
}
question_input() {
title=$1
question=$2
value=$3
if $gui ; then
zenity --entry --title="$title" --text="$question" --entry-text="$value"
else
echo "$title" >&2
read -r -p "$question ($value)" answer
[[ -z $answer ]] && answer=$value
echo "$answer"
fi
}
# Perform setup operation # Perform setup operation
do_setup() { do_setup() {
if test -e "$config" ; then if test -e "$config" ; then
read -r -p "config=$config already exists, overwrite it? (Y|n) " answer if question_yesno "$config already exists, overwrite it?"; then
if ! test -w "$config" ; then
case "$answer" in error "$config is not writable."
y|Y|"") return 1 ;
if ! test -w "$config" ; then elif ! test -r "$config" ; then
echo "ERROR: config=$config is not writable." >&2 ; error "$config is not readable."
return 1 ; return 1 ;
elif ! test -r "$config" ; then else
echo "ERROR: config=$config is not readable." >&2 ; . "$config"
return 1 ; fi
else else
. "$config" error "Setup aborted."
fi
;;
*)
echo "ERROR: Setup aborted." >&2
return 1 return 1
esac fi
elif ! test -e "$config_dir" ; then elif ! test -e "$config_dir" ; then
mkdir -p "$config_dir" || { mkdir -p "$config_dir" || {
echo "ERROR: Could not create config_dir=$config_dir." >&2 ; error "Could not create $config_dir"
return 1 ; return 1 ;
} }
elif ! test -d "$config_dir" ; then elif ! test -d "$config_dir" ; then
echo "ERROR: config_dir=$config_dir is not a directory." >&2 error "$config_dir is not a directory"
return 1 return 1
elif ! test -w "$config_dir" ; then elif ! test -w "$config_dir" ; then
echo "ERROR: config_dir=$config_dir is not writable." >&2 error "$config_dir is not writable"
return 1 return 1
fi fi
while true ; do while true ; do
read -r -p "Enter IP address of remote host ($remote_ip): " remote_ip_in if remote_ip_in=$(question_input "Set remote host" "Enter name or IP address of remote host:" "$remote_ip") ; then
if test -n "$remote_ip_in" ; then
break
elif test -n "$remote_ip" ; then
remote_ip_in=$remote_ip
break break
fi fi
done done
while true ; do while true ; do
read -r -p "Enter username on remote host $remote_ip ($remote_user): " remote_user_in if remote_user_in=$(question_input "Set remote user" "Enter username on remote host:" "$remote_user") ; then
if test -n "$remote_user_in" ; then
break
elif test -n "$remote_user" ; then
remote_user_in=$remote_user
break break
fi fi
done done
@ -99,65 +162,25 @@ do_setup() {
inbound=${inbound:-true} inbound=${inbound:-true}
while true ; do while true ; do
if "$inbound" ; then if question_yesno "Enable inbound audio?"; then
prompt="Y/n" inbound_in=true
break
else else
prompt="y/N" inbound_in=false
break
fi fi
read -r -p "Enable inbound audio ($prompt): " inbound_in
case "$inbound_in" in
"")
if test -n "$inbound" ; then
inbound_in=$inbound
break 2
fi
;;
y|Y)
inbound_in=true
break 2
;;
n|N)
inbound_in=false
break 2
;;
*)
echo "ERROR: Please type \"y\" or \"n\"." >&2
;;
esac
done done
outbound=${outbound:-true} outbound=${outbound:-true}
while true ; do while true ; do
if "$outbound" ; then if question_yesno "Enable outbound audio?"; then
prompt="Y/n" outbound_in=true
break
else else
prompt="y/N" outbound_in=false
break
fi fi
read -r -p "Enable outbound audio ($prompt): " outbound_in
case "$outbound_in" in
"")
if test -n "$outbound" ; then
outbound_in=$outbound
break 2
fi
;;
y|Y)
outbound_in=true
break 2
;;
n|N)
outbound_in=false
break 2
;;
*)
echo "ERROR: Please type \"y\" or \"n\"." >&2
;;
esac
done done
cat > "$config" << EOF cat > "$config" << EOF
@ -192,37 +215,40 @@ check_pa_ssh() {
# Perform status operation # Perform status operation
do_status() { do_status() {
rv=0 rv=0
errors=()
if ! check_pa_ssh ; then if ! check_pa_ssh ; then
rv=1 rv=1
fi fi
if ! _ssh pactl list modules | grep -Fq "Name: module-native-protocol-tcp" ; then if ! _ssh pactl list modules | grep -Fq "Name: module-native-protocol-tcp" ; then
echo "ERROR: PulseAudio module \"module-native-protocol-tcp\" is not loaded on remote_ip=$remote_ip." >&2 errors+=("PulseAudio module \"module-native-protocol-tcp\" is not loaded on remote host $remote_ip.")
rv=1 rv=1
fi fi
if "$outbound" ; then if "$outbound" ; then
if ! pactl list modules | grep -Fq "Name: module-tunnel-sink" ; then if ! pactl list modules | grep -Fq "Name: module-tunnel-sink" ; then
echo "ERROR: PulseAudio module \"module-tunnel-sink\" is not loaded." >&2 errors+=("PulseAudio module \"module-tunnel-sink\" is not loaded.")
rv=1 rv=1
fi fi
if ! pactl get-default-sink | grep -Fq "tunnel-sink.tcp:127.0.0.1" ; then if ! pactl get-default-sink | grep -Fq "tunnel-sink.tcp:127.0.0.1" ; then
echo "ERROR: \"tunnel-sink.tcp:127.0.0.1\" is not the default PulseAudio sink." >&2 errors+=("\"tunnel-sink.tcp:127.0.0.1\" is not the default PulseAudio sink.")
rv=1 rv=1
fi fi
fi fi
if "$inbound" ; then if "$inbound" ; then
if ! pactl list modules | grep -Fq "Name: module-tunnel-source" ; then if ! pactl list modules | grep -Fq "Name: module-tunnel-source" ; then
echo "ERROR: PulseAudio module \"module-tunnel-source\" is not loaded." >&2 errors+=("PulseAudio module \"module-tunnel-source\" is not loaded.")
rv=1 rv=1
fi fi
fi fi
if test "$rv" -eq 0 ; then if [[ $rv -eq 0 ]] ; then
echo "INFO: All checks passed; pulseaudio-tcp status is okay." >&2 info "All checks passed; pulseaudio-tcp status is okay."
else
error "pulseaudio-tcp status is not okay: ${errors[*]}"
fi fi
return "$rv" return "$rv"
@ -231,10 +257,10 @@ do_status() {
# Acquire PulseAudio cookie from remote host # Acquire PulseAudio cookie from remote host
sync_pa_cookie() { sync_pa_cookie() {
if scp -q "$remote_user"@"$remote_ip":.config/pulse/cookie ~/.config/pulse/cookie ; then if scp -q "$remote_user"@"$remote_ip":.config/pulse/cookie ~/.config/pulse/cookie ; then
echo "INFO: Synced PulseAudio cookie from remote_ip=$remote_ip." >&2 debug "Synced PulseAudio cookie from remote host $remote_ip."
return 0 return 0
else else
echo "ERROR: Unable to sync PulseAudio cookie from remote_ip=$remote_ip." >&2 error "Unable to sync PulseAudio cookie from remote host $remote_ip."
return 1 return 1
fi fi
} }
@ -247,13 +273,13 @@ establish_ssh_portforward() {
# Enable PulseAudio TCP tunnel server on remote host # Enable PulseAudio TCP tunnel server on remote host
enable_remote_pa_tunnel_server() { enable_remote_pa_tunnel_server() {
if _ssh pactl list modules | grep -Fq "Name: module-native-protocol-tcp" ; then if _ssh pactl list modules | grep -Fq "Name: module-native-protocol-tcp" ; then
echo "INFO: PulseAudio module \"module-native-protocol-tcp\" already loaded on remote_ip=$remote_ip." >&2 debug "PulseAudio module \"module-native-protocol-tcp\" already loaded on remote host $remote_ip."
return 0 return 0
elif _ssh pactl load-module module-native-protocol-tcp listen=127.0.0.1 auth-ip-acl=127.0.0.1 ; then elif _ssh pactl load-module module-native-protocol-tcp listen=127.0.0.1 auth-ip-acl=127.0.0.1 ; then
echo "INFO: Loaded PulseAudio module \"module-native-protocol-tcp\" on remote_ip=$remote_ip." >&2 debug "Loaded PulseAudio module \"module-native-protocol-tcp\" on remote host $remote_ip."
return 0 return 0
else else
echo "ERROR: Unable to load PulseAudio module \"module-native-protocol-tcp\" on remote_ip=$remote_ip." >&2 error "Unable to load PulseAudio module \"module-native-protocol-tcp\" on remote host $remote_ip."
return 1 return 1
fi fi
} }
@ -261,13 +287,13 @@ enable_remote_pa_tunnel_server() {
# Enable tunnel sink on local host # Enable tunnel sink on local host
enable_local_pa_tunnel_sink() { enable_local_pa_tunnel_sink() {
if pactl list modules | grep -Fq "Name: module-tunnel-sink" ; then if pactl list modules | grep -Fq "Name: module-tunnel-sink" ; then
echo "INFO: PulseAudio module \"module-tunnel-sink\" already loaded." >&2 debug "PulseAudio module \"module-tunnel-sink\" already loaded."
return 0 return 0
elif pactl load-module module-tunnel-sink server=tcp:127.0.0.1 ; then elif pactl load-module module-tunnel-sink server=tcp:127.0.0.1 ; then
echo "INFO: Loaded PulseAudio module \"module-tunnel-sink\"." >&2 debug "Loaded PulseAudio module \"module-tunnel-sink\"."
return 0 return 0
else else
echo "ERROR: Unable to load PulseAudio module \"module-tunnel-sink\"." >&2 error "Unable to load PulseAudio module \"module-tunnel-sink\"."
return 1 return 1
fi fi
} }
@ -275,13 +301,13 @@ enable_local_pa_tunnel_sink() {
# Enable tunnel source on local host # Enable tunnel source on local host
enable_local_pa_tunnel_source() { enable_local_pa_tunnel_source() {
if pactl list modules | grep -Fq "Name: module-tunnel-source" ; then if pactl list modules | grep -Fq "Name: module-tunnel-source" ; then
echo "INFO: PulseAudio module \"module-tunnel-source\" already loaded." >&2 debug "PulseAudio module \"module-tunnel-source\" already loaded."
return 0 return 0
elif pactl load-module module-tunnel-source server=tcp:127.0.0.1 ; then elif pactl load-module module-tunnel-source server=tcp:127.0.0.1 ; then
echo "INFO: Loaded PulseAudio module \"module-tunnel-source\"." >&2 debug "Loaded PulseAudio module \"module-tunnel-source\"."
return 0 return 0
else else
echo "ERROR: Unable to load PulseAudio module \"module-tunnel-source\"." >&2 error "Unable to load PulseAudio module \"module-tunnel-source\"."
return 1 return 1
fi fi
} }
@ -289,10 +315,10 @@ enable_local_pa_tunnel_source() {
# Set tunnel sink as default sink on local host # Set tunnel sink as default sink on local host
set_local_pa_tunnel_sink_as_default() { set_local_pa_tunnel_sink_as_default() {
if pactl set-default-sink tunnel-sink.tcp:127.0.0.1 ; then if pactl set-default-sink tunnel-sink.tcp:127.0.0.1 ; then
echo "INFO: Set \"tunnel-sink.tcp:127.0.0.1\" as default PulseAudio sink." >&2 debug "Set \"tunnel-sink.tcp:127.0.0.1\" as default PulseAudio sink."
return 0 return 0
else else
echo "ERROR: Failed to set \"tunnel-sink.tcp:127.0.0.1\" as default PulseAudio sink." >&2 error "Failed to set \"tunnel-sink.tcp:127.0.0.1\" as default PulseAudio sink."
return 1 return 1
fi fi
} }
@ -320,10 +346,10 @@ do_start() {
# Remove PulseAudio TCP tunnel sink on local host # Remove PulseAudio TCP tunnel sink on local host
remove_local_pa_tunnel_sink() { remove_local_pa_tunnel_sink() {
if ! pactl list modules | grep -Fq "Name: module-tunnel-sink" ; then if ! pactl list modules | grep -Fq "Name: module-tunnel-sink" ; then
echo "INFO: PulseAudio module \"module-tunnel-sink\" is not loaded." >&2 debug " PulseAudio module \"module-tunnel-sink\" is not loaded."
return 0 return 0
elif ! pactl list sinks | grep -Fq "tunnel-sink.tcp:127.0.0.1" ; then elif ! pactl list sinks | grep -Fq "tunnel-sink.tcp:127.0.0.1" ; then
echo "INFO: No PulseAudio tunnel sink to 127.0.0.1 exists." >&2 debug "No PulseAudio tunnel sink to 127.0.0.1 exists."
return 0 return 0
else else
owner_module=$( owner_module=$(
@ -332,10 +358,10 @@ remove_local_pa_tunnel_sink() {
) )
if ! pactl unload-module "$owner_module" ; then if ! pactl unload-module "$owner_module" ; then
echo "ERROR: Could not unload owner module $owner_module of PulseAudio sink \"tunnel-sink.tcp:127.0.0.1\"." >&2 error "Could not unload owner module $owner_module of PulseAudio sink \"tunnel-sink.tcp:127.0.0.1\"."
return 1 return 1
else else
echo "INFO: Unloaded owner module $owner_module of PulseAudio sink \"tunnel-sink.tcp:127.0.0.1\"." >&2 debug "Unloaded owner module $owner_module of PulseAudio sink \"tunnel-sink.tcp:127.0.0.1\"."
return 0 return 0
fi fi
fi fi
@ -344,10 +370,10 @@ remove_local_pa_tunnel_sink() {
# Remove PulseAudio TCP tunnel source on local host # Remove PulseAudio TCP tunnel source on local host
remove_local_pa_tunnel_source() { remove_local_pa_tunnel_source() {
if ! pactl list modules | grep -Fq "Name: module-tunnel-source" ; then if ! pactl list modules | grep -Fq "Name: module-tunnel-source" ; then
echo "INFO: PulseAudio module \"module-tunnel-source\" is not loaded." >&2 debug "PulseAudio module \"module-tunnel-source\" is not loaded."
return 0 return 0
elif ! pactl list sources | grep -Fq "tunnel-source.tcp:127.0.0.1" ; then elif ! pactl list sources | grep -Fq "tunnel-source.tcp:127.0.0.1" ; then
echo "INFO: No PulseAudio tunnel source from 127.0.0.1 exists." >&2 debug "No PulseAudio tunnel source from 127.0.0.1 exists."
return 0 return 0
else else
owner_module=$( owner_module=$(
@ -356,10 +382,10 @@ remove_local_pa_tunnel_source() {
) )
if ! pactl unload-module "$owner_module" ; then if ! pactl unload-module "$owner_module" ; then
echo "ERROR: Could not unload owner module $owner_module of PulseAudio source \"tunnel-source.tcp:127.0.0.1\"." >&2 error "Could not unload owner module $owner_module of PulseAudio source \"tunnel-source.tcp:127.0.0.1\"."
return 1 return 1
else else
echo "INFO: Unloaded owner module $owner_module of PulseAudio source \"tunnel-source.tcp:127.0.0.1\"." >&2 debug "Unloaded owner module $owner_module of PulseAudio source \"tunnel-source.tcp:127.0.0.1\"."
return 0 return 0
fi fi
fi fi
@ -368,13 +394,13 @@ remove_local_pa_tunnel_source() {
# Stop PulseAudio TCP tunnel server on remote host. # Stop PulseAudio TCP tunnel server on remote host.
disable_remote_pa_tunnel_server() { disable_remote_pa_tunnel_server() {
if ! _ssh pactl list modules | grep -Fq "Name: module-native-protocol-tcp" ; then if ! _ssh pactl list modules | grep -Fq "Name: module-native-protocol-tcp" ; then
echo "INFO: PulseAudio module \"module-native-protocol-tcp\" not loaded on remote_ip=$remote_ip." >&2 debug "PulseAudio module \"module-native-protocol-tcp\" not loaded on remote_ip=$remote_ip."
return 0 return 0
elif ! _ssh pactl unload-module module-native-protocol-tcp ; then elif ! _ssh pactl unload-module module-native-protocol-tcp ; then
echo "ERROR: Could not unload PulseAudio module \"module-native-protocol-tcp\" on remote_ip=$remote_ip." >&2 error "Could not unload PulseAudio module \"module-native-protocol-tcp\" on remote host $remote_ip."
return 1 return 1
else else
echo "INFO: Unloaded PulseAudio module \"module-native-protocol-tcp\" on remote_ip=$remote_ip." >&2 debug "Unloaded PulseAudio module \"module-native-protocol-tcp\" on remote host $remote_ip."
return 0 return 0
fi fi
} }
@ -404,58 +430,93 @@ do_stop() {
return 0 return 0
} }
##
# Arguments
for arg in "$@" ; do
case "$arg" in
--debug)
debug_cmdline=true
;;
--no-gui)
gui_cmdline=false
;;
--help)
help_cmdline=true
;;
*)
operation=$arg
;;
esac
done
##
# Configuration
config_dir="$HOME"/.config/pulseaudio-tcp
config=$config_dir/config.inc.sh
if [[ $help_cmdline = true ]] ; then
usage
exit 0
fi
if [[ $debug_cmdline = true ]] ; then
debug=true
else
debug=false
fi
if [[ $gui_cmdline = false ]] ; then
gui=false
else
gui=true
fi
## ##
# Main Program # Main Program
rv=0 rv=0
if test "$operation" = setup ; then if test "$operation" = setup ; then
echo "Entering setup mode ..." info "Entering setup mode ..."
else else
if ! test -e "$config" ; then if ! test -e "$config" ; then
echo "ERROR: Configfile $config does not exist (use \"$0 setup\" first)." >&2 error "Configfile $config does not exist (use \"$0 setup\" first)."
rv=1 rv=1
elif ! test -r "$config" ; then elif ! test -r "$config" ; then
echo "ERROR: Configfile $config is not readable." >&2 error "Configfile $config is not readable."
rv=1 rv=1
elif ! test -f "$config" ; then elif ! test -f "$config" ; then
echo "ERROR: Configfile $config is not a regular file." >&2 error "Configfile $config is not a regular file."
rv=1 rv=1
else else
. "$config" . "$config"
if test -z "$remote_ip" ; then if [[ -z $remote_ip ]] ; then
echo "ERROR: \"remote_ip=<IP address>\" not set in configfile $config." >&2 error "\"remote_ip=<IP address>\" not set in configfile $config."
rv=1 rv=1
elif test -z "$remote_user" ; then elif [[ -z $remote_user ]] ; then
echo "ERROR: \"remote_user=<username>\" not set in configfile $config." >&2 error "\"remote_user=<username>\" not set in configfile $config."
rv=1 rv=1
fi fi
fi fi
if test -z "$(type -p jq)" ; then required_cmds=( jq pactl ssh )
echo "ERROR: Required executable \"jq\" not found." >&2 "$gui" && required_cmds+=( zenity )
rv=1
elif test -z "$(type -p pactl)" ; then for exe in "${required_cmds[@]}" ; do
echo "ERROR: Required executable \"pactl\" not found." >&2 if [[ -z $(type -p "$exe") ]] ; then
rv=1 error "Required executable \"$exe\" not found."
elif test -z "$(type -p ssh)" ; then rv=1
echo "ERROR: Required executable \"ssh\" not found." >&2 fi
rv=1 done
fi
fi fi
if test "$rv" -ne 0 ; then if [[ $rv -ne 0 ]] ; then
echo "ERROR: Preliminary checks failed, skipping operation." >&2 error "Preliminary checks failed, skipping operation."
else else
case "$operation" in case "$operation" in
""|-h|--help)
cat << EOF
Setup and run encrypted connection to remote PulseAudio/Pipewire server
Usage: $0 restart|setup|start|status|stop
EOF
rv=0
;;
setup) setup)
do_setup do_setup
rv=$? rv=$?
@ -478,7 +539,7 @@ EOF
rv=$? rv=$?
;; ;;
*) *)
echo "ERROR: Usage: $0 restart|setup|start|status|stop" >&2 usage
rv=1 rv=1
;; ;;
esac esac