mirror of
https://github.com/Kaelio/ktx.git
synced 2026-06-25 08:48:08 +02:00
refactor(cli): unify output formatting across commands (#111)
* refactor(cli): unify output formatting across search and status commands Replace clack-style box borders (◇/│/└) and bullets (●/◆) in printList pretty mode with a clean status-style layout: bold headers, indented aligned rows, no decorative framing. Migrate status-project.ts from hand-rolled ANSI escape codes to shared symbols.ts color helpers. Remove dead clack symbols from SYMBOLS, add yellow() for warnings. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(cli): update stale badge role docstring after dim removal Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
beeeda4437
commit
50ffebd98b
5 changed files with 52 additions and 50 deletions
|
|
@ -139,7 +139,7 @@ function stripAnsi(s: string): string {
|
|||
}
|
||||
|
||||
describe('printList — pretty mode', () => {
|
||||
it('renders a Clack-style header, grouped rows, and footer', () => {
|
||||
it('renders a bold header, grouped rows, and footer', () => {
|
||||
const r = recorder();
|
||||
printList<SlRow>({
|
||||
rows: [ORDERS, USERS],
|
||||
|
|
@ -152,13 +152,14 @@ describe('printList — pretty mode', () => {
|
|||
io: r.io,
|
||||
});
|
||||
const out = stripAnsi(r.out());
|
||||
expect(out).toContain(`${SYMBOLS.barStart} sl list`);
|
||||
expect(out).toContain(`${SYMBOLS.group} warehouse`);
|
||||
expect(out).toContain('sl list');
|
||||
expect(out).toContain('warehouse');
|
||||
expect(out).toContain('(2 sources)');
|
||||
expect(out).toMatch(new RegExp(`${escapeRegExp(SYMBOLS.item)} orders\\s+5 cols ${escapeRegExp(SYMBOLS.middot)} 3 measures ${escapeRegExp(SYMBOLS.middot)} 1 join\\b`));
|
||||
expect(out).toMatch(new RegExp(`${escapeRegExp(SYMBOLS.item)} users\\s+8 cols ${escapeRegExp(SYMBOLS.middot)} 2 measures ${escapeRegExp(SYMBOLS.middot)} 2 joins\\b`));
|
||||
expect(out).toMatch(/orders\s+5 cols/);
|
||||
expect(out).toMatch(new RegExp(`3 measures ${escapeRegExp(SYMBOLS.middot)} 1 join\\b`));
|
||||
expect(out).toMatch(new RegExp(`2 measures ${escapeRegExp(SYMBOLS.middot)} 2 joins\\b`));
|
||||
expect(out).toContain(`${SYMBOLS.emDash} User profile + auth`);
|
||||
expect(out).toContain(`${SYMBOLS.barEnd} 2 sources`);
|
||||
expect(out).toContain('2 sources');
|
||||
});
|
||||
|
||||
it('renders an empty-state message when no rows', () => {
|
||||
|
|
@ -174,11 +175,11 @@ describe('printList — pretty mode', () => {
|
|||
io: r.io,
|
||||
});
|
||||
const out = stripAnsi(r.out());
|
||||
expect(out).toContain(`${SYMBOLS.barStart} sl list`);
|
||||
expect(out).toContain(`${SYMBOLS.barEnd} No semantic-layer sources found in /tmp/proj`);
|
||||
expect(out).toContain('sl list');
|
||||
expect(out).toContain('No semantic-layer sources found in /tmp/proj');
|
||||
});
|
||||
|
||||
it('renders empty-state with hint and zero-count footer when emptyHint is provided', () => {
|
||||
it('renders empty-state with hint when emptyHint is provided', () => {
|
||||
const r = recorder();
|
||||
printList<SlRow>({
|
||||
rows: [],
|
||||
|
|
@ -192,9 +193,8 @@ describe('printList — pretty mode', () => {
|
|||
io: r.io,
|
||||
});
|
||||
const out = stripAnsi(r.out());
|
||||
expect(out).toContain(`${SYMBOLS.bar} No sources matched "foo"`);
|
||||
expect(out).toContain(`${SYMBOLS.bar} Run \`ktx sl list\` to see available sources.`);
|
||||
expect(out).toContain(`${SYMBOLS.barEnd} 0 sources`);
|
||||
expect(out).toContain('No sources matched "foo"');
|
||||
expect(out).toContain('Run `ktx sl list` to see available sources.');
|
||||
});
|
||||
|
||||
it('singularizes the footer when there is one row', () => {
|
||||
|
|
@ -210,7 +210,7 @@ describe('printList — pretty mode', () => {
|
|||
io: r.io,
|
||||
});
|
||||
const out = stripAnsi(r.out());
|
||||
expect(out).toContain(`${SYMBOLS.barEnd} 1 source`);
|
||||
expect(out).toContain('1 source');
|
||||
});
|
||||
|
||||
it('uses the provided unit in pluralization and group counts', () => {
|
||||
|
|
@ -236,10 +236,10 @@ describe('printList — pretty mode', () => {
|
|||
});
|
||||
const out = stripAnsi(r.out());
|
||||
expect(out).toContain('(2 pages)');
|
||||
expect(out).toContain(`${SYMBOLS.barEnd} 2 pages`);
|
||||
expect(out).toContain('2 pages');
|
||||
});
|
||||
|
||||
it('renders a leading dim badge column with prettyFormat in pretty mode', () => {
|
||||
it('renders a leading badge column with prettyFormat in pretty mode', () => {
|
||||
const r = recorder();
|
||||
interface SearchRow { score: number; scope: string; key: string; summary: string }
|
||||
const SEARCH_COLUMNS: ReadonlyArray<PrintListColumn<SearchRow>> = [
|
||||
|
|
@ -270,9 +270,8 @@ describe('printList — pretty mode', () => {
|
|||
io: r.io,
|
||||
});
|
||||
const out = stripAnsi(r.out());
|
||||
// Badge displays as right-padded percentage before the name column.
|
||||
expect(out).toMatch(new RegExp(`${escapeRegExp(SYMBOLS.item)} 87%\\s+alpha\\s+`));
|
||||
expect(out).toMatch(new RegExp(`${escapeRegExp(SYMBOLS.item)} 4%\\s+beta\\s+`));
|
||||
expect(out).toMatch(/87%\s+alpha\s+/);
|
||||
expect(out).toMatch(/4%\s+beta\s+/);
|
||||
});
|
||||
|
||||
it('emits the badge column in plain mode using its plain prefix', () => {
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ export interface PrintListColumn<Row> {
|
|||
dim?: boolean;
|
||||
/**
|
||||
* Pretty-mode role override. When omitted, role is auto-detected:
|
||||
* - `'badge'` — leading dim cell before the name column (right-padded across rows).
|
||||
* - `'badge'` — leading cell before the name column (right-padded across rows).
|
||||
* - `'name'` — name column. Default: first non-grouped, non-metric, non-optional column.
|
||||
* - `'metric'` — `"N word"` cell. Default: any column with a non-empty `plain` prefix.
|
||||
* - `'suffix'` — trailing em-dash optional value. Default: any column with `optional: true`.
|
||||
|
|
@ -202,20 +202,19 @@ function printListPretty<Row extends object>(args: PrintListArgs<Row>): void {
|
|||
const { io, command, rows, columns, groupBy, emptyMessage, emptyHint } = args;
|
||||
const unit = args.unit ?? 'result';
|
||||
|
||||
io.stdout.write(`${SYMBOLS.barStart} ${command}\n`);
|
||||
io.stdout.write(`${SYMBOLS.bar}\n`);
|
||||
io.stdout.write(`${bold(command)}\n`);
|
||||
|
||||
if (rows.length === 0) {
|
||||
io.stdout.write(`\n ${emptyMessage}\n`);
|
||||
if (emptyHint !== undefined && emptyHint !== '') {
|
||||
io.stdout.write(`${SYMBOLS.bar} ${emptyMessage}\n`);
|
||||
io.stdout.write(`${SYMBOLS.bar} ${dim(emptyHint)}\n`);
|
||||
io.stdout.write(`${SYMBOLS.barEnd} ${dim(`0 ${unit}s`)}\n`);
|
||||
} else {
|
||||
io.stdout.write(`${SYMBOLS.barEnd} ${emptyMessage}\n`);
|
||||
io.stdout.write(` ${dim(emptyHint)}\n`);
|
||||
}
|
||||
io.stdout.write('\n');
|
||||
return;
|
||||
}
|
||||
|
||||
io.stdout.write('\n');
|
||||
|
||||
const resolved = resolveColumns(columns, groupBy);
|
||||
|
||||
const buckets = groupBy ? groupRows(rows, groupBy) : new Map<string, Row[]>([['', [...rows]]]);
|
||||
|
|
@ -231,14 +230,14 @@ function printListPretty<Row extends object>(args: PrintListArgs<Row>): void {
|
|||
for (const [groupValue, groupRowList] of buckets) {
|
||||
if (groupBy) {
|
||||
io.stdout.write(
|
||||
`${SYMBOLS.bar} ${SYMBOLS.group} ${bold(groupValue)} ${dim(`(${pluralize(groupRowList.length, unit)})`)}\n`,
|
||||
` ${bold(groupValue)} ${dim(`(${pluralize(groupRowList.length, unit)})`)}\n`,
|
||||
);
|
||||
}
|
||||
for (const row of groupRowList) {
|
||||
const segments: string[] = [];
|
||||
|
||||
resolved.badge.forEach((col, idx) => {
|
||||
segments.push(dim(formatCellValue(col, row).padStart(badgeWidths[idx] ?? 0)));
|
||||
segments.push(formatCellValue(col, row).padStart(badgeWidths[idx] ?? 0));
|
||||
});
|
||||
|
||||
if (resolved.name) {
|
||||
|
|
@ -265,10 +264,10 @@ function printListPretty<Row extends object>(args: PrintListArgs<Row>): void {
|
|||
if (optionalSuffix.length > 0) segments.push(optionalSuffix);
|
||||
|
||||
const indent = groupBy ? ' ' : ' ';
|
||||
io.stdout.write(`${SYMBOLS.bar}${indent}${SYMBOLS.item} ${segments.join(' ')}\n`);
|
||||
io.stdout.write(`${indent}${segments.join(' ')}\n`);
|
||||
}
|
||||
io.stdout.write('\n');
|
||||
}
|
||||
|
||||
io.stdout.write(`${SYMBOLS.bar}\n`);
|
||||
io.stdout.write(`${SYMBOLS.barEnd} ${pluralize(rows.length, unit)}\n`);
|
||||
io.stdout.write(`${pluralize(rows.length, unit)}\n`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,11 +15,6 @@ function detectUnicodeSupport(env: NodeJS.ProcessEnv = process.env): boolean {
|
|||
const unicode = detectUnicodeSupport();
|
||||
|
||||
export const SYMBOLS = {
|
||||
bar: unicode ? '│' : '|',
|
||||
barStart: unicode ? '◇' : 'o',
|
||||
barEnd: unicode ? '└' : '—',
|
||||
group: unicode ? '●' : '*',
|
||||
item: unicode ? '◆' : '*',
|
||||
middot: unicode ? '·' : '-',
|
||||
emDash: unicode ? '—' : '--',
|
||||
} as const;
|
||||
|
|
@ -43,3 +38,7 @@ export function green(text: string): string {
|
|||
export function red(text: string): string {
|
||||
return styleText('red', text);
|
||||
}
|
||||
|
||||
export function yellow(text: string): string {
|
||||
return styleText('yellow', text);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue