Wren Linux

wren-linux / bin / sway-status

#!/bin/sh
# Status line for swaybar: Wren Menu (click: the menu) | wifi (click: nmtui) | dns OFF (only while
# secure DNS is off; click: on) | volume (scroll: louder/quieter,
# click: mute) | brightness (scroll) | battery (click: time left; yellow at 15%, red at
# 5%, with a notification at each, and a clean shutdown at 3%) | next timer | date. Only while they're so: mem low, ssh login,
# mic ON, cam ON, reboot needed, disk full, vault open.
# (click on the time or a timer: the timer menu; the format is set by `timer clock`)
# Written by Wren Linux install-base.sh. Each part only shows when this
# system has what it needs, so it copes with anything missing or removed.
# Redraws every 5 seconds, or right away on SIGUSR1 (the brightness keys
# send it; the PID is in $XDG_RUNTIME_DIR/sway-status.pid).
# Speaks swaybar's JSON protocol so that a click on Wren Menu can be heard.

PIDFILE=${XDG_RUNTIME_DIR:-/tmp}/sway-status.pid
POWER=${WREN_POWER_DIR:-/sys/class/power_supply}   # (a test can point it elsewhere)
wifi() {
    if command -v nmcli >/dev/null; then
        name=$(nmcli -t -f NAME,TYPE connection show --active 2>/dev/null |
            awk -F: '$2 == "802-11-wireless" { print $1; exit }')
        if [ -n "$name" ]; then echo "$name"; return; fi
    fi
    if command -v iw >/dev/null; then
        for d in /sys/class/net/*; do
            [ -d "$d/wireless" ] || continue
            name=$(iw dev "${d##*/}" link 2>/dev/null | sed -n 's/^[[:space:]]*SSID: //p')
            if [ -n "$name" ]; then echo "$name"; return; fi
        done
    fi
    if [ -n "$(ip route show default 2>/dev/null)" ]; then echo online; else echo offline; fi
}

volume() {
    command -v wpctl >/dev/null || return 0
    wpctl get-volume @DEFAULT_AUDIO_SINK@ 2>/dev/null |
        awk '{ v = int($2 * 100 + 0.5); print ($3 == "[MUTED]") ? "vol muted" : "vol " v "%" }'
}

