feat(tpagectl): add table rendering improvements #604

Merged
fuzzy merged 1 commit from feat/tpagectl-table-improvements into main 2026-08-08 10:28:29 +00:00
Owner

What

Implements Phase 7 table rendering improvements (roadmap #273).

  • New output.RenderTable — shared by site list, deployment list, and preview list, replacing their three hand-rolled tabwriter helpers.
  • Terminal-width wrapping (#270) — when the destination writer is a terminal, its width is measured via golang.org/x/term on the writer's own fd (deterministic: no stdout/fallback coupling). Columns are shrunk to fit the terminal with a per-column floor, and cells are word-wrapped (long URLs hard-split). When output is redirected/piped (not a terminal), tables render byte-identical to before.
  • --no-headers (#271) — omits the header row from table output on all three list commands.
  • --sort <column> (#272) — stable sort by column before rendering (also applies to --format json/yaml): site list → name/owner/status/domain/url; deployment list → version/commit/deployed/status; preview list → branch/path/status/deployed. deployed sorts by timestamp. Unknown columns return a validation error (exit 5). No --sort keeps server order (no behavior change).

Why

Roadmap task #273.

Testing

  • output tests: plain table byte-identical to tabwriter; NoHeaders drops the header; forced-width wrap keeps every line within the budget; wrapCell word-wrap and long-word hard-split; columnBudgets shrink within available width; terminalWidth(bytes.Buffer) == 0
  • Command tests: --no-headers on all three lists; --sort ordering via index checks (site name/owner/status, deployment version/deployed, preview branch/deployed); invalid --sort → "invalid sort column"
  • Existing list tests pass unchanged (substring assertions)
  • go test -race ./... passes (27 packages)
  • golangci-lint run clean
  • pre-commit hooks pass (gofmt, go mod tidy, go test)

Breaking Changes

None. Non-terminal table output is byte-identical; --sort/--no-headers are additive flags.

Notes

golang.org/x/term promoted from indirect to direct dependency (already vendored); no new packages added.

Closes #270
Closes #271
Closes #272
Closes #273

## What Implements Phase 7 table rendering improvements (roadmap #273). - **New `output.RenderTable`** — shared by `site list`, `deployment list`, and `preview list`, replacing their three hand-rolled `tabwriter` helpers. - **Terminal-width wrapping (#270)** — when the destination writer is a terminal, its width is measured via `golang.org/x/term` on the writer's own fd (deterministic: no stdout/fallback coupling). Columns are shrunk to fit the terminal with a per-column floor, and cells are word-wrapped (long URLs hard-split). When output is redirected/piped (not a terminal), tables render byte-identical to before. - **`--no-headers` (#271)** — omits the header row from table output on all three list commands. - **`--sort <column>` (#272)** — stable sort by column before rendering (also applies to `--format json/yaml`): `site list` → name/owner/status/domain/url; `deployment list` → version/commit/deployed/status; `preview list` → branch/path/status/deployed. `deployed` sorts by timestamp. Unknown columns return a validation error (exit 5). No `--sort` keeps server order (no behavior change). ## Why Roadmap task #273. ## Testing - [x] `output` tests: plain table byte-identical to tabwriter; `NoHeaders` drops the header; forced-width wrap keeps every line within the budget; `wrapCell` word-wrap and long-word hard-split; `columnBudgets` shrink within available width; `terminalWidth(bytes.Buffer) == 0` - [x] Command tests: `--no-headers` on all three lists; `--sort` ordering via index checks (site name/owner/status, deployment version/deployed, preview branch/deployed); invalid `--sort` → "invalid sort column" - [x] Existing list tests pass unchanged (substring assertions) - [x] `go test -race ./...` passes (27 packages) - [x] `golangci-lint run` clean - [x] pre-commit hooks pass (gofmt, go mod tidy, go test) ## Breaking Changes None. Non-terminal table output is byte-identical; `--sort`/`--no-headers` are additive flags. ## Notes `golang.org/x/term` promoted from indirect to direct dependency (already vendored); no new packages added. Closes #270 Closes #271 Closes #272 Closes #273
feat(tpagectl): add table rendering improvements
All checks were successful
Test and Release / lint (pull_request) Successful in 4m32s
Test and Release / test (pull_request) Successful in 24m23s
96a352e9d9
Add output.RenderTable shared by site/deployment/preview list, centralizing
column alignment while keeping tabwriter output byte-identical when not on
a terminal.

- Terminal-width wrapping (#270): when the writer is a terminal its width is
  measured via golang.org/x/term on the writer's own fd, columns are shrunk
  to fit (with a per-column floor), and cells are word-wrapped (long URLs
  hard-split). Redirected/piped output is unchanged.
- --no-headers (#271): omits the header row from table output on all three
  list commands.
- --sort (#272): stable sort by column (site: name/owner/status/domain/url;
  deployment: version/commit/deployed/status; preview: branch/path/status/
  deployed), applied before json/yaml rendering too; unknown columns return
  a validation error (exit 5).

golang.org/x/term promoted to a direct dependency (already vendored).

Closes #270
Closes #271
Closes #272
Closes #273
fuzzy scheduled this pull request to auto merge when all checks succeed 2026-08-08 10:25:29 +00:00
the.auditor left a comment

Clean consolidation. RenderTable shares one tabwriter path with byte-identical non-terminal output (same tabwriter params as the three hand-rolled writers), the wrapped path is sound (rune-count budgets, word-wrap with hard-split for long URLs, per-column minimum floor, stable sorting), and --no-headers/--sort are wired consistently across all three list commands with validation errors mapping to exit 5. golang.org/x/term promoted correctly with no vendor change. Approving; one low-priority finding filed.

Suggestions

  1. internal/tpagectl/deployment/list.go:84--limit truncates before --sort, so --sort only reorders the already-limited subset and cannot select the top N across the full set (--limit 5 --sort deployed picks the first 5 in server order, then sorts them). Sort first, then limit. Filed as #605.

Questions

  • Version/branch columns sort lexicographically (v10 sorts before v2). Natural sort would be more intuitive for version columns; fine as a CLI tradeoff?
  • On very narrow terminals, columns are floored at 8 chars each and avail is clamped to n*8 even when that exceeds the terminal width, so wrapped output can still overflow. Commented as an intentional tradeoff — confirming it's acceptable.

Praise

  • Deterministic terminal-width detection on the writer's own fd, with cols < 20 treated as non-terminal, keeps piped output byte-identical — verified by the existing substring tests passing unchanged.
  • wrapCell correctly handles words longer than the budget without losing content (the long-URL case is covered by a test).
  • Stable sort preserves server order for ties.
Clean consolidation. `RenderTable` shares one tabwriter path with byte-identical non-terminal output (same `tabwriter` params as the three hand-rolled writers), the wrapped path is sound (rune-count budgets, word-wrap with hard-split for long URLs, per-column minimum floor, stable sorting), and `--no-headers`/`--sort` are wired consistently across all three list commands with validation errors mapping to exit 5. `golang.org/x/term` promoted correctly with no vendor change. Approving; one low-priority finding filed. ## Suggestions 1. `internal/tpagectl/deployment/list.go:84` – `--limit` truncates before `--sort`, so `--sort` only reorders the already-limited subset and cannot select the top N across the full set (`--limit 5 --sort deployed` picks the first 5 in server order, then sorts them). Sort first, then limit. Filed as #605. ## Questions - Version/branch columns sort lexicographically (`v10` sorts before `v2`). Natural sort would be more intuitive for version columns; fine as a CLI tradeoff? - On very narrow terminals, columns are floored at 8 chars each and `avail` is clamped to `n*8` even when that exceeds the terminal width, so wrapped output can still overflow. Commented as an intentional tradeoff — confirming it's acceptable. ## Praise - Deterministic terminal-width detection on the writer's own fd, with `cols < 20` treated as non-terminal, keeps piped output byte-identical — verified by the existing substring tests passing unchanged. - `wrapCell` correctly handles words longer than the budget without losing content (the long-URL case is covered by a test). - Stable sort preserves server order for ties.
fuzzy merged commit 96a352e9d9 into main 2026-08-08 10:28:29 +00:00
fuzzy deleted branch feat/tpagectl-table-improvements 2026-08-08 10:28:29 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
thwap/thwap-pagesd!604
No description provided.