i3status-rust: the bars as a module, claiming node-bar (hq ADR 0208)
Owns both bars and their icons, the bar blocks as an i3 drop-in; the battery, GPU and headset blocks leave (hardware and a person's devices), the weather needs no key; the update count and the bar watchdog become module code, the watchdog started once per session. Go tools reload, blocks, block-run and themes.
This commit is contained in:
+64
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env bash
|
||||
# i3bar-watchdog [session-pid] (module i3status-rust, novox/hq ADR 0208): respawns a bar that died.
|
||||
#
|
||||
# i3 starts one i3bar per `bar { }` block and never restarts one that exits, and a reload does not
|
||||
# either. Changing the monitor setup reliably kills the bar that owns the tray. This brings back the
|
||||
# one missing i3bar, without restarting i3, and logs the outputs at that moment: the evidence for the
|
||||
# cause, which is i3bar's.
|
||||
#
|
||||
# Started once per session from the session's start, with the session's own pid; it ends when that
|
||||
# process does. Adopted from the predecessor's i3-bar-watchdog user unit of 2026-10-04.
|
||||
set -uo pipefail
|
||||
|
||||
session_pid="${1:-}"
|
||||
interval="${I3_BAR_WATCHDOG_INTERVAL:-5}"
|
||||
# Two misses in a row before acting: an i3 restart tears every bar down and starts them again.
|
||||
confirm="${I3_BAR_WATCHDOG_CONFIRM:-2}"
|
||||
# Never respawn the same bar more often than this, so a bar that dies at once is not a tight loop.
|
||||
cooldown="${I3_BAR_WATCHDOG_COOLDOWN:-30}"
|
||||
|
||||
log() { printf 'i3bar-watchdog: %s\n' "$*" >&2; }
|
||||
|
||||
for tool in i3-msg i3bar pgrep; do
|
||||
command -v "$tool" >/dev/null 2>&1 || {
|
||||
log "$tool is missing; not watching"
|
||||
exit 1
|
||||
}
|
||||
done
|
||||
|
||||
declare -A missed=() fixed=()
|
||||
|
||||
bar_ids() { i3-msg -t get_bar_config 2>/dev/null | grep -oE '"[^"]+"' | tr -d '"'; }
|
||||
outputs() { xrandr --listmonitors 2>/dev/null | tail -n +2 | awk '{print $2" "$3}' | tr '\n' ' '; }
|
||||
session_alive() { [ -z "$session_pid" ] || kill -0 "$session_pid" 2>/dev/null; }
|
||||
|
||||
while session_alive; do
|
||||
sleep "$interval"
|
||||
socket="$(i3 --get-socketpath 2>/dev/null)"
|
||||
[ -n "$socket" ] && [ -S "$socket" ] || continue
|
||||
ids="$(bar_ids)"
|
||||
[ -n "$ids" ] || continue
|
||||
while read -r id; do
|
||||
[ -n "$id" ] || continue
|
||||
if pgrep -u "$EUID" -f -- "i3bar --bar_id=$id" >/dev/null 2>&1; then
|
||||
missed[$id]=0
|
||||
continue
|
||||
fi
|
||||
missed[$id]=$((${missed[$id]:-0} + 1))
|
||||
[ "${missed[$id]}" -ge "$confirm" ] || continue
|
||||
now="$(date +%s)"
|
||||
if [ $((now - ${fixed[$id]:-0})) -lt "$cooldown" ]; then
|
||||
continue
|
||||
fi
|
||||
log "$id is gone; respawning it. Outputs now: $(outputs)"
|
||||
nohup i3bar --bar_id="$id" --socket="$socket" >/dev/null 2>&1 &
|
||||
disown 2>/dev/null || true
|
||||
fixed[$id]="$now"
|
||||
sleep 2
|
||||
if pgrep -u "$EUID" -f -- "i3bar --bar_id=$id" >/dev/null 2>&1; then
|
||||
missed[$id]=0
|
||||
else
|
||||
log "$id exited within 2s of respawning; check its status_command"
|
||||
fi
|
||||
done <<<"$ids"
|
||||
done
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env bash
|
||||
# i3status-updates [threshold] (module i3status-rust, novox/hq ADR 0208): the pending updates, for
|
||||
# the bottom bar's custom block (json = true). Emits one JSON line.
|
||||
#
|
||||
# Adopted from the operator's pkg-updates of 2026-10-04. The native `packages` block showed a raw
|
||||
# red error on every transient network failure, which on a roaming laptop is often. This keeps the
|
||||
# last good result through a failure and shows an error only after `threshold` failures in a row
|
||||
# (default 10). Official packages through checkupdates (pacman-contrib, a temporary database, no
|
||||
# root); AUR packages through paru when it is installed, and none counted when it is not.
|
||||
set -uo pipefail
|
||||
|
||||
threshold="${1:-10}"
|
||||
icon="update"
|
||||
state_dir="${XDG_RUNTIME_DIR:-/tmp}/i3status-updates"
|
||||
mkdir -p "$state_dir"
|
||||
last="$state_dir/last_good.json"
|
||||
failures="$state_dir/failures"
|
||||
|
||||
json() { printf '{"icon":"%s","state":"%s","text":"%s","short_text":"%s"}' "$icon" "$1" "$2" "$3"; }
|
||||
count() { [ -n "$1" ] && printf '%s\n' "$1" | grep -c . || echo 0; }
|
||||
|
||||
official="$(checkupdates 2>/dev/null)"
|
||||
official_rc=$?
|
||||
aur="" aur_rc=0
|
||||
if command -v paru >/dev/null 2>&1; then
|
||||
aur="$(paru -Qua 2>/dev/null)"
|
||||
aur_rc=$?
|
||||
fi
|
||||
|
||||
# checkupdates: 0 updates listed, 2 none pending; anything else is a failure.
|
||||
if { [ "$official_rc" -eq 0 ] || [ "$official_rc" -eq 2 ]; } && [ "$aur_rc" -eq 0 ]; then
|
||||
o="$(count "$official")"
|
||||
a="$(count "$aur")"
|
||||
total=$((o + a))
|
||||
if [ "$total" -eq 0 ]; then
|
||||
payload="$(json Good "up to date" "")"
|
||||
else
|
||||
# A kernel update wants a reboot: the one worth not putting off.
|
||||
state=Info
|
||||
printf '%s\n' "$official" | grep -qE '^(linux|linux-lts|linux-zen) ' && state=Warning
|
||||
label="$o + $a = $total updates"
|
||||
[ "$a" -eq 0 ] && label="$total updates"
|
||||
[ "$total" -eq 1 ] && label="1 update"
|
||||
payload="$(json "$state" "$label" "$total")"
|
||||
fi
|
||||
printf '%s' "$payload" >"$last"
|
||||
printf '0' >"$failures"
|
||||
printf '%s\n' "$payload"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
n=$(($(cat "$failures" 2>/dev/null || echo 0) + 1))
|
||||
printf '%s' "$n" >"$failures"
|
||||
if [ "$n" -ge "$threshold" ]; then
|
||||
json Critical "update check failing (${n}x)" "!"
|
||||
echo
|
||||
elif [ -s "$last" ]; then
|
||||
cat "$last"
|
||||
echo
|
||||
else
|
||||
json Idle "" ""
|
||||
echo
|
||||
fi
|
||||
@@ -0,0 +1,100 @@
|
||||
# The bottom bar (module i3status-rust, novox/hq ADR 0208). Owned by the mesh: replaced at every
|
||||
# push. Adopted from the workstations of 2026-10-04 with what follows a machine's hardware or one
|
||||
# person's devices left out: the battery and the GPU belong to a machine model's hardware module,
|
||||
# and a headset's address is not the bar's to know. Every block here works the same on any machine.
|
||||
icons_format = "{icon}"
|
||||
|
||||
[theme]
|
||||
theme = "plain"
|
||||
[theme.overrides]
|
||||
idle_bg = "#000000"
|
||||
idle_fg = "#f1f1f1"
|
||||
info_bg = "#000000"
|
||||
info_fg = "#f1f1f1"
|
||||
good_bg = "#000000"
|
||||
good_fg = "#859900"
|
||||
warning_bg = "#000000"
|
||||
warning_fg = "#de5200"
|
||||
critical_bg = "#000000"
|
||||
critical_fg = "#ff0000"
|
||||
separator = " | "
|
||||
separator_fg = "#ffffff"
|
||||
separator_bg = "#000000"
|
||||
|
||||
[icons]
|
||||
icons = "custom-icons"
|
||||
|
||||
[[block]]
|
||||
block = "tea_timer"
|
||||
format = "$icon{ $minutes:$seconds|}"
|
||||
done_cmd = "notify-send 'Timer finished'"
|
||||
|
||||
# Pending updates: official and, where an AUR helper is installed, AUR. The script keeps the last good
|
||||
# count through a network gap and shows an error only after ten failed checks in a row. Click: the
|
||||
# list, in the launcher's menu.
|
||||
[[block]]
|
||||
block = "custom"
|
||||
command = "~/.local/bin/i3status-updates 10"
|
||||
json = true
|
||||
interval = 1800
|
||||
[[block.click]]
|
||||
button = "left"
|
||||
cmd = "checkupdates | dmenu -l 20 -p Updates"
|
||||
|
||||
[[block]]
|
||||
block = "cpu"
|
||||
|
||||
[[block]]
|
||||
block = "disk_space"
|
||||
path = "/"
|
||||
info_type = "used"
|
||||
alert_unit = "GB"
|
||||
interval = 20
|
||||
warning = 850
|
||||
alert = 920
|
||||
format = "$icon / $used.eng(w:2) ($percentage.eng(w:2))"
|
||||
|
||||
[[block]]
|
||||
block = "disk_space"
|
||||
path = "/home"
|
||||
info_type = "used"
|
||||
alert_unit = "GB"
|
||||
interval = 20
|
||||
warning = 850
|
||||
alert = 920
|
||||
format = "$icon /home $used.eng(w:2) ($percentage.eng(w:2))"
|
||||
|
||||
[[block]]
|
||||
block = "memory"
|
||||
format = "$icon $mem_total_used.eng(w:2) ($mem_total_used_percents.eng(w:2))"
|
||||
format_alt = "$icon_swap $swap_used.eng(w:2) ($swap_used_percents.eng(w:2))"
|
||||
|
||||
[[block]]
|
||||
block = "docker"
|
||||
interval = 2
|
||||
format = "$icon $running/$total"
|
||||
|
||||
[[block]]
|
||||
block = "sound"
|
||||
[[block.click]]
|
||||
button = "left"
|
||||
cmd = "pavucontrol"
|
||||
|
||||
# The weather from the Norwegian Meteorological Institute, which needs no key, where the machine is.
|
||||
[[block]]
|
||||
block = "weather"
|
||||
format = "$icon $weather_verbose ($location) $temp"
|
||||
autolocate = true
|
||||
autolocate_interval = 600
|
||||
[block.service]
|
||||
name = "metno"
|
||||
|
||||
[[block]]
|
||||
block = "notify"
|
||||
driver = "dunst"
|
||||
format = " $icon {($notification_count.eng(w:1))|}"
|
||||
|
||||
[[block]]
|
||||
block = "time"
|
||||
interval = 1
|
||||
format = "$timestamp.datetime(f:'%a %d/%m %H:%M:%S')"
|
||||
@@ -0,0 +1,34 @@
|
||||
# The bars (module i3status-rust, novox/hq ADR 0208). Owned by the mesh: replaced at every push.
|
||||
# i3 reads this file through its configuration's `include ~/.config/i3/config.d/*.conf`. The bottom
|
||||
# bar shows the machine; the top bar the focused window and the tray, on the primary output. The
|
||||
# face is the monospace one every desktop module names.
|
||||
bar {
|
||||
font pango:JetBrainsMono Nerd Font 11
|
||||
position bottom
|
||||
status_command i3status-rs ~/.config/i3status-rust/bottom-bar.toml
|
||||
tray_output none
|
||||
colors {
|
||||
separator #ffffff
|
||||
background #000000
|
||||
statusline #ffffff
|
||||
# The text on an accent-coloured button is dark: light text on the accent was barely
|
||||
# legible.
|
||||
focused_workspace #de5200 #de5200 #000000
|
||||
active_workspace #de5200 #de5200 #000000
|
||||
inactive_workspace #000000 #000000 #ffffff
|
||||
urgent_workspace #2f343a #900000 #ffffff
|
||||
}
|
||||
}
|
||||
|
||||
bar {
|
||||
font pango:JetBrainsMono Nerd Font 11
|
||||
position top
|
||||
status_command i3status-rs ~/.config/i3status-rust/top-bar.toml
|
||||
workspace_buttons no
|
||||
tray_output primary
|
||||
colors {
|
||||
separator #ffffff
|
||||
background #000000
|
||||
statusline #ffffff
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
# Icons for the bars (module i3status-rust, novox/hq ADR 0208), from the JetBrains Mono Nerd Font.
|
||||
# Owned by the mesh: replaced at every push. Adopted unchanged from the workstations of 2026-10-04.
|
||||
# Material from NerdFont
|
||||
# https://www.nerdfonts.com/cheat-sheet
|
||||
backlight = [
|
||||
"\ue38d", # nf-weather-moon_new
|
||||
"\ue3d4", # nf-weather-moon_alt_waxing_gibbous_6
|
||||
"\ue3d3", # nf-weather-moon_alt_waxing_gibbous_5
|
||||
"\ue3d2", # nf-weather-moon_alt_waxing_gibbous_4
|
||||
"\ue3d1", # nf-weather-moon_alt_waxing_gibbous_3
|
||||
"\ue3d0", # nf-weather-moon_alt_waxing_gibbous_2
|
||||
"\ue3cf", # nf-weather-moon_alt_waxing_gibbous_1
|
||||
"\ue3ce", # nf-weather-moon_alt_first_quarter
|
||||
"\ue3cd", # nf-weather-moon_alt_waxing_crescent_6
|
||||
"\ue3cc", # nf-weather-moon_alt_waxing_crescent_5
|
||||
"\ue3cb", # nf-weather-moon_alt_waxing_crescent_4
|
||||
"\ue3ca", # nf-weather-moon_alt_waxing_crescent_3
|
||||
"\ue3c9", # nf-weather-moon_alt_waxing_crescent_2
|
||||
"\ue3c8", # nf-weather-moon_alt_waxing_crescent_1
|
||||
"\ue39b", # nf-weather-moon_full
|
||||
]
|
||||
bat_charging = "\U000f0084" # nf-md-battery_charging
|
||||
bat_not_available = "\U000f0091" # nf-md-battery_unknown
|
||||
bat = [
|
||||
"\U000f007a", # nf-md-battery_10
|
||||
"\U000f007b", # nf-md-battery_20
|
||||
"\U000f007c", # nf-md-battery_30
|
||||
"\U000f007d", # nf-md-battery_40
|
||||
"\U000f007e", # nf-md-battery_50
|
||||
"\U000f007f", # nf-md-battery_60
|
||||
"\U000f0080", # nf-md-battery_70
|
||||
"\U000f0081", # nf-md-battery_80
|
||||
"\U000f0082", # nf-md-battery_90
|
||||
"\U000f0079", # nf-md-battery
|
||||
]
|
||||
bell = "\U000f009c" # nf-md-bell_outline
|
||||
bell-slash = "\U000f009b" # nf-md-bell_off
|
||||
blackberry = "\uf307" # nf-md-blackberry
|
||||
bluetooth = "\U000f00af" # nf-md-bluetooth
|
||||
calendar = "\U000f00ed" # nf-md-calendar
|
||||
cogs = "\U000f0493" # nf-md-cog
|
||||
cpu = [
|
||||
"\U000F0F86", # nf-md-speedometer_slow
|
||||
"\U000F0F85", # nf-md-speedometer_medium
|
||||
"\U000F04C5", # nf-md-speedometer
|
||||
]
|
||||
cpu_boost_on = "\U000f0521" # nf-md-toggle_switch
|
||||
cpu_boost_off = "\U000f0a19" # nf-md-toggle_switch_off_outline
|
||||
disk_drive = "\U000f02ca" # nf-md-harddisk
|
||||
docker = "\uf308" # nf-linux-docker
|
||||
github = "\U000f02a4" # nf-md-github
|
||||
gpu = "\U000f0379" # nf-md-monitor
|
||||
headphones = "\U000f02cb" # nf-md-headphones
|
||||
joystick = "\U000f0297" # nf-md-gamepad_variant
|
||||
keyboard = "\U000f030c" # nf-md-keyboard
|
||||
mail = "\U000f01ee" # nf-md-email
|
||||
memory_mem = "\U000f035b" # nf-md-memory
|
||||
memory_swap = "\U000f02ca" # nf-md-harddisk
|
||||
mouse = "\U000f037d" # nf-md-mouse
|
||||
music = "\U000f075a" # nf-md-music
|
||||
music_next = "\U000f04ad" # nf-md-skip_next
|
||||
music_pause = "\U000f03e4" # nf-md-pause
|
||||
music_play = "\U000f040a" # nf-md-play
|
||||
music_prev = "\U000f04ae" # nf-md-skip_previous
|
||||
net_bridge = "\U000f04aa" # nf-md-sitemap
|
||||
net_down = "\U000f01da" # nf-md-download
|
||||
net_loopback = "\U000f006f" # nf-md-backup_restore
|
||||
net_modem = "\U000f03f2" # nf-md-phone
|
||||
net_cellular = [
|
||||
"\U000F08FD", # nf-md-network_strength_off_outline
|
||||
"\U000F08FE", # nf-md-network_strength_outline
|
||||
"\U000F08F4", # nf-md-network_strength_1
|
||||
"\U000F08F6", # nf-md-network_strength_2
|
||||
"\U000F08F8", # nf-md-network_strength_3
|
||||
"\U000F08FA", # nf-md-network_strength_4
|
||||
]
|
||||
net_up = "\U000f0552" # nf-md-upload
|
||||
net_vpn = "\U000f0582" # nf-md-vpn
|
||||
net_wired = "\U000f0200" # nf-md-ethernet
|
||||
net_wireless = [
|
||||
"\U000F092F", # nf-md-wifi_strength_outline
|
||||
"\U000F091F", # nf-md-wifi_strength_1
|
||||
"\U000F0922", # nf-md-wifi_strength_2
|
||||
"\U000F0925", # nf-md-wifi_strength_3
|
||||
"\U000F0928", # nf-md-wifi_strength_4
|
||||
]
|
||||
notification = "\U000f009c" # nf-md-bell_outline
|
||||
phone = "\U000f03f2" # nf-md-phone
|
||||
phone_disconnected = "\U000f0658" # nf-md-phone_minus
|
||||
ping = "\U000f051f" # nf-md-timer_sand
|
||||
pomodoro = "\ue001" # nf-pom-pomodoro_done
|
||||
pomodoro_break = "\U000f0176" # nf-md-coffee
|
||||
pomodoro_paused = "\U000f03e4" # nf-md-pause
|
||||
pomodoro_started = "\U000f040a" # nf-md-play
|
||||
pomodoro_stopped = "\U000f04db" # nf-md-stop
|
||||
resolution = "\U000f0293" # nf-md-fullscreen
|
||||
tasks = "\U000f05c7" # nf-md-playlist_check
|
||||
tea = "\U000f0d9e" # nf-md-tea
|
||||
thermometer = [
|
||||
"\U000f10c3", # nf-md-thermometer_low
|
||||
"\U000f050f", # nf-md-thermometer
|
||||
"\U000f10c2", # nf-md-thermometer_high
|
||||
]
|
||||
time = "\U000f0150" # nf-md-clock_outline
|
||||
toggle_off = "\U000f0a19" # nf-md-toggle_switch_off_outline
|
||||
toggle_on = "\U000f0521" # nf-md-toggle_switch
|
||||
unknown = "\U000f0186" # nf-md-comment_question_outline | TODO: Make default?
|
||||
update = "\U000f03d5" # nf-md-package_up
|
||||
uptime = "\U000f0153" # nf-md-clock_in
|
||||
volume_muted = "\U000f075f" # nf-md-volume_mute
|
||||
volume = [
|
||||
"\U000f057f", # nf-md-volume_low
|
||||
"\U000f0580", # nf-md-volume_medium
|
||||
"\U000f057e", # nf-md-volume_high
|
||||
]
|
||||
microphone_muted = "\U000f036d" # nf-md-microphone_off
|
||||
microphone = [
|
||||
"\U000f036e", # nf-md-microphone_outline
|
||||
"\U000f036c", # nf-md-microphone
|
||||
"\U000f036c", # nf-md-microphone
|
||||
]
|
||||
xrandr = "\U000f037a" # nf-md-monitor_multiple
|
||||
|
||||
# Weather icons (https://greshake.github.io/i3status-rust/i3status_rs/blocks/weather/index.html)
|
||||
weather_sun = "\ue30d" # nf-weather-day_sunny (when weather is reported as “Clear” during the day)
|
||||
weather_moon = "\ue32b" # nf-weather-night_clear (when weather is reported as “Clear” at night)
|
||||
weather_clouds = "\ue312" # nf-weather-cloudy (when weather is reported as “Clouds” during the day)
|
||||
weather_clouds_night = "\ue37e" # nf-weather-night_alt_cloudy (when weather is reported as “Clouds” at night)
|
||||
weather_fog = "\ue313" # nf-weather-fog (when weather is reported as “Fog” or “Mist” during the day)
|
||||
weather_fog_night = "\ue346" # nf-weather-night_fog (when weather is reported as “Fog” or “Mist” at night)
|
||||
weather_rain = "\ue318" # nf-weather-rain (when weather is reported as “Rain” or “Drizzle” during the day)
|
||||
weather_rain_night = "\ue325" # nf-weather-night_alt_rain (when weather is reported as “Rain” or “Drizzle” at night)
|
||||
weather_snow = "\ue31a" # nf-weather-snow (when weather is reported as “Snow”)
|
||||
weather_thunder = "\ue31d" # nf-weather-thunderstorm (when weather is reported as “Thunderstorm” during the day)
|
||||
weather_thunder_night = "\ue32a" # nf-weather-night_alt_thunderstorm (when weather is reported as “Thunderstorm” at night)
|
||||
@@ -0,0 +1,33 @@
|
||||
# The top bar (module i3status-rust, novox/hq ADR 0208): the focused window's title and the uptime.
|
||||
# Owned by the mesh: replaced at every push. Adopted unchanged from the workstations of 2026-10-04.
|
||||
icons_format = "{icon}"
|
||||
|
||||
[theme]
|
||||
theme = "plain"
|
||||
[theme.overrides]
|
||||
idle_bg = "#000000"
|
||||
idle_fg = "#f1f1f1"
|
||||
info_bg = "#000000"
|
||||
info_fg = "#f1f1f1"
|
||||
good_bg = "#000000"
|
||||
good_fg = "#859900"
|
||||
warning_bg = "#000000"
|
||||
warning_fg = "#de5200"
|
||||
critical_bg = "#000000"
|
||||
critical_fg = "#ff0000"
|
||||
separator = " | "
|
||||
separator_fg = "#ffffff"
|
||||
separator_bg = "#000000"
|
||||
|
||||
[icons]
|
||||
icons = "custom-icons"
|
||||
|
||||
[[block]]
|
||||
block = "focused_window"
|
||||
[block.format]
|
||||
full = " $title.str(max_w:50) |"
|
||||
short = " $title.str(max_w:100) |"
|
||||
|
||||
[[block]]
|
||||
block = "uptime"
|
||||
interval = 60
|
||||
Reference in New Issue
Block a user