|
1 | 1 | import chalk from 'chalk' |
| 2 | +import { dump } from 'js-yaml' |
2 | 3 | import type { OutputFormat } from '../config/index.js' |
3 | 4 |
|
4 | 5 | export interface Column<T> { |
5 | 6 | header: string |
6 | 7 | value: (row: T) => string |
7 | 8 | } |
8 | 9 |
|
| 10 | +/** The glyph standing in for "no value", before colour is applied. */ |
| 11 | +const EMPTY_GLYPH = '—' |
| 12 | + |
9 | 13 | /** Cell text for values that have no useful rendering, kept visually quiet. */ |
10 | | -const EMPTY = chalk.dim('—') |
| 14 | +const EMPTY = chalk.dim(EMPTY_GLYPH) |
11 | 15 |
|
12 | 16 | export function text(value: unknown): string { |
13 | 17 | if (value === null || value === undefined || value === '') return EMPTY |
@@ -67,6 +71,18 @@ export function visibleWidth(value: string): number { |
67 | 71 | return value.replace(ANSI_PATTERN, '').length |
68 | 72 | } |
69 | 73 |
|
| 74 | +/** |
| 75 | + * Plain text for a rendered cell. |
| 76 | + * |
| 77 | + * The empty placeholder collapses to an actual empty field: `cut -f3` returning |
| 78 | + * a literal `—` for a null would be worse than useless, since every downstream |
| 79 | + * emptiness test would read it as a value. |
| 80 | + */ |
| 81 | +function stripAnsi(value: string): string { |
| 82 | + const plain = value.replace(ANSI_PATTERN, '') |
| 83 | + return plain === EMPTY_GLYPH ? '' : plain |
| 84 | +} |
| 85 | + |
70 | 86 | function pad(value: string, width: number): string { |
71 | 87 | return value + ' '.repeat(Math.max(0, width - visibleWidth(value))) |
72 | 88 | } |
@@ -94,25 +110,61 @@ function renderTable<T>(rows: T[], columns: Column<T>[]): string { |
94 | 110 | return [header, ...body].join('\n') |
95 | 111 | } |
96 | 112 |
|
| 113 | +/** |
| 114 | + * Renders the machine-readable formats from the RAW value. |
| 115 | + * |
| 116 | + * Deliberately not the table's formatted cells: `--output json` piped into `jq` |
| 117 | + * must yield the API's own field names and types, so a `1500` stays a number |
| 118 | + * rather than becoming the `"1.5s"` the table would show. `yaml` follows the |
| 119 | + * same rule, so switching format never changes the data. |
| 120 | + * |
| 121 | + * Returns null when the format wants the human rendering instead. |
| 122 | + */ |
| 123 | +function renderMachine(format: OutputFormat, raw: unknown): string | null { |
| 124 | + if (format === 'json') return JSON.stringify(raw, null, 2) |
| 125 | + // `lineWidth: 0` disables YAML's line folding — a wrapped value is technically |
| 126 | + // valid but is miserable to eyeball and breaks naive line-oriented greps. |
| 127 | + if (format === 'yaml') return dump(raw, { lineWidth: 0, noRefs: true }).trimEnd() |
| 128 | + return null |
| 129 | +} |
| 130 | + |
97 | 131 | /** |
98 | 132 | * Prints a list in the profile's output format. |
99 | 133 | * |
100 | | - * The JSON branch prints the raw rows, not the table's formatted cells — piping |
101 | | - * to `jq` should yield the API's own field names and types, so `--output json` |
102 | | - * is a passthrough rather than a second rendering. |
| 134 | + * `text` emits the table's cells tab-separated with no header and no colour — |
| 135 | + * the shape `cut -f2` and `while read` expect. It uses the formatted cells |
| 136 | + * rather than the raw values on purpose: it is a human-ish format for shell |
| 137 | + * plumbing, and a raw ISO timestamp or byte count is worse in that context. |
103 | 138 | */ |
104 | 139 | export function printList<T>(format: OutputFormat, rows: T[], columns: Column<T>[]): void { |
105 | | - if (format === 'json') { |
106 | | - console.log(JSON.stringify(rows, null, 2)) |
| 140 | + const machine = renderMachine(format, rows) |
| 141 | + if (machine !== null) { |
| 142 | + console.log(machine) |
| 143 | + return |
| 144 | + } |
| 145 | + |
| 146 | + if (format === 'text') { |
| 147 | + for (const row of rows) { |
| 148 | + console.log(columns.map((column) => stripAnsi(column.value(row))).join('\t')) |
| 149 | + } |
107 | 150 | return |
108 | 151 | } |
| 152 | + |
109 | 153 | console.log(renderTable(rows, columns)) |
110 | 154 | } |
111 | 155 |
|
112 | | -/** Prints a single record: JSON as-is, table format as aligned key/value lines. */ |
| 156 | +/** Prints a single record: machine formats from the raw value, otherwise aligned lines. */ |
113 | 157 | export function printRecord(format: OutputFormat, fields: Array<[string, string]>, raw: unknown) { |
114 | | - if (format === 'json') { |
115 | | - console.log(JSON.stringify(raw, null, 2)) |
| 158 | + const machine = renderMachine(format, raw) |
| 159 | + if (machine !== null) { |
| 160 | + console.log(machine) |
| 161 | + return |
| 162 | + } |
| 163 | + |
| 164 | + if (format === 'text') { |
| 165 | + for (const [label, value] of fields) { |
| 166 | + console.log(`${label}\t${stripAnsi(value)}`) |
| 167 | + } |
116 | 168 | return |
117 | 169 | } |
118 | 170 |
|
|
0 commit comments