506 lines
13 KiB
Bash
506 lines
13 KiB
Bash
#!/bin/bash
|
|
#
|
|
# SPDX-FileCopyrightText: 2021-2025 BlueALSA developers
|
|
# SPDX-License-Identifier: MIT
|
|
#
|
|
# Bash completion for bluez-alsa project applications.
|
|
|
|
# helper function gets available profiles
|
|
# @param $1 the bluealsad executable name
|
|
_bluealsa_profiles() {
|
|
"$1" --help | while read -r line; do
|
|
[[ "$line" = "Available BT profiles:" ]] && start=yes && continue
|
|
[[ "$start" ]] || continue
|
|
[[ "$line" ]] || break
|
|
read -ra words <<< "$line"
|
|
echo "${words[1]}"
|
|
done
|
|
}
|
|
|
|
# helper function gets available codecs
|
|
# @param $1 the bluealsad executable name
|
|
_bluealsa_codecs() {
|
|
"$1" --help | while read -r line; do
|
|
[[ "$line" = "Available BT audio codecs:" ]] && start=yes && continue
|
|
[[ "$start" ]] || continue
|
|
[[ "$line" ]] || break
|
|
line=${line,,}
|
|
IFS=", " read -ra words <<< "${line,,}"
|
|
echo "${words[@]:1}"
|
|
done
|
|
}
|
|
|
|
# helper function gets available enum values
|
|
# @param $1 the bluealsad executable name
|
|
# @param $2 the bluealsad option to inspect
|
|
_bluealsa_enum_values() {
|
|
"$1" "${2}=" 2>&1 | while read -r line; do
|
|
[[ $line =~ \{([^}]*)\} ]] || continue
|
|
echo "${BASH_REMATCH[1]//,/}"
|
|
done
|
|
}
|
|
|
|
# helper function gets dbus services
|
|
# note that this function does not detect if default service (no suffix) is running.
|
|
_bluealsa_list_dbus_suffices() {
|
|
dbus-send --system --dest=org.freedesktop.DBus --type=method_call \
|
|
--print-reply /org/freedesktop/DBus \
|
|
org.freedesktop.DBus.ListNames 2>/dev/null | \
|
|
while read -r line; do
|
|
[[ $line =~ org\.bluealsa\.([^'"']+) ]] || continue
|
|
echo "${BASH_REMATCH[1]}"
|
|
done
|
|
}
|
|
|
|
# helper function gets codecs for given pcm
|
|
# before calling this function, make sure that the bautil_args was properly
|
|
# initialized with the _bluealsa_util_init function
|
|
# @param $1 pcm path
|
|
_bluealsa_pcm_codecs() {
|
|
"${bautil_args[@]}" codec "$1" | while read -r line; do
|
|
[[ $line =~ ^Available\ codecs:\ ([^[]+$) ]] || continue
|
|
echo "${BASH_REMATCH[1]}"
|
|
done
|
|
}
|
|
|
|
# helper function completes supported bluealsactl monitor properties
|
|
_bluealsactl_properties() {
|
|
local properties=( codec running softvolume volume )
|
|
if [[ "$cur" == *,* ]]; then
|
|
local realcur prefix chosen remaining
|
|
realcur="${cur##*,}"
|
|
prefix="${cur%,*}"
|
|
IFS="," read -ra chosen <<< "${prefix,,}"
|
|
readarray -t remaining < <(printf '%s\n' "${properties[@]}" "${chosen[@]}" | sort | uniq -u)
|
|
if [[ ${#remaining[@]} -gt 0 ]]; then
|
|
readarray -t COMPREPLY < <(compgen -W "${remaining[*]}" -- "$realcur")
|
|
if [[ ${#COMPREPLY[@]} -eq 1 ]] ; then
|
|
COMPREPLY[0]="$prefix,${COMPREPLY[0]}"
|
|
fi
|
|
if [[ ${#remaining[@]} -gt 0 && "$cur" == "${COMPREPLY[0]}" ]] ; then
|
|
COMPREPLY=( "${COMPREPLY[0]}," )
|
|
fi
|
|
if [[ ${#remaining[@]} -gt 1 ]]; then
|
|
compopt -o nospace
|
|
fi
|
|
fi
|
|
else
|
|
readarray -t COMPREPLY < <(compgen -W "${properties[*]}" -- "$cur")
|
|
if [[ ${#COMPREPLY[@]} -eq 1 && "$cur" == "${COMPREPLY[0]}" ]]; then
|
|
COMPREPLY=("${COMPREPLY[0]},")
|
|
fi
|
|
compopt -o nospace
|
|
fi
|
|
}
|
|
|
|
# helper function gets ALSA pcms
|
|
# @param $1 is the current word to match
|
|
_bluealsa_aplay_pcms() {
|
|
eval local cur="$1"
|
|
cur="${cur/%\\/\\\\}"
|
|
while read -r; do
|
|
[[ "$REPLY" == " "* ]] && continue
|
|
[[ "$REPLY" == "$cur"* ]] || continue
|
|
echo "${REPLY// /\\ }"
|
|
done < <(aplay -L 2>/dev/null)
|
|
}
|
|
|
|
# helper function gets rfcomm dbus paths
|
|
# @param $1 the full BlueALSA service name ( org.bluealsa* )
|
|
_bluealsa_rfcomm_paths() {
|
|
busctl --list tree "$1" 2>/dev/null | while read -r line; do
|
|
[[ "$line" = /org/bluealsa/hci[0-9]/dev*/rfcomm ]] || continue
|
|
echo "${line/\/rfcomm/}"
|
|
done
|
|
}
|
|
|
|
# helper function gets options from command line
|
|
# @param $1 the executable name
|
|
# puts exec and dbus arg ( --dbus=aaa ) into array variable bautil_args
|
|
# puts service name ( org.bluealsa.aaa ) into variable service
|
|
# puts offset of first non-option argument into variable nonopt_offset
|
|
# @return 0 if no errors found, 1 if dbus check failed
|
|
_bluealsa_util_init() {
|
|
local valopts="-B --dbus"
|
|
bautil_args=( "$1" )
|
|
service="org.bluealsa"
|
|
nonopt_offset=0
|
|
local i
|
|
for (( i=1; i <= COMP_CWORD; i++ )); do
|
|
case "${COMP_WORDS[i]}" in
|
|
--dbus)
|
|
if (( i == COMP_CWORD )) ; then
|
|
break
|
|
elif (( i == COMP_CWORD - 1 )) ; then
|
|
[[ "${COMP_WORDS[i+1]}" = = ]] && break
|
|
bautil_args+=( "--dbus=${COMP_WORDS[i+1]}" )
|
|
service="org.bluealsa.${COMP_WORDS[i+1]}"
|
|
break
|
|
else
|
|
[[ "${COMP_WORDS[i+1]}" = = ]] && (( i++ ))
|
|
if [[ "${COMP_WORDS[i+1]}" ]] ; then
|
|
(( i++ ))
|
|
bautil_args+=( "--dbus=${COMP_WORDS[i]}" )
|
|
service="org.bluealsa.${COMP_WORDS[i]}"
|
|
continue
|
|
fi
|
|
fi
|
|
;;
|
|
-B)
|
|
if (( i == COMP_CWORD )) ; then
|
|
break
|
|
elif (( i == COMP_CWORD - 1 )) ; then
|
|
bautil_args+=( "--dbus=${COMP_WORDS[i+1]}" )
|
|
service="org.bluealsa.${COMP_WORDS[i+1]}"
|
|
break
|
|
else
|
|
if [[ "${COMP_WORDS[i+1]}" ]] ; then
|
|
(( i++ ))
|
|
bautil_args+=( "--dbus=${COMP_WORDS[i]}" )
|
|
service="org.bluealsa.${COMP_WORDS[i]}"
|
|
continue
|
|
fi
|
|
fi
|
|
;;
|
|
-*|=)
|
|
continue
|
|
;;
|
|
esac
|
|
|
|
[[ "${COMP_WORDS[i-1]}" == = ]] && (( i < COMP_CWORD )) && continue
|
|
[[ "$valopts " == *"${COMP_WORDS[i-1]} "* ]] && continue
|
|
|
|
nonopt_offset=$i
|
|
break
|
|
|
|
done
|
|
|
|
return 0
|
|
}
|
|
|
|
# helper function completes options
|
|
_bluealsa_complete_options() {
|
|
readarray -t COMPREPLY < <(compgen -W "$(_parse_help "$1")" -- "$cur")
|
|
[[ ${COMPREPLY[0]} == *= ]] && compopt -o nospace
|
|
}
|
|
|
|
# completion function for bluealsad
|
|
# complete available devices and profiles in addition to options
|
|
_bluealsad() {
|
|
|
|
local cur prev words cword split
|
|
_init_completion -s || return
|
|
|
|
local prefix list
|
|
|
|
case "$prev" in
|
|
--device|-i)
|
|
readarray -t list < <(ls -I '*:*' /sys/class/bluetooth)
|
|
readarray -t COMPREPLY < <(compgen -W "${list[*]}" -- "$cur")
|
|
return
|
|
;;
|
|
--profile|-p)
|
|
[[ $cur =~ ^[+-].* ]] && prefix=${cur:0:1} && cur=${cur:1}
|
|
readarray -t list < <(_bluealsa_profiles "$1")
|
|
readarray -t COMPREPLY < <(compgen -P "$prefix" -W "${list[*]}" -- "$cur")
|
|
return
|
|
;;
|
|
--codec|-c)
|
|
[[ $cur =~ ^[+-].* ]] && prefix=${cur:0:1} && cur=${cur:1}
|
|
readarray -t list < <(_bluealsa_codecs "$1")
|
|
readarray -t COMPREPLY < <(compgen -P "$prefix" -W "${list[*]}" -- "$cur")
|
|
return
|
|
;;
|
|
--loglevel|--sbc-quality|--aac-latm-version|--mp3-algorithm|--mp3-vbr-quality|--ldac-quality)
|
|
readarray -t list < <(_bluealsa_enum_values "$1" "$prev")
|
|
readarray -t COMPREPLY < <(compgen -W "${list[*]}" -- "$cur")
|
|
return
|
|
;;
|
|
--xapl-resp-name)
|
|
readarray -t COMPREPLY < <(compgen -W "BlueALSA iPhone" -- "$cur")
|
|
return
|
|
;;
|
|
--*)
|
|
[[ ${COMP_WORDS[COMP_CWORD]} = = ]] && return
|
|
;;
|
|
esac
|
|
|
|
_bluealsa_complete_options "$1"
|
|
}
|
|
|
|
# completion function for bluealsa-aplay
|
|
# completes available dbus suffices and ALSA pcms in addition to options
|
|
# - does not complete MAC addresses
|
|
# requires aplay to list ALSA pcms
|
|
_bluealsa_aplay() {
|
|
|
|
local cur prev words cword split
|
|
_init_completion -s -n : || return
|
|
|
|
local list
|
|
|
|
case "$prev" in
|
|
--dbus|-B)
|
|
_have dbus-send || return
|
|
readarray -t list < <(_bluealsa_list_dbus_suffices)
|
|
readarray -t COMPREPLY < <(compgen -W "${list[*]}" -- "$cur")
|
|
return
|
|
;;
|
|
--pcm|-D)
|
|
_have aplay || return
|
|
|
|
# do not attempt completion on words containing ' or "
|
|
[[ "$cur" =~ [\'\"] ]] && return
|
|
|
|
readarray -t COMPREPLY < <(_bluealsa_aplay_pcms "$cur")
|
|
|
|
# ALSA pcm names can contain '=' and ':', both of which cause
|
|
# problems for bash completion if it considers them to be word
|
|
# terminators. So we adjust the list of candidate matches to allow
|
|
# for this.
|
|
if [[ "$cur" == *=* && "$COMP_WORDBREAKS" == *=* ]]; then
|
|
# Remove equal-word prefix from COMPREPLY items
|
|
local equal_prefix=${cur%"${cur##*=}"}
|
|
local i=${#COMPREPLY[*]}
|
|
while [[ $((--i)) -ge 0 ]]; do
|
|
COMPREPLY[i]=${COMPREPLY[$i]#"$equal_prefix"}
|
|
done
|
|
fi
|
|
__ltrim_colon_completions "$cur"
|
|
return
|
|
;;
|
|
--loglevel|--resampler|--volume)
|
|
readarray -t list < <(_bluealsa_enum_values "$1" "$prev")
|
|
readarray -t COMPREPLY < <(compgen -W "${list[*]}" -- "$cur")
|
|
return
|
|
;;
|
|
--*)
|
|
[[ ${COMP_WORDS[COMP_CWORD]} = = ]] && return
|
|
;;
|
|
esac
|
|
|
|
_bluealsa_complete_options "$1"
|
|
}
|
|
|
|
# completion function for bluealsactl
|
|
# complete available dbus suffices, command names and pcm paths in addition to options
|
|
_bluealsactl() {
|
|
|
|
local cur prev words cword split
|
|
_init_completion -s || return
|
|
|
|
local bautil_args service
|
|
local -i nonopt_offset
|
|
_bluealsa_util_init "$1" || return
|
|
|
|
# the command names supported by this version of bluealsactl
|
|
local simple_commands="list-pcms list-services monitor status"
|
|
local path_commands="codec client-delay info mute open soft-volume volume"
|
|
|
|
# options that may appear before or after the command
|
|
local global_shortopts="-h"
|
|
local global_longopts="--help"
|
|
|
|
# options that may appear only before the command
|
|
local base_shortopts="-B -V -q -v"
|
|
local base_longopts="--dbus= --version --quiet --verbose"
|
|
|
|
local command path list
|
|
|
|
# process pre-command options
|
|
case "$nonopt_offset" in
|
|
0)
|
|
case "$prev" in
|
|
--dbus|-B)
|
|
_have dbus-send || return
|
|
readarray -t list < <(_bluealsa_list_dbus_suffices)
|
|
readarray -t COMPREPLY < <(compgen -W "${list[*]}" -- "$cur")
|
|
return
|
|
;;
|
|
esac
|
|
case "$cur" in
|
|
-|--*)
|
|
readarray -t COMPREPLY < <(compgen -W "$global_longopts $base_longopts" -- "$cur")
|
|
[[ "${COMPREPLY[0]}" == *= ]] && compopt -o nospace
|
|
;;
|
|
-?)
|
|
readarray -t COMPREPLY < <(compgen -W "$global_shortopts $base_shortopts" -- "$cur")
|
|
;;
|
|
esac
|
|
return
|
|
;;
|
|
"$COMP_CWORD")
|
|
# list available commands
|
|
readarray -t COMPREPLY < <(compgen -W "$simple_commands $path_commands" -- "$cur")
|
|
return
|
|
;;
|
|
esac
|
|
|
|
# check for valid command
|
|
command="${COMP_WORDS[nonopt_offset]}"
|
|
[[ "$simple_commands $path_commands" =~ $command ]] || return
|
|
|
|
# process command-specific options
|
|
case "$command" in
|
|
monitor)
|
|
case "$prev" in
|
|
--properties)
|
|
_bluealsactl_properties
|
|
return
|
|
;;
|
|
esac
|
|
global_longopts+=" --properties"
|
|
global_shortopts+=" -p"
|
|
case "$cur" in
|
|
-p)
|
|
COMPREPLY=( "--properties" )
|
|
compopt -o nospace
|
|
return
|
|
;;
|
|
-p?*)
|
|
COMPREPLY=( "--properties=${cur:2}" )
|
|
compopt -o nospace
|
|
return
|
|
;;
|
|
--properties)
|
|
COMPREPLY=( "--properties=" )
|
|
compopt -o nospace
|
|
return
|
|
;;
|
|
-)
|
|
COMPREPLY=( "--" )
|
|
compopt -o nospace
|
|
return
|
|
;;
|
|
esac
|
|
;;
|
|
esac
|
|
|
|
# find path argument
|
|
for (( i=(nonopt_offset + 1); i < COMP_CWORD; i++ )); do
|
|
[[ "${COMP_WORDS[i]}" == -* ]] && continue
|
|
path="${COMP_WORDS[i]}"
|
|
path_offset=i
|
|
break
|
|
done
|
|
|
|
# process global options and path
|
|
if [[ -z "$path" ]] ; then
|
|
case "$cur" in
|
|
-|--*)
|
|
readarray -t COMPREPLY < <(compgen -W "$global_longopts" -- "$cur")
|
|
[[ "${COMPREPLY[0]}" == --properties* ]] && compopt -o nospace
|
|
return
|
|
;;
|
|
-?)
|
|
readarray -t COMPREPLY < <(compgen -W "$global_shortopts" -- "$cur")
|
|
return
|
|
;;
|
|
esac
|
|
if [[ "$path_commands" =~ $command ]]; then
|
|
readarray -t list < <("${bautil_args[@]}" list-pcms 2>/dev/null)
|
|
readarray -t COMPREPLY < <(compgen -W "${list[*]}" -- "$cur")
|
|
return
|
|
fi
|
|
fi
|
|
|
|
# process command positional arguments
|
|
case "$command" in
|
|
codec)
|
|
if (( COMP_CWORD == path_offset + 1 )) ; then
|
|
readarray -t list < <(_bluealsa_pcm_codecs "$path")
|
|
readarray -t COMPREPLY < <(compgen -W "${list[*]}" -- "$cur")
|
|
fi
|
|
;;
|
|
mute)
|
|
if (( COMP_CWORD < path_offset + 3 )) ; then
|
|
if [[ "$cur" == "" ]] ; then
|
|
COMPREPLY=( true false )
|
|
else
|
|
readarray -t COMPREPLY < <(compgen -W "on yes true 1 off no false 0" -- "${cur,,}")
|
|
fi
|
|
fi
|
|
;;
|
|
soft-volume)
|
|
if (( COMP_CWORD < path_offset + 2 )) ; then
|
|
if [[ "$cur" == "" ]] ; then
|
|
COMPREPLY=( true false )
|
|
else
|
|
readarray -t COMPREPLY < <(compgen -W "on yes true 1 off no false 0" -- "${cur,,}")
|
|
fi
|
|
fi
|
|
;;
|
|
monitor)
|
|
case "$prev" in
|
|
--properties)
|
|
_bluealsactl_properties
|
|
;;
|
|
-p)
|
|
return
|
|
;;
|
|
esac
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# completion function for bluealsa-rfcomm
|
|
# complete available dbus suffices and device paths in addition to options
|
|
# requires busctl (part of elogind or systemd) to list device paths
|
|
_bluealsa_rfcomm() {
|
|
|
|
local cur prev words cword split
|
|
_init_completion -s || return
|
|
|
|
local bautil_args service
|
|
local -i nonopt_offset
|
|
_bluealsa_util_init "$1" || return
|
|
|
|
local list
|
|
|
|
# check for dbus service suffix first
|
|
case "$prev" in
|
|
--dbus|-B)
|
|
_have dbus-send || return
|
|
readarray -t list < <(_bluealsa_list_dbus_suffices)
|
|
readarray -t COMPREPLY < <(compgen -W "${list[*]}" -- "$cur")
|
|
return
|
|
;;
|
|
--*)
|
|
[[ ${COMP_WORDS[COMP_CWORD]} = = ]] && return
|
|
;;
|
|
esac
|
|
|
|
if (( nonopt_offset == COMP_CWORD )) ; then
|
|
_have busctl || return
|
|
readarray -t list < <(_bluealsa_rfcomm_paths "$service")
|
|
readarray -t COMPREPLY < <(compgen -W "${list[*]}" -- "$cur")
|
|
return
|
|
fi
|
|
|
|
# do not list options if path was found
|
|
(( nonopt_offset > 0 )) && return
|
|
|
|
_bluealsa_complete_options "$1"
|
|
}
|
|
|
|
# completion function for a2dpconf and hcitop
|
|
# complete only the options
|
|
_bluealsa_others() {
|
|
# shellcheck disable=SC2034
|
|
local cur prev words cword split
|
|
_init_completion -s || return
|
|
case "$prev" in
|
|
--*)
|
|
[[ ${COMP_WORDS[COMP_CWORD]} = = ]] && return
|
|
;;
|
|
esac
|
|
_bluealsa_complete_options "$1"
|
|
}
|
|
|
|
complete -F _bluealsad bluealsad
|
|
complete -F _bluealsactl bluealsactl
|
|
complete -F _bluealsa_aplay bluealsa-aplay
|
|
complete -F _bluealsa_rfcomm bluealsa-rfcomm
|
|
complete -F _bluealsa_others hcitop
|
|
complete -F _bluealsa_others a2dpconf
|