# shellcheck shell=bash export ZERO="$0" HELP="$ZERO: tracker [-1] [-v [-v ...]] [q [-q ...]] -1 run once and exit -v increase verbosity (default >= warn) -q decrease verbosity " trap 'kill -15 $$' INT trace() { (( LOG_LEVEL > 2 )) && (exec 1>&2; builtin echo "$@"|grep .||true); } debug() { (( LOG_LEVEL > 1 )) && (exec 1>&2; builtin echo "$@"|grep .||true); } info() { (( LOG_LEVEL > 0 )) && (exec 1>&2; builtin echo "$@"|grep .||true); } warn() { (( LOG_LEVEL > -1 )) && (exec 1>&2; builtin echo "$@"|grep .||true); } error() { (( LOG_LEVEL > -2 )) && (exec 1>&2; builtin echo "$@"|grep .||true); } fatal() { (exec 1>&2; builtin echo "$@"|grep .||true); exit 4; } log() { info "$@"; } unbuf() { SCRIPT="$1"; shift ; stdbuf -oL -eL bash -c "$SCRIPT" - "$@" ; } start() { if ! options="$(getopt vq1 -- "$@")";then fatal "$HELP" fi eval set "$options" while [ $# -gt 0 ];do case "$1" in -1) ONCE=1 ;; -h) info "$HELP"; exit 0 ;; -v) LOG_LEVEL=$(( LOG_LEVEL + 1 )) ;; -q) LOG_LEVEL=$(( LOG_LEVEL - 1 )) ;; --) shift;break;; # we're _explicitly_ done with options -*) error "invalid option $1"; fatal "$HELP" ;; *) break;; # we're done with options. esac shift done if [ "$VALIDATION" ];then # fix up the validation regex to not search. VALIDATION="${VALIDATION#^}" VALIDATION="${VALIDATION%'$'}" VALIDATION='^'"$VALIDATION"'$' fi info "Running with LOG_LEVEL=$LOG_LEVEL" debug " VALIDATION: $VALIDATION" debug " RATE_LIMIT: $RATE_LIMIT" debug "POLL_INTERVAL: $POLL_INTERVAL" debug " watch_cmd: $watch_cmd" if [ "$(echo "$POLL_INTERVAL < $RATE_LIMIT"|bc)" = 1 ];then warn "POLL_INTERVAL cannot be less than RATE_LIMIT." info "increasing POLL_INTERVAL to $RATE_LIMIT." POLL_INTERVAL=$RATE_LIMIT fi while true;do _poll || exit $? (( ONCE )) && exit 0 $watch_cmd | while sleep $RATE_LIMIT;do # read with a maximum of the poll interval minus the previous rate limit. # rounding _up_ to the nearest second. (bc floors when scale is unset.) if read -r -t "$( echo "$POLL_INTERVAL - $RATE_LIMIT + .5 " | bc )";then debug "received poll. removing other poll requests." else debug "reached the polling interval. forcing poll." fi N=0 while read -t 0;do read -r; N=$((N + 1));done debug "removed $N extra events" _poll debug "sleeping for $RATE_LIMIT" done warn "tracker died. restarting after $RATE_LIMIT seconds." sleep $RATE_LIMIT done | unbuf "$_filter" info "bye" } _poll() { NEWVAL="$(poll | head -1)" local skipped=0 echo "$NEWVAL" | while read -r LINE;do (( skipped )) && info "$LINE" skipped=1 done local failed=0 if [ "$JQ_VALIDATION" ];then echo "$NEWVAL" | jq -e "$JQ_VALIDATION" > /dev/null || failed=2 fi if [ "$VALIDATION" ];then echo "$NEWVAL" | grep -qE "$VALIDATION" || failed=1 fi if (( failed == 2 ));then warn "$NEWVAL failed jq validation" elif (( failed ));then warn "$NEWVAL failed validation" else echo "$NEWVAL" fi } # MODULE OPTIONS RATE_LIMIT=.1 rate_limit() { RATE_LIMIT=$1 } POLL_INTERVAL=60 poll_interval() { POLL_INTERVAL=$1 } _filter="uniq" filter() { _filter="$(printf " %q" "$@")" } # VALIDATORS VALIDATION= output_integer() { VALIDATION='^-?[0-9]+$' } output_boolean() { VALIDATION='^true|false$' } output_string() { VALIDATION="${1-.*}" } JQ_VALIDATION= output_json() { JQ_VALIDATION="${1-.}" } # EVENT HANDLERS # each event source needs to emit a single _EMPTY_ line per event. # poll will be called for each line. on_volume() { on _on_volume; } _on_volume() { pactl subscribe | while read -r ;do case "$REPLY" in *change*) echo;; esac done } on_sway() { on _on_sway; } _on_sway() { swaymsg -t subscribe -m '[ "workspace", "window" ]' | jq --unbuffered -c . | while read -r;do echo done } on_system_dbus() { on _on_system_dbus; } _on_system_dbus() { unbuf dbus-monitor --system | while read -r;do case "$REPLY" in signal*) echo;; esac done } on() { watch_cmd="$1"; }