brightness() {
    for d in /sys/class/backlight/*; do
        [ -r "$d/brightness" ] || continue
        max=$(cat "$d/max_brightness")
        [ "$max" -gt 0 ] || continue
        echo "bright $((($(cat "$d/brightness") * 100 + max / 2) / max))%"
        return
    done
}

battery() {
    for b in "$POWER"/BAT*; do
        [ -r "$b/capacity" ] || continue
        cap=$(cat "$b/capacity")
        if [ "$(cat "$b/status" 2>/dev/null)" = Charging ]; then
            echo "bat $cap%+"
        else
            echo "bat $cap%"
        fi
        return
    done
}

# battery_time: a sentence on how long the battery will last (or take to fill)
battery_time() {
    for b in "$POWER"/BAT*; do
        [ -r "$b/capacity" ] || continue
        status=$(cat "$b/status" 2>/dev/null)
        # energy (uWh) and power (uW), or on some laptops charge (uAh) and current (uA):
        # either way the ratio is hours
        for pair in "energy_now energy_full power_now" "charge_now charge_full current_now"; do
            # shellcheck disable=SC2086  # the three names are meant to split
            set -- $pair
            [ -r "$b/$1" ] && [ -r "$b/$3" ] && break
        done
        now=$(cat "$b/$1" 2>/dev/null); full=$(cat "$b/$2" 2>/dev/null); rate=$(cat "$b/$3" 2>/dev/null)
        case $status in
            Discharging) left=$now ;;
            Charging) left=$((full - now)) ;;
            Full) echo "Fully charged"; return ;;
            *) echo "Plugged in, not charging ($(cat "$b/capacity")%)"; return ;;
        esac
        if [ "${rate:-0}" -le 0 ] 2>/dev/null; then echo "Not enough data yet, try again in a minute"; return; fi
        mins=$((left * 60 / rate))
        if [ "$mins" -ge 60 ]; then t="$((mins / 60)) h $((mins % 60)) min"; else t="$mins min"; fi
        if [ "$status" = Charging ]; then echo "$t until full"; else echo "$t left"; fi
        return
    done
    echo "No battery found"
}

# dns: "dns OFF" while secure DNS is set up (parts/secure-dns.sh) but off. A
# program running as you can switch it off as easily as you can, so it shows,
# and the loop below sends a notification when it goes off. A click switches
# it on again.
RESOLV=${WREN_RESOLV_CONF:-/etc/resolv.conf}
SECDNS_SWITCH=${WREN_SECDNS_SWITCH:-/usr/local/libexec/wren-secure-dns}
dns() {
    [ -x "$SECDNS_SWITCH" ] || return 0
    if [ ! -L "$RESOLV" ] && grep -qsE '^# Written by Wren Linux (parts|extras)/secure-dns\.sh' "$RESOLV"; then return 0; fi
    echo "dns OFF"
}
# --dns: what the bar would show for DNS (for a test)
if [ "${1:-}" = --dns ]; then dns; exit 0; fi
# startup: "startup changed" while the startup watch (parts/startup-watch.sh)
# has seen a change to what starts at your login that you haven't accepted.
# root writes that list; you can read it but not change it. A click shows it.
WATCH_RUN=${WREN_WATCH_RUN:-/run/wren-watch}
startup() { if [ -r "$WATCH_RUN/$(id -un)" ]; then echo "startup changed"; fi; }
if [ "${1:-}" = --startup ]; then startup; exit 0; fi
# updates: says so when security updates have stopped working (parts/unattended-upgrades.sh):
# today's download failed though there's a network, or nothing has come for 3 days on a
# computer that has been on and online for 12 hours. Nothing while offline.
UPDATE_RUN=${WREN_UPDATE_RUN:-/run/wren-updates}
UPDATE_STAMP=${WREN_UPDATE_STAMP:-/var/lib/apt/periodic/update-stamp}
UPDATE_DROPIN=${WREN_UPDATE_DROPIN:-/etc/systemd/system/apt-daily.service.d/60-wren-update-result.conf}
updates() {
    [ -e "$UPDATE_DROPIN" ] || return 0
    [ -n "$(ip route show default 2>/dev/null)$(ip -6 route show default 2>/dev/null)" ] || return 0
    if [ "$(cat "$UPDATE_RUN/state" 2>/dev/null)" = failed ]; then echo "updates failed"; return 0; fi
    up=$(cut -d. -f1 "${WREN_UPTIME:-/proc/uptime}" 2>/dev/null)
    [ "${up:-0}" -ge 43200 ] || return 0
    [ -n "$(find "$UPDATE_STAMP" -mmin -4320 2>/dev/null)" ] || echo "updates old"
}
# whonix: says so once Whonix hasn't had an update that worked in a week (the file is
# bin/whonix's). A click runs it, in a window that stays until you've read how it went.
WHONIX_STAMP=$HOME/.local/state/wren/whonix-updated
whonix_due() {
    [ -e "$WHONIX_STAMP" ] && [ -x "$HOME/.local/bin/whonix" ] || return 0
    [ -z "$(find "$WHONIX_STAMP" -mmin +$((${WHONIX_UPDATE_DAYS:-7} * 1440)) 2>/dev/null)" ] || echo "whonix update"
}
if [ "${1:-}" = --whonix ]; then whonix_due; exit 0; fi
# What's on that you may want to know of, shown only while it is. These catch mistakes and careless
# programs: something running as you could also change this bar, so no alert isn't proof of anything.
RADIOS=$HOME/.local/bin/radios
radio_on() { [ "$("$RADIOS" "$1" status 2>/dev/null)" = on ]; }
mic_on() { if [ -x "$RADIOS" ] && radio_on mic; then echo "mic ON"; fi; }
cam_on() { if [ -x "$RADIOS" ] && radio_on camera; then echo "cam ON"; fi; }
# a login from another computer: who names where it came from
ssh_login() { if who 2>/dev/null | grep -qE '\([0-9a-fA-F:.]+\)$'; then echo "ssh login"; fi; }
# a package saying so, or a kernel newer than the running one: its fixes need a restart
reboot_due() {
    if [ -e "${WREN_REBOOT_FILE:-/run/reboot-required}" ]; then echo "reboot needed"; return 0; fi
    # shellcheck disable=SC2012  # kernel versions: plain names
    rd_newest=$(ls "${WREN_MODULES:-/lib/modules}" 2>/dev/null | sort -V | tail -n1)
    if [ -n "$rd_newest" ] && [ "$rd_newest" != "$(uname -r)" ]; then echo "reboot needed"; fi
}
# 95% of the disk your home is on: updates and VMs start failing soon after
disk_full() {
    df_use=$(df -P "$HOME" 2>/dev/null | awk 'NR == 2 { sub(/%/, "", $5); print $5 }')
    if [ "${df_use:-0}" -ge 95 ]; then echo "disk full"; fi
}
vault_open() { if [ -x "$HOME/.local/bin/vault" ] && mountpoint -q "$HOME/Vault" 2>/dev/null; then echo "vault open"; fi; }
UPDATE_HELP="Security updates aren't reaching this computer. To see why: sudo apt update. If apt goes through Tor, its bridge may be down: wrenctl install apt-tor chooses again."
if [ "${1:-}" = --updates ]; then updates; exit 0; fi
# json TEXT: TEXT made safe to put inside a JSON string (a network name can hold quotes, and
# control characters: one of those would break the bar, and with it every warning on it)
json() { printf '%s' "$1" | LC_ALL=C tr -d '\000-\037\177' | sed 's/\\/\\\\/g; s/"/\\"/g'; }

# clock_format: the date(1) format for the time, as chosen with `timer clock`
clock_format() {
    conf=${XDG_CONFIG_HOME:-$HOME/.config}/wren/clock
    case $(sed -n 's/^date=//p' "$conf" 2>/dev/null) in no) echo '%a %-I:%M %p' ;; *) echo '%a %Y-%m-%d %-I:%M %p' ;; esac
}

# timer_text: the next timer or alarm ("tea 4m"), if there is one
timer_text() {
    [ -x "$HOME/.local/bin/timer" ] && "$HOME/.local/bin/timer" next
}

# block NAME TEXT: one part of the bar, " | TEXT", nothing if TEXT is empty
block() {
    [ -n "$2" ] || return 0
    if [ -n "$TMUX_BAR" ]; then
        printf ' | #[range=user|%s]%s#[norange]' "$1" "$(printf '%s' "$2" | sed 's/#/##/g')"
        return 0
    fi
    printf ',{"name":"%s","full_text":" | %s","separator":false,"separator_block_width":0}' "$1" "$(json "$2")"
}

# alert NAME TEXT: like block, but hard to miss: yellow in sway's bar, bold in tmux's
alert() {
    [ -n "$2" ] || return 0
    if [ -n "$TMUX_BAR" ]; then
        printf ' | #[range=user|%s,bold,fg=yellow]%s#[norange,nobold,default]' "$1" "$(printf '%s' "$2" | sed 's/#/##/g')"
        return 0
    fi
    # (the divider a block of its own, so the alert's color stops at its text)
    printf ',{"full_text":" | ","separator":false,"separator_block_width":0}'
    printf ',{"name":"%s","full_text":"%s","color":"#ffd000","urgent":true,"separator":false,"separator_block_width":0}' "$1" "$(json "$2")"
}

# battery_level: low at 15% or less, critical at 5% or less, while it runs on the battery;
# nothing otherwise (charging, full, plugged in and not charging, or no battery)
battery_level() {
    for b in "$POWER"/BAT*; do
        [ -r "$b/capacity" ] || continue
        case $(cat "$b/status" 2>/dev/null) in Charging | Full | "Not charging") return 0 ;; esac
        cap=$(cat "$b/capacity")
        if [ "$cap" -le 5 ]; then echo critical; elif [ "$cap" -le 15 ]; then echo low; fi
        return 0
    done
}

# battery_block: the battery in the bar: yellow when low, red when critical. Backgrounds, so
# that it reads on either theme. Critical, it says when it shuts down, short and the same
# down to 3% (or to plug in, with the shutdown switched off).
battery_block() {
    bb_level=$(battery_level)
    if [ "$bb_level" = critical ]; then
        if [ -e "$HOME/.config/wren/no-battery-shutdown" ]; then bb_say=", plug in"; else bb_say=", dies @ 3%"; fi
    else
        bb_say=
    fi
    level_block bat "$(battery)$bb_say" "$bb_level"
}
# battery_shutting_down: yes at 3% or less on the battery. A clean shutdown beats the power
# just going: programs get to clean up after themselves, and VMs' disks to close.
# ~/.config/wren/no-battery-shutdown turns it off.
battery_shutting_down() {
    [ "$(battery_level)" = critical ] && [ ! -e "$HOME/.config/wren/no-battery-shutdown" ] || return 0
    for b in "$POWER"/BAT*; do
        [ -r "$b/capacity" ] || continue
        if [ "$(cat "$b/capacity")" -le 3 ] 2>/dev/null; then echo yes; fi
        return 0
    done
}
# ram_level: low under 15% of memory left, critical under 5% (programs start being killed
# not far below that). ram_block: nothing at all while there's enough.
MEMINFO=${WREN_MEMINFO:-/proc/meminfo}
ram_left() { awk '$1 == "MemTotal:" { t = $2 } $1 == "MemAvailable:" { a = $2 } END { if (t) print int(a * 100 / t) }' "$MEMINFO" 2>/dev/null; }
ram_level() {
    rl=$(ram_left)
    [ -n "$rl" ] || return 0
    if [ "$rl" -lt 5 ]; then echo critical; elif [ "$rl" -lt 15 ]; then echo low; fi
}
ram_block() {
    rb_level=$(ram_level)
    [ -z "$rb_level" ] || level_block ram "mem $(ram_left)% left" "$rb_level"
}
# level_block NAME TEXT LEVEL: yellow when low, red when critical, solid: a flash changed its
# width (the red one has a space each side), and the rest of the bar moved with it
level_block() {
    bb_text=$2 bb_level=$3
    if [ -z "$bb_level" ]; then
        block "$1" "$bb_text"
        return 0
    fi
    if [ -n "$TMUX_BAR" ]; then
        if [ "$bb_level" = critical ]; then bb_tmux=bold,fg=red; else bb_tmux=bold,fg=yellow; fi
        printf ' | #[range=user|%s,%s]%s#[norange,nobold,default]' "$1" "$bb_tmux" "$bb_text"
        return 0
    fi
    if [ "$bb_level" = critical ]; then bb_bg=d00000 bb_fg=ffffff; else bb_bg=ffd000 bb_fg=000000; fi
    printf ',{"full_text":" | ","separator":false,"separator_block_width":0}'
    printf ',{"name":"%s","full_text":" %s ","background":"#%s","color":"#%s","separator":false,"separator_block_width":0}' \
        "$1" "$(json "$bb_text")" "$bb_bg" "$bb_fg"
}

# --watch-screen: the window a click on "startup changed" opens: what changed,
# then 1 the changes, 2 take them as yours, 3 or Esc leave. The screen is
# cleared first, so sudo's password question is at the top.
if [ "${1:-}" = --watch-screen ]; then
    while :; do
        printf '\033[H\033[2J'
        wren-watch
        printf '\n  1  See the changes\n  2  Take them as yours (sudo asks your password)\n  3  Leave (or Esc)\n\n'
        ws_old=$(stty -g 2>/dev/null) || ws_old=
        stty -icanon -echo min 1 2>/dev/null || true
        ws_key=$(dd bs=1 count=1 2>/dev/null)
        [ -z "$ws_old" ] || stty "$ws_old" 2>/dev/null || true
        case $ws_key in
            1) printf '\033[H\033[2J'
               # sudo asks first, on the cleared screen, and only then the pager opens: piped
               # into less, its question was hidden under it, and a wrong password looked like
               # an empty file. Ctrl+C at the question closes the window.
               trap 'exit 130' INT
               if sudo -v; then
                   trap - INT
                   sudo wren-watch diff | less
               else
                   trap - INT
                   printf '\n%s ' "sudo didn't take the password, so nothing is shown. Press Enter to go back."
                   read -r _ || exit 0
               fi ;;
            2) printf '\033[H\033[2J'
               sudo wren-watch accept && printf '\nTaken as yours. ' || printf '\nNot taken. '
               printf 'Press Enter to close. '; read -r _; exit 0 ;;
            *) exit 0 ;;
        esac
    done
fi

# --battery: the sentence a click on bat shows (console-click uses it)
if [ "${1:-}" = --battery ]; then
    echo "Battery $(battery | sed 's/^bat //; s/+$//'): $(battery_time)"
    exit 0
fi

# --tmux: the same bar as one line of tmux status text (extras/console-only.sh),
# each part a click range named like the bar's; then exit
if [ "${1:-}" = --tmux ]; then
    TMUX_BAR=1
    printf '#[range=user|menu]Wren Menu#[norange]%s%s%s%s%s%s%s' \
        "$(block wifi "$(wifi)")" "$(alert updates "$(updates)")$(alert whonix "$(whonix_due)")$(alert startup "$(startup)")$(alert ssh "$(ssh_login)")$(alert mic "$(mic_on)")$(alert cam "$(cam_on)")$(alert reboot "$(reboot_due)")$(alert disk "$(disk_full)")$(alert vault "$(vault_open)")$(block dns "$(dns)")" "$(block vol "$(volume)")" "$(block bright "$(brightness)")" \
        "$(ram_block)$(battery_block)" "$(block timer "$(timer_text)")" "$(block date "$(date +"$(clock_format)")")"
    exit 0
fi
# open_or_close APP-ID COMMAND...: a click on the bar opens that window, and a
# click while it is open closes it (sway's kill fails when no window has that
# app id). The Wren menu's window is left open while it runs Update or
# Get more apps (it marks itself busy, by its PID): that click brings it to
# the front instead, so an update is never cut off. One of these windows at a
# time: opening one closes the others (but not a busy menu).
menu_busy() {
    mb_pid=$(cat "${XDG_RUNTIME_DIR:-/tmp}/wren-menu-busy" 2>/dev/null)
    case $mb_pid in '' | *[!0-9]*) return 1 ;; esac
    kill -0 "$mb_pid" 2>/dev/null
}
open_or_close() {
    oc_id=$1
    shift
    if [ "$oc_id" = wren-run ] && menu_busy; then
        swaymsg -q "[app_id=\"$oc_id\"] focus" >/dev/null 2>&1
        return 0
    fi
    swaymsg -q "[app_id=\"$oc_id\"] kill" >/dev/null 2>&1 && return 0
    for oc_other in wren-run wren-keys wren-nmtui wren-timer wren-watch; do
        [ "$oc_other" != "$oc_id" ] || continue
        [ "$oc_other" != wren-run ] || ! menu_busy || continue
        swaymsg -q "[app_id=\"$oc_other\"] kill" >/dev/null 2>&1 || true
    done
    # In a session of its own: sway restarts swaybar on every reload, and
    # swaybar then stops this script's whole process group. A window left in
    # it (the menu removing an extra that reloads sway) would be killed too.
    setsid -f "$@" >/dev/null 2>&1
}
# Super+m (install-base.sh): the same as a click on "Wren Menu", from a key;
# it ends here, before anything the running bar owns (its PID file)
if [ "${1:-}" = --menu ]; then
    if [ -x "$HOME/.local/bin/wren-menu" ]; then
        open_or_close wren-run "$HOME/.local/bin/wren-menu"
    fi
    exit 0
fi

echo $$ >"$PIDFILE"
trap 'rm -f "$PIDFILE"' EXIT
trap 'exit' TERM INT HUP
# (a redraw asked for: also ends a flash, below)
redraw_now=
trap 'redraw_now=1' USR1

# Clicks and scrolls arrive on stdin, one line of JSON each (sway puts spaces in it)
exec 3<&0

while read -r event <&3; do
    case $(printf '%s' "$event" | tr -d ' ') in
        *'"name":"menu"'*'"button":1,'*)
            # the menu (Help, Get more apps, Update, Status); the key cheat sheet
            # directly if the menu isn't installed
            if [ -x "$HOME/.local/bin/wren-menu" ]; then
                open_or_close wren-run "$HOME/.local/bin/wren-menu"
            else
                open_or_close wren-keys foot -o colors.alpha=0.85 --app-id=wren-keys "$HOME/.local/bin/keys"
            fi ;;
        *'"name":"wifi"'*'"button":1,'*)
            # nmtui measures its window once at the start, so wait for sway to size it
            # (the Wren Linux wrapper if there is one: nmtui in the Wren Linux colors)
            # shellcheck disable=SC2016  # expanded by the inner sh, on purpose
            if [ -x "$HOME/.local/bin/nmtui" ]; then
                open_or_close wren-nmtui foot -o colors.alpha=0.85 --app-id=wren-nmtui sh -c 'sleep 0.5; exec "$HOME/.local/bin/nmtui"'
            elif command -v nmtui >/dev/null; then
                open_or_close wren-nmtui foot -o colors.alpha=0.85 --app-id=wren-nmtui sh -c 'sleep 0.5; exec nmtui'
            fi ;;
        # the scroll wheel is button 4 (up) and 5 (down); the volume and brightness
        # commands also tell this script to redraw
        *'"name":"vol"'*'"button":1,'*) "$HOME/.local/bin/volume" mute ;;
        *'"name":"vol"'*'"button":4,'*) "$HOME/.local/bin/volume" up ;;
        *'"name":"vol"'*'"button":5,'*) "$HOME/.local/bin/volume" down ;;
        *'"name":"bright"'*'"button":4,'*) "$HOME/.local/bin/brightness" up ;;
        *'"name":"bright"'*'"button":5,'*) "$HOME/.local/bin/brightness" down ;;
        *'"name":"date"'*'"button":1,'* | *'"name":"timer"'*'"button":1,'*)
            open_or_close wren-timer foot -o colors.alpha=0.85 --app-id=wren-timer "$HOME/.local/bin/timer" ;;
        *'"name":"updates"'*'"button":1,'*)
            command -v notify-send >/dev/null && notify-send -a wren -u critical "Security updates" "$UPDATE_HELP" ;;
        *'"name":"whonix"'*'"button":1,'*)
            # (never closed by another click: that can't cut off an update, but it would hide it)
            # shellcheck disable=SC2016  # expanded by the inner sh, on purpose
            setsid -f foot -o colors.alpha=0.85 --app-id=wren-whonix sh -c \
                '"$HOME/.local/bin/whonix" update; echo; printf "Press Enter to close. "; read -r _' >/dev/null 2>&1 ;;
        *'"name":"startup"'*'"button":1,'*)
            open_or_close wren-watch foot -o colors.alpha=0.85 --app-id=wren-watch "$HOME/.local/bin/sway-status" --watch-screen ;;
        *'"name":"dns"'*'"button":1,'*)
            [ ! -x "$HOME/.local/bin/secure-dns" ] || "$HOME/.local/bin/secure-dns" on >/dev/null 2>&1
            kill -USR1 $$ 2>/dev/null ;;
        *'"name":"bat"'*'"button":1,'*)
            command -v notify-send >/dev/null &&
                notify-send -a wren -t 8000 "Battery $(battery | sed 's/^bat //; s/+$//')" "$(battery_time)" ;;
    esac
done &
reader=$!
trap 'kill $reader 2>/dev/null; rm -f "$PIDFILE"' EXIT

echo '{"version":1,"click_events":true}'
echo '['
dns_was='' startup_was='' updates_was='' battery_was='' ram_was='' ssh_was='' mic_was='' cam_was='' shutdown_at=''
while :; do
    updates_now=$(updates)
    if [ -n "$updates_now" ] && [ -z "$updates_was" ] && command -v notify-send >/dev/null; then
        notify-send -a wren -u critical "Security updates" "$UPDATE_HELP"
    fi
    updates_was=$updates_now
    startup_now=$(startup)
    if [ -n "$startup_now" ] && [ -z "$startup_was" ] && command -v notify-send >/dev/null; then
        notify-send -a wren -u critical "Your startup files changed" \
            "$(head -n 3 "$WATCH_RUN/$(id -un)" 2>/dev/null | sed 's|^|~/|' | paste -sd' ') - if it wasn't you or Wren Linux, something running as you did. Click \"startup changed\" in the bar." 2>/dev/null
    fi
    startup_was=$startup_now
    dns_now=$(dns)
    if [ -n "$dns_now" ] && [ -z "$dns_was" ] && command -v notify-send >/dev/null; then
        notify-send -a wren -u critical "Secure DNS is off" \
            "Lookups go to the network unencrypted. If you didn't switch it off, something running as you did. Click \"dns OFF\" in the bar to switch it on." 2>/dev/null
    fi
    dns_was=$dns_now
    # A notification on reaching each level, once; a charge starts that again
    battery_now=$(battery_level)
    if [ "$battery_now" != "$battery_was" ] && command -v notify-send >/dev/null; then
        case $battery_was:$battery_now in
            :low) notify-send -a wren "Battery low" "$(battery | sed 's/^bat //'). Plug it in soon." 2>/dev/null ;;
            *:critical) notify-send -a wren -u critical "Battery very low" "$(battery | sed 's/^bat //'). Save your work and plug it in now." 2>/dev/null ;;
        esac
    fi
    # At 3%, a last minute to plug in; plugged in, it is called off. (One odd reading won't do it.)
    if [ "$(battery_shutting_down)" = yes ]; then
        if [ -z "$shutdown_at" ]; then
            shutdown_at=$(($(date +%s) + ${WREN_BATTERY_SHUTDOWN_WAIT:-60}))
            command -v notify-send >/dev/null && notify-send -a wren -u critical "Shutting down soon" "The battery is at 3%: this computer shuts down cleanly in a minute. Plug it in to stop that, and save your work." 2>/dev/null
        elif [ "$(date +%s)" -ge "$shutdown_at" ]; then
            systemctl poweroff
        fi
    else
        shutdown_at=
    fi
    # (from critical up to low, say with a charge cut short, it doesn't warn again until it has charged)
    if [ "$battery_was" != critical ] || [ "$battery_now" != low ]; then battery_was=$battery_now; fi
    ram_now=$(ram_level)
    if [ "$ram_now" != "$ram_was" ] && command -v notify-send >/dev/null; then
        case $ram_was:$ram_now in
            :low) notify-send -a wren "Memory low" "$(ram_left)% left. Close something soon: when it runs out, programs get killed." 2>/dev/null ;;
            *:critical) notify-send -a wren -u critical "Memory nearly gone" "$(ram_left)% left. Close something now." 2>/dev/null ;;
        esac
    fi
    if [ "$ram_was" != critical ] || [ "$ram_now" != low ]; then ram_was=$ram_now; fi
    ssh_now=$(ssh_login) mic_now=$(mic_on) cam_now=$(cam_on)
    if command -v notify-send >/dev/null; then
        if [ -n "$ssh_now" ] && [ -z "$ssh_was" ]; then notify-send -a wren -u critical "Someone logged in over ssh" "If that wasn't you, the command who shows from where." 2>/dev/null; fi
        if [ -n "$mic_now" ] && [ -z "$mic_was" ]; then notify-send -a wren "Microphone on" "If you didn't switch it on, something running as you did." 2>/dev/null; fi
        if [ -n "$cam_now" ] && [ -z "$cam_was" ]; then notify-send -a wren "Webcam on" "If you didn't switch it on, something running as you did." 2>/dev/null; fi
    fi
    ssh_was=$ssh_now mic_was=$mic_now cam_was=$cam_now
    bar_start=$(block wifi "$(wifi)")$(alert updates "$updates_now")$(alert whonix "$(whonix_due)")$(alert startup "$startup_now")$(alert ssh "$ssh_now")$(alert mic "$mic_now")$(alert cam "$cam_now")$(alert reboot "$(reboot_due)")$(alert disk "$(disk_full)")$(alert vault "$(vault_open)")$(block dns "$dns_now")$(block vol "$(volume)")$(block bright "$(brightness)")
    bar_end=$(block timer "$(timer_text)")$(block date "$(date +"$(clock_format)")")
    draw() { # BATTERY-BLOCK
        printf '[{"name":"menu","full_text":"Wren Menu","separator":false,"separator_block_width":0}%s%s%s],\n' "$bar_start" "$1" "$bar_end"
    }
    draw "$(ram_block)$(battery_block)"
    redraw_now=
    # wait returns early when SIGUSR1 arrives; the sleep ends by itself
    sleep 5 &
    wait $!
done