#!/bin/bash # Global readonly variables can't be shadowed by local variables so wrap # our code in a function so we can declare all variables local main() { # Customizable configuration constants local -r DEFAULT_MOD_LIST='cpu mem bl vol-amixer bat dt' local -r DEFAULT_PRE=' ' local -r DEFAULT_SEP_L='| ' local -r DEFAULT_SEP_R=' ' local -r DEFAULT_SUF=' ' local -r mod_list="${1-${DEFAULT_MOD_LIST}}" local -r pre="${2-${DEFAULT_PRE}}" local -r sep_l="${3-${DEFAULT_SEP_L}}" local -r sep_r="${4-${DEFAULT_SEP_R}}" local -r suf="${5-${DEFAULT_SUF}}" local -r MOD_DIR="$(dirname "$0")"/module local -r ACTION_DIR=/tmp/xrsb-action local -i ACTION_DIR_LEN=${#ACTION_DIR} # Cache module values so we can reuse them without recomputing them local -A stat_cache # Since stat_cache is hash ordered, maintain the display order (as defined # by mod_list) of the keys so we can loop over the cache in display order # when generating the full status local -a stat_cache_ordered_mods mods local mod mod_file # Map the module file name to the module function mod_to_fn() { printf 'mod_%s' "${1//-/_}" } # For each module in the list, if the module file exists then source it, add # its name to the ordered array, and call its function and cache the value IFS=', ' read -r -a mods <<< "${mod_list}" for mod in ${mods[@]}; do mod_file="${MOD_DIR}/${mod}" if [[ -r "${mod_file}" ]]; then # shellcheck source=/dev/null source "${mod_file}" stat_cache_ordered_mods+=("${mod}") stat_cache["${mod}"]="$("$(mod_to_fn "${mod}")")" fi done # Construct and display the status by looping over the cached values in order draw_status() { local mod stat for mod in "${stat_cache_ordered_mods[@]}"; do printf -v stat '%b%b%b%b' \ "${stat}" "${sep_l}" "${stat_cache[${mod}]}" "${sep_r}" done # Trim the leading left separator and trailing right separator, and # display the status local -ri offset=${#sep_l} local -ri len=$((${#stat} - offset - ${#sep_r})) xsetroot -name "${pre}${stat:${offset}:${len}}${suf}" } # Draw the initial status draw_status # For each file in the action directory, remove the file, and if a module for # the action is cached, call the module function and update the cache. If # any cache entries were updated, redraw the status. process_signal () { local -a action_paths local action_path mod is_changed readarray -d '' action_paths< \ <(find "${ACTION_DIR}" -maxdepth 1 -type f -exec rm -f {} + -print0) for action_path in "${action_paths[@]}"; do mod="${action_path:$((ACTION_DIR_LEN + 1))}" if [[ -v stat_cache[${mod}] ]]; then stat_cache["${mod}"]="$("$(mod_to_fn "${mod}")")" is_changed=1 fi done if [[ -v is_changed ]]; then draw_status; fi } # Begin trapping signals mkdir -p "${ACTION_DIR}" trap process_signal SIGUSR1 # Wait for signals efficiently. In a loop begin a long-running sleep command # in the background, then wait on it. If we trap a signal before the wait # is over and sleep is still running, trap will call process_signal, then # code execution will resume at the line after the wait statement. So on # that line we kill the (probably) still running sleep command so they # don't pile up, and loop to sleep and wait for the next signal. If we # don't trap a signal during the long running sleep, then the wait ends, # we try to kill the sleep command that has already exited, so it doesn't # matter, and loop to sleep and wait again. Note that we don't make the # sleep too long because if the daemon is killed, the sleep will become # an orphaned process until the sleep period elapses. local -i sleep_pid while :; do sleep 30m & sleep_pid="$!" wait "${sleep_pid}" kill "${sleep_pid}" 2>/dev/null done } main "$@"