ping-uin altosaxplayer
winget install --id=altosaxplayer.ping-uin -e Btop-style TUI for monitoring IPs and hostnames
winget install --id=altosaxplayer.ping-uin -e Btop-style TUI for monitoring IPs and hostnames
A btop-style terminal UI for monitoring hosts. Built for sysadmins who live in a terminal.
> This project is 100% AI-created — from the original PS/PowerShell ping loop > to the Rust + ratatui TUI, every refactor, feature, theme, and bug fix was > written by an AI coding assistant. Treat it accordingly: it works, but it > has not been through a human review process.
A pink little penguin face ((•O•)) watches over your network.
ping-uin is a lightweight TUI that pings (or TCP-checks) your devices on
per-host intervals and shows the results in a rolling, btop-style interface:
30s, 5m, or 2h per host, down to 5 secondsdb:5432) for connect checks, or any shell command (exit 0 = up)■ green blocks for up, red _ for down, newest on the leftFLAP, fresh transitions flash underlined, outages ticker ↓ 14mEnter) and down-first ordering/ search across names, aliases, and groups! = 1h) and upstream dependencies that silence downstream noisestill_down_5m / still_down_30m webhooks plus bell--once mode — one pass over all hosts as text or JSON, exit code doubles as the probe resultW serves the live table as a website (flat like the TUI, themed like it, updates live with no reloads), --serve runs it headless, --install-startup or R starts it on bootY pairs instances with join codes; adds, edits, and removals converge both waysv), session restore, first-run wizardbtop, dracula, nord, gruvbox-dark, ayu-light, archwave1.1.1.1 into Cloudflare, server3.internal into DB hostBuilt for sysadmins monitoring servers, routers, VPN endpoints, IoT devices, or anything else you'd rather not drop into a heavy dashboard for.
# Debian/Ubuntu (amd64) — signed apt repo
curl -fsSL https://altosaxplayer.github.io/ping-uin/apt/key.asc | sudo gpg --dearmor -o /usr/share/keyrings/ping-uin-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/ping-uin-archive-keyring.gpg] https://altosaxplayer.github.io/ping-uin/apt stable main" | sudo tee /etc/apt/sources.list.d/ping-uin.list
sudo apt update && sudo apt install ping-uin
# anywhere else: clone, build, run
cargo install --path . # builds the 'ping-uin' binary
cargo run --release # or just run it straight
Single .deb files are also attached to every
GitHub release for
dpkg -i. Note: ICMP checks shell out to the system ping binary — on
minimal distros install it first (sudo apt install iputils-ping).
A starter host set is built in on first launch, so it works even before you
add any config: Google DNS (8.8.8.8), Cloudflare (1.1.1.1), your local
gateway (192.168.1.1), and google.com. Starting truly empty instead?
A welcome popup offers CSV import, manual add, or the demo set — and the
app re-selects last session's host on startup.
| Key | Action |
|---|---|
↑ / ↓ | move selection |
Space / p | ping selected host now |
a | add a host (single form) |
e | edit the selected host — name/IP/interval/group/alias/port in one form |
d | delete the selected host (confirmation popup) |
h | per-host history (8h / 24h / 7d, Tab compares a second host) |
c | clear stats for selected host |
! | mute/unmute selected host for 1h (maintenance) |
v | compact table density (hides IP + Group columns) |
i | import from hosts.csv — merge in bulk, new rows added, existing rows updated |
E | export timestamped CSV plus an HTML status page |
W | serve read-only LAN web page on/off — http://:8080/, grouped + themed |
Y | device sync menu — join code, paired hostnames, join/last-sync times |
B | open the served page in the default browser (starts serving first if off) |
g | toggle grouped/flat view |
f | filter by group (Space = show all, Esc = cancel) |
s | view picker — off / down-first / up-first / name / group / down only (1-6 quick-pick, Space = show all; group sort hides the per-group up/down tallies) |
Esc | reset view — clear any sort/group filter and show all hosts |
Enter | collapse/expand the selected host's group |
/ | search names, aliases, groups (Enter keeps, Esc clears) |
? | full key-binding cheat sheet |
| mouse | click to select, wheel to scroll |
t | theme picker (live preview, persists) |
o | email (SMTP) alert settings — threshold, escalations, client-safe styling |
u | check for updates / install when available |
M | full menu (actionable) |
q / Ctrl+C | quit (cleanly!) |
> Intervals accept 30s, 5m, 2h, or bare minutes (2 = 2m), minimum 5s.
> Set a TCP port per host to check host:port connects instead of pinging.
> The bottom menu lives in its own bordered box with a fixed height — it
> never resizes or shifts the table. On narrow windows labels abbreviate,
> and anything left over collapses into a +N more [M] marker.
>
> History strips read newest-first: the most recent ping is always the
> leftmost block, and the strip grows left-to-right.
Press i and the app prompts for a CSV path (defaulting to the hosts.csv
in your config dir) and merges it — new rows added, existing rows updated:
name,interval,group,alias,port,warn_ms,check_cmd,depends_on
8.8.8.8,1m,public-dns,Google DNS,,,,
1.1.1.1,2m,public-dns,Cloudflare,,,,,
server3.internal,5m,router,Server 3,,,,
db.internal,30s,databases,Primary DB,5432,200,,
web.internal,30s,web,Web,,500,,db.internal
192.168.1.1,3m,router,,,,,
Intervals accept 30s/5m/2h (bare numbers mean minutes); a port
turns the row into a TCP connect check, warn_ms flags slow-but-up hosts
amber, check_cmd runs a shell command instead (exit 0 = up), and
depends_on names an upstream whose outage silences this host's alerts.
Shorter legacy files still import fine — missing columns stay unset.
New rows become new pings; existing rows get updated. Sync round-trips both
ways — hosts.csv is also rewritten on every in-TUI change, so you can always
edit it by hand and import.
Everything stays local by default. The app talks to the network for monitoring probes against your own targets, the release checker (GitHub API, every 15 min), and — only if you configure them — alert webhooks, SMTP email, the opt-in LAN page, and paired device sync. See PRIVACY.md for the full accounting of what is stored and what leaves the device.
On Linux: ~/.config/ping-uin/
On macOS: ~/Library/Application Support/ping-uin/
On Windows: %APPDATA%\ping-uin\ (usually C:\Users\\AppData\Roaming\ping-uin\)
Full details live in PRIVACY.md.
| File | Purpose |
|---|---|
ping-uin.json | full config (gitignored by default — your IPs are yours; an old ip-top.json is migrated automatically) |
hosts.csv | bulk-import/export mirror |
uptime-log.csv | rolling ping history (last ~10k events) |
To keep the data files next to the executable (e.g., on a USB drive or in a
self-contained folder), create an empty file named ping-uin.portable in the
same directory as ping-uin/ping-uin.exe:
# Linux/macOS
touch ping-uin.portable
# Windows (PowerShell)
New-Item -ItemType File -Name ping-uin.portable
On the next launch, ping-uin.json, hosts.csv, and uptime-log.csv will be
read from and written to that same directory instead of the system config path.
The app checks GitHub releases at startup and then every 15 minutes
(failed checks — e.g. launching before the VPN connects — retry every
minute until one succeeds), so the ↑ vX.Y.Z ready — u to update pill
appears in the top-right corner (plus a badge in the menu box) without
restarting. Press u any time to check manually — press u again
to install:
ping-uin.exe./Cellar/ping-uin/): runs brew upgrade ping-uin
(falls back to altosaxplayer/tap/ping-uin) in the background.winget upgrade altosaxplayer.ping-uin
in place — just quit and relaunch when it completes.~/.cargo/bin you own): same in-place
flow as portable. If the install dir isn't writable you'll get a clear
message instead of a late failure — update with
brew upgrade ping-uin / winget upgrade altosaxplayer.ping-uin /
cargo install --path ..Theme, group, sort/view, and collapsed-group prefs persist in ping-uin.json.
A corrupt config is backed up to ping-uin.json.corrupt instead of being
discarded.
ping-uin can shout when hosts change state. Webhook + bell live in
ping-uin.json (edit the file directly); email has a TUI form — press o:
{
"webhook_url": "https://hooks.slack.com/services/…",
"notify_bell": true,
"smtp": {
"enabled": true,
"host": "smtp.gmail.com",
"port": 587,
"username": "you@gmail.com",
"password": "app-password",
"from": "ping-uin@gmail.com",
"to": "ops@example.com, noc@example.com",
"use_tls": true,
"down_threshold": 3,
"escalations": false
}
}
webhook_url — POSTs {app, host, status, event, latency_ms, timestamp}
JSON on every up/down transition (Slack-compatible shape), plus
still_down_5m / still_down_30m escalation events for long outages.notify_bell — rings the terminal bell on down-transitions (and again
at the 30-minute escalation).smtp — sends one DOWN email after down_threshold consecutive failures
(default 3, 1–100, one mail per outage), plus a recovery UP email when
the host comes back. Outage-mail state is persisted in ping-uin.json, so
quitting and reopening never resends DOWN mail for an already-mailed
outage (and a still-owed recovery still goes out). Set "escalations": true to also mail the
still_down_5m / still_down_30m reminders. Port 465 uses
SMTPS, other ports use STARTTLS; set use_tls: false only for local
plaintext relays. to accepts a comma-separated list. Delivery is
fire-and-forget so a slow relay never stalls pinging.Emails use the same modern card layout as the web page: unmissable status
banner (● DOWN / ● UP), status pill, and detail card with host, target,
group, timestamp, latency, and streak. Because mail clients mangle dark
backgrounds, emails always render on a light, client-safe canvas — theme
status and accent colors are carried over but darkened until they pass
contrast checks, so white-on-dark theme fonts never arrive invisible.
Downstream hosts with depends_on pointing at a down upstream read DEP
and stay silent — fix the upstream, not the noise. ! mutes a host for an
hour of maintenance (countdown in the Latency column, excluded from tallies).
For cron, systemd timers, or quick checks without the TUI:
ping-uin --once # text table, exit 0 = all up, 2 = any down
ping-uin --once --format json # {"hosts": […], "down": 0} for scripts
Checks run in parallel (ICMP and TCP alike), touch no files, and the exit code doubles as the probe result.
Nothing is served unless you ask. Press W in the TUI to start serving
the live table as a website, press W again to stop it — or run it
headless (no terminal needed) with --serve. Press B to open the
page in your default browser (starts serving first when off). The M menu
always shows whether the page is currently being served. Your choice
persists: a shown page comes back on every launch with no flags or clicks.
ping-uin --serve --bind 0.0.0.0 --port 8080
# HTML: http://:8080/ (live updates, no reloads; plain refresh without JS)
# Health: http://:8080/health ("ok", for supervisors/monitors)
The TUI takes the same flags (ping-uin --port 8090) — its W page, B
browser shortcut, join codes, and sync listener all follow the configured
address, so a busy default port (or a second copy on the box) is escapable
without touching anything else. And if the chosen port is busy anyway, the
app takes the next free one automatically and tells you which (join codes
carry the actual port).
GET / serves HTML and nothing else; there is
no ping-now, mute, or config surface, so no auth is needed on a trusted
LAN. Unknown paths get 404, non-GET gets 405.0.0.0.0 (the default) to make it visible anywhere on the local
network; use --bind 127.0.0.1 for local-only. The startup banner and the
W popup print the real LAN IP + port (e.g. http://192.168.1.42:8080/),
so you know exactly what to type from other devices. Want port 80 instead
of :8080? Ports below 1024 need privilege once:
sudo setcap 'cap_net_bind_service=+ep' $(which ping-uin) (re-apply after
each update), then serve with --port 80. Installing start-on-boot with
--install-startup --port 80 attempts that grant for you, and if the
grant is ever missing at bind time (an update replaces the binary and
drops it), the TUI offers to re-grant it on the spot — the interface
suspends while sudo asks your password, then retries the bind.Flat chip (or ?view=flat) switches to one table with
every host its own row. ?group= filters to one group either way,
plus one-click group filter chips./?sort=status&order=desc accepts
name|status|group|latency|uptime|sla + asc|desc (status puts problems
first, like down-first view).W serves the exact live session; --serve runs its
own probing loop with the same intervals, logging, and webhooks.Keep two devices in two places on the same host list — no discovery, no accounts, no cloud. One side shows a join code, the other types it in:
# on device A (or press Y → g in its TUI)
ping-uin --sync-code --port 8080
# PUIN-192.168.1.42-8080-abcd-efgh-jklm
# on device B (or press Y → j in its TUI and paste it)
ping-uin --sync-join PUIN-192.168.1.42-8080-abcd-efgh-jklm --port 8080
--serve). Last-write-wins
per host; deletes travel as tombstones so they can't be resurrected by a
stale peer. Probing, history, and logs stay local — only the host list
converges.Y in the TUI: shows this device's hostname and
join code, plus every paired device with its hostname, join date/time, and
last sync time ([g] new code, [j] join, [1-9] forget a peer).--sync-peers lists pairs, --sync-forget
unpairs (hosts stay, pushes stop). CLI commands edit the config file, so
quit the TUI on that device first if it's running.--serve) and listening — the Y menu shows the listener
address, and a busy port is reported instead of failing silently. Same
Wi-Fi (no client isolation), allow ping-uin through the device firewall,
and make sure the IP in the join code is reachable (VPNs/Docker can put a
wrong one in — join errors say which of these it looks like). If the
code's IP is wrong, keep the code and append the right address when
joining: Y → j, paste PUIN-…-…, add @ 192.168.1.42, Enter
(CLI: --sync-join "CODE @ 192.168.1.42" or --sync-join CODE 192.168.1.42).
Even without that: if the code's address is unreachable, joining
automatically scans your subnet for the peer, across the code's port and
the whole fallback window above it (so a peer that moved ports after its
code was generated is still found) — a stale IP usually just works
(wrong tokens and outdated peers are never scanned).Nothing is installed automatically. Only --install-startup (or R in the
M menu) creates a boot entry, and --uninstall-startup (or R again)
removes it. The menu always shows whether it is on. To start the status
page each time the machine restarts:
ping-uin --install-startup --bind 0.0.0.0 --port 8080
ping-uin --startup-status # show whether it is installed
ping-uin --uninstall-startup # remove it again
~/Library/LaunchAgents/com.ping-uin.plist
(starts at login, kept alive, logs to ~/Library/Logs/ping-uin.log).systemctl --user enable --now ping-uin),
falling back to XDG autostart (~/.config/autostart/ping-uin.desktop)
where systemd isn't available. Reinstalling applies new flags (the service
is restarted, not left on the old config). Headless servers: without
lingering the service stops at logout — install enables it when possible,
otherwise run once as admin: sudo loginctl enable-linger $USER.
Installing with a privileged port (--port 80) automatically tries to
grant the binary cap_net_bind_service (works as root or with
passwordless sudo) so the service can bind it; without either, the
install message prints the one-time manual command.ping-uin (on logon).Short for ping + pnpm what? honestly just because it's a fun word and the ((•O•)) face looked like a tiny penguin. Better suggestions welcome.
Fully AI-created, evolving in chat-driven sessions:
rich live tableRust + ratatui with real-time updatesNo human review was involved. If anything weird happens, open an issue and the next AI watch-commander will fix it. Probably.
MIT. Permissive as it gets — fork it, remix it, ship it in your company tooling. Just keep the copyright header.
> Pro tip for sysadmins: point the CSV at your asset inventory export
> every morning and keep ping-uin running in a tmux pane. Ships as a
> single binary — no metrics endpoint, no mutable web UI. Just pinging,
> plus an optional read-only status page (W / --serve) for the LAN.