From 124acaa78909d66c5307fab52ad0ab5a1325c04b Mon Sep 17 00:00:00 2001 From: James Andariese Date: Sat, 30 Mar 2024 14:44:17 -0500 Subject: [PATCH] add track_anything.sh and use it for volume and mute --- README.md | 80 +++++++++++++++++++ scripts/track_anything.sh | 164 ++++++++++++++++++++++++++++++++++++++ scripts/track_mute | 21 +++-- scripts/track_volume | 17 ++-- 4 files changed, 269 insertions(+), 13 deletions(-) create mode 100644 scripts/track_anything.sh diff --git a/README.md b/README.md index 30d9bc3..4074da5 100644 --- a/README.md +++ b/README.md @@ -3,3 +3,83 @@ My personal eww config. Install by linking or cloning to ~/.config/eww + +## `track_anything.sh` + +This config includes a helper bash source which may be used +to setup trackers of values. It requires an on_event emitter +to already exist and should have one of the three output +formats: integer, boolean, and string. + +When specifying a string output, you may include a custom regex +which will not be searched (it will be wrapped in `^`...`$`). + +### `poll` + +In addition to the configuration, you must provide a poll function. + +Your provided poll function must return a valid output as its first +line and may provide log data on subsequent lines which will be +emitted at the info level (stderr, single -v). + +A simple example is the pulseaudio mute state poller: + +```bash +poll() { + pamixer --get-mute +} +``` + +### `Configuration` + +Options: + + - rate_limit + - seconds to wait between running poll + (default: `1`) + + - poll_interval + - seconds to wait for an update before forcing a poll + (default: `60`) + - NOTE: must satisfy poll_interval >= rate_limit + + - filter: an arbitrary command to filter the total output of the poller in a loop + (default: `uniq`) + - NOTE: all arguments are collected and executed with eval. + - Example: `cat` will allow duplicates. `uniq` will filter them. + +Event Emitters: + + - `on_volume` + - emits on changes to pulseaudio sinks + + - `on_sway` + - emits on changes to sway windows or workspaces + + - `on` + - uses your own function to trigger an event + which should be passed as the only argument to `on` + - NOTE: the function in question _must_ emit events + as newlines with no text. multiple events may be + emitted in general safety if `rate_limit` is > 0 + + +Outputs: + + - `output_integer` + - output a decimal integer + + - `output_boolean` + - output a boolean (true, false) + + - `output_string` + - output an arbitrary string, validated by a regex + - args: + - a validation regex string as the first argument + (default: `.*`) + + - `output_json` + - output an arbitrary json value, validated by jq + - args: + - a validation jq query which must not fail or return null + (default: `.`) \ No newline at end of file diff --git a/scripts/track_anything.sh b/scripts/track_anything.sh new file mode 100644 index 0000000..300c62c --- /dev/null +++ b/scripts/track_anything.sh @@ -0,0 +1,164 @@ +export ZERO="$0" +HELP="$ZERO: tracker [-1] [-v [-v ...]] [q [-q ...]] + -1 run once and exit + -v increase verbosity (default >= warn) + -q decrease verbosity +" +EXITING=0 +trap 'EXITING=1' 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 "$@"; } + +start() { + if ! options="$(getopt vq1 -- "$@")";then + fatal "$HELP" + fi + + eval set "$options" + + while [ $# -gt 0 ];do + case "$1" in + -1) shift;ONCE=1 ;; + -h) echo "requesting help" ; help; exit 0 ;; + -v) LOG_LEVEL=$(( LOG_LEVEL + 1 )) ;echo increased log level ;; + -q) LOG_LEVEL=$(( LOG_LEVEL - 1 )) ;; + --) shift;break;; # we're _explicitly_ done with options + *) 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) -eq 1 ];then + warn "POLL_INTERVAL cannot be less than RATE_LIMIT." + info "increasing POLL_INTERVAL to $RATE_LIMIT." + POLL_INTERVAL=$RATE_LIMIT + fi + + while ! (( EXITING ));do + ( + _poll || exit $? + (( ONCE )) && EXITING=1 + + $watch_cmd | while ! (( EXITING ));do + sleep $RATE_LIMIT + + # 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 -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; N=$((N + 1));done + debug "removed $N extra events" + _poll + debug "sleeping for $RATE_LIMIT" + done + ) | eval "$_filter" + (( EXITING == 0 )) && (warn "tracker died. restarting after $RATE_LIMIT seconds.";sleep $RATE_LIMIT) + done + 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=1 + fi + if [ $VALIDATION ];then + echo "$NEWVAL" | grep -qE "$VALIDATION" || failed=1 + fi + if (( 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-.*}" +} + +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() { watch_cmd="$1"; } diff --git a/scripts/track_mute b/scripts/track_mute index 938d735..ad7a4f2 100755 --- a/scripts/track_mute +++ b/scripts/track_mute @@ -1,7 +1,16 @@ -#!/bin/sh +#!/usr/bin/env bash -pactl subscribe | while read -r ;do - case "$REPLY" in - *change*) pamixer --get-mute ;; - esac -done +. "$(dirname "$0")/track_anything.sh" + + +poll() { + pamixer --get-mute +} + +rate_limit .1 +poll_interval 60 +on_volume +output_boolean +filter uniq + +start "$@" diff --git a/scripts/track_volume b/scripts/track_volume index 7d81600..d97f4cf 100755 --- a/scripts/track_volume +++ b/scripts/track_volume @@ -1,11 +1,14 @@ -#!/bin/sh +#!/usr/bin/env bash + +. "$(dirname "$0")/track_anything.sh" poll() { - pamixer --get-volume | grep -E '^[0-9]+$' + pamixer --get-volume } -pactl subscribe | while read -r ;do - case "$REPLY" in - *change*) poll ;; - esac -done +on_volume +output_integer +poll_interval 10 +rate_limit .1 + +start "$@"