Skip to content

Add interactive pager for list commands with a row template#5015

Open
simonfaltum wants to merge 3 commits intomainfrom
simonfaltum/list-simple-paginated
Open

Add interactive pager for list commands with a row template#5015
simonfaltum wants to merge 3 commits intomainfrom
simonfaltum/list-simple-paginated

Conversation

@simonfaltum
Copy link
Copy Markdown
Member

@simonfaltum simonfaltum commented Apr 17, 2026

Why

List commands with a row template (jobs list, clusters list, apps list, pipelines list, workspace list, etc.) drain the full iterator and render every row at once. In workspaces with hundreds of resources, the output scrolls past before you can read it. An interactive terminal should get a chance to step through the output.

This PR is an alternative to #4729 (the Bubble Tea TUI). It only solves pagination, nothing else. Smaller diff, no new public API, no override file changes, no external deps beyond golang.org/x/term.

Changes

Before: databricks <resource> list drained the full iterator through the existing template + tabwriter pipeline before showing anything.

Now: when stdin, stdout, and stderr are all TTYs and the command has a row template, the CLI streams 50 rows at a time and prompts on stderr:

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

SPACE fetches the next page. ENTER drains the rest (still interruptible by q/esc/Ctrl+C between pages). q/esc/Ctrl+C stop immediately. Piped output and --output json keep the existing non-paged behavior.

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.

New files under libs/cmdio/:

  • capabilities.go: SupportsPager() (stdin + stdout + stderr all TTYs, not Git Bash).
  • pager.go: raw-mode stdin setup with a key-reader goroutine, pagerNextKey / pagerShouldQuit, a crlfWriter to compensate for the terminal's cleared OPOST flag while raw mode is active, and the prompt/key constants.
  • 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 the same widths. Single-page output is visually indistinguishable from tabwriter; columns stay aligned across pages for longer lists.
  • render.go: RenderIterator routes to the template pager when the capability check passes and a row template is set.

No cmd/ changes. No new public API beyond Capabilities.SupportsPager.

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

  • term.MakeRaw clears the TTY's OPOST flag, which disables \n to \r\n translation. Newlines become bare LF and output staircases down the terminal. The crlfWriter 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).

History: this consolidates #5016 (shared pager infrastructure) and drops an earlier JSON-output pager. JSON output is mostly consumed by scripts, so paging it adds complexity without a clear win.

Test plan

  • go test ./libs/cmdio/... passes (new coverage includes crlfWriter, the key helpers, and every pager control path: page size, SPACE, ENTER, quit keys, Ctrl+C-mid-drain, --limit integration, empty iterator, header + rows regression, cross-batch column stability, byte-for-byte equivalence to the non-paged path for single-page lists).
  • make checks passes.
  • make lint 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 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

6 files changed
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.

@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
@simonfaltum simonfaltum changed the title Add simple text pager for interactive list commands (alternative to #4729) Add interactive pager for list commands with a row template Apr 21, 2026
@simonfaltum simonfaltum changed the base branch from simonfaltum/list-json-pager to main April 21, 2026 09:50
When stdin, stdout, and stderr are all TTYs and the command has a row
template (jobs list, clusters list, apps list, pipelines list, etc.), the
CLI streams 50 rows at a time and prompts on stderr:

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

SPACE fetches the next page. ENTER drains the rest (interruptible by
q/esc/Ctrl+C between pages). q/esc/Ctrl+C quit immediately. Piped
output and --output json keep the existing non-paged behavior.

Rendering reuses the existing template + headerTemplate annotations
(same colors, same alignment as today). Column widths are locked from
the first page so they stay stable across batches.

Co-authored-by: Isaac
@simonfaltum simonfaltum force-pushed the simonfaltum/list-simple-paginated branch from ab60e08 to cbd549c Compare April 21, 2026 09:59
@simonfaltum simonfaltum requested a review from pietern April 21, 2026 10:03
Cuts ~100 lines without behavior changes:
- Trim over-long doc blocks on SupportsPager, startRawStdinKeyReader,
  and renderIteratorPagedTemplateCore.
- Drop comments that restate the code.
- Extract the flushPage closure into a templatePager struct.
- Collapse q/Q/esc/Ctrl+C exit tests into a table-driven test.
- Drop the brittle hard-coded-offset column test; single-page
  equivalence and the header-once test already cover the behavior.

Co-authored-by: Isaac
Two principles from docs/go-code-structure:
- Table-driven tests: collapse four pagination behavior tests into one.
- Test the pure logic directly: add unit tests for visualWidth,
  computeWidths, and padRow instead of exercising them only via the
  integration path. Failures now point directly at the broken helper.

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