spike: full-screen ratatui TUI for live TTY runs#33
Conversation
Add an alternate live renderer for interactive terminals: a yazi-style bordered layout with a scope header, a scrolling per-repo table (spinner while running, colored ✓/●/✗ by outcome), and a progress footer with a gauge, counts, and elapsed clock. The TUI owns the runner's RepoEvent channel and polls ~100ms for spinner animation, the elapsed clock, and key input (q/Ctrl-C to quit, j/k/arrows to scroll, g/G for top/follow). A guard plus a panic hook always restore raw mode and the alt-screen; a plain per-repo record is printed to scrollback on exit. It engages only when stdout is a TTY and not --dry-run or trace mode; piped, redirected, dry-run, and trace runs keep the existing line printers exactly. ExecutionContext gains command_label for the header.
Quitting the TUI (q / Ctrl-C / Esc) restored the terminal instantly but the process then blocked in thread::scope until every already-spawned git finished — up to the full run length on a slow fetch — and pending repos even kept spawning new gits. Add a CancelRegistry that tracks each running child's pid. On quit the TUI signals them SIGTERM (git's ssh/helper subprocesses cascade-exit when git closes their pipes), then a background thread escalates to SIGKILL after a 1.5s grace for anything that ignores it. Workers also check the cancel flag after acquiring a worker slot so no new git starts once a quit is in flight. Children are signalled by pid (not isolated into new process groups), so they stay in git-all's group and an external SIGHUP/SIGINT still reaches the whole tree — no orphans. Verified under tmux with a slow-git stand-in: quit went from blocking 30s+ (8 children left running) to exiting in ~0.07s with 0 children left. Normal completion and non-TTY output are unchanged. libc is a Unix-only dep; the signalling is a no-op elsewhere.
|
Follow-up: fixed the early-quit issue (commit d60384b). Quitting ( Now a Signalling is by pid rather than isolating children into new process groups, so they stay in git-all's group and an external SIGHUP/SIGINT still tears down the whole tree — no orphans. A/B under tmux with a slow-git stand-in (8 workers all in flight, then quit):
Normal completion and non-TTY output unchanged. 45 tests green (+2 for the registry); |
Two rough edges found driving real fetches at various terminal sizes:
1. Failed fetches were miscounted. A 404 ("remote: Repository not
found.") rendered yellow and counted as "changed" because the outcome
was inferred from the message string. Decide finished-row outcomes
from the command's exit status instead, so any failed command is an
error regardless of wording. The TUI returns its outcomes so the plain
scrollback summary counts match the live footer.
2. The footer counts collided with the key hint below ~90 cols
("○ 62 pe q quit..."). Move the "q quit · j/k scroll" hint onto the
gauge line so the counts line gets the full width and degrades by
dropping trailing counts cleanly instead of mid-word.
Verified at 80 cols fetching ~/src/oss: counts line shows all states
uncollided, and the nb-cli 404 now reads "0 changed · 1 error".
|
Follow-up from the real-world shakedown (driving 1. Error counts are now trustworthy. A real 404 ( 2. Footer no longer collides at narrow widths. Below ~90 cols the counts line truncated mid-word into the hint ( Verified end-to-end at 80 cols on a real Overall from the shakedown: renders cleanly from 200×60 down to 65×16, handles 104-repo real fetches with live spinners / auto-follow / scrollbar, dynamic resize reflows without corruption, clean restore + summary every time, zero orphaned processes. |
Branch 2 spike — full-screen ratatui TUI
An exploratory alternate renderer for interactive runs: when stdout is a real
TTY,
git-all status|fetch|pulltakes over the screen with a yazi-style,bordered, colorful live view instead of the sticky-footer line printer. This is
a spike to compare against the incremental sticky-footer approach — favoring a
polished, working demo over minimalism.
Built on
ratatui0.29 (its bundled crossterm 0.28; the crate's own crossterm0.29 is untouched — the TUI uses
ratatui::crossterminternally so no typescross the boundary).
Layout / widgets
Block+Paragraphscope banner(
git-all fetch · 40 repos · ~/work · 16 workers) with a right-aligned liveelapsed clock.
Table+TableState, one row per repo kept in stablealphabetical order and updated in place. Column 1 is a status glyph
(braille spinner while running,
✓/●/✗/○whenpending/done), colored by outcome: green = clean/no-new-commits, yellow =
changes present, red = error, dim = pending. A
Scrollbarappears when thelist overflows the viewport; the view auto-follows the active frontier so
progress stays on screen (press
j/k/arrows to scroll manually,Gtoresume following).
Gauge(done/total ratio + %) over a color-coded counts line(ok / changed / error / running / pending) and a key hint.
Behavior
stdout.is_tty() && !dry_run && !trace. Piped/redirectedoutput,
--dry-run, andGIT_ALL_TRACEall keep the existing lineprinters byte-for-byte (verified below) — scripts and pipes are unaffected.
RepoEventchannel (the TUI owns the receiver) andticks ~100 ms for spinner/clock animation and to poll keys. Handles resize
(redraw each frame reads the current size).
q/Esc/Ctrl-Cquit;j/k/arrows scroll;gtop;Gfollow.Dropguard for normal/early exit plus apanic hook that leaves the alt-screen and re-shows the cursor before the panic
prints. Auto-exits when every repo finishes.
summary is printed to normal scrollback so the run leaves a useful trail.
How it fits the runner
run_parallelstill spawns a thread per repo and emitsStarted/Completed.After spawning, it branches: TUI path calls
crate::tui::run(rx, ..); otherwisethe existing
PlainPrinter/TtyTablePrinterloop runs unchanged (including alltrace emission).
ExecutionContextgainscommand_labelfor the header. Newcode lives in a self-contained
src/tui.rs.Captured frames (tmux 120×40,
script/tty-lab)git-all statusmid-run — spinners, mixed outcomes, scrollbar, gauge:git-all fetch(local remotes, 16 workers) — all "no new commits":After exit the alt-screen closes and scrollback holds the record:
Verification
cargo build,cargo testgreen (43 lib tests incl. 5 new TUI tests viaratatui::TestBackend, + integration tests).cargo fmt/clippyclean fornew code (the only pre-existing
fmt/clippyfindings are in untouchedprinter.rs/repo.rs/ssh test onmain).script/tty-lab 12 --run status | cat→ plain lines, 0ANSI escape bytes.
GIT_ALL_TRACE=1in a TTYfalls back to the sticky-footer
SUMMARYprinter (no ratatui).statusandfetch; verified animaterunning→done, list scrolls past the viewport, and the terminal restores
cleanly (no garbage, cursor visible) on both auto-complete and
q.Trade-offs / rough edges
q/Ctrl-C), the terminal restores instantly but the processthen blocks in
thread::scopeuntil already-spawned git workers finish beforeprinting the summary — in-flight children aren't killed (out of scope for a
spike). With the fake-latency lab this is a ~1–3 s wait.
scrollbar overlays the table's right border by design.
record; for very large repo sets that's a lot of lines.
0.7.4(this branch is based onmain; not bumped).