Skip to content

Commit e8c0a13

Browse files
committed
Revert "feat(output): optimize table formatting with width capping and key/value layout (#1081)"
This reverts commit 3bbea01.
1 parent 3bbea01 commit e8c0a13

2 files changed

Lines changed: 8 additions & 190 deletions

File tree

src/output.test.ts

Lines changed: 0 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,8 @@
11
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
22
import { render } from './output.js';
33

4-
function stripAnsi(str: string): string {
5-
return str.replace(/\u001B\[[0-9;]*m/g, '');
6-
}
7-
84
describe('output TTY detection', () => {
95
const originalIsTTY = process.stdout.isTTY;
10-
const originalColumns = process.stdout.columns;
116
let logSpy: ReturnType<typeof vi.spyOn>;
127

138
beforeEach(() => {
@@ -16,7 +11,6 @@ describe('output TTY detection', () => {
1611

1712
afterEach(() => {
1813
Object.defineProperty(process.stdout, 'isTTY', { value: originalIsTTY, writable: true });
19-
Object.defineProperty(process.stdout, 'columns', { value: originalColumns, writable: true });
2014
logSpy.mockRestore();
2115
});
2216

@@ -51,45 +45,4 @@ describe('output TTY detection', () => {
5145
expect(out).not.toContain('name: alice');
5246
expect(out).toContain('alice');
5347
});
54-
55-
it('renders single-row table output as key/value pairs', () => {
56-
Object.defineProperty(process.stdout, 'isTTY', { value: true, writable: true });
57-
render(
58-
[{ name: 'alice', score: 10, description: 'single row detail' }],
59-
{ fmt: 'table', columns: ['name', 'score', 'description'], title: 'Sample' },
60-
);
61-
const out = stripAnsi(logSpy.mock.calls.map((c: unknown[]) => c[0]).join('\n'));
62-
expect(out).toContain('Sample');
63-
expect(out).toContain(' Name alice');
64-
expect(out).toContain(' Score 10');
65-
expect(out).toContain(' Description single row detail');
66-
expect(out).toContain('1 items');
67-
});
68-
69-
it('caps wide table columns to terminal width and truncates long values', () => {
70-
Object.defineProperty(process.stdout, 'isTTY', { value: true, writable: true });
71-
Object.defineProperty(process.stdout, 'columns', { value: 40, writable: true });
72-
render(
73-
[
74-
{
75-
name: 'alpha',
76-
status: 'ok',
77-
description: 'This is a very long description that should wrap cleanly in a narrow terminal width.',
78-
},
79-
{
80-
name: 'beta',
81-
status: 'warn',
82-
description: 'Another long description that should also wrap instead of making the table extremely wide.',
83-
},
84-
],
85-
{ fmt: 'table', columns: ['name', 'status', 'description'] },
86-
);
87-
const out = stripAnsi(logSpy.mock.calls.map((c: unknown[]) => c[0]).join('\n'));
88-
expect(out).toContain('This is a very l...');
89-
expect(out).toContain('Another long des...');
90-
expect(out).not.toContain('terminal width.');
91-
92-
const maxLineLength = out.split('\n').reduce((max: number, line: string) => Math.max(max, line.length), 0);
93-
expect(maxLineLength).toBeLessThanOrEqual(40);
94-
});
9548
});

src/output.ts

Lines changed: 8 additions & 143 deletions
Original file line numberDiff line numberDiff line change
@@ -47,166 +47,31 @@ export function render(data: unknown, opts: RenderOptions = {}): void {
4747
}
4848
}
4949

50-
// ── CJK-aware string width ──
51-
52-
function isWideCodePoint(cp: number): boolean {
53-
return (
54-
(cp >= 0x4E00 && cp <= 0x9FFF) || // CJK Unified Ideographs
55-
(cp >= 0x3400 && cp <= 0x4DBF) || // CJK Extension A
56-
(cp >= 0x20000 && cp <= 0x2A6DF) || // CJK Extension B
57-
(cp >= 0xF900 && cp <= 0xFAFF) || // CJK Compatibility Ideographs
58-
(cp >= 0xFF01 && cp <= 0xFF60) || // Fullwidth Forms
59-
(cp >= 0xFFE0 && cp <= 0xFFE6) || // Fullwidth Signs
60-
(cp >= 0xAC00 && cp <= 0xD7AF) || // Hangul Syllables
61-
(cp >= 0x3000 && cp <= 0x303F) || // CJK Symbols
62-
(cp >= 0x3040 && cp <= 0x309F) || // Hiragana
63-
(cp >= 0x30A0 && cp <= 0x30FF) // Katakana
64-
);
65-
}
66-
67-
function displayWidth(str: string): number {
68-
let w = 0;
69-
for (const ch of str) {
70-
w += isWideCodePoint(ch.codePointAt(0)!) ? 2 : 1;
71-
}
72-
return w;
73-
}
74-
75-
function truncateToWidth(str: string, maxWidth: number): string {
76-
if (maxWidth <= 0 || displayWidth(str) <= maxWidth) return str;
77-
78-
const ellipsis = '...';
79-
const ellipsisWidth = displayWidth(ellipsis);
80-
if (maxWidth <= ellipsisWidth) return ellipsis.slice(0, maxWidth);
81-
82-
let out = '';
83-
let width = 0;
84-
for (const ch of str) {
85-
const nextWidth = displayWidth(ch);
86-
if (width + nextWidth + ellipsisWidth > maxWidth) break;
87-
out += ch;
88-
width += nextWidth;
89-
}
90-
91-
return out + ellipsis;
92-
}
93-
94-
// ── Table rendering ──
95-
96-
// Fits typical date, status, and ID columns without truncation.
97-
const SHORT_COL_THRESHOLD = 15;
98-
99-
const NUMERIC_RE = /^-?[\d,]+\.?\d*$/;
100-
10150
function renderTable(data: unknown, opts: RenderOptions): void {
10251
const rows = normalizeRows(data);
10352
if (!rows.length) { console.log(styleText('dim', '(no data)')); return; }
10453
const columns = resolveColumns(rows, opts);
10554

106-
if (rows.length === 1) {
107-
renderKeyValue(rows[0], columns, opts);
108-
return;
109-
}
110-
111-
const cells: string[][] = rows.map(row =>
112-
columns.map(c => {
113-
const v = (row as Record<string, unknown>)[c];
114-
return v === null || v === undefined ? '' : String(v);
115-
}),
116-
);
117-
11855
const header = columns.map(c => capitalize(c));
119-
const colCount = columns.length;
120-
121-
// Single pass: measure column widths + detect numeric columns
122-
const colContentWidths = header.map(h => displayWidth(h));
123-
const numericCounts = new Array<number>(colCount).fill(0);
124-
const totalCounts = new Array<number>(colCount).fill(0);
125-
126-
for (const row of cells) {
127-
for (let ci = 0; ci < colCount; ci++) {
128-
const w = displayWidth(row[ci]);
129-
if (w > colContentWidths[ci]) colContentWidths[ci] = w;
130-
const v = row[ci].trim();
131-
if (v) {
132-
totalCounts[ci]++;
133-
if (NUMERIC_RE.test(v)) numericCounts[ci]++;
134-
}
135-
}
136-
}
137-
138-
const colAligns: Array<'left' | 'right'> = columns.map((_, ci) =>
139-
totalCounts[ci] > 0 && numericCounts[ci] / totalCounts[ci] > 0.8 ? 'right' : 'left',
140-
);
141-
142-
// Calculate column widths to fit terminal.
143-
// cli-table3 colWidths includes cell padding (1 space each side).
144-
const termWidth = process.stdout.columns || 120;
145-
// Border chars: '│' between every column + edges = colCount + 1
146-
const borderOverhead = colCount + 1;
147-
const availableWidth = Math.max(termWidth - borderOverhead, colCount * 5);
148-
149-
let shortTotal = 0;
150-
const longIndices: number[] = [];
151-
152-
for (let i = 0; i < colCount; i++) {
153-
// +2 for cell padding (1 space each side)
154-
const padded = colContentWidths[i] + 2;
155-
if (colContentWidths[i] <= SHORT_COL_THRESHOLD) {
156-
colContentWidths[i] = padded;
157-
shortTotal += padded;
158-
} else {
159-
longIndices.push(i);
160-
}
161-
}
162-
163-
const remainingWidth = availableWidth - shortTotal;
164-
if (longIndices.length > 0) {
165-
const perLong = Math.max(Math.floor(remainingWidth / longIndices.length), 12);
166-
for (const i of longIndices) {
167-
colContentWidths[i] = Math.min(colContentWidths[i] + 2, perLong);
168-
}
169-
}
170-
17156
const table = new Table({
17257
head: header.map(h => styleText('bold', h)),
17358
style: { head: [], border: [] },
174-
colWidths: colContentWidths,
175-
colAligns,
59+
wordWrap: true,
60+
wrapOnWordBoundary: true,
17661
});
17762

178-
for (const row of cells) {
179-
table.push(row.map((cell, ci) => truncateToWidth(cell, colContentWidths[ci] - 2)));
63+
for (const row of rows) {
64+
table.push(columns.map(c => {
65+
const v = (row as Record<string, unknown>)[c];
66+
return v === null || v === undefined ? '' : String(v);
67+
}));
18068
}
18169

18270
console.log();
18371
if (opts.title) console.log(styleText('dim', ` ${opts.title}`));
18472
console.log(table.toString());
185-
printFooter(rows.length, opts);
186-
}
187-
188-
function renderKeyValue(row: Record<string, unknown>, columns: string[], opts: RenderOptions): void {
189-
const entries = columns.map(c => ({
190-
key: capitalize(c),
191-
value: row[c] === null || row[c] === undefined ? '' : String(row[c]),
192-
}));
193-
194-
const maxKeyWidth = Math.max(...entries.map(e => displayWidth(e.key)));
195-
196-
console.log();
197-
if (opts.title) console.log(styleText('dim', ` ${opts.title}`));
198-
console.log();
199-
for (const { key, value } of entries) {
200-
const padding = ' '.repeat(maxKeyWidth - displayWidth(key));
201-
console.log(` ${styleText('bold', key)}${padding} ${value}`);
202-
}
203-
console.log();
204-
printFooter(1, opts);
205-
}
206-
207-
function printFooter(count: number, opts: RenderOptions): void {
20873
const footer: string[] = [];
209-
footer.push(`${count} items`);
74+
footer.push(`${rows.length} items`);
21075
if (opts.elapsed) footer.push(`${opts.elapsed.toFixed(1)}s`);
21176
if (opts.source) footer.push(opts.source);
21277
if (opts.footerExtra) footer.push(opts.footerExtra);

0 commit comments

Comments
 (0)