feat: add Go table output compatible with the legacy CLI, with browser-like column widths - #196
pjcdawkins wants to merge 9 commits into
Conversation
Add internal/table, a Go equivalent of the legacy Table service, as a building block for moving list/info commands to Go. - AddFlags registers --format, --columns/-c and --no-header with the same descriptions as the legacy CLI. - Column selection supports repeated or comma/whitespace-separated values, % and * wildcards, and "+" for the default columns, with the same "Column not found" and "Invalid format" errors. - The csv, tsv and plain formats match the legacy output (checked against the PHP Table class), including quoting and LF line breaks. - The table format uses the Symfony default border style, without the legacy column wrapping. - RenderProperties covers the legacy renderSimple (Property/Value). Value formatting (PropertyFormatter, --date-fmt) is left to callers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…styles Port the legacy AdaptiveTable column-width algorithm: columns get a share of the terminal width in proportion to their natural width, but never less than 10 characters (or their own width). Headers and columns marked NoWrap are not wrapped. The width comes from $COLUMNS, then the first standard stream that is a terminal, then 80. Measure and wrap cells with charmbracelet/x/ansi (replacing go-runewidth), so ANSI-styled text is aligned correctly. A style that is still active at the end of a cell's line is reset there and re-applied on the next line, so it does not leak into the borders or other cells. The csv, tsv and plain formats strip ANSI sequences. Wrapping matches the legacy output except that ansi.Wrap also breaks after hyphens and drops leading spaces on wrapped lines. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A zero in an extended color (e.g. 38;2;0;255;0) was taken as a reset, dropping other active styles on continuation lines. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Warning
Changes suggested — 🟡 2 warnings · ⚪ 1 nitpick
🔍 Full review · 6 files reviewed
⚪ Nitpick
internal/table/table.go:110— When two columns share a lower-cased name,indexeskeeps the first column (if _, ok := indexes[name]; !ok). The legacyavailableColumnsoverwrites with the last key ($availableColumns[strtolower($columnName)] = $key). Selecting that name therefore shows a different column's data than the legacy CLI does, which contradicts the parity goal.
Verification
- csvCell quotes on
",\nand the delimiter before normalizing\Rbreaks, the same order as legacy Csv::formatCell with an LF line break. - plainCell uses the same
[\r\n\t]+→ space replacement as legacy PlainFormat::formatCell. - splitColumns applies the
+separation regexes only when exactly one value is given, matching legacy specifiedColumns. - wrapCell adds the leading indent to every wrapped line, matching legacy AdaptiveTable's
preg_replace('/^/m', $indent, ...). - sgrResets skips the 2 or 4 sub-parameters of 38/48/58 extended colors, so
38;5;0and38;2;0;0;0are not treated as resets.
The new internal/table/table_test.go covers the formats, column selection, wrapping and style isolation. Nothing covers DefaultColumns names that match no column, duplicate column names, or non-SGR escape sequences. I wrote a throwaway test against the package to reproduce the first two findings; the repository's own test target (make test / go test ./...) runs the new tests.
Review details
- Commit: a8d8b2b
- Model: claude-opus-5-5
Review 1 of 10 for this pull request · View the full run
- A default column that matches no column now gives empty cells, as in the legacy CLI, instead of showing the first column's data. - A duplicate column name refers to the last column with that name, like the legacy availableColumns(). - OSC 8 hyperlinks still open at the end of a cell's line are closed there and re-opened on the next line, like SGR styles. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Superseded: the latest Upsun Dispatch review no longer requests changes.
Change the AdaptiveTable column-width algorithm, in both the legacy PHP CLI and the Go table package, to be closer to a browser's automatic table layout: - A column's minimum width is its longest word, capped at 20 instead of a fixed 10, so words are not cut when the table can fit. If the minimums don't fit, the cap is reduced down to 10. - Space above the minimums is shared in proportion to each column's maximum minus minimum width, using integer arithmetic so that PHP and Go give the same widths. In Go, replace ansi.Wrap with a port of PHP's wordwrap() (measuring display width and keeping ANSI sequences), so wrapping matches the legacy output and doesn't break at hyphens. It matched PHP on 100,000 random ASCII inputs, and table output matched PHP's AdaptiveTable in 309 of 313 generated tables (the rest had duplicate column names). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…wrap boundaries - Indentation is repeated on each wrapped line, so add it to a cell's minimum width. PHP's wordwrap() threw a ValueError when the width left after the indentation was zero. - Zero-width ANSI sequences no longer trigger a break in the Go wordwrap(), which produced extra lines for styled text that exactly filled the width. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
📋 PR Summary This PR adds Changes
|
There was a problem hiding this comment.
Warning
Changes suggested — 🟡 1 warning
🔁 Incremental · 6 files reviewed
Verification
flexTotalis always positive in the share step: the early returns guarantee maxTotal > available > minTotal.- The new
wordwrapcuts a too-wide grapheme only oncecur > lastStart, so a wide character at a line start cannot loop or produce an empty line. wrapCellandlongestWordWidthboth treat only leading ASCII spaces as indentation, and tabs are expanded in renderTable before either runs.- Both column-lookup paths in
Render(the row filter andnoWrap) now check theindexeskey exists, and duplicate names map to the last column.
This change updates the unit tests in internal/table/table_test.go: new wrapping cases and TestWordwrap. It also updates legacy/tests/Console/AdaptiveTableTest.php and the expected tables in the integration tests for act and env:deploy. No test covers multi-line indented cells in the width algorithm, where the panic occurs. I reproduced it with a throwaway test.
Review 3 of 10 for this pull request · View the full run
The indentation added to the longest word could make a multi-line cell's minimum exceed its width, when the word is on a later, unindented line. Negative shares then panicked in Go (slice bounds) and gave wrong widths in PHP. A cell at its full width is not wrapped, so its minimum is now capped at that width. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Superseded: the latest Upsun Dispatch review no longer requests changes.
Adds
internal/table, a Go equivalent of the legacyTableservice, as a building block for moving list/info commands to Go (starting withauth:info). Also changes how the legacyAdaptiveTablepicks column widths, so that both CLIs wrap tables the same way.Go table package
Table.AddFlagsregisters--format,--columns/-cand--no-headerwith the legacy descriptions.-cis skipped if the shorthand is taken.%/*wildcards and+for the default columns, with the legacyColumn not foundandInvalid formaterrors.csv,tsvandplainoutput matches the legacy PHPTablebyte for byte (compared by running both on the same data), with ANSI escape sequences removed.tableformat uses the Symfony default border style and wraps cells to the terminal width. Headers andNoWrapcolumns aren't wrapped.charmbracelet/x/ansi. Styles and OSC 8 hyperlinks still open at the end of a cell's line are closed and re-opened on the next line, so they don't leak into borders or other cells.RenderPropertiescovers the legacyrenderSimple(Property/Value table).Column widths (PHP and Go)
The legacy
AdaptiveTablegave every column a fixed 10-character minimum, so it cut words even when the table could fit. Both sides now work more like a browser's automatic table layout:wordwrap()(measuring display width and keeping ANSI sequences). It matched PHP on 100,000 random inputs, and the Go and PHP tables were identical in 410 generated cases.This changes the wrapped
tableoutput of legacy commands. Two integration tests with wrapped tables at 120 columns were updated.Not included: value formatting (
PropertyFormatter,--date-fmt), table separator rows, and the deprecated-column helpers.🤖 Generated with Claude Code