#!/bin/sh

# vol - Adjust volume, with on-screen display.

# (c) 2020 B. Watson <urchlay@slackware.uk>
# Released under the WTFPL, see http://www.wtfpl.net/txt/copying/

# Requires aosd_cat (on Slackware, libaosd from SBo).
# Intended for use with xbindkeys, like so:

#$ cat ~/.xbindkeysrc
#"vol up &"
#  Control+Alt + Prior
#
#"vol down &"
#  Control+Alt + Next
#
#"vol mute &"
#  Control+Alt + Pause

# "Next" and "Prior" are PageDown and PageUp. Replace with "Up" and "Down"
# to use arrow keys. If you have multimedia keys, you can probably use
# them (try XF86AudioRaiseVolume, XF86AudioLowerVolume, and
# XF86AudioMute). See xbindkeys(1).

# Can also be used from the command line. Run with no args for help.

# --------------------
# Configurable options
# --------------------

# Should be "Master" for most (all?) cards. Use "amixer scontrols" to
# get a list.
CHANNEL=Master

# Each up/down adjustment is by this much. Use the dB suffix if you
# prefer decibels, % percentage, no suffix for raw units (on one of my
# sound cards, the raw unit range is 0 to 87; on another, 0-127).
ADJ=1%

# Pango font string, see:
# https://docs.gtk.org/Pango/type_func.FontDescription.from_string.html
# The size is in points; 30px would be pixels.
FONT="Mono Normal 30"

# What color is the OSD bar and text? See /usr/share/X11/rgb.txt,
# or use hex escapes (with #).
# COLOR is for when we're unmuted, MUTECOLOR is for muted.
COLOR='#00ff00'
MUTECOLOR='#ff0000'

# How long does the OSD persist? In milliseconds.
DELAY=2000

# amixer options. Could use -c, -D here. Don't use -q.
# default is "-M", see amixer(1).
AMIXER_OPTS="-M"

# ------------------------------------------
# End of configurable options, rest is code.
# ------------------------------------------

SELF="$( basename $0 )"

PIDFILE="$HOME/.$SELF.osd.pid"

BLOCKS="██████████████████████████████████████████████████"
BLANKS="──────────────────────────────────────────────────"
PARTIAL="▌"

# progress() prints a UTF-8 progress bar with 50 characters.
# takes a percentage, 0 to 100
progress() {
  local text="$1"
  local count="$2"
  local solidblocks
  local partial
  local trailing

  solidblocks="$(( $count / 2 ))"
  partial="$(( $count % 2 ))"
  trailing="$(( 50 - ( $solidblocks + $partial) ))"

  echo -n "$text${BLOCKS:0:$solidblocks}"
  [ "$partial" = "1" ] && echo -n $PARTIAL
  echo -n "${BLANKS:0:$trailing}"
}

# osd() will kill any previously-spawned aosd_cat process.  Ideally this
# means rapid multiple keypresses won't "step on" each other. In practice
# it seems to work OK, but if you press the key really fast on a slow
# system, or hold it down with your key-repeat rate cranked up (on any
# system), the OSD bar will never get a chance to display.
osd() {
  local got="$( amixer $AMIXER_OPTS get $CHANNEL | tail -1 )"
  local muted="$( echo "$got" | grep '\[off\]$' )"
  local volpct="$( echo "$got" | cut -d'[' -f2 | cut -d% -f1 )"
  local db="$( echo "$got" | cut -d'[' -f3 | cut -d']' -f1 )"
  local color="$COLOR"
  local text="$( printf "%8s" $db) $( printf "%-3s" $volpct% ) "
  local oldpid

  if [ -n "$muted" ]; then
    color="$MUTECOLOR"
  fi

  # the silliness with grepping in /proc is meant to avoid stale PID
  # files whose PIDs have been recycled.
  if [ -e $PIDFILE ]; then
    oldpid="$( cat $PIDFILE )"
    grep -q '^aosd_cat' /proc/$oldpid/cmdline 2>/dev/null && \
      kill -9 "$oldpid" 2>/dev/null
    rm -f $PIDFILE
  fi

  # TODO: make these variables in the config section rather than
  # hardcoding them.
  progress "$text" "$volpct" | \
    aosd_cat \
      --fore-color $color \
      --back-color '#808080' \
      --font "$FONT" \
      --output 1 \
      --x-offset 159 \
      --y-offset 0 \
      --position 7 \
      --transparency 1 \
      --back-opacity 128 \
      --padding 7 \
      --alignment 0 \
      --fade-in 0 \
      --fade-out 0 \
      --fade-full $DELAY &

  echo "$!" > $PIDFILE
}

amixer_set() {
  amixer -q $AMIXER_OPTS set $CHANNEL $1
}

vol() {
  amixer_set ${ADJ}${1}
}

mute() {
  amixer_set toggle
}

usage() {
  cat <<EOF
$SELF [up|down|mute|<nn>]

<nn> is a numeric volume, possibly followed by "dB" or "%", and/or
     "+" or "-". It will be passed to 'amixer $AMIXER_OPTS set $CHANNEL' as-is.
EOF
  exit 1
}

check_deps() {
  local missing="no"
  for dep in aosd_cat amixer; do
    if ! type -p "$dep" > /dev/null; then
      echo "$SELF: missing required executable '$dep'" 1>&2
      missing="yes"
    fi
  done
  if [ "$missing" = "yes" ]; then
    echo "$SELF: please install the missing executable(s)" 1>&2
    exit 1
  fi
}

###main()

check_deps

case "$1" in
  up)     vol + ;;
  down)   vol - ;;
  mute)   mute  ;;
  [0-9]*) amixer_set "$1" ;;
  *)      usage ;;
esac

osd
exit 0
