Skip to content

Add simple text pager for interactive list commands (alternative to #4729)#5015

Open
simonfaltum wants to merge 3 commits intosimonfaltum/list-json-pagerfrom
simonfaltum/list-simple-paginated
Open

Add simple text pager for interactive list commands (alternative to #4729)#5015
simonfaltum wants to merge 3 commits intosimonfaltum/list-json-pagerfrom
simonfaltum/list-simple-paginated

Conversation

@simonfaltum
Copy link
Copy Markdown
Member

@simonfaltum simonfaltum commented Apr 17, 2026

Why

Follow-up to #5016 (JSON pagination). Extends the same interactive pager to commands that register a row template — jobs list, clusters list, apps list, pipelines list, workspace list, etc. — so the first page renders immediately, then you control when to fetch more.

Depends on #5016. Base branch is simonfaltum/list-json-pager. The shared plumbing (raw-mode key reader, crlfWriter, SupportsPager, prompt helpers) lives in that PR; this one only adds the template-specific rendering on top.

This PR is an alternative to #4729 (the Bubble Tea TUI):

This PR (+ #5016) #4729 (TUI)
Approach Reuse existing template + tabwriter, add a pager on top New Bubble Tea scrollable viewport with cursor
Controls space page, enter drain, q/esc/Ctrl+C quit Arrow keys, / search, n/N match nav, q quit
Server-side search No Yes
This PR's diff ~500 lines in libs/cmdio/ only ~2,900 lines across libs/tableview/ and 15+ override files
New public API None TableConfig, ColumnDef, Col[T], Optional, etc.
Override changes 0 18 files
External deps None beyond #5016 charmbracelet/bubbles, bubbletea, lipgloss

Changes

Before: databricks <resource> list with a row template drained the full iterator and rendered every row up front through the existing renderUsingTemplate pipeline.

Now: when stdin, stdout, and stderr are all TTYs, the CLI streams the first 50 rows and prompts on stderr:

[space] more  [enter] all  [q|esc] quit

SPACE fetches and renders the next page. ENTER drains the remaining iterator (still interruptible by q/esc/Ctrl+C between pages). q/esc/Ctrl+C stop immediately. Piped output and --output json keep the existing behavior. Commands without a row template use the JSON pager from #5016.

Rendering reuses the existing Annotations["template"] and Annotations["headerTemplate"]: colors, alignment, and row format come from the same code path as today's non-paged jobs list. No new TableConfig, no new ColumnDef, no changes to any override files.

Implementation (one new file in libs/cmdio/):

  • paged_template.go — the template pager. Executes the header + row templates into an intermediate buffer per batch, splits by tab, computes visual column widths (stripping ANSI SGR so colors don't inflate), locks those widths from the first page, and pads every subsequent page to them. Output is visually indistinguishable from tabwriter for single-page results and stays aligned across pages for longer ones.
  • render.goRenderIterator routes to the template pager when a row template is set and to the JSON pager (from Add interactive pagination for JSON list output #5016) otherwise.
  • paged_template_test.go — unit tests: page size, SPACE, ENTER, quit keys, Ctrl+C-mid-drain, --limit integration, empty iterator, header + rows regression, cross-batch column stability, and byte-for-byte equivalence to the non-paged path for single-page lists.

Subtle rendering bugs caught along the way (regression tests included):

  • term.MakeRaw clears the TTY's OPOST flag, which disables \n\r\n translation — newlines become bare LF and output staircases down the terminal. The shared crlfWriter from Add interactive pagination for JSON list output #5016 puts the \r back.
  • The header and row templates MUST parse into independent *template.Template instances. Sharing one receiver causes the second Parse to overwrite the first, which made apps list render the header in place of every data row.
  • Always flush at the end, even when the iterator is empty, so a command with zero results still shows its header.
  • Tabwriter computes column widths per-flush and resets them. Bypassing tabwriter and doing the padding ourselves — with widths locked from the first batch — keeps columns aligned across pages (a short final batch no longer compresses visually against wider pages above it).

Test plan

  • go test ./libs/cmdio/... (passes)
  • make checks passes
  • make lintfull passes (0 issues)
  • Manual smoke in a TTY: apps list, jobs list, clusters list, workspace list / — first page renders immediately, SPACE fetches next, ENTER drains, Ctrl+C/Esc/q all quit (and interrupt a drain).
  • Manual smoke with piped stdout and --output json: output unchanged from main.

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 17, 2026

Approval status: pending

/libs/cmdio/ - needs approval

Files: libs/cmdio/paged_template.go, libs/cmdio/paged_template_test.go, libs/cmdio/render.go
Suggested: @mihaimitrea-db
Also eligible: @tanmay-db, @renaudhartert-db, @hectorcast-db, @parthban-db, @Divyansh-db, @tejaskochar-db, @chrisst, @rauchy

General files (require maintainer)

Files: NEXT_CHANGELOG.md, NOTICE, go.mod
Based on git history:

  • @pietern -- recent work in libs/cmdio/, ./

Any maintainer (@andrewnester, @anton-107, @denik, @pietern, @shreyas-goenka, @renaudhartert-db) can approve all areas.
See OWNERS for ownership rules.

Depends on #5016.

Extends the interactive pager introduced in #5016 to commands that
register a row template (jobs, clusters, apps, pipelines, etc.).
Reuses the shared plumbing from that PR — raw-mode key reader,
crlfWriter, prompt helpers, SupportsPager capability — and adds only
the template-specific rendering on top.

Shape of the new code:

- paged_template.go: the template pager. Executes the header + row
  templates into an intermediate buffer per batch, splits by tab,
  locks visual column widths from the first batch, and pads every
  subsequent batch to those widths. The output matches the
  non-paged tabwriter path byte-for-byte for single-page results
  and stays aligned across pages for longer ones.
- render.go: `RenderIterator` now routes to the template pager
  when a row template is set, and to the JSON pager otherwise.

Covers the subtle rendering bugs that come up when you drop into
raw mode and page output:

- `term.MakeRaw` clears OPOST, disabling '\n'→'\r\n' translation; the
  already-shared crlfWriter fixes the staircase effect.
- Header and row templates must parse into independent
  *template.Template instances so the second Parse doesn't overwrite
  the first (otherwise every row flush re-emits the header text).
- An empty iterator still flushes the header.
- Column widths are locked from the first batch so a short final
  batch doesn't visibly compress vs the wider batches above it.

Co-authored-by: Isaac
@simonfaltum simonfaltum changed the base branch from main to simonfaltum/list-json-pager April 17, 2026 19:26
@simonfaltum simonfaltum force-pushed the simonfaltum/list-simple-paginated branch from 15b0327 to 8003087 Compare April 17, 2026 19:55
Mirrors the fix in the base PR (#5016). go mod tidy promoted
golang.org/x/term from indirect to a direct dependency, and the
repo's TestRequireSPDXLicenseComment in internal/build rejects
direct dependencies without an SPDX identifier comment — failing
`make test` on every platform.

Move the dependency into the main require block with the correct
`// BSD-3-Clause` comment. This commit is independent from #5016
so 5015 can land on top of main; once the base PR merges, git will
resolve this trivially on rebase.

Co-authored-by: Isaac
TestNoticeFileCompleteness cross-checks the BSD-3-Clause section of
NOTICE against the go.mod require block. Adding golang.org/x/term
as a direct dependency (for raw-mode stdin) also requires adding
its attribution to NOTICE. Mirror the existing entries for
golang.org/x/sys and golang.org/x/text.

Co-authored-by: Isaac
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