Skip to content

Fit the table in 80 columns, badge errors, coarsen the countdown - #40

Merged
callicles merged 2 commits into
mainfrom
feat/tui-design-improvements
Aug 7, 2026
Merged

Fit the table in 80 columns, badge errors, coarsen the countdown#40
callicles merged 2 commits into
mainfrom
feat/tui-design-improvements

Conversation

@callicles

Copy link
Copy Markdown
Contributor

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.error grew from a single Color to a theme::Paint { fg, bg: Option }, written "<fg> on <bg>" in the config (spaced separator, so ratatui's two-word aliases like bright red still parse). The default is white 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 in app.rs where 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.

--once output 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 — 59s1m59m1h23h1d, saturating at 99d. Truncating, not rounding, so 1m means 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 precise fmt_secs format — those are single figures where precision is the point. Expired rows show 0s (fits the 3-cell column; "expired" didn't).

Verification

Quality gates (what CI runs):

cargo fmt --check                         → clean
cargo clippy --all-targets -- -D warnings → clean
cargo test                                → 120 passed; 0 failed

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; Paint parsing with/without background and badge-vs-tint semantics; config theme.error = "black on yellow" round-trip and error-key attribution.

PTY verification (expect + pyte, per the AGENTS.md gotchas):

  • 80×24: ✓ 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.10 intact), right-aligned numerics, coarse Exp (39s, 1m, 4m), status word shed, answer visible.
  • 160×24: status word back, full answers, globe panel present — wide layout unchanged.
  • ERR rows (TEST-NET resolver 192.0.2.1 via DNSGLOBE_CONFIG): the cell reads back fg=ffffff bg=ff0000 bold from the terminal emulator, while the timeout message 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 --once and 514.ax --once TXT confirm 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

callicles and others added 2 commits August 7, 2026 11:43
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
callicles merged commit ec8dd01 into main Aug 7, 2026
13 checks passed
@callicles
callicles deleted the feat/tui-design-improvements branch August 7, 2026 15:55
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>
@callicles callicles mentioned this pull request Aug 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

TUI design improvements

1 participant