#!/bin/bash APP_SVNID='$HeadURL$ $LastChangedRevision$' # Modules . $(ade-config ade_share_prefix)/include/ade.sh || { echo "${0##*/}: INTERNAL ERROR: failed to load ade.sh" >&2; exit 3; } # Configuration # Other globals WRA_DEFINED_ERRORS=( "KEY=WRA_ERR_MISC; FMT=\"%s\"" ) MAX_VOLUME=100 WRA_SHARE_PREFIX=$(wra-config wra_share_prefix) INIT_WRARC=$WRA_SHARE_PREFIX/wrarc FADEIN_STEP_INTERVAL=5 declare -A CONFIG_HASH wra() { local ERRSTACK_REF="$1"; shift local OPTVAL RC USERSEL_STATION PROGNAME local -a DOLLARAT # Register application-specific errors ade_register_error_types WRA_DEFINED_ERRORS # Defaults for options OPT_MODE=normal # There's no nice way to determine the .config dir. Stephen Kitt on # https://unix.stackexchange.com/questions/726717 suggests to use # ${XDG_CONFIG_HOME:-$HOME/.config}. ade_get_progname "$ERRSTACK_REF" PROGNAME OPT_RC_FILE=${XDG_CONFIG_HOME:-$HOME/.config}/$PROGNAME/${PROGNAME}rc # Register wra's options ade_register_options "$ERRSTACK_REF" -o fl --longoptions=config-file:,list-stations --callback-template="wra_opt_handler_%s" || return $? ade_set_callbacks "$ERRSTACK_REF" wra_usage_help wra_version wra_paths || return $? # Process options ade_process_options "$ERRSTACK_REF" NEW_DOLLAR_AT "$@" || return $? set -- "${NEW_DOLLAR_AT[@]}" # Process arguments # (later) # Sanity checks and derivations #ade_set_messaging_parameters "$ERRSTACK_REF" writers="stderr,logfile" logfile=/tmp/wra.$$.log HELPER_DIRS=( $HOME/.config/${PROGNAME}/helpers $WRA_SHARE_PREFIX/helpers ) # Create rc file if none exists. # The rc file used to be a file, then it was a symlink to a package-provided # rc file, but now, in order to support personal helper scripts, we need it # to be a directory. All these possibilties require some careful handling. if [ -h ~/.wrarc ] && [[ $(readlink ~/.wrarc) =~ .*/usr/share/wra/wrarc$ ]] && [ ! -h $OPT_RC_FILE -a ! -e $OPT_RC_FILE ]; then # ~/.wrarc is a symlink, points to the package provided-file, so it can be recreated in ~/.config/wra/ rm ~/.wrarc mkdir -p ${OPT_RC_FILE%/*} ln -sr $INIT_WRARC $OPT_RC_FILE elif [ -h ~/.wrarc ]; then ade_error "$ERRSTACK_REF" WRA_ERR_MISC "~/.wrarc is a non-standard symlink (hint: resolve this yourself)" return $ADE_FAIL elif [ -f ~/.wrarc ] && [ ! -h $OPT_RC_FILE -a ! -e $OPT_RC_FILE ]; then mkdir -p ${OPT_RC_FILE%/*} mv ~/.wrarc $OPT_RC_FILE elif [ -f ~/.wrarc ]; then ade_error "$ERRSTACK_REF" WRA_ERR_MISC "~/.wrarc is a file but new location already exists (hint: resolve this yourself)" return $ADE_FAIL elif [ -e ~/.wrarc ]; then ade_error "$ERRSTACK_REF" WRA_ERR_MISC "~/.wrarc exists but is not a file or a symlink (hint: resolve this yourself)" return $ADE_FAIL elif [ ! -h $OPT_RC_FILE -a ! -e $OPT_RC_FILE ]; then mkdir -p ${OPT_RC_FILE%/*} ln -sr $INIT_WRARC $OPT_RC_FILE elif [ -h $OPT_RC_FILE ] && [[ $(readlink $OPT_RC_FILE) =~ .*/usr/share/wra/wrarc$ ]]; then : else ade_warning "$ERRSTACK_REF" WRA_ERR_MISC "custom wrarc" fi # Set a default volume control command. write_multi_key_hash_entry "$ERRSTACK_REF" control $FACILITY command 'echo pactl set-sink-volume $(pacmd list-sinks | sed -n "s/^ \\* index: //p") %{PERCENT}%' # Load configuration. ade_debug "$ERRSTACK_REF" 10 "wra: checking $OPT_RC_FILE is loadable ..." sh -c "station() { :; }; control() { :; }; helper() { :; }; . $OPT_RC_FILE > /dev/null 2>&1" || { ade_error "$ERRSTACK_REF" WRA_ERR_MISC "$OPT_RC_FILE: not loadable (hint: run 'sh -c \"station() { :; }; control() { :; }; helper() { :; }; . $OPT_RC_FILE\"' to see why)" return $ADE_FAIL } ade_debug "$ERRSTACK_REF" 10 "wra: loading $OPT_RC_FILE ..." # See comments below in station() function regarding CONFIG_FILE_AGGR_RC. CONFIG_FILE_AGGR_RC=$ADE_OK . $OPT_RC_FILE if [ $CONFIG_FILE_AGGR_RC != $ADE_OK ]; then return $CONFIG_FILE_AGGR_RC fi # Guts mode_$OPT_MODE "$@" || return $? return $ADE_OK } mode_list() { local ERRSTACK_REF="$1"; shift local STATION DEFAULT DESCRIPTION IS_DEFAULT_STATION_FLAG STATIONS get_stations "$ERRSTACK_REF" STATIONS # Get longest station name LONGEST_STATION_LENGTH=0 for STATION in ${STATIONS[*]}; do if [ ${#STATION} -gt $LONGEST_STATION_LENGTH ]; then LONGEST_STATION_LENGTH=${#STATION} fi done # Get longest station description LONGEST_DESCRIPTION_LENGTH=0 for STATION in ${STATIONS[*]}; do read_multi_key_hash_entry "$ERRSTACK_REF" station $STATION description DESCRIPTION if [ ${#DESCRIPTION} -gt $LONGEST_DESCRIPTION_LENGTH ]; then LONGEST_DESCRIPTION_LENGTH=${#DESCRIPTION} fi done if [ -t 1 ]; then # Get screen width eval `resize -u` # How many chars to display each station? Add '2' for ': ' and '4' is inter-column spacing RECORD_LENGTH=$(( LONGEST_STATION_LENGTH + LONGEST_DESCRIPTION_LENGTH + 2 + 4)) # To calculate the number of columns we can display *and the number of inter-column # 4-spaces is much eased if we add 4 to the width of the screen and then divide # by the record length. RECORDS_PER_LINE_COUNT=$(( (COLUMNS+4) / RECORD_LENGTH )) else RECORDS_PER_LINE_COUNT=1 fi # Get the number of rows and columns we'll have (though some cells may be empty # because the number of stations isn't a precise multiple of the number of # columns). COLS=$RECORDS_PER_LINE_COUNT ROWS=$(( (${#STATIONS[*]}+$COLS-1) / $COLS )) # We go row by row ... for ((ROW=0; ROW<$ROWS; ROW++)); do # ... and column by column. for ((COL=0; COL<$COLS; COL++)); do # When all columns are not entirely full, then we would shortly to # access a non-existent cell. Prevent this. if (($ROWS*COL+ROW >= ${#STATIONS[*]})); then continue fi # Construct the record (without padding). STATION=${STATIONS[$(($ROWS*COL+ROW))]} read_multi_key_hash_entry "$ERRSTACK_REF" station $STATION is_default IS_DEFAULT_STATION_FLAG read_multi_key_hash_entry "$ERRSTACK_REF" station $STATION description DESCRIPTION # The printfs below work fine except for diacriticals, which get wrongly # padded (bash's printf thinks that 'รก' is 2 chars long, so adds extra spaces # than after 'a' for example). Accordingly, we need to add an adjustment to # the padding. Most of the time this adjustment is zero, because most of # the time there are no diacriticals. The adjustment is the difference between # 'wc -c' and 'wc -m'. See https://unix.stackexchange.com/questions/609125/padding-unicode-strings-with-bashs-printf. ADJUSTMENT=$(( $(wc -m <<<"$DESCRIPTION") - $(wc -c <<<"$DESCRIPTION") )) if $IS_DEFAULT_STATION_FLAG; then BOLD_ON_OR_EMPTY=$(tput smso) BOLD_OFF_OR_EMPTY=$(tput rmso) else BOLD_ON_OR_EMPTY= BOLD_OFF_OR_EMPTY= fi # +1 is because the ':' is displayed as part of station name to avoid mis-padding. printf -v RECORD "$BOLD_ON_OR_EMPTY%-$((LONGEST_STATION_LENGTH+1))s %-$((LONGEST_DESCRIPTION_LENGTH-ADJUSTMENT))s$BOLD_OFF_OR_EMPTY" "$STATION$ASTERISK_OR_NOTHING:" "$DESCRIPTION" # If this is the last column then output without padding and with newline. if ((COL+1==COLS)); then echo "$RECORD" # If the cell to the right (COL+1) would next loop we would try to access an # empty cell (were it not for the code above to prevent that) but that means # that *this* cell is the last one and should be without padding and with newline. elif (($ROWS*(COL+1)+ROW >= ${#STATIONS[*]})); then echo "$RECORD" # Otherwise add 4 spaces padding and no newline. else echo -n "$RECORD " fi done done return $ADE_OK } mode_normal() { local CHECK EXPECT # Process arguments twice. Once as a check and once for real. for CHECK in true false; do # STOP_ALL_HELPERS_FLAG is only used if CHECK is false. It is used # to tell start_station() or stop_station() that they don't know # which helpers (if any) are active, so they should stop all helpers # before starting a station of stopping a helper. STOP_ALL_HELPERS_FLAG=true EXPECT=onorofforstation for ARG in "$@"; do ade_debug "$ERRSTACK_REF" 10 "wra: CHECK=$CHECK, EXPECT=$EXPECT, ARG=$ARG" if [ $EXPECT = on -a $ARG = on ]; then EXPECT=ontime elif [ $EXPECT = on ]; then ade_show_bad_usage "$ERRSTACK_REF" elif [ $EXPECT = off -a $ARG = off ]; then EXPECT=offtime elif [ $EXPECT = off ]; then ade_show_bad_usage "$ERRSTACK_REF" elif [ $EXPECT = onoroff -a $ARG = on ]; then EXPECT=ontime elif [ $EXPECT = onoroff -a $ARG = off ]; then EXPECT=offtime elif [ $EXPECT = onoroff ]; then ade_show_bad_usage "$ERRSTACK_REF" elif [ $EXPECT = onorofforstation -a $ARG = on ]; then EXPECT=ontime elif [ $EXPECT = onorofforstation -a $ARG = off ]; then EXPECT=offtime elif [ $EXPECT = onorofforstation ]; then validate_station "$ERRSTACK_REF" "$ARG" || return $? $CHECK || process_station "$ERRSTACK_REF" $ARG || return $? EXPECT=onoroff elif [ $EXPECT = onorstation -a $ARG = on ]; then EXPECT=ontime elif [ $EXPECT = onorstation ]; then validate_station "$ERRSTACK_REF" "$ARG" || return $? $CHECK || process_station "$ERRSTACK_REF" $ARG || return $? EXPECT=on elif [ $EXPECT = offorstation -a $ARG = off ]; then EXPECT=offtime elif [ $EXPECT = offorstation ]; then validate_station "$ERRSTACK_REF" "$ARG" || return $? $CHECK || process_station "$ERRSTACK_REF" $ARG || return $? EXPECT=off elif [ $EXPECT = ontime ]; then validate_time "$ERRSTACK_REF" "$ARG" || return $? $CHECK || process_ontime "$ERRSTACK_REF" $ARG || return $? EXPECT=offorstation elif [ $EXPECT = offtime ]; then validate_time "$ERRSTACK_REF" "$ARG" || return $? $CHECK || process_offtime "$ERRSTACK_REF" $ARG || return $? EXPECT=onorstation else ade_show_bad_usage "$ERRSTACK_REF" fi done # Check last argument left state engine in an acceptable state [ $EXPECT = onorstation -o $EXPECT = offorstation ] || ade_show_bad_usage "$ERRSTACK_REF" done return $ADE_OK } wra_opt_handler_l() { wra_opt_handler_list_stations "$@" } wra_opt_handler_list_stations() { local ERRSTACK_REF="$1"; shift OPT_MODE=list return $ADE_OK } wra_opt_handler_f() { wra_opt_handler_config_file "$@" } wra_opt_handler_config_file() { local ERRSTACK_REF="$1"; shift OPT_RC_FILE="$1" return $ADE_OK } wra_usage_help() { local ERRSTACK_REF="$1"; shift local USAGE_TEXT_SHORT_REF="$1"; shift local USAGE_TEXT_LONG_REF="$1"; shift eval "$USAGE_TEXT_SHORT_REF=\"{ on