Skip to content

spike: full-screen ratatui TUI for live TTY runs#33

Draft
rsanheim wants to merge 3 commits into
mainfrom
rjs/git-all-tty-ratatui
Draft

spike: full-screen ratatui TUI for live TTY runs#33
rsanheim wants to merge 3 commits into
mainfrom
rjs/git-all-tty-ratatui

Conversation

@rsanheim

Copy link
Copy Markdown
Owner

Branch 2 spike — full-screen ratatui TUI

An exploratory alternate renderer for interactive runs: when stdout is a real
TTY, git-all status|fetch|pull takes 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 ratatui 0.29 (its bundled crossterm 0.28; the crate's own crossterm
0.29 is untouched — the TUI uses ratatui::crossterm internally so no types
cross the boundary).

Layout / widgets

  • Header — a rounded Block + Paragraph scope banner
    (git-all fetch · 40 repos · ~/work · 16 workers) with a right-aligned live
    elapsed clock.
  • Repo table — a Table + TableState, one row per repo kept in stable
    alphabetical order and updated in place. Column 1 is a status glyph
    (braille spinner while running, / / / when
    pending/done), colored by outcome: green = clean/no-new-commits, yellow =
    changes present, red = error, dim = pending. A Scrollbar appears when the
    list overflows the viewport; the view auto-follows the active frontier so
    progress stays on screen (press j/k/arrows to scroll manually, G to
    resume following).
  • Footer — a Gauge (done/total ratio + %) over a color-coded counts line
    (ok / changed / error / running / pending) and a key hint.

Behavior

  • Engages only when stdout.is_tty() && !dry_run && !trace. Piped/redirected
    output, --dry-run, and GIT_ALL_TRACE all keep the existing line
    printers byte-for-byte (verified below) — scripts and pipes are unaffected.
  • Drives off the runner's RepoEvent channel (the TUI owns the receiver) and
    ticks ~100 ms for spinner/clock animation and to poll keys. Handles resize
    (redraw each frame reads the current size).
  • Keys: q / Esc / Ctrl-C quit; j/k/arrows scroll; g top; G follow.
  • Terminal is always restored — a Drop guard for normal/early exit plus a
    panic hook that leaves the alt-screen and re-shows the cursor before the panic
    prints. Auto-exits when every repo finishes.
  • Since the alt-screen vanishes on exit, a plain per-repo record + a one-line
    summary is printed to normal scrollback so the run leaves a useful trail.

How it fits the runner

run_parallel still spawns a thread per repo and emits Started/Completed.
After spawning, it branches: TUI path calls crate::tui::run(rx, ..); otherwise
the existing PlainPrinter/TtyTablePrinter loop runs unchanged (including all
trace emission). ExecutionContext gains command_label for the header. New
code lives in a self-contained src/tui.rs.

Captured frames (tmux 120×40, script/tty-lab)

git-all status mid-run — spinners, mixed outcomes, scrollbar, gauge:

╭ git-all ─────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│git-all status · 40 repos · /private/var/folders/jb/rtxjz4cd0kg651dc89hcxc6m0000gn/T/git-all-tty-lab.zU6vP3/      1.6s│
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
╭ repositories · 13/40 done ───────────────────────────────────────────────────────────────────────────────────────────╮
│  repository    result                                                                                                │
│● activities    1 untracked                                                                                           │
│● agentic-dev   1 untracked                                                                                           │
│● amion-api     1 untracked                                                                                           │
│● backend-core  1 untracked                                                                                           │
│✓ billing-svc   clean                                                                                                 │
│✓ cache-layer   clean                                                                                                 │
│✓ dashboard     clean                                                                                                 │
│✓ data-pipeline clean                                                                                                 │
│✓ edge-proxy    clean                                                                                                 █
│✓ feature-flags clean                                                                                                 █
│✓ gateway       clean                                                                                                 █
│⠇ growth-tools  running…                                                                                              █
│⠇ identity      running…                                                                                              █
│✓ infra-tools   clean                                                                                                 █
│⠇ kafka-bridge  running…                                                                                              █
│⠇ ledger        running…                                                                                              █
│⠇ metrics-agent running…                                                                                              █
│✓ notifications clean                                                                                                 █
│⠇ orders-api    running…                                                                                              █
│○ payments      pending                                                                                               █
│⠇ quota-svc     running…                                                                                              █
│⠇ repo-027      running…                                                                                              █
│○ repo-028      pending                                                                                               │
│○ repo-029      pending                                                                                               │
│○ repo-030      pending                                                                                               │
│○ repo-031      pending                                                                                               │
│○ repo-032      pending                                                                                               │
│○ repo-033      pending                                                                                               │
│○ repo-034      pending                                                                                               │
│○ repo-035      pending                                                                                               │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
╭ progress ────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│██████████████████████████████████████                13/40  32%                                                      │
│✓ 9 ok   ● 4 changed   ✗ 0 error   ◍ 8 running   ○ 19 pending                                      q quit · j/k scroll│
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯

git-all fetch (local remotes, 16 workers) — all "no new commits":

