Files
2026-08-24 10:03:24 +08:00

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