summaryrefslogtreecommitdiff
path: root/scripts
diff options
context:
space:
mode:
authorivan-boikov <ik.boikov.public@protonmail.com>2021-05-24 22:45:27 +0200
committerivan-boikov <ik.boikov.public@protonmail.com>2021-05-24 22:45:27 +0200
commit1079eb062f2f48eb4bcd11528cf811ba5a4cf9e9 (patch)
tree60e58930ffa603ca40244be27d7562cff155cd29 /scripts
parent50252728d8159e8b7fbdf4636fb9c41fdb77d269 (diff)
volume control, better dunst colors
Diffstat (limited to 'scripts')
-rwxr-xr-xscripts/.local/bin/volume1273
1 files changed, 1273 insertions, 0 deletions
diff --git a/scripts/.local/bin/volume b/scripts/.local/bin/volume
new file mode 100755
index 0000000..4593733
--- /dev/null
+++ b/scripts/.local/bin/volume
@@ -0,0 +1,1273 @@
+#!/bin/bash
+#
+# i3-volume
+#
+# Volume control and volume notifications.
+#
+# Dependencies:
+# awk (POSIX compatible)
+# pulseaudio-utils - if using PulseAudio
+# alsa-utils - if using amixer
+#
+# Copyright (c) 2016 Beau Hastings. All rights reserved.
+# License: GNU General Public License v2
+#
+# Author: Beau Hastings <beau@saweet.net>
+# URL: https://github.com/hastinbe/i3-volume
+# Wiki: https://github.com/hastinbe/i3-volume/wiki/
+
+define_helpers() {
+ empty() {
+ [[ -z $1 ]]
+ }
+
+ not_empty() {
+ [[ -n $1 ]]
+ }
+
+ isset() {
+ [[ -v $1 ]]
+ }
+
+ command_exists() {
+ command -v "$1" >/dev/null 2>&1;
+ }
+
+ error() {
+ echo "$COLOR_RED$*$COLOR_RESET"
+ }
+
+ has_color() {
+ (( $(tput colors 2>/dev/null || echo 0) >= 8 )) && [ -t 1 ]
+ }
+
+ # Converts milliseconds to seconds with rounding up
+ #
+ # Arguments:
+ # milliseconds (integer) An integer in milliseconds
+ ms_to_secs() {
+ echo $(( ($1 + (1000 - 1)) / 1000 ))
+ }
+
+ is_command_hookable() {
+ ! [[ ${POST_HOOK_EXEMPT_COMMANDS[*]} =~ $1 ]]
+ }
+
+ has_capability() {
+ [[ "${NOTIFY_CAPS[*]}" =~ $1 ]]
+ }
+
+ max() {
+ echo $(( $1 > $2 ? $1 : $2 ))
+ }
+}
+
+define_notify() {
+ # Display a notification indicating muted or current volume.
+ notify_volume() {
+ local -r vol=$(get_volume)
+ local icon
+
+ if is_muted; then
+ text="Volume muted"
+
+ if $USE_FULLCOLOR_ICONS; then
+ icon=${ICONS[0]}
+ else
+ icon=${ICONS_SYMBOLIC[0]}
+ fi
+ else
+ printf -v text "Volume %3s%%" "$vol"
+
+ icon=$(get_volume_icon "$vol")
+
+ if $SHOW_VOLUME_PROGRESS; then
+ local -r progress=$(progress_bar "$vol")
+ text="$text $progress"
+ fi
+ fi
+
+ case "$NOTIFICATION_METHOD" in
+ xosd ) notify_volume_xosd "$vol" "$text" ;;
+ herbe ) notify_volume_herbe "$text" ;;
+ volnoti ) notify_volume_volnoti "$vol" ;;
+ kosd ) notify_volume_kosd "$vol" ;;
+ dunst ) notify_volume_libnotify "$vol" "$icon" "$text" ;;
+ notify-osd ) notify_volume_libnotify "$vol" "$icon" "$text" ;;
+ libnotify ) notify_volume_libnotify "$vol" "$icon" "$text" ;;
+ haskell-notification-daemon) notify_volume_libnotify "$vol" "$icon" "$text" ;;
+ * ) notify_volume_libnotify "$vol" "$icon" "$text" ;;
+ esac
+ }
+
+ list_notification_methods() {
+ awk -W posix 'match($0,/ notify_volume_([[:alnum:]]+)/) {print substr($0, 19, RLENGTH-18)}' "${BASH_SOURCE[0]}" || exit "$EX_USAGE"
+ exit "$EX_OK"
+ }
+
+ setup_notification_icons() {
+ if not_empty "$SYMBOLIC_ICON_SUFFIX"; then
+ apply_symbolic_icon_suffix
+ fi
+ }
+
+ show_volume_notification() {
+ $DISPLAY_NOTIFICATIONS || return
+
+ if empty "$NOTIFICATION_METHOD"; then
+ load_notify_server_info
+ NOTIFICATION_METHOD=$NOTIFY_SERVER
+ fi
+
+ setup_notification_icons
+ notify_volume
+ }
+
+ # Loads notification system information via DBus
+ load_notify_server_info() {
+ command_exists dbus-send || return
+ IFS=$'\t' read -r NOTIFY_SERVER _ _ _ < <(dbus-send --print-reply --dest=org.freedesktop.Notifications /org/freedesktop/Notifications org.freedesktop.Notifications.GetServerInformation | awk 'BEGIN { ORS="\t" }; match($0, /^ string ".*"/) {print substr($0, RSTART+11, RLENGTH-12)}')
+ }
+
+ # Load notification system capabilities via DBus
+ load_notify_server_caps() {
+ command_exists dbus-send || return
+ IFS= read -r -d '' -a NOTIFY_CAPS < <(dbus-send --print-reply=literal --dest="${DBUS_NAME}" "${DBUS_PATH}" "${DBUS_IFAC_FDN}.GetCapabilities" | awk 'RS=" " { if (NR > 2) print $1 }')
+ }
+
+ # Send notifcation for libnotify-compatible notification daemons.
+ #
+ # Arguments:
+ # Volume (integer) An integer indicating the volume.
+ # Icon (string) Icon to display.
+ # Text (string) Notification text.
+ notify_volume_libnotify() {
+ local -r vol=$1
+ local -r icon=$2
+ local -r text=${*:3}
+ local -a args=(
+ -t "$EXPIRES"
+ )
+ local -a hints=(
+ # Replaces previous notification in some notification servers
+ string:synchronous:volume
+ # Replaces previous notification in NotifyOSD
+ string:x-canonical-private-synchronous:volume
+ )
+
+ # If we're not drawing our own progress bar, allow the notification daemon to draw its own (if supported)
+ if ! $SHOW_VOLUME_PROGRESS; then
+ hints+=(int:value:"$vol")
+ fi
+
+ (( ${#NOTIFY_CAPS[@]} < 1 )) && load_notify_server_caps
+
+ if has_capability icon-static; then
+ args+=(-i "$icon")
+
+ # haskell-notification-daemon (aka deadd-notification-center) does not support -i|--icon
+ hints+=(string:image-path:"$icon")
+ fi
+
+ if $PLAY_SOUND && has_capability sound; then
+ hints+=(string:sound-name:audio-volume-change)
+ fi
+
+ if ! isset NO_NOTIFY_COLOR && [[ $NOTIFICATION_METHOD == "dunst" ]]; then
+ if is_muted; then
+ hints+=(string:fgcolor:"$COLOR_MUTED")
+ else
+ hints+=(string:fgcolor:"$(volume_color "$vol")")
+ fi
+ fi
+
+ if $USE_DUNSTIFY; then
+ args+=(-r 1000)
+
+ # Transient notifications will bypass the idle_threshold setting.
+ # Should be boolean, but Notify-OSD doesn't support boolean yet. Dunst checks
+ # for int and bool with transient so lets play nice with both servers.
+ hints+=(int:transient:1)
+
+ read -ra hints <<< "${hints[@]/#/-h }"
+ "${DUNSTIFY_PATH:+${DUNSTIFY_PATH%/}/}dunstify" "${hints[@]}" "${args[@]}" "$text"
+ elif isset USE_NOTIFY_SEND_PY; then
+ # Replaces previous notification, but leaves itself running in the bg to work
+ args+=(--replaces-process volume)
+
+ # By-pass the server's persistence capability, if it should exist
+ hints+=(boolean:transient:true)
+
+ "${NOTIFY_SEND_PATH:+${NOTIFY_SEND_PATH%/}/}notify-send.py" --hint "${hints[@]}" "${args[@]}" "$text" &
+ else
+ read -ra hints <<< "${hints[@]/#/-h }"
+ "${NOTIFY_SEND_PATH:+${NOTIFY_SEND_PATH%/}/}notify-send" "${hints[@]}" "${args[@]}" "$text"
+ fi
+ }
+
+ # Send notification to XOSD.
+ #
+ # Arguments:
+ # Volume (integer) An integer indicating the volume.
+ # Text (string) Notification text.
+ notify_volume_xosd() {
+ local -r vol=$1
+ local -r text=${*:2}
+ local -r delay=$(ms_to_secs "$EXPIRES")
+ local percentage
+
+ if is_muted; then
+ color=$COLOR_MUTED
+ percentage=0
+ else
+ color=$(volume_color "$vol")
+ percentage=$vol
+ fi
+
+ "${XOSD_PATH:+${XOSD_PATH%/}/}osd_cat" --align center -b percentage -P "$percentage" -d "$delay" -p top -A center -c "$color" -T "$text" -O 2 -u "$COLOR_XOSD_OUTLINE" & disown
+ }
+
+ # Send notification to herbe.
+ #
+ # Arguments:
+ # Text (string) Notification text.
+ #
+ # Note: a patch with a notify-send script for herbe, not in the current version at this
+ # time but would make this irrelevant. See https://github.com/dudik/herbe/pull/10
+ notify_volume_herbe() {
+ local -r text=$*
+
+ # Dismiss existing/pending notifications to prevent queuing
+ pkill -SIGUSR1 herbe
+
+ "${HERBE_PATH:+${HERBE_PATH%/}/}herbe" "$text" & disown
+ }
+
+ # Send notification to volnoti.
+ #
+ # Arguments:
+ # Volume (integer) An integer indicating the volume.
+ notify_volume_volnoti() {
+ local -r vol=$1
+
+ if is_muted; then
+ "${VOLNOTI_PATH:+${VOLNOTI_PATH%/}/}volnoti-show" -m "$vol"
+ else
+ "${VOLNOTI_PATH:+${VOLNOTI_PATH%/}/}volnoti-show" "$vol"
+ fi
+ }
+
+ # Send notification to KOSD.
+ #
+ # Arguments:
+ # Volume (integer) An integer indicating the volume.
+ notify_volume_kosd() {
+ local -r vol=$1
+
+ if is_muted; then
+ qdbus org.kde.kded /modules/kosd showVolume "$vol" 1
+ else
+ qdbus org.kde.kded /modules/kosd showVolume "$vol" 0
+ fi
+ }
+}
+
+define_output_formats() {
+ # Outputs the current volume in the default format.
+ output_volume_default() {
+ if is_muted; then
+ echo MUTE
+ else
+ echo "$(get_volume)%"
+ fi
+ }
+
+ # Outputs the current volume using a custom format string.
+ #
+ # Format options:
+ # %v = volume percentage or "MUTE" when muted
+ # %s = sink name (PulseAudio only)
+ # %c = card (alsamixer only)
+ # %m = mixer (alsamixer only)
+ # %p = volume progress bar
+ # %i = volume icon
+ output_volume_custom() {
+ local -r format=$*
+ local -r vol=$(get_volume)
+ local string
+
+ if is_muted; then
+ string=${format//\%v/MUTE}
+ else
+ string=${format//\%v/$vol%}
+ fi
+
+ string=${string//\%s/$SINK}
+ string=${string//\%c/$CARD}
+ string=${string//\%m/$MIXER}
+ string=${string//\%p/$(progress_bar "$vol")}
+ string=${string//\%i/$(get_volume_emoji "$vol")}
+
+ echo -ne "$string"
+ }
+
+ # Outputs the current volume for i3blocks.
+ output_volume_i3blocks() {
+ local short_text
+ local full_text
+
+ if is_muted; then
+ short_text="<span color=\"$COLOR_MUTED\">MUTE</span>\n"
+ full_text="<span color=\"$COLOR_MUTED\">MUTE</span>\n"
+ else
+ local -r vol=$(get_volume)
+ local -r color=$(volume_color "$vol")
+
+ short_text="<span color=\"$color\">${vol}%</span>\n"
+ full_text="<span color=\"$color\">${vol}%</span>\n"
+
+ if isset MAX_VOLUME && (( vol > MAX_VOLUME )); then
+ EXITCODE=$EX_URGENT
+ fi
+ fi
+
+ echo -ne "$full_text$short_text"
+ }
+
+ # Outputs the current volume for xob.
+ output_volume_xob() {
+ local -ir vol=$(get_volume)
+
+ if is_muted; then
+ echo "${vol}!"
+ else
+ echo "$vol"
+ fi
+ }
+}
+
+define_commands() {
+ # Increase volume relative to current volume.
+ #
+ # Arguments:
+ # Step (integer) Percentage to increase by.
+ # Max Volume (optional) (integer|percentage) Maximum volume limit.
+ increase_volume() {
+ local step=${1:?$(error 'Step is required')}
+ local -r max_volume=$2
+
+ if not_empty "$max_volume"; then
+ local -r vol=$(get_volume)
+
+ if (( vol + step > max_volume )); then
+ # Instead of doing nothing, step to max_volume
+ step=$( max "0" "$(( max_volume - vol ))" )
+ fi
+ fi
+
+ if $USE_AMIXER; then
+ amixer_increase_volume "$CARD" "$step"
+ else
+ pa_increase_volume "$SINK" "$step"
+ fi
+ }
+
+ # Decrease volume relative to current volume.
+ #
+ # Arguments:
+ # Step (integer) Percentage to decrease by.
+ decrease_volume() {
+ local -r step=${1:?$(error 'Step is required')}
+
+ if $USE_AMIXER; then
+ amixer_decrease_volume "$CARD" "$step"
+ else
+ pa_decrease_volume "$SINK" "$step"
+ fi
+ }
+
+ # Set volume.
+ #
+ # Arguments:
+ # Volume (integer|linear factor|percentage|decibel)
+ # Max Volume (optional) (integer|percentage) Maximum volume limit.
+ set_volume() {
+ local -r vol=${1:?$(error 'Volume is required')}
+ local -r max_volume=$2
+
+ if not_empty "$max_volume" && (( vol > max_volume )); then
+ return
+ fi
+
+ if $USE_AMIXER; then
+ amixer_set_volume "${vol}%" "$CARD"
+ else
+ pa_set_volume "$SINK" "${vol}%"
+ fi
+ }
+
+ toggle_mute() {
+ if $USE_AMIXER; then
+ amixer_toggle_mute "$CARD"
+ else
+ pa_toggle_mute "$SINK"
+ fi
+ }
+
+ # Outputs the current volume.
+ #
+ # Arguments
+ # Output method (string) Method to use to output volume.
+ output_volume() {
+ local -r for=${1:?$(error 'Output method is required')}
+
+ case "$for" in
+ i3blocks ) output_volume_i3blocks ;;
+ xob ) output_volume_xob ;;
+ default ) output_volume_default ;;
+ * ) output_volume_custom "$*" ;;
+ esac
+ }
+
+ list_output_formats() {
+ awk -W posix 'match($0,/ output_volume_([[:alnum:]]+)/) {print substr($0, 19, RLENGTH-18)}' "${BASH_SOURCE[0]}" || exit "$EX_USAGE"
+ exit "$EX_OK"
+ }
+
+ usage() {
+ cat <<- EOF 1>&2
+${COLOR_YELLOW}Usage:${COLOR_RESET} $0 [<options>] <command> [<args>]
+Control volume and related notifications.
+
+${COLOR_YELLOW}Commands:${COLOR_RESET}
+ ${COLOR_GREEN}up <value>${COLOR_RESET} increase volume
+ ${COLOR_GREEN}down <value>${COLOR_RESET} decrease volume
+ ${COLOR_GREEN}set <value>${COLOR_RESET} set volume
+ ${COLOR_GREEN}mute${COLOR_RESET} toggle mute
+ ${COLOR_GREEN}listen${COLOR_RESET} listen for changes to a PulseAudio sink
+ ${COLOR_GREEN}output <format>${COLOR_RESET} output volume in a supported format
+ custom format substitutions:
+ %v = volume
+ %s = sink name (PulseAudio only)
+ %c = card (alsamixer only)
+ %m = mixer (alsamixer only)
+ %p = volume progress bar
+ %i = volume icon/emoji
+
+ examples:
+ "Volume is %v" = Volume is 50%
+ "%i %v %p \n" = 奔 50% ██████████
+ ${COLOR_GREEN}outputs${COLOR_RESET} show available output formats
+ ${COLOR_GREEN}notifications${COLOR_RESET} show available notification methods
+ ${COLOR_GREEN}help${COLOR_RESET} display help
+
+${COLOR_YELLOW}Options:${COLOR_RESET}
+ ${COLOR_GREEN}-a${COLOR_RESET} use amixer
+ ${COLOR_GREEN}-n${COLOR_RESET} enable notifications
+ ${COLOR_GREEN}-C${COLOR_RESET} use libcanberra for playing event sounds
+ ${COLOR_GREEN}-P${COLOR_RESET} play sound for volume changes
+ ${COLOR_GREEN}-j <muted,high,low,medium>${COLOR_RESET} specify custom volume emojis as a comma separated list
+ ${COLOR_GREEN}-t <process_name>${COLOR_RESET} process name of status bar (${COLOR_MAGENTA}requires -u${COLOR_RESET})
+ ${COLOR_GREEN}-u <signal>${COLOR_RESET} signal to update status bar (${COLOR_MAGENTA}requires -t${COLOR_RESET})
+ ${COLOR_GREEN}-x <value>${COLOR_RESET} maximum volume
+ ${COLOR_GREEN}-X <value>${COLOR_RESET} maximum amplification; if supported (${COLOR_MAGENTA}default: 2${COLOR_RESET})
+ ${COLOR_GREEN}-h${COLOR_RESET} display help
+
+${COLOR_YELLOW}amixer Options:${COLOR_RESET}
+ ${COLOR_GREEN}-c <card>${COLOR_RESET} card number to control
+ ${COLOR_GREEN}-m <mixer>${COLOR_RESET} set mixer (${COLOR_MAGENTA}default: Master${COLOR_RESET})
+
+${COLOR_YELLOW}PulseAudio Options:${COLOR_RESET}
+ ${COLOR_GREEN}-s <sink>${COLOR_RESET} symbolic name of sink
+
+${COLOR_YELLOW}Notification Options:${COLOR_RESET}
+ ${COLOR_GREEN}-N <method>${COLOR_RESET} notification method (${COLOR_MAGENTA}default: libnotify${COLOR_RESET})
+ ${COLOR_GREEN}-p${COLOR_RESET} enable progress bar
+ ${COLOR_GREEN}-e <expires>${COLOR_RESET} expiration time of notifications in ms
+ ${COLOR_GREEN}-l${COLOR_RESET} use fullcolor instead of symbolic icons
+ ${COLOR_GREEN}-S <suffix>${COLOR_RESET} append suffix to symbolic icon names
+ ${COLOR_GREEN}-y${COLOR_RESET} use dunstify (${COLOR_MAGENTA}default: notify-send${COLOR_RESET})
+
+${COLOR_YELLOW}Environment Variables:${COLOR_RESET}
+ ${COLOR_CYAN}XOSD_PATH${COLOR_RESET} path to osd_cat
+ ${COLOR_CYAN}HERBE_PATH${COLOR_RESET} path to herbe
+ ${COLOR_CYAN}VOLNOTI_PATH${COLOR_RESET} path to volnoti-show
+ ${COLOR_CYAN}DUNSTIFY_PATH${COLOR_RESET} path to dunstify
+ ${COLOR_CYAN}CANBERRA_PATH${COLOR_RESET} path to canberra-gtk-play
+ ${COLOR_CYAN}NOTIFY_SEND_PATH${COLOR_RESET} path to notify-send or notify-send.py
+ ${COLOR_CYAN}USE_NOTIFY_SEND_PY${COLOR_RESET} flag to use notify-send.py instead of notify-send
+ ${COLOR_CYAN}NO_NOTIFY_COLOR${COLOR_RESET} flag to disable colors in notifications
+EOF
+ exit "$EX_USAGE"
+ }
+}
+
+# Get the volume as a percentage.
+get_volume() {
+ if $USE_AMIXER; then
+ amixer_get_volume "$CARD" "$MIXER"
+ else
+ pa_get_volume "$SINK"
+ fi
+}
+
+is_muted() {
+ if $USE_AMIXER; then
+ amixer_is_muted "$CARD"
+ return $?
+ else
+ pa_is_muted "$SINK"
+ return $?
+ fi
+}
+
+# Gets an icon for the provided volume.
+#
+# Arguments:
+# Volume (integer) An integer indicating the volume.
+#
+# Returns:
+# The volume icon name.
+get_volume_icon() {
+ local -r vol=${1:?$(error 'Volume is required')}
+ local icon
+
+ if $USE_FULLCOLOR_ICONS; then
+ if (( vol >= 70 )); then icon=${ICONS[1]}
+ elif (( vol >= 40 )); then icon=${ICONS[3]}
+ elif (( vol > 0 )); then icon=${ICONS[2]}
+ else icon=${ICONS[2]}
+ fi
+ else
+ # Get overamplified icon if available, otherwise default to high volume icon
+ if (( vol > 100 )); then icon=${ICONS_SYMBOLIC[4]:-${ICONS_SYMBOLIC[1]}}
+ elif (( vol >= 70 )); then icon=${ICONS_SYMBOLIC[1]}
+ elif (( vol >= 40 )); then icon=${ICONS_SYMBOLIC[3]}
+ elif (( vol > 0 )); then icon=${ICONS_SYMBOLIC[2]}
+ else icon=${ICONS_SYMBOLIC[2]}
+ fi
+ fi
+
+ echo "$icon"
+}
+
+# Gets an emoji for the provided volume.
+#
+# Arguments:
+# Volume (integer) An integer indicating the volume.
+#
+# Returns:
+# The volume emoji.
+get_volume_emoji() {
+ local -r vol=${1:?$(error 'Volume is required')}
+ local icon
+
+ if is_muted; then
+ icon=${ICONS_EMOJI[0]}
+ else
+ if (( vol >= 70 )); then icon=${ICONS_EMOJI[1]}
+ elif (( vol >= 40 )); then icon=${ICONS_EMOJI[3]}
+ elif (( vol > 0 )); then icon=${ICONS_EMOJI[2]}
+ else icon=${ICONS_EMOJI[2]}
+ fi
+ fi
+
+ echo "$icon"
+}
+
+# Updates the status line.
+#
+# Arguments:
+# signal (string) The signal used to update the status line.
+# proc (string) The name of the status line process.
+update_statusline() {
+ local -r signal=${1:?$(error 'Signal is required')}
+ local -r proc=${2:?$(error 'Process name is required')}
+
+ pkill "-$signal" "$proc"
+}
+
+# Generates a progress bar for the provided value.
+#
+# Arguments:
+# Percentage (integer) Percentage of progress.
+# Maximum (integer) Maximum percentage. (default: 100)
+# Divisor (integer) For calculating the ratio of blocks to progress (default: 5)
+#
+# Returns:
+# The progress bar.
+progress_bar() {
+ local -r percent=${1:?$(error 'Percentage is required')}
+ local -r max_percent=${2:-100}
+ local -r divisor=${3:-5}
+ local -r progress=$(( (percent > max_percent ? max_percent : percent) / divisor ))
+
+ printf -v bar "%*s" $progress ""
+ echo "${bar// /█}"
+}
+
+apply_symbolic_icon_suffix() {
+ for i in "${!ICONS_SYMBOLIC[@]}"; do
+ ICONS_SYMBOLIC[$i]="${ICONS_SYMBOLIC[$i]}${SYMBOLIC_ICON_SUFFIX}"
+ done
+}
+
+# Get color for the given volume
+#
+# Arguments:
+# $1 - The volume
+volume_color() {
+ local -ir vol=${1:?$(error 'A volume is required')}
+
+ if $USE_AMIXER; then
+ amixer_volume_color "$vol"
+ else
+ pa_volume_color "$vol"
+ fi
+}
+
+# Updates the status bars
+#
+# Returns
+# 0 when no problem occurred
+# 1 when one $of signal or $statusline are set but not both
+update_statusbar() {
+ if not_empty "$SIGNAL"; then
+ if empty "$STATUSLINE"; then
+ return 1
+ fi
+ update_statusline "$SIGNAL" "$STATUSLINE"
+ else
+ if not_empty "$STATUSLINE"; then
+ return 1
+ fi
+ fi
+
+ return 0
+}
+
+setup_audio() {
+ if $USE_AMIXER; then
+ setup_amixer
+ else
+ setup_pulseaudio
+ fi
+}
+
+# All PulseAudio functions are defined here
+define_pulseaudio_functions() {
+ # Executes `pactl list sinks` or return its output if called previously
+ pa_list_sinks() {
+ if $OPT_LISTEN || empty "$PA_LIST_SINKS"; then
+ PA_LIST_SINKS=$(pactl list sinks)
+ fi
+ echo "$PA_LIST_SINKS"
+ }
+
+ pa_invalidate_cache() {
+ unset PA_LIST_SINKS
+ }
+
+ pa_default_sink_name() {
+ pactl info | awk -F': ' '/^Default Sink: /{print $2}'
+ }
+
+ # Get the index of a sink name.
+ #
+ # Arguments
+ # Sink name (string) Symbolic name of sink.
+ pa_get_sink_index() {
+ local -r sink=${1:?$(error 'Sink name is required')}
+
+ pa_list_sinks | \
+ awk -W posix '/^Sink #/{gsub("#", ""); idx = $2}
+ /^[ \t]+Name: / {insink = $2 == "'"$sink"'"; if (insink) { print idx }; exit}'
+ }
+
+ # Get the volume as a percentage.
+ #
+ # Arguments
+ # Sink name (string) Symbolic name of sink.
+ pa_get_volume() {
+ local -r sink=${1:?$(error 'Sink name is required')}
+
+ pa_list_sinks | \
+ awk -W posix '/^[ \t]+Name: / {insink = $2 == "'"$sink"'"}
+ /^[ \t]+Volume: / && insink {gsub("%,?", ""); print $5; exit}'
+ }
+
+ # Get the max volume as a percentage.
+ #
+ # Arguments
+ # Sink name (string) Symbolic name of sink.
+ pa_get_base_volume() {
+ local -r sink=${1:?$(error 'Sink name is required')}
+
+ pa_list_sinks | \
+ awk -W posix '/^[ \t]+Name: / {insink = $2 == "'"$sink"'"}
+ /^[ \t]+Base Volume: / && insink {gsub("%", ""); print $5; exit}'
+ }
+
+ # Increase volume relative to current volume using pulseaudio.
+ #
+ # Arguments:
+ # Sink name (string) Symbolic name of sink.
+ # Step (integer) Percentage to increase by.
+ pa_increase_volume() {
+ local -r sink=$1
+ local -r step=${2:=-5}
+
+ pa_set_volume "$sink" "+${step}%"
+ }
+
+ # Decrease volume relative to current volume using pulseaudio.
+ #
+ # Arguments:
+ # Sink name (string) Symbolic name of sink.
+ # Step (integer|percentage) Percentage to decrease by.
+ pa_decrease_volume() {
+ local -r sink=$1
+ local -r step=${2:=-5}
+
+ pa_set_volume "$sink" "-${step}%"
+ }
+
+ # Set volume using pulseaudio.
+ #
+ # Arguments:
+ # Sink name (string) Symbolic name of sink.
+ # Volume (integer|linear factor|percentage|decibel)
+ pa_set_volume() {
+ local -r sink=${1:?$(error 'Sink name is required')}
+ local -r vol=${2:?$(error 'Volume is required')}
+
+ pa_invalidate_cache
+
+ pactl set-sink-volume "$sink" "$vol"
+ }
+
+ # Toggle mute using pulseaudio.
+ #
+ # Arguments:
+ # Sink name (string) Symbolic name of sink.
+ pa_toggle_mute() {
+ local -r sink=${1:?$(error 'Sink name is required')}
+
+ pa_invalidate_cache
+
+ pactl set-sink-mute "$sink" toggle
+ }
+
+ # Check if sink is muted.
+ #
+ # Arguments:
+ # Sink name (string) Symbolic name of sink.
+ #
+ # Returns:
+ # 0 when true, 1 when false.
+ pa_is_muted() {
+ local -r sink=${1:?$(error 'Sink name is required')}
+
+ pa_list_sinks | \
+ awk -W posix '/^[ \t]+Name: / {insink = $2 == "'"$sink"'"}
+ /^[ \t]+Mute: / && insink && $2 ~ /^yes$/ { exitcode=1 }; END { exit !exitcode }'
+ }
+
+ # Get the flags of the PulseAudio sink.
+ #
+ # Arguments
+ # Sink name (string) Symbolic name of sink.
+ pa_get_sink_flags() {
+ local -r sink=${1:?$(error 'Sink name is required')}
+
+ pa_list_sinks | \
+ awk -W posix '/^[ \t]+Name: / {insink = $2 == "'"$sink"'"}
+ /^[ \t]+Flags: / && insink { for(i = 2; i <= NF; ++i) printf $i FS; exit}'
+ }
+
+ # Get color for the given volume for PulseAudio
+ #
+ # Arguments:
+ # $1 - The volume
+ pa_volume_color() {
+ local -ir vol=${1:?$(error 'A volume is required')}
+
+ if (( vol >= PA_VOLUME_MUTED && vol < PA_BASE_VOLUME )); then
+ echo "$COLOR_MUTED_TO_BASE"
+ elif (( vol >= PA_BASE_VOLUME && vol <= PA_VOLUME_NORM )); then
+ echo "$COLOR_BASE_TO_NORM"
+ elif (( vol > PA_VOLUME_NORM && vol <= MAX_VOLUME )); then
+ echo "$COLOR_NORM_TO_MAX"
+ else
+ echo "$COLOR_OTHER"
+ fi
+ }
+
+ # Listens for PulseAudio events
+ #
+ # Arguments:
+ # Output (optional) (string) An output mode. When set, outputs volume in the output mode format.
+ listen() {
+ local -r output=$*
+ local -r index=$(pa_get_sink_index "$SINK")
+
+ # Output volume so statusbars have something to display before any event occurs
+ not_empty "$output" && output_volume "$output"
+
+ while IFS= read -r; do
+ show_volume_notification
+ update_statusbar
+ play_volume_changed
+ not_empty "$output" && output_volume "$output"
+ done < <(pactl subscribe | stdbuf -oL grep -e "Event 'change' on sink #$index")
+ }
+
+ # Play a sound file.
+ #
+ # Arguments:
+ # Sound file (string)
+ pa_play() {
+ local -r file=$1
+
+ paplay -d "$SINK" "$file" &
+ }
+}
+
+# Register PulseAudio related functions and settings
+setup_pulseaudio() {
+ define_pulseaudio_functions
+
+ PA_LIST_SINKS=$(pactl list sinks) || exit 1
+
+ if empty "$SINK"; then
+ SINK="$(pa_default_sink_name)"
+ fi
+
+ # Determine a max volume when it's not specified
+ if ! isset MAX_VOLUME; then
+ read -ra SINK_FLAGS < <(pa_get_sink_flags "$SINK")
+ PA_BASE_VOLUME=$(pa_get_base_volume "$SINK")
+
+ # Does the sink support digital (software) amplification?
+ if [[ "${SINK_FLAGS[*]}" =~ "DECIBEL_VOLUME" ]]; then
+ MAX_VOLUME=$((PA_VOLUME_NORM * MAX_AMPLIFICATION))
+ else
+ MAX_VOLUME=$PA_VOLUME_NORM
+ fi
+ fi
+}
+
+# All amixer functions are defined here
+define_amixer_functions() {
+ # Get the volume as a percentage.
+ #
+ # Arguments
+ # Card (integer) Card number to control.
+ # Mixer (string) Name of the mixer.
+ amixer_get_volume() {
+ local -r card=$1
+ local -r mixer=${2:-Master}
+
+ amixer ${card:+-c "$card" --} sget "$mixer" | \
+ awk -W posix -F'[][]' '/dB/ { gsub("%", ""); print $2 }'
+ }
+
+ # Increase volume relative to current volume using amixer.
+ #
+ # Arguments:
+ # Card (integer) Card number to control.
+ # Step (integer) Percentage to increase by.
+ amixer_increase_volume() {
+ local -r card=$1
+ local -r step=${2:=-5}
+
+ amixer_set_volume "${step}%+" "$card"
+ }
+
+ # Decrease volume relative to current volume using amixer.
+ #
+ # Arguments:
+ # Card (integer) Card number to control.
+ # Step (integer) Percentage to decrease by.
+ amixer_decrease_volume() {
+ local -r card=$1
+ local -r step=${2:=-5}
+
+ amixer_set_volume "${step}%-" "$card"
+ }
+
+ # Set volume using amixer.
+ #
+ # Arguments:
+ # Volume (integer|linear factor|percentage|decibel)
+ # Card (optional) (integer) Card number to control.
+ amixer_set_volume() {
+ local -r vol=${1:?$(error 'Volume is required')}
+ local -r card=$2
+
+ amixer -q ${card:+-c "$card" --} set "$MIXER" "$vol"
+ }
+
+ # Toggle mute using amixer.
+ #
+ # Arguments:
+ # Card (integer) Card number to control.
+ amixer_toggle_mute() {
+ local -r card=$1
+
+ amixer -q ${card:+-c "$card" --} set "$MIXER" toggle
+ }
+
+ # Check if card is muted.
+ #
+ # Arguments:
+ # Card (optional) (integer) Card number to control.
+ #
+ # Returns:
+ # 0 when true, 1 when false.
+ amixer_is_muted() {
+ local -r card=$1
+
+ amixer ${card:+-c "$card" --} sget "$MIXER" | \
+ awk -W posix -F'[][]' '/dB/ && $6 ~ /^off$/ { exitcode=1 }; END { exit !exitcode }'
+ }
+
+ # Get color for the given volume for amixer
+ #
+ # Arguments:
+ # $1 - The volume
+ amixer_volume_color() {
+ local -ir vol=${1:?$(error 'A volume is required')}
+
+ if (( vol >= 0 && vol < 100 )); then
+ echo "$COLOR_MUTED_TO_BASE"
+ elif (( vol == 100 )); then
+ echo "$COLOR_BASE_TO_NORM"
+ elif (( vol > 100 && vol <= MAX_VOLUME )); then
+ echo "$COLOR_NORM_TO_MAX"
+ else
+ echo "$COLOR_OTHER"
+ fi
+ }
+
+ # Play a sound file.
+ #
+ # Arguments:
+ # Sound file (string)
+ amixer_play() {
+ local -r file=$1
+
+ aplay -q "$file" &
+ }
+}
+
+# Register amixer related functions and settings
+setup_amixer() {
+ define_amixer_functions
+}
+
+setup_color() {
+ if has_color; then
+ COLOR_RESET=$'\033[0m'
+ COLOR_RED=$'\033[0;31m'
+ COLOR_GREEN=$'\033[0;32m'
+ COLOR_YELLOW=$'\033[0;33m'
+ COLOR_MAGENTA=$'\033[0;35m'
+ COLOR_CYAN=$'\033[0;36m'
+ fi
+}
+
+# Rearrange all options to place flags first
+# Author: greycat
+# URL: https://mywiki.wooledge.org/ComplexOptionParsing
+arrange_opts() {
+ local flags args optstr=$1
+ shift
+
+ while (($#)); do
+ case $1 in
+ --)
+ args+=("$@")
+ break;
+ ;;
+ -*)
+ flags+=("$1")
+ if [[ $optstr == *"${1: -1}:"* ]]; then
+ flags+=("$2")
+ shift
+ fi
+ ;;
+ *)
+ args+=("$1")
+ ;;
+ esac
+ shift
+ done
+ OPTARR=("${flags[@]}" "${args[@]}")
+}
+
+parse_opts() {
+ local optstring=:ac:Ce:hj:lm:nN:pPs:S:t:u:x:X:y
+
+ arrange_opts "$optstring" "$@"
+ set -- "${OPTARR[@]}"
+
+ OPTIND=1
+
+ while getopts "$optstring" opt; do
+ case "$opt" in
+ a ) USE_AMIXER=true ;;
+ c ) CARD=$OPTARG ;;
+ C ) USE_CANBERRA=true ;;
+ e ) EXPIRES=$OPTARG ;;
+ j ) IFS=, read -ra ICONS_EMOJI <<< "$OPTARG" ;;
+ l ) USE_FULLCOLOR_ICONS=true ;;
+ m ) MIXER=${OPTARG@Q} ;;
+ n ) DISPLAY_NOTIFICATIONS=true ;;
+ N ) NOTIFICATION_METHOD=$OPTARG ;;
+ p ) SHOW_VOLUME_PROGRESS=true ;;
+ P ) PLAY_SOUND=true ;;
+ s ) SINK=$OPTARG ;;
+ S ) SYMBOLIC_ICON_SUFFIX=$OPTARG ;;
+ t ) STATUSLINE=$OPTARG ;;
+ u ) SIGNAL=$OPTARG ;;
+ x ) MAX_VOLUME=$OPTARG ;;
+ X ) MAX_AMPLIFICATION=$OPTARG ;;
+ y ) USE_DUNSTIFY=true ;;
+ h | *) usage ;;
+ esac
+ done
+
+ read -ra CMDARGS <<< "${OPTARR[@]:$((OPTIND-1))}"
+}
+
+exec_command() {
+ IFS=' ' read -ra ARGS <<< "$1"
+ set -- "${ARGS[@]}"
+
+ COMMAND=${1:?$(error 'A command is required')}
+ shift
+
+ case "$COMMAND" in
+ up|raise|increase)
+ case "$#" in 1) ;; *) usage ;; esac
+ increase_volume "$1" "$MAX_VOLUME"
+ ;;
+ down|lower|decrease)
+ case "$#" in 1) ;; *) usage ;; esac
+ decrease_volume "$1"
+ ;;
+ set)
+ case "$#" in 1) ;; *) usage ;; esac
+ case "$1" in
+ +*) increase_volume "${1:1}" "$MAX_VOLUME" ;;
+ -*) decrease_volume "${1:1}" ;;
+ *) set_volume "$1" "$MAX_VOLUME" ;;
+ esac
+ ;;
+ mute)
+ toggle_mute
+ ;;
+ listen)
+ listen "$*"
+ ;;
+ output)
+ case "$#" in 0) usage ;; esac
+ output_volume "$*"
+ exit "${EXITCODE:-$EX_OK}"
+ ;;
+ outputs)
+ list_output_formats
+ ;;
+ notifications)
+ list_notification_methods
+ ;;
+ *)
+ usage
+ ;;
+ esac
+}
+
+play_volume_changed() {
+ $PLAY_SOUND || return
+
+ # Sound can be handled by the notification method
+ if $DISPLAY_NOTIFICATIONS && has_capability sound; then
+ return
+ fi
+
+ if $USE_CANBERRA; then
+ ca_play "$SOUND_VOLUME_CHANGED" "Volume Changed"
+ else
+ if $USE_AMIXER; then
+ amixer_play "$SOUND_VOLUME_CHANGED"
+ else
+ pa_play "$SOUND_VOLUME_CHANGED"
+ fi
+ fi
+}
+
+ca_play() {
+ local -r file=$1 desc=$2
+
+ if [[ -f $file ]]; then
+ "${CANBERRA_PATH:+${CANBERRA_PATH%/}/}canberra-gtk-play" -f "$file" -d "$desc"
+ else
+ "${CANBERRA_PATH:+${CANBERRA_PATH%/}/}canberra-gtk-play" -i "audio-volume-change" -d "$desc"
+ fi
+}
+
+post_command_hook() {
+ if is_command_hookable "$COMMAND"; then
+ show_volume_notification
+ play_volume_changed
+ update_statusbar || usage
+ fi
+}
+
+main() {
+ # Getopt parsing variables
+ declare OPTIND
+ declare -a OPTARR CMDARGS
+
+ ###########################################################
+ # Non-command line option variables
+ ###########################################################
+
+ # Commands which will not use post_command_hook(), usually because
+ # they handle notifications and/or statusbar updates manually
+ declare -a POST_HOOK_EXEMPT_COMMANDS=(
+ listen
+ )
+
+ # Exit codes
+ declare -ir \
+ EX_OK=0 \
+ EX_URGENT=33 \
+ EX_USAGE=64
+
+ # Main program exit code
+ declare -i EXITCODE=$EX_OK
+
+ # Standard notification icons. Usually full color
+ # Note: order matters; muted, high, low, medium, and optionally overamplified
+ declare -a ICONS=(
+ audio-volume-muted
+ audio-volume-high
+ audio-volume-low
+ audio-volume-medium
+ )
+
+ # Symbolic notification icons. Usually low color or monochrome
+ # Note: order matters; muted, high, low, medium, and optionally overamplified
+ declare -a ICONS_SYMBOLIC=(
+ audio-volume-muted-symbolic
+ audio-volume-high-symbolic
+ audio-volume-low-symbolic
+ audio-volume-medium-symbolic
+ ## Only exists in some icon sets
+ # audio-volume-overamplified-symbolic
+ )
+
+ # Emoji-based icons.
+ declare -a ICONS_EMOJI=(
+ ﱝ
+ 墳
+ 奄
+ 奔
+ )
+
+ # Volume changed sound.
+ declare SOUND_VOLUME_CHANGED=${SOUND_VOLUME_CHANGED:-/usr/share/sounds/freedesktop/stereo/audio-volume-change.oga}
+
+ # DBUS constants
+ declare -r \
+ DBUS_NAME=org.freedesktop.Notifications \
+ DBUS_PATH=/org/freedesktop/Notifications \
+ DBUS_IFAC_FDN=org.freedesktop.Notifications
+
+ # Notification server information
+ declare \
+ NOTIFY_SERVER
+ # NOTIFY_VENDOR \
+ # NOTIFY_VERSION \
+ # NOTIFY_SPEC_VERSION
+
+ # Notification capabilities
+ declare -a NOTIFY_CAPS=()
+
+ # PulseAudio sink flags
+ declare -a SINK_FLAGS=()
+
+ # PulseAudio volume variables and constants.
+ # Note: unlike in PA, PA_VOLUME_* here are percentages instead of integers
+ declare -i PA_BASE_VOLUME=100
+ declare -ir \
+ PA_VOLUME_NORM=100 \
+ PA_VOLUME_MUTED=0
+
+ # Cached output of `pactl list sinks`; so we don't have to call it each time we need it
+ declare PA_LIST_SINKS
+
+ # Output volume colors
+ declare -r \
+ COLOR_MUTED=${COLOR_MUTED:-#FFFF00} \
+ COLOR_MUTED_TO_BASE=${COLOR_MUTED_TO_BASE:-#00FF00} \
+ COLOR_BASE_TO_NORM=${COLOR_BASE_TO_NORM:-#FFFF00} \
+ COLOR_NORM_TO_MAX=${COLOR_NORM_TO_MAX:-#FF0000} \
+ COLOR_OTHER=${COLOR_OTHER:-#FFFFFF} \
+ COLOR_XOSD_OUTLINE=${COLOR_XOSD_OUTLINE:-#222222}
+
+ declare \
+ COLOR_RESET \
+ COLOR_RED \
+ COLOR_GREEN \
+ COLOR_YELLOW \
+ COLOR_MAGENTA \
+ COLOR_CYAN
+
+ ###########################################################
+ # Command line option variables
+ ###########################################################
+ declare -l NOTIFICATION_METHOD
+
+ declare \
+ COMMAND \
+ DISPLAY_NOTIFICATIONS=false \
+ SHOW_VOLUME_PROGRESS=false \
+ USE_AMIXER=false \
+ USE_DUNSTIFY=false \
+ USE_FULLCOLOR_ICONS=false \
+ CARD \
+ MIXER=Master \
+ SIGNAL \
+ SINK \
+ STATUSLINE \
+ SYMBOLIC_ICON_SUFFIX \
+ NOTIFICATION_METHOD \
+ PLAY_SOUND=false \
+ USE_CANBERRA=false
+
+ declare -i \
+ EXPIRES=1500 \
+ MAX_VOLUME \
+ MAX_AMPLIFICATION=2
+
+ define_helpers
+ define_notify
+ define_output_formats
+ define_commands
+
+ setup_color
+
+ parse_opts "$@"
+
+ # Requires options to be parsed first
+ setup_audio
+
+ exec_command "${CMDARGS[*]}" && post_command_hook
+
+ exit ${EXITCODE:-$EX_OK}
+}
+
+main "$@"