Fit the table in 80 columns, badge errors, coarsen the countdown - #40
Merged
Conversation
Three pieces of design feedback from issue #33, all about reading the table rather than what it says. Width: the columns are now sized in `app.rs` (`TableLayout`) rather than left to ratatui's constraint solver, because which column gives way first is a judgement call worth testing. At 80 columns everything the reporter asked for survives — resolver name, Loc, a full IPv4 address, Ping, TTL, Exp and the start of the answer — by shedding the spelled-out status first: the verdict moves to a glyph in the left margin, where a scan down the edge finds the failures. Numeric columns are right-aligned, and the units the header already implies ("ms", the "s" on a raw TTL) are gone. An IPv6 resolver's address is shown whole when there's room and clipped when there isn't; the alternative is cropping the columns the issue asked us to fit. Wide terminals are unchanged: the same status word, the same answer width, the same map/globe thresholds. Errors: `theme.error` is now a `Paint` — a foreground with an optional background, written "<fg> on <bg>" — defaulting to white on bright red, the reporter's `\033[101m`. Only the marker is filled: the row's glyph and the status word. Error messages, map dots, the propagation gauge and slow ping times take the hue alone, since a background behind a sentence turns the row into a red bar and behind a map dot paints a block over the coastline. Countdown: the Exp column uses `fmt_countdown` — two digits and a unit, 59s → 1m → 59m → 1h → 23h → 1d → 99d, truncating so the reading stays a lower bound. A whole column of seconds ticking out of unison was a distraction with no payoff. The advisory notes ("TTL ≈ 2h23m", "old answers expire in ≤ …") keep `fmt_secs`: those are single figures where the precision is the point, not a column that shimmers. Verified with `--once` and through a PTY at 80x24 and 160x24, plus a config of TEST-NET resolvers to draw real ERR rows (white-on-red badge confirmed in the rendered cell attributes). Demo GIF re-recorded. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
callicles
added a commit
that referenced
this pull request
Aug 7, 2026
A docs audit and QA pass over main (0.4.0 plus #37-#40) turned up a handful of places where the docs and the UI had drifted apart: - CHANGELOG: link issue #35 alongside PR #38 on the TTL-outlier entry, to match how the sibling entries cite their issue. Amend the 80-column entry to cover the Time -> Ping header rename and the matching `--once` column widths and right-aligned TTL, and the error-badge entry to mention that the badge also covers the "not a domain name" label. Drop a stray blank line between two bullets in the released 0.4.0 section. - README: bump the Nix pin example to v0.4.0, the newest tag that exists. - app.rs: `SortMode::label` still said "time" for the latency sort, so the footer disagreed with the column #40 renamed to Ping. Say "ping", and update the README key-binding row to match. No changelog entry of its own: the label ships unreleased, covered by the amended #40 entry. - main.rs: document the `"<fg> on <bg>"` theme form in the long --help, which #40 added but never wrote down. - AGENTS.md: the module map was missing theme.rs, globe.rs and world_data.rs, still described sites.rs as id.server-only (NSID leads since #39) and config.rs as resolvers-only (it owns theme/ecs/view too), and app.rs now owns TableLayout. Verified: cargo fmt --check, clippy -D warnings and 120 tests pass; `--once` prints its table; --help shows the new theme line; and driving the real binary through a PTY shows the footer read "sort: ping" after cycling Ctrl+S twice to the latency mode. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #33.
Three TUI design improvements from @forthrin's feedback (continuing the #25 thread), plus a re-recorded demo GIF.
1. Errors as a white-on-red badge
theme.errorgrew from a singleColorto atheme::Paint { fg, bg: Option }, written"<fg> on <bg>"in the config (spaced separator, so ratatui's two-word aliases likebright redstill parse). The default iswhite on lightred— the reporter's\033[101m.Only the failure markers take the background: the row's verdict glyph, the status word, and the "not a domain name" label. Everything else that uses the error role — map dots, gauge fill, legend swatch, footer counters, slow-ping times, and all error prose — renders the role's hue as a plain foreground. A background behind a 42-character error sentence turns the row into a red bar, and the error role doubles as the slow-latency color, so badging it everywhere would paint the Ping column red. A plain color in the config (
error = "lightred") still works and simply drops the badge.2. The table fits 80 columns
Column widths now come from
app::TableLayout::fit(width, resolvers)— computed inapp.rswhere it's testable, not left to ratatui's constraint solver, because which column gives way first is a judgement call worth pinning down. Sheds cheapest-first: spelled-out status word → an IPv6 resolver's full address → the resolver name. Never shed: the numeric columns, the 15 columns a full IPv4 needs, and a 16-column answer.At 80 columns this lands exactly (mark 1 + name 17 + loc 8 + ip 15 + ping 5 + ttl 6 + exp 3 + answer 16 + 7 gaps + 2 borders = 80). The verdict glyph (
✓ ≠ ! ↻ ∅ ✗) moves to the left margin so failures scan down one edge; when the status word is shed the glyph still carries the verdict. Ping/TTL/Exp are right-aligned (headers too) and the per-row units are dropped — the header names them.IPv6 resolver addresses: the IP column sizes to the widest address in the list, clamped to [15, 39] — shown whole on a wide terminal, clipped to IPv4 width at 80 columns (cropping one rare address beats cropping the columns the issue asked to fit).
No wide regression:
TableLayout::reserved_width()reproduces the old 103-column reservation exactly, so the globe still appears at ≥131 and the flat map at ≥157, and the answer column is unchanged next to a map panel.--onceoutput matches the TUI's name/loc/ip widths and right-aligns the TTL; it keeps status words and units since it's a headerless, grep-able plain format.3. Coarse countdown
app::fmt_countdown: at most two digits and a unit —59s→1m→59m→1h→23h→1d, saturating at99d. Truncating, not rounding, so1mmeans at least a minute remains. Applied only to the per-row Exp column (the one that ticked out of unison); the TTL advisory notes, the outlier attribution, and the gauge's "old answers expire in ≤" keep the precisefmt_secsformat — those are single figures where precision is the point. Expired rows show0s(fits the 3-cell column; "expired" didn't).Verification
Quality gates (what CI runs):
New tests: the full countdown ladder from the issue plus boundaries (59s→1m, 59m→1h, 23h→1d, >99d,
u64::MAX, width ≤3 always); the 80-column layout summing to exactly 80 with untouched numerics; wide layouts spending extra width only on the answer; IPv6 wide-vs-narrow behavior;Paintparsing with/without background and badge-vs-tint semantics; configtheme.error = "black on yellow"round-trip and error-key attribution.PTY verification (expect + pyte, per the AGENTS.md gotchas):
✓ Google Public DNS →YUL 8.8.8.8 29 246 3m 104.20.23.154, 1…— full IPs (208.67.222.222,149.112.121.10intact), right-aligned numerics, coarse Exp (39s,1m,4m), status word shed, answer visible.192.0.2.1viaDNSGLOBE_CONFIG): the✗cell reads backfg=ffffff bg=ff0000 boldfrom the terminal emulator, while thetimeoutmessage carries no background — the badge stays on the marker only. Same verified for∅NONE rows and the "not a domain name" gauge badge.cargo run -- example.com --onceand514.ax --once TXTconfirm the plain-text output.Demo GIF re-recorded with vhs from
demo/demo.tape(2.4 MB, was 2.6 MB).Known pre-existing limitation (out of scope): the gauge and footer hint lines still run past 80 columns and clip — they're un-wrapped
Paragraphs and the key-hint line can't fit 80 regardless; worth its own follow-up if it matters.🤖 Generated with Claude Code