From 1079eb062f2f48eb4bcd11528cf811ba5a4cf9e9 Mon Sep 17 00:00:00 2001 From: ivan-boikov Date: Mon, 24 May 2021 22:45:27 +0200 Subject: volume control, better dunst colors --- dunst/.config/dunst/dunstrc | 333 ++++++++++ i3/.config/i3/config | 47 +- pix/.local/share/icons/bell.png | Bin 0 -> 14065 bytes scripts/.local/bin/volume | 1273 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 1650 insertions(+), 3 deletions(-) create mode 100644 dunst/.config/dunst/dunstrc create mode 100644 pix/.local/share/icons/bell.png create mode 100755 scripts/.local/bin/volume diff --git a/dunst/.config/dunst/dunstrc b/dunst/.config/dunst/dunstrc new file mode 100644 index 0000000..ab0bc02 --- /dev/null +++ b/dunst/.config/dunst/dunstrc @@ -0,0 +1,333 @@ +[global] + ### Display ### + + # Which monitor should the notifications be displayed on. + monitor = 0 + + # Display notification on focused monitor. Possible modes are: + # mouse: follow mouse pointer + # keyboard: follow window with keyboard focus + # none: don't follow anything + # + # "keyboard" needs a window manager that exports the + # _NET_ACTIVE_WINDOW property. + # This should be the case for almost all modern window managers. + # + # If this option is set to mouse or keyboard, the monitor option + # will be ignored. + follow = keyboard + + # The geometry of the window: + # [{width}]x{height}[+/-{x}+/-{y}] + # The geometry of the message window. + # The height is measured in number of notifications everything else + # in pixels. If the width is omitted but the height is given + # ("-geometry x2"), the message window expands over the whole screen + # (dmenu-like). If width is 0, the window expands to the longest + # message displayed. A positive x is measured from the left, a + # negative from the right side of the screen. Y is measured from + # the top and down respectively. + # The width can be negative. In this case the actual width is the + # screen width minus the width defined in within the geometry option. + geometry = "300x5-30+20" + + # Show how many messages are currently hidden (because of geometry). + indicate_hidden = yes + + # Shrink window if it's smaller than the width. Will be ignored if + # width is 0. + shrink = no + + # The transparency of the window. Range: [0; 100]. + # This option will only work if a compositing window manager is + # present (e.g. xcompmgr, compiz, etc.). + transparency = 20 + + # The height of the entire notification. If the height is smaller + # than the font height and padding combined, it will be raised + # to the font height and padding. + notification_height = 0 + + # Draw a line of "separator_height" pixel height between two + # notifications. + # Set to 0 to disable. + separator_height = 2 + + # Padding between text and separator. + padding = 8 + + # Horizontal padding. + horizontal_padding = 8 + + # Defines width in pixels of frame around the notification window. + # Set to 0 to disable. + frame_width = 3 + + # Defines color of the frame around the notification window. + frame_color = "#aaaaaa" + + # Define a color for the separator. + # possible values are: + # * auto: dunst tries to find a color fitting to the background; + # * foreground: use the same color as the foreground; + # * frame: use the same color as the frame; + # * anything else will be interpreted as a X color. + separator_color = frame + + # Sort messages by urgency. + sort = yes + + # Don't remove messages, if the user is idle (no mouse or keyboard input) + # for longer than idle_threshold seconds. + # Set to 0 to disable. + # Transient notifications ignore this setting. + idle_threshold = 120 + + ### Text ### + + font = Liberation Sans 8 + + # The spacing between lines. If the height is smaller than the + # font height, it will get raised to the font height. + line_height = 0 + + # Possible values are: + # full: Allow a small subset of html markup in notifications: + # bold + # italic + # strikethrough + # underline + # + # For a complete reference see + # . + # + # strip: This setting is provided for compatibility with some broken + # clients that send markup even though it's not enabled on the + # server. Dunst will try to strip the markup but the parsing is + # simplistic so using this option outside of matching rules for + # specific applications *IS GREATLY DISCOURAGED*. + # + # no: Disable markup parsing, incoming notifications will be treated as + # plain text. Dunst will not advertise that it has the body-markup + # capability if this is set as a global setting. + # + # It's important to note that markup inside the format option will be parsed + # regardless of what this is set to. + markup = full + + # The format of the message. Possible variables are: + # %a appname + # %s summary + # %b body + # %i iconname (including its path) + # %I iconname (without its path) + # %p progress value if set ([ 0%] to [100%]) or nothing + # %n progress value if set without any extra characters + # %% Literal % + # Markup is allowed + #format = "%s\n%b" + format = "%a\n%s\n%b" + + # Alignment of message text. + # Possible values are "left", "center" and "right". + alignment = left + + # Show age of message if message is older than show_age_threshold + # seconds. + # Set to -1 to disable. + show_age_threshold = 60 + + # Split notifications into multiple lines if they don't fit into + # geometry. + word_wrap = yes + + # When word_wrap is set to no, specify where to ellipsize long lines. + # Possible values are "start", "middle" and "end". + ellipsize = middle + + # Ignore newlines '\n' in notifications. + ignore_newline = no + + # Merge multiple notifications with the same content + stack_duplicates = true + + # Hide the count of merged notifications with the same content + hide_duplicate_count = false + + # Display indicators for URLs (U) and actions (A). + show_indicators = yes + + ### Icons ### + + # Align icons left/right/off + icon_position = left + + # Scale larger icons down to this size, set to 0 to disable + max_icon_size = 32 + + # Paths to default icons. + #icon_path = /usr/share/icons/gnome/16x16/status/:/usr/share/icons/gnome/16x16/devices/ + icon_path = /usr/share/icons/oxygen/16x16/status/:/usr/share/icons/oxygen/16x16/devices/ + + ### History ### + + # Should a notification popped up from history be sticky or timeout + # as if it would normally do. + sticky_history = yes + + # Maximum amount of notifications kept in history + history_length = 20 + + ### Misc/Advanced ### + + # dmenu path. + dmenu = /usr/bin/dmenu -p dunst: + + # Browser for opening urls in context menu. + browser = $HOME/soft/firefox/firefox -new-tab + + # Always run rule-defined scripts, even if the notification is suppressed + always_run_script = true + + # Define the title of the windows spawned by dunst + title = Dunst + + # Define the class of the windows spawned by dunst + class = Dunst + + # Print a notification on startup. + # This is mainly for error detection, since dbus (re-)starts dunst + # automatically after a crash. + startup_notification = false + + # Rounded corners + corner_radius = 20 + + ### Legacy + + # Use the Xinerama extension instead of RandR for multi-monitor support. + # This setting is provided for compatibility with older nVidia drivers that + # do not support RandR and using it on systems that support RandR is highly + # discouraged. + # + # By enabling this setting dunst will not be able to detect when a monitor + # is connected or disconnected which might break follow mode if the screen + # layout changes. + force_xinerama = false + +# Experimental features that may or may not work correctly. Do not expect them +# to have a consistent behaviour across releases. +[experimental] + # Calculate the dpi to use on a per-monitor basis. + # If this setting is enabled the Xft.dpi value will be ignored and instead + # dunst will attempt to calculate an appropriate dpi value for each monitor + # using the resolution and physical size. This might be useful in setups + # where there are multiple screens with very different dpi values. + per_monitor_dpi = false + +[shortcuts] + + # Shortcuts are specified as [modifier+][modifier+]...key + # Available modifiers are "ctrl", "mod1" (the alt-key), "mod2", + # "mod3" and "mod4" (windows-key). + # Xev might be helpful to find names for keys. + + # Close notification. + close = ctrl+space + + # Close all notifications. + close_all = ctrl+shift+space + + # Redisplay last message(s). + # On the US keyboard layout "grave" is normally above TAB and left + # of "1". Make sure this key actually exists on your keyboard layout, + # e.g. check output of 'xmodmap -pke' + history = ctrl+grave + + # Context menu. + context = ctrl+shift+period + +[urgency_low] + # IMPORTANT: colors have to be defined in quotation marks. + # Otherwise the "#" and following would be interpreted as a comment. + background = "#000000" + foreground = "#ffffff" + frame_color = "#084698" + timeout = 2 + # Icon for notifications with low urgency, uncomment to enable + icon = $HOME/.local/share/icons/bell.png + +[urgency_normal] + background = "#084698" + foreground = "#ffffff" + frame_color = "#ffffff" + timeout = 5 + # Icon for notifications with normal urgency, uncomment to enable + icon = $HOME/.local/share/icons/bell.png + +[urgency_critical] + background = "#884444" + foreground = "#ffffff" + frame_color = "#ff0000" + timeout = 0 + # Icon for notifications with critical urgency, uncomment to enable + icon = $HOME/.local/share/icons/bell.png + +# Every section that isn't one of the above is interpreted as a rules to +# override settings for certain messages. +# Messages can be matched by "appname", "summary", "body", "icon", "category", +# "msg_urgency" and you can override the "timeout", "urgency", "foreground", +# "background", "new_icon" and "format". +# Shell-like globbing will get expanded. +# +# SCRIPTING +# You can specify a script that gets run when the rule matches by +# setting the "script" option. +# The script will be called as follows: +# script appname summary body icon urgency +# where urgency can be "LOW", "NORMAL" or "CRITICAL". +# +# NOTE: if you don't want a notification to be displayed, set the format +# to "". +# NOTE: It might be helpful to run dunst -print in a terminal in order +# to find fitting options for rules. + +#[espeak] +# summary = "*" +# script = dunst_espeak.sh + +#[script-test] +# summary = "*script*" +# script = dunst_test.sh + +#[ignore] +# # This notification will not be displayed +# summary = "foobar" +# format = "" + +#[history-ignore] +# # This notification will not be saved in history +# summary = "foobar" +# history_ignore = yes + +#[signed_on] +# appname = Pidgin +# summary = "*signed on*" +# urgency = low +# +#[signed_off] +# appname = Pidgin +# summary = *signed off* +# urgency = low +# +#[says] +# appname = Pidgin +# summary = *says* +# urgency = critical +# +#[twitter] +# appname = Pidgin +# summary = *twitter.com* +# urgency = normal +# +# vim: ft=cfg diff --git a/i3/.config/i3/config b/i3/.config/i3/config index a89ef0e..5e4e878 100644 --- a/i3/.config/i3/config +++ b/i3/.config/i3/config @@ -135,10 +135,43 @@ mode "resize" { bindsym $mod+r mode "resize" + # pulseaudio controls -bindsym XF86AudioRaiseVolume exec --no-startup-id pactl set-sink-volume @DEFAULT_SINK@ +5% #increase sound volume -bindsym XF86AudioLowerVolume exec --no-startup-id pactl set-sink-volume @DEFAULT_SINK@ -5% #decrease sound volume -bindsym XF86AudioMute exec --no-startup-id pactl set-sink-mute @DEFAULT_SINK@ toggle # mute sound +#bindsym XF86AudioRaiseVolume exec --no-startup-id pactl set-sink-volume @DEFAULT_SINK@ +5% #increase sound volume +#bindsym XF86AudioLowerVolume exec --no-startup-id pactl set-sink-volume @DEFAULT_SINK@ -5% #decrease sound volume +#bindsym XF86AudioMute exec --no-startup-id pactl set-sink-mute @DEFAULT_SINK@ toggle # mute sound + + +#################################################################################### Volume control +# from https://github.com/hastinbe/i3-volume + +# Path to volume control, without trailing slash +set $volumepath ~/.local/bin/ + +# Command for the status line +# ie: i3blocks, i3status +set $statuscmd i3status + +# Signal used to update the status line +# i3blocks uses SIGRTMIN+10 by default +# i3status uses SIGUSR1 by default +set $statussig SIGUSR1 + +# Amount to increase/decrease volume as a percentage +set $volumestep 5 + +# pulseaudio-utils settings +# +# Symbolic name for sink (numeric index not supported) +# Recommended: comment out this setting and omit the -s option to use default sink +# List sink names: pacmd list-sinks | awk -F "[<>]" '/^\s+name: <.*>/{print $2}' +#set $sinkname alsa_output.pci-0000_00_1b.0.analog-stereo + +# Using pulseaudio-utils (append "-s $sinkname" without quotes to override default sink) +bindsym XF86AudioRaiseVolume exec --no-startup-id $volumepath/volume -n -t $statuscmd -u $statussig up $volumestep +bindsym XF86AudioLowerVolume exec --no-startup-id $volumepath/volume -n -t $statuscmd -u $statussig down $volumestep +bindsym XF86AudioMute exec --no-startup-id $volumepath/volume -n -t $statuscmd -u $statussig mute + # screen brightness controls bindsym XF86MonBrightnessUp exec xbacklight -inc 5 @@ -281,3 +314,11 @@ exec_always --no-startup-id "wal -i ~/.local/share/wallpapers/bg.jpg" # restart compton exec_always --no-startup-id "pkill compton ; compton --config ~/.config/compton/compton.conf" + +# notification daemon +exec_always --no-startup-id "pkill dunst ; dunst" +# Review the lines below and append to ~/.config/i3/config + + + + diff --git a/pix/.local/share/icons/bell.png b/pix/.local/share/icons/bell.png new file mode 100644 index 0000000..c3941b9 Binary files /dev/null and b/pix/.local/share/icons/bell.png differ 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 +# 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="MUTE\n" + full_text="MUTE\n" + else + local -r vol=$(get_volume) + local -r color=$(volume_color "$vol") + + short_text="${vol}%\n" + full_text="${vol}%\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 [] [] +Control volume and related notifications. + +${COLOR_YELLOW}Commands:${COLOR_RESET} + ${COLOR_GREEN}up ${COLOR_RESET} increase volume + ${COLOR_GREEN}down ${COLOR_RESET} decrease volume + ${COLOR_GREEN}set ${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 ${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 ${COLOR_RESET} specify custom volume emojis as a comma separated list + ${COLOR_GREEN}-t ${COLOR_RESET} process name of status bar (${COLOR_MAGENTA}requires -u${COLOR_RESET}) + ${COLOR_GREEN}-u ${COLOR_RESET} signal to update status bar (${COLOR_MAGENTA}requires -t${COLOR_RESET}) + ${COLOR_GREEN}-x ${COLOR_RESET} maximum volume + ${COLOR_GREEN}-X ${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 ${COLOR_RESET} card number to control + ${COLOR_GREEN}-m ${COLOR_RESET} set mixer (${COLOR_MAGENTA}default: Master${COLOR_RESET}) + +${COLOR_YELLOW}PulseAudio Options:${COLOR_RESET} + ${COLOR_GREEN}-s ${COLOR_RESET} symbolic name of sink + +${COLOR_YELLOW}Notification Options:${COLOR_RESET} + ${COLOR_GREEN}-N ${COLOR_RESET} notification method (${COLOR_MAGENTA}default: libnotify${COLOR_RESET}) + ${COLOR_GREEN}-p${COLOR_RESET} enable progress bar + ${COLOR_GREEN}-e ${COLOR_RESET} expiration time of notifications in ms + ${COLOR_GREEN}-l${COLOR_RESET} use fullcolor instead of symbolic icons + ${COLOR_GREEN}-S ${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 "$@" -- cgit v1.2.3