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