feat(output): optimize table formatting with width capping and key/value layout#1081
feat(output): optimize table formatting with width capping and key/value layout#1081Benjamin-eecs wants to merge 1 commit intojackwener:mainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
Improves -f table output readability by introducing terminal-width-aware table rendering with truncation, numeric right-alignment, and a key/value layout for single-row results (with CJK-aware width measurement).
Changes:
- Added CJK-aware display width + truncation helpers and applied them to table cell rendering.
- Updated
renderTable()to infer numeric columns, cap column widths to terminal width, and truncate overflow. - Added
renderKeyValue()for single-row results and extractedprintFooter()for shared footer rendering.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const availableWidth = Math.max(termWidth - borderOverhead, colCount * 5); | ||
|
|
||
| const colWidths = new Array<number>(colCount); | ||
| let shortTotal = 0; | ||
| const longIndices: number[] = []; | ||
|
|
||
| for (let i = 0; i < colCount; i++) { | ||
| if (colContentWidths[i] <= SHORT_COL_THRESHOLD) { | ||
| colWidths[i] = colContentWidths[i]; | ||
| shortTotal += colContentWidths[i]; | ||
| } else { | ||
| longIndices.push(i); | ||
| } | ||
| } | ||
|
|
||
| const remainingWidth = availableWidth - shortTotal; | ||
| if (longIndices.length > 0 && remainingWidth > 0) { | ||
| const perLong = Math.max(Math.floor(remainingWidth / longIndices.length), 10); | ||
| for (const i of longIndices) { | ||
| colWidths[i] = Math.min(colContentWidths[i], perLong); |
There was a problem hiding this comment.
availableWidth is computed with Math.max(termWidth - borderOverhead, colCount * 5), which can exceed the actual terminal width when colCount is large or termWidth - borderOverhead is small/negative. In those cases the subsequent width-capping logic cannot keep the rendered table within the terminal, contradicting the intended behavior. Consider clamping availableWidth to Math.max(0, termWidth - borderOverhead) (and then handling narrow terminals by shrinking columns below 5 or switching to a different layout).
| const availableWidth = Math.max(termWidth - borderOverhead, colCount * 5); | |
| const colWidths = new Array<number>(colCount); | |
| let shortTotal = 0; | |
| const longIndices: number[] = []; | |
| for (let i = 0; i < colCount; i++) { | |
| if (colContentWidths[i] <= SHORT_COL_THRESHOLD) { | |
| colWidths[i] = colContentWidths[i]; | |
| shortTotal += colContentWidths[i]; | |
| } else { | |
| longIndices.push(i); | |
| } | |
| } | |
| const remainingWidth = availableWidth - shortTotal; | |
| if (longIndices.length > 0 && remainingWidth > 0) { | |
| const perLong = Math.max(Math.floor(remainingWidth / longIndices.length), 10); | |
| for (const i of longIndices) { | |
| colWidths[i] = Math.min(colContentWidths[i], perLong); | |
| const availableWidth = Math.max(0, termWidth - borderOverhead); | |
| const colWidths = new Array<number>(colCount).fill(0); | |
| let shortTotal = 0; | |
| const longIndices: number[] = []; | |
| for (let i = 0; i < colCount; i++) { | |
| if (colContentWidths[i] <= SHORT_COL_THRESHOLD) { | |
| colWidths[i] = Math.min(colContentWidths[i], availableWidth - shortTotal); | |
| shortTotal += colWidths[i]; | |
| } else { | |
| longIndices.push(i); | |
| } | |
| } | |
| let remainingWidth = Math.max(0, availableWidth - shortTotal); | |
| if (longIndices.length > 0 && remainingWidth > 0) { | |
| for (let li = 0; li < longIndices.length; li++) { | |
| const i = longIndices[li]; | |
| const colsLeft = longIndices.length - li; | |
| const perLong = Math.floor(remainingWidth / colsLeft); | |
| colWidths[i] = Math.min(colContentWidths[i], perLong); | |
| remainingWidth -= colWidths[i]; |
| const remainingWidth = availableWidth - shortTotal; | ||
| if (longIndices.length > 0 && remainingWidth > 0) { | ||
| const perLong = Math.max(Math.floor(remainingWidth / longIndices.length), 10); | ||
| for (const i of longIndices) { | ||
| colWidths[i] = Math.min(colContentWidths[i], perLong); | ||
| } | ||
| } |
There was a problem hiding this comment.
colWidths is only assigned for the “long” columns when remainingWidth > 0. If remainingWidth <= 0, the entries for longIndices remain undefined, and later cellWidths[ri][ci] > colWidths[ci] will never be true (because undefined becomes NaN in the comparison), so no truncation happens for those columns. Ensure every column gets an explicit width (even under tight width budgets), and then truncate based on that.
| for (let i = 0; i < colCount; i++) { | ||
| if (colContentWidths[i] <= SHORT_COL_THRESHOLD) { | ||
| colWidths[i] = colContentWidths[i]; | ||
| shortTotal += colContentWidths[i]; | ||
| } else { | ||
| longIndices.push(i); | ||
| } | ||
| } |
There was a problem hiding this comment.
The width allocation logic only caps columns whose measured width is above SHORT_COL_THRESHOLD; “short” columns always keep their full content width. With many columns (or a narrow terminal), the sum of short columns alone can exceed the available width, so the table will still overflow even when there are no “long” columns to shrink. Consider adding a fallback pass that enforces a global width budget (e.g., iteratively shrink the widest columns above a minimum width until the sum fits).
| if (rows.length === 1) { | ||
| renderKeyValue(rows[0], columns, opts); | ||
| return; | ||
| } |
There was a problem hiding this comment.
The PR introduces several new output behaviors (single-row key/value rendering, numeric alignment inference, terminal-width capping + truncation) but src/output.test.ts currently only asserts TTY auto-downgrade behavior. Adding focused tests for these new formatting rules would help prevent regressions (e.g., a snapshot-style assertion for truncation/ellipsis and for the key/value layout).
… key/value layout Closes jackwener#1017
c473135 to
26ae6ec
Compare
Summary
Closes #1017.
Improves the default table output to fit within the terminal width and be easier to scan.
Before: long text columns (titles, URLs) push the table way past terminal width, making output unreadable.
After:
...bilibili me) render as vertical key/value pairs instead of a wide single-row tableFull content is always available via
-f jsonor-f yaml.Changes
All changes are in
src/output.ts:isWideCodePoint(),displayWidth(),truncateToWidth()for CJK-aware width handlingrenderTable()now measures columns, caps widths to terminal, and truncates long textrenderKeyValue()for single-row resultsprintFooter()to share between table and key/value renderersTest plan
npx tsc --noEmitpassesopencli bilibili search "AI" --limit 5 -f table- long titles truncated, scores right-alignedopencli bilibili me -f table- renders as key/value layoutopencli bilibili hot --limit 5 -f json- full content preserved