╭ git-all ─────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│git-all fetch · 40 repos · /private/var/folders/jb/rtxjz4cd0kg651dc89hcxc6m0000gn/T/git-all-tty-lab.345AeW/w      0.6s│
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
╭ repositories · 6/40 done ────────────────────────────────────────────────────────────────────────────────────────────╮
│  repository    result                                                                                                │
│⠦ activities    running…                                                                                              │
│✓ agentic-dev   no new commits                                                                                        │
│⠦ amion-api     running…                                                                                              │
│⠦ backend-core  running…                                                                                              │
│✓ billing-svc   no new commits                                                                                        │
│⠦ cache-layer   running…                                                                                              │
│⠦ dashboard     running…                                                                                              │
│⠦ data-pipeline running…                                                                                              │
│✓ edge-proxy    no new commits                                                                                        █
│⠦ feature-flags running…                                                                                              █
│✓ gateway       no new commits                                                                                        █
│⠦ growth-tools  running…                                                                                              █
│✓ identity      no new commits                                                                                        █
│✓ infra-tools   no new commits                                                                                        █
│⠦ kafka-bridge  running…                                                                                              █
│⠦ ledger        running…                                                                                              █
│⠦ metrics-agent running…                                                                                              █
│⠦ notifications running…                                                                                              █
│⠦ orders-api    running…                                                                                              █
│⠦ payments      running…                                                                                              █
│⠦ quota-svc     running…                                                                                              █
│⠦ repo-027      running…                                                                                              █
│○ repo-028      pending                                                                                               │
│○ repo-029      pending                                                                                               │
│○ repo-030      pending                                                                                               │
│○ repo-031      pending                                                                                               │
│○ repo-032      pending                                                                                               │
│○ repo-033      pending                                                                                               │
│○ repo-034      pending                                                                                               │
│○ repo-035      pending                                                                                               │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
╭ progress ────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│██████████████████                                    6/40  15%                                                       │
│✓ 6 ok   ● 0 changed   ✗ 0 error   ◍ 16 running   ○ 18 pending                                     q quit · j/k scroll│
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯

After exit the alt-screen closes and scrollback holds the record:

[activities   ] 1 untracked
...
[zebra-cache  ] clean
40 of 40 done · 36 ok · 4 changed · 0 error · 4.3s

Verification

  • cargo build, cargo test green (43 lib tests incl. 5 new TUI tests via
    ratatui::TestBackend, + integration tests). cargo fmt/clippy clean for
    new code (the only pre-existing fmt/clippy findings are in untouched
    printer.rs/repo.rs/ssh test on main).
  • Non-TTY unchanged: script/tty-lab 12 --run status | cat → plain lines, 0
    ANSI escape bytes.
  • Dry-run in a TTY prints planned commands, no TUI. GIT_ALL_TRACE=1 in a TTY
    falls back to the sticky-footer SUMMARY printer (no ratatui).
  • Driven live under tmux for status and fetch; verified animate
    running→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

  • On early quit (q/Ctrl-C), the terminal restores instantly but the process
    then blocks in thread::scope until already-spawned git workers finish before
    printing 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.
  • Truecolor is avoided in favor of named/indexed colors for portability; the
    scrollbar overlays the table's right border by design.
  • Scrollback dumps every repo row (like the plain printer) for a greppable
    record; for very large repo sets that's a lot of lines.
  • Version left at 0.7.4 (this branch is based on main; not bumped).

rsanheim added 2 commits July 9, 2026 20:44
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.
@rsanheim

Copy link
Copy Markdown
Owner Author

Follow-up: fixed the early-quit issue (commit d60384b).

Quitting (q/Esc/Ctrl-C) used to restore the terminal instantly but then block in thread::scope until every in-flight git finished — up to the full run length on a slow fetch — and pending repos kept spawning new gits.

Now a CancelRegistry tracks each running child's pid; on quit the TUI sends 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. Workers also short-circuit after acquiring a slot if a quit is in flight, so no new git starts.

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):

quit → exit children left
before did not exit within 30s (blocked) 8
after 0.07s 0

Normal completion and non-TTY output unchanged. 45 tests green (+2 for the registry); libc added as a Unix-only dep (was already transitive).

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".
@rsanheim

Copy link
Copy Markdown
Owner Author

Follow-up from the real-world shakedown (driving fetch on ~/work / ~/src/oss at 65–200 cols + a live-resize run): fixed the two rough edges that surfaced. Commit ad03ed0.

1. Error counts are now trustworthy. A real 404 (nb-cli → "remote: Repository not found.") was rendering yellow and counted as changed, because the outcome was inferred from the message text. Outcomes are now decided from the command's exit status, so any failed command is a red error regardless of wording. tui::run returns its outcomes so the plain scrollback summary matches the live footer.

before:  104 of 104 done · 103 ok · 1 changed · 0 error
after:   104 of 104 done · 103 ok · 0 changed · 1 error

2. Footer no longer collides at narrow widths. Below ~90 cols the counts line truncated mid-word into the hint (○ 62 pe q quit…). The q quit · j/k scroll hint moved onto the gauge line, so the counts get the full width:

80 cols:
│███████████████████    34/104  33%                         q quit · j/k scroll│
│✓ 34 ok   ● 0 changed   ✗ 0 error   ◍ 16 running   ○ 54 pending               │

Verified end-to-end at 80 cols on a real ~/src/oss fetch. Resize reflow, alt-screen restore, non-TTY fallback, and the quit-teardown all still hold. 46 tests green (+1 pinning exit-status classification).

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.

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.

1 participant