A login banner that draws a different mark every time — and is still, recognizably, your machine's mark.
╭─ system health solteris-tower · Fri 19:01 ██▒▒· ·▒▒██
│ ▓░█▒· ·▒█░▓
│ uptime 8 hours, 42 minutes
│ load 2.28 2.14 2.16 (24 cores)
│ pressure cpu 0.00% mem 0.00% io 50.80%
│ cpu 57°C powersave
│ memory 25% 16.0 / 62.4 GiB
│ gpu 44°C 13% util 1.1 / 31.9 GiB 31W 894rpm Radeon AI PRO R9700
│ vm ● win2k22 running 6 vcpu · 7.8 GiB
│ passthrough: GeForce GTX 1060 6GB · looking-glass 64 MiB
│ disk / 73% · /home 90% · ws-hdd1 69%
│ ssd ● 5 ok worst life 98% (0m ago)
│ services 0 failed
│ updates 53 pending
│ vpn Disconnected
╰─
Runs unprivileged, finishes in ~0.2s, and never blocks a shell from appearing — including the rows backed by the network.
The two-line block in the top-right regenerates on every render. One shot, pure bash arithmetic, no subprocess. Lineage, honestly stolen from four traditions:
- OpenSSH randomart — the mechanism. A drunken-bishop walk staggers across a grid; visit counts render as a density ramp. Randomart's purpose was "recognize this machine at a glance," which is a login banner's job description.
- GitHub identicons — mirror symmetry, the one trick that makes random data look designed rather than like line noise.
- Urbit sigils — the seed
philosophy: pure randomness is undifferentiated mush, so identity has to
anchor it. The stable half of the seed hashes
/etc/machine-id; the volatile half mixes uptime, loadavg and$RANDOM. Every render differs, every render is still this box. Another machine yields a visibly different family of crests. - Roguelike worldgen — the
·░▒▓█shade ramp.
The crest draws from its own violet palette lane. Green/yellow/red are reserved for data — decoration never wears the colors that mean something, and nothing decorative is shaped like a chart.
Local — no network, no privileges:
| Row | Source |
|---|---|
uptime load memory |
/proc |
pressure |
/proc/pressure/* — PSI some avg10 |
cpu |
sensors; AMD Tctl/Tdie and Intel Package id 0 |
gpu |
amdgpu sysfs: temp, util, VRAM, watts, fan. One row per card; cards bound to vfio-pci are skipped — they belong to a guest |
vm |
libvirt over the read-only socket, plus passthrough GPUs and the looking-glass buffer |
disk |
df over SH_DISKS |
ssd |
SMART from a root-written cache (see smart/) |
services |
systemctl --failed |
updates |
checkupdates, cached |
reboot |
shown only when needed |
vpn |
mullvad status |
Two notes on why those last few are shaped as they are.
pressure exists because load average lies. On a 24-core box a load of 2
looks idle while I/O is stalled half the time — PSI measures the share of
time work spent waiting on a resource, which is the number you actually
want when the machine feels bad but load looks fine.
reboot renders only when a reboot is genuinely required, because a row
that says "no" every day trains you to stop reading it. The load-bearing
check is whether /usr/lib/modules/$(uname -r) still exists: if the running
kernel's modules are gone you can no longer modprobe anything, which on a
vfio/amdgpu box you will discover at the worst possible moment. A pacman
version comparison backs it up.
The wire — network-backed, all off by default:
| Row | Source | Default TTL |
|---|---|---|
sites |
any URL; status code + latency | 300s |
weather |
open-meteo — no API key, no account | 1800s |
live |
Twitch via decapi, YouTube via channel page | 120s / 1800s |
Three rules, none of them optional.
Rendering never touches the network. A wire row prints whatever is in its
cache file and, if that file is stale, forks a bounded refresh whose result
lands on the next login. The fetch cost is never paid at the prompt. This
is the same pattern the updates row has always used.
A failed fetch must not destroy a good cache. The refresh captures output
first and only replaces the cache if the fetch produced something. Piping
straight into the sanitizer would hand the pipeline the sanitizer's exit
status, so a failed fetch would cheerfully truncate a perfectly good value.
On failure the previous reading stays: a reading 40 minutes old beats no
reading, and a network error must never appear where data belongs. Because
the cache's mtime only advances on success, the age the volatile rows print
is honest — live shows (3m ago), since "live" checked 25 minutes ago is
a materially different claim from "live" checked a minute ago.
Sanitize remote bytes before they reach a terminal. Everything fetched is
stripped of control characters on write, at the single point where
untrusted input enters. Terminals execute escape sequences: piping a
remote server's raw bytes into every login shell hands that server your
cursor, your title bar, and on some emulators considerably more. This is a
real and well-documented class of bug, not a hypothetical. If you add a wire
row, route it through wire_sane — and if you fetch something that isn't
plain text, sanitize the extracted field, not the envelope.
Two honest caveats about the live row:
- The YouTube check is a scrape, keyed on a marker ~700 KB into the
channel page, so a partial fetch can't shortcut it (~150–190 KB
compressed per channel per refresh — mind
SH_TTL_YOUTUBEbefore adding many channels). If the page still parses as a channel page but the live marker is absent, that's a real "offline"; if the page doesn't parse at all, the row reports unknown rather than confidently claiming offline. A scrape that silently lies is worse than one that admits confusion. - decapi is a third party. It never learns who you are, but it does learn which channels you watch. Swap in the official Helix API if that matters to you.
install -m 755 sys-health ~/.local/bin/Call it from ~/.bashrc for top-level interactive shells only:
if [ -z "$TMUX" ] && [ "${SHLVL:-1}" -eq 1 ] && command -v sys-health >/dev/null 2>&1; then
sys-health
fiFor the SMART row (optional, needs root once via a timer):
sudo install -m 755 smart/sys-health-smart-collect /usr/local/bin/
sudo install -m 644 smart/sys-health-smart.{service,timer} /etc/systemd/system/
sudo install -d /var/lib/sys-health
sudo systemctl enable --now sys-health-smart.timerDisks are autodetected (ATA and NVMe both understood); override with
SMART_DEVS="sda nvme0n1" in /etc/sys-health/smart.conf.
Everything lives in ~/.config/sys-health/config, sourced as shell — so the
script itself never needs editing and stays a git pull from upstream.
SH_DISKS=(/ /home /mnt/bulk)
SH_WIRE=1
SH_SITES=("https://example.com")
SH_WEATHER="47.61,-122.33" # lat,lon
SH_TWITCH=("channelname")
SH_YOUTUBE=("UCxxxxxxxxxxxxxxxxxxxxxx=LofiGirl") # channel ID, not @handleLive entries accept name=Label; the label is what renders. Worth using for
YouTube, whose channel IDs are 24 unreadable characters. The live row wraps
onto continuation rows rather than overrunning the pane, so following several
channels stays readable at any width.
| Variable | Default | Effect |
|---|---|---|
SH_CREST |
1 |
generative header mark |
SH_VM |
1 |
libvirt guests + passthrough |
SH_SMART |
1 |
SMART row |
SH_DISKS |
(/ /home) |
df targets |
SH_WIRE |
0 |
master switch for all network rows |
SH_WIRE_TIMEOUT |
8 |
hard ceiling on any one request |
SH_TTL_SITES |
300 |
per-row refresh intervals — these endpoints |
SH_TTL_WEATHER |
1800 |
differ by ~1000× in weight, so one global |
SH_TTL_TWITCH |
120 |
TTL would be wrong in both directions at once |
SH_TTL_YOUTUBE |
1800 |
SYS_HEALTH_CREST / SYS_HEALTH_VM still work as env overrides.
The layout measures the pane and sheds optional segments narrowest-first rather than wrapping and shredding the gutter:
| Width | Behavior |
|---|---|
| < 92 | per-drive SMART collapses to ● 5 ok · worst life 98% |
| < 88 | GPU card name drops |
| < 78 | site latency drops |
| < 74 | looking-glass segment drops |
| < 66 | fan rpm drops; crest sits out — a wrapped crest is worse than none |
No line ever exceeds the terminal width.
This is my banner, built for my machine. A fork will want to look at the
vpn row (expects mullvad) and the news row (expects
informant) — both
self-skip when the command is absent. Everything else is config.
See ROADMAP.md for what's next.