@f5-sales-demo/pi-tui 23.3.3 → 23.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/src/components/markdown.ts +24 -199
- package/src/components/table.ts +187 -0
- package/src/index.ts +1 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@f5-sales-demo/pi-tui",
|
|
4
|
-
"version": "23.
|
|
4
|
+
"version": "23.4.0",
|
|
5
5
|
"description": "Terminal User Interface library with differential rendering for efficient text-based applications",
|
|
6
6
|
"homepage": "https://github.com/f5-sales-demo/xcsh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
"fmt": "biome format --write ."
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@f5-sales-demo/pi-natives": "23.
|
|
41
|
-
"@f5-sales-demo/pi-utils": "23.
|
|
40
|
+
"@f5-sales-demo/pi-natives": "23.4.0",
|
|
41
|
+
"@f5-sales-demo/pi-utils": "23.4.0",
|
|
42
42
|
"marked": "18.0.11"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
visibleWidth,
|
|
15
15
|
wrapTextWithAnsi,
|
|
16
16
|
} from "../utils";
|
|
17
|
+
import { renderTable } from "./table";
|
|
17
18
|
|
|
18
19
|
interface LatexToken extends Tokens.Generic {
|
|
19
20
|
type: "latex" | "latexBlock";
|
|
@@ -236,6 +237,8 @@ export interface MarkdownMediaOptions {
|
|
|
236
237
|
}
|
|
237
238
|
|
|
238
239
|
export interface MarkdownOptions {
|
|
240
|
+
/** Complete data-row preview; omitted means show every row. */
|
|
241
|
+
tablePreviewRows?: number;
|
|
239
242
|
/** Render supported LaTeX expressions as Unicode text (default: true). */
|
|
240
243
|
renderLatex?: boolean;
|
|
241
244
|
/** Number of spaces used to indent fenced code content (default: 2). */
|
|
@@ -303,7 +306,12 @@ interface InlineStyleContext {
|
|
|
303
306
|
|
|
304
307
|
type ListToken = Token & { items: Array<{ tokens?: Token[] }>; ordered: boolean; start?: number };
|
|
305
308
|
type TableCellToken = { tokens?: Token[] };
|
|
306
|
-
type TableToken = Token & {
|
|
309
|
+
type TableToken = Token & {
|
|
310
|
+
header: TableCellToken[];
|
|
311
|
+
rows: TableCellToken[][];
|
|
312
|
+
raw?: string;
|
|
313
|
+
align?: ("left" | "right" | "center" | null)[];
|
|
314
|
+
};
|
|
307
315
|
|
|
308
316
|
function isImageToken(token: Token): token is Tokens.Image {
|
|
309
317
|
return (
|
|
@@ -366,6 +374,7 @@ export class Markdown implements Component {
|
|
|
366
374
|
#codeBlockIndent: number;
|
|
367
375
|
#mediaOptions?: MarkdownMediaOptions;
|
|
368
376
|
#renderLatex: boolean;
|
|
377
|
+
#tablePreviewRows?: number;
|
|
369
378
|
#linkBasePath?: string;
|
|
370
379
|
#mediaCache = new Map<
|
|
371
380
|
string,
|
|
@@ -419,6 +428,7 @@ export class Markdown implements Component {
|
|
|
419
428
|
this.#mediaOptions = options.mediaOptions;
|
|
420
429
|
this.#renderLatex = options.renderLatex !== false;
|
|
421
430
|
this.#linkBasePath = options.linkBasePath;
|
|
431
|
+
this.#tablePreviewRows = options.tablePreviewRows;
|
|
422
432
|
}
|
|
423
433
|
|
|
424
434
|
setText(text: string): void {
|
|
@@ -1068,210 +1078,25 @@ export class Markdown implements Component {
|
|
|
1068
1078
|
return lines;
|
|
1069
1079
|
}
|
|
1070
1080
|
|
|
1071
|
-
/**
|
|
1072
|
-
* Get the visible width of the longest word in a string.
|
|
1073
|
-
*/
|
|
1074
|
-
#getLongestWordWidth(text: string, maxWidth?: number): number {
|
|
1075
|
-
const words = text.split(/\s+/).filter(word => word.length > 0);
|
|
1076
|
-
let longest = 0;
|
|
1077
|
-
for (const word of words) {
|
|
1078
|
-
longest = Math.max(longest, visibleWidth(word));
|
|
1079
|
-
}
|
|
1080
|
-
if (maxWidth === undefined) {
|
|
1081
|
-
return longest;
|
|
1082
|
-
}
|
|
1083
|
-
return Math.min(longest, maxWidth);
|
|
1084
|
-
}
|
|
1085
|
-
|
|
1086
|
-
/**
|
|
1087
|
-
* Wrap a table cell to fit into a column.
|
|
1088
|
-
*
|
|
1089
|
-
* Delegates to wrapTextWithAnsi() so ANSI codes + long tokens are handled
|
|
1090
|
-
* consistently with the rest of the renderer.
|
|
1091
|
-
*/
|
|
1092
|
-
#wrapCellText(text: string, maxWidth: number): string[] {
|
|
1093
|
-
return wrapTextWithAnsi(text, Math.max(1, maxWidth));
|
|
1094
|
-
}
|
|
1095
|
-
|
|
1096
|
-
/**
|
|
1097
|
-
* Render a table with width-aware cell wrapping.
|
|
1098
|
-
* Cells that don't fit are wrapped to multiple lines.
|
|
1099
|
-
*/
|
|
1081
|
+
/** Shared layout for Markdown and structured plugin tables. */
|
|
1100
1082
|
#renderTable(
|
|
1101
1083
|
token: TableToken,
|
|
1102
1084
|
availableWidth: number,
|
|
1103
1085
|
nextTokenType?: string,
|
|
1104
1086
|
styleContext?: InlineStyleContext,
|
|
1105
1087
|
): string[] {
|
|
1106
|
-
const lines
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
if (
|
|
1118
|
-
// Too narrow to render a stable table. Fall back to raw markdown.
|
|
1119
|
-
const fallbackLines = token.raw ? wrapTextWithAnsi(token.raw, availableWidth) : [];
|
|
1120
|
-
if (nextTokenType && nextTokenType !== "space") {
|
|
1121
|
-
fallbackLines.push("");
|
|
1122
|
-
}
|
|
1123
|
-
return fallbackLines;
|
|
1124
|
-
}
|
|
1125
|
-
|
|
1126
|
-
const maxUnbrokenWordWidth = 30;
|
|
1127
|
-
|
|
1128
|
-
// Calculate natural column widths (what each column needs without constraints)
|
|
1129
|
-
const naturalWidths: number[] = [];
|
|
1130
|
-
const minWordWidths: number[] = [];
|
|
1131
|
-
for (let i = 0; i < numCols; i++) {
|
|
1132
|
-
const headerText = this.#renderInlineTokens(token.header[i].tokens || [], styleContext);
|
|
1133
|
-
naturalWidths[i] = visibleWidth(headerText);
|
|
1134
|
-
minWordWidths[i] = Math.max(1, this.#getLongestWordWidth(headerText, maxUnbrokenWordWidth));
|
|
1135
|
-
}
|
|
1136
|
-
for (const row of token.rows) {
|
|
1137
|
-
for (let i = 0; i < row.length; i++) {
|
|
1138
|
-
const cellText = this.#renderInlineTokens(row[i].tokens || [], styleContext);
|
|
1139
|
-
naturalWidths[i] = Math.max(naturalWidths[i] || 0, visibleWidth(cellText));
|
|
1140
|
-
minWordWidths[i] = Math.max(
|
|
1141
|
-
minWordWidths[i] || 1,
|
|
1142
|
-
this.#getLongestWordWidth(cellText, maxUnbrokenWordWidth),
|
|
1143
|
-
);
|
|
1144
|
-
}
|
|
1145
|
-
}
|
|
1146
|
-
|
|
1147
|
-
let minColumnWidths = minWordWidths;
|
|
1148
|
-
let minCellsWidth = minColumnWidths.reduce((a, b) => a + b, 0);
|
|
1149
|
-
|
|
1150
|
-
if (minCellsWidth > availableForCells) {
|
|
1151
|
-
minColumnWidths = new Array(numCols).fill(1);
|
|
1152
|
-
const remaining = availableForCells - numCols;
|
|
1153
|
-
|
|
1154
|
-
if (remaining > 0) {
|
|
1155
|
-
const totalWeight = minWordWidths.reduce((total, width) => total + Math.max(0, width - 1), 0);
|
|
1156
|
-
const growth = minWordWidths.map(width => {
|
|
1157
|
-
const weight = Math.max(0, width - 1);
|
|
1158
|
-
return totalWeight > 0 ? Math.floor((weight / totalWeight) * remaining) : 0;
|
|
1159
|
-
});
|
|
1160
|
-
|
|
1161
|
-
for (let i = 0; i < numCols; i++) {
|
|
1162
|
-
minColumnWidths[i] += growth[i] ?? 0;
|
|
1163
|
-
}
|
|
1164
|
-
|
|
1165
|
-
const allocated = growth.reduce((total, width) => total + width, 0);
|
|
1166
|
-
let leftover = remaining - allocated;
|
|
1167
|
-
for (let i = 0; leftover > 0 && i < numCols; i++) {
|
|
1168
|
-
minColumnWidths[i]++;
|
|
1169
|
-
leftover--;
|
|
1170
|
-
}
|
|
1171
|
-
}
|
|
1172
|
-
|
|
1173
|
-
minCellsWidth = minColumnWidths.reduce((a, b) => a + b, 0);
|
|
1174
|
-
}
|
|
1175
|
-
|
|
1176
|
-
// Calculate column widths that fit within available width
|
|
1177
|
-
const totalNaturalWidth = naturalWidths.reduce((a, b) => a + b, 0) + borderOverhead;
|
|
1178
|
-
let columnWidths: number[];
|
|
1179
|
-
|
|
1180
|
-
if (totalNaturalWidth <= availableWidth) {
|
|
1181
|
-
// Everything fits naturally
|
|
1182
|
-
columnWidths = naturalWidths.map((width, index) => Math.max(width, minColumnWidths[index]));
|
|
1183
|
-
} else {
|
|
1184
|
-
// Need to shrink columns to fit
|
|
1185
|
-
const totalGrowPotential = naturalWidths.reduce((total, width, index) => {
|
|
1186
|
-
return total + Math.max(0, width - minColumnWidths[index]);
|
|
1187
|
-
}, 0);
|
|
1188
|
-
const extraWidth = Math.max(0, availableForCells - minCellsWidth);
|
|
1189
|
-
columnWidths = minColumnWidths.map((minWidth, index) => {
|
|
1190
|
-
const naturalWidth = naturalWidths[index];
|
|
1191
|
-
const minWidthDelta = Math.max(0, naturalWidth - minWidth);
|
|
1192
|
-
let grow = 0;
|
|
1193
|
-
if (totalGrowPotential > 0) {
|
|
1194
|
-
grow = Math.floor((minWidthDelta / totalGrowPotential) * extraWidth);
|
|
1195
|
-
}
|
|
1196
|
-
return minWidth + grow;
|
|
1197
|
-
});
|
|
1198
|
-
|
|
1199
|
-
// Adjust for rounding errors - distribute remaining space
|
|
1200
|
-
const allocated = columnWidths.reduce((a, b) => a + b, 0);
|
|
1201
|
-
let remaining = availableForCells - allocated;
|
|
1202
|
-
while (remaining > 0) {
|
|
1203
|
-
let grew = false;
|
|
1204
|
-
for (let i = 0; i < numCols && remaining > 0; i++) {
|
|
1205
|
-
if (columnWidths[i] < naturalWidths[i]) {
|
|
1206
|
-
columnWidths[i]++;
|
|
1207
|
-
remaining--;
|
|
1208
|
-
grew = true;
|
|
1209
|
-
}
|
|
1210
|
-
}
|
|
1211
|
-
if (!grew) {
|
|
1212
|
-
break;
|
|
1213
|
-
}
|
|
1214
|
-
}
|
|
1215
|
-
}
|
|
1216
|
-
|
|
1217
|
-
const t = this.#theme.symbols.table;
|
|
1218
|
-
const h = t.horizontal;
|
|
1219
|
-
const v = t.vertical;
|
|
1220
|
-
|
|
1221
|
-
// Render top border
|
|
1222
|
-
const topBorderCells = columnWidths.map(w => h.repeat(w));
|
|
1223
|
-
lines.push(`${t.topLeft}${h}${topBorderCells.join(`${h}${t.teeDown}${h}`)}${h}${t.topRight}`);
|
|
1224
|
-
|
|
1225
|
-
// Render header with wrapping
|
|
1226
|
-
const headerCellLines: string[][] = token.header.map((cell, i) => {
|
|
1227
|
-
const text = this.#renderInlineTokens(cell.tokens || [], styleContext);
|
|
1228
|
-
return this.#wrapCellText(text, columnWidths[i]);
|
|
1229
|
-
});
|
|
1230
|
-
const headerLineCount = Math.max(...headerCellLines.map(c => c.length));
|
|
1231
|
-
|
|
1232
|
-
for (let lineIdx = 0; lineIdx < headerLineCount; lineIdx++) {
|
|
1233
|
-
const rowParts = headerCellLines.map((cellLines, colIdx) => {
|
|
1234
|
-
const text = cellLines[lineIdx] || "";
|
|
1235
|
-
const padded = text + padding(Math.max(0, columnWidths[colIdx] - visibleWidth(text)));
|
|
1236
|
-
return this.#theme.bold(padded);
|
|
1237
|
-
});
|
|
1238
|
-
lines.push(`${v} ${rowParts.join(` ${v} `)} ${v}`);
|
|
1239
|
-
}
|
|
1240
|
-
|
|
1241
|
-
// Render separator
|
|
1242
|
-
const separatorCells = columnWidths.map(w => h.repeat(w));
|
|
1243
|
-
const separatorLine = `${t.teeRight}${h}${separatorCells.join(`${h}${t.cross}${h}`)}${h}${t.teeLeft}`;
|
|
1244
|
-
lines.push(separatorLine);
|
|
1245
|
-
|
|
1246
|
-
// Render rows with wrapping
|
|
1247
|
-
for (let rowIndex = 0; rowIndex < token.rows.length; rowIndex++) {
|
|
1248
|
-
const row = token.rows[rowIndex];
|
|
1249
|
-
const rowCellLines: string[][] = row.map((cell, i) => {
|
|
1250
|
-
const text = this.#renderInlineTokens(cell.tokens || [], styleContext);
|
|
1251
|
-
return this.#wrapCellText(text, columnWidths[i]);
|
|
1252
|
-
});
|
|
1253
|
-
const rowLineCount = Math.max(...rowCellLines.map(c => c.length));
|
|
1254
|
-
|
|
1255
|
-
for (let lineIdx = 0; lineIdx < rowLineCount; lineIdx++) {
|
|
1256
|
-
const rowParts = rowCellLines.map((cellLines, colIdx) => {
|
|
1257
|
-
const text = cellLines[lineIdx] || "";
|
|
1258
|
-
return text + padding(Math.max(0, columnWidths[colIdx] - visibleWidth(text)));
|
|
1259
|
-
});
|
|
1260
|
-
lines.push(`${v} ${rowParts.join(` ${v} `)} ${v}`);
|
|
1261
|
-
}
|
|
1262
|
-
|
|
1263
|
-
if (rowIndex < token.rows.length - 1) {
|
|
1264
|
-
lines.push(separatorLine);
|
|
1265
|
-
}
|
|
1266
|
-
}
|
|
1267
|
-
|
|
1268
|
-
// Render bottom border
|
|
1269
|
-
const bottomBorderCells = columnWidths.map(w => h.repeat(w));
|
|
1270
|
-
lines.push(`${t.bottomLeft}${h}${bottomBorderCells.join(`${h}${t.teeUp}${h}`)}${h}${t.bottomRight}`);
|
|
1271
|
-
|
|
1272
|
-
if (nextTokenType && nextTokenType !== "space") {
|
|
1273
|
-
lines.push(""); // Add spacing after table
|
|
1274
|
-
}
|
|
1088
|
+
const lines = renderTable(
|
|
1089
|
+
token.header.map(cell => this.#renderInlineTokens(cell.tokens || [], styleContext)),
|
|
1090
|
+
(this.#tablePreviewRows === undefined ? token.rows : token.rows.slice(0, this.#tablePreviewRows)).map(row =>
|
|
1091
|
+
row.map(cell => this.#renderInlineTokens(cell.tokens || [], styleContext)),
|
|
1092
|
+
),
|
|
1093
|
+
availableWidth,
|
|
1094
|
+
{ symbols: this.#theme.symbols.table, bold: text => this.#theme.bold(text) },
|
|
1095
|
+
token.align,
|
|
1096
|
+
);
|
|
1097
|
+
if (this.#tablePreviewRows !== undefined && token.rows.length > this.#tablePreviewRows)
|
|
1098
|
+
lines.push(...wrapTextWithAnsi(`${token.rows.length - this.#tablePreviewRows} more rows`, availableWidth));
|
|
1099
|
+
if (nextTokenType && nextTokenType !== "space") lines.push("");
|
|
1275
1100
|
return lines;
|
|
1276
1101
|
}
|
|
1277
1102
|
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import type { BoxSymbols } from "../symbols";
|
|
2
|
+
import { padding, visibleWidth, wrapTextWithAnsi } from "../utils";
|
|
3
|
+
|
|
4
|
+
export interface TableTheme {
|
|
5
|
+
symbols: BoxSymbols;
|
|
6
|
+
bold(text: string): string;
|
|
7
|
+
}
|
|
8
|
+
function longestWordWidth(text: string, maxWidth: number): number {
|
|
9
|
+
return Math.min(maxWidth, Math.max(1, ...text.split(/\s+/).map(visibleWidth)));
|
|
10
|
+
}
|
|
11
|
+
/** Width includes borders and cell padding. Narrow records reflow without losing fields. */
|
|
12
|
+
export function renderTable(
|
|
13
|
+
headers: readonly string[],
|
|
14
|
+
rows: readonly (readonly string[])[],
|
|
15
|
+
availableWidth: number,
|
|
16
|
+
theme: TableTheme,
|
|
17
|
+
alignment?: readonly ("left" | "right" | "center" | null)[],
|
|
18
|
+
): string[] {
|
|
19
|
+
const lines: string[] = [];
|
|
20
|
+
const numCols = headers.length;
|
|
21
|
+
|
|
22
|
+
if (numCols === 0) {
|
|
23
|
+
return lines;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Calculate border overhead: "│ " + (n-1) * " │ " + " │"
|
|
27
|
+
// = 2 + (n-1) * 3 + 2 = 3n + 1
|
|
28
|
+
const borderOverhead = 3 * numCols + 1;
|
|
29
|
+
const availableForCells = availableWidth - borderOverhead;
|
|
30
|
+
const reflow = (): string[] => {
|
|
31
|
+
const records = rows.length ? rows : [headers.map(() => "")];
|
|
32
|
+
for (const row of records) {
|
|
33
|
+
for (let i = 0; i < numCols; i++) {
|
|
34
|
+
lines.push(...wrapTextWithAnsi(`${theme.bold(headers[i])}: ${row[i] ?? ""}`, Math.max(1, availableWidth)));
|
|
35
|
+
}
|
|
36
|
+
lines.push("");
|
|
37
|
+
}
|
|
38
|
+
return lines;
|
|
39
|
+
};
|
|
40
|
+
if (availableForCells < numCols * 6) return reflow();
|
|
41
|
+
|
|
42
|
+
const maxUnbrokenWordWidth = 30;
|
|
43
|
+
|
|
44
|
+
// Calculate natural column widths (what each column needs without constraints)
|
|
45
|
+
const naturalWidths: number[] = [];
|
|
46
|
+
const minWordWidths: number[] = [];
|
|
47
|
+
for (let i = 0; i < numCols; i++) {
|
|
48
|
+
const headerText = headers[i];
|
|
49
|
+
naturalWidths[i] = visibleWidth(headerText);
|
|
50
|
+
minWordWidths[i] = Math.max(1, longestWordWidth(headerText, maxUnbrokenWordWidth));
|
|
51
|
+
}
|
|
52
|
+
for (const row of rows) {
|
|
53
|
+
for (let i = 0; i < row.length; i++) {
|
|
54
|
+
const cellText = row[i];
|
|
55
|
+
naturalWidths[i] = Math.max(naturalWidths[i] || 0, visibleWidth(cellText));
|
|
56
|
+
minWordWidths[i] = Math.max(minWordWidths[i] || 1, longestWordWidth(cellText, maxUnbrokenWordWidth));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let minColumnWidths = minWordWidths;
|
|
61
|
+
let minCellsWidth = minColumnWidths.reduce((a, b) => a + b, 0);
|
|
62
|
+
|
|
63
|
+
if (minCellsWidth > availableForCells) {
|
|
64
|
+
minColumnWidths = new Array(numCols).fill(1);
|
|
65
|
+
const remaining = availableForCells - numCols;
|
|
66
|
+
|
|
67
|
+
if (remaining > 0) {
|
|
68
|
+
const totalWeight = minWordWidths.reduce((total, width) => total + Math.max(0, width - 1), 0);
|
|
69
|
+
const growth = minWordWidths.map(width => {
|
|
70
|
+
const weight = Math.max(0, width - 1);
|
|
71
|
+
return totalWeight > 0 ? Math.floor((weight / totalWeight) * remaining) : 0;
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
for (let i = 0; i < numCols; i++) {
|
|
75
|
+
minColumnWidths[i] += growth[i] ?? 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const allocated = growth.reduce((total, width) => total + width, 0);
|
|
79
|
+
let leftover = remaining - allocated;
|
|
80
|
+
for (let i = 0; leftover > 0 && i < numCols; i++) {
|
|
81
|
+
minColumnWidths[i]++;
|
|
82
|
+
leftover--;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
minCellsWidth = minColumnWidths.reduce((a, b) => a + b, 0);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Calculate column widths that fit within available width
|
|
90
|
+
const totalNaturalWidth = naturalWidths.reduce((a, b) => a + b, 0) + borderOverhead;
|
|
91
|
+
let columnWidths: number[];
|
|
92
|
+
|
|
93
|
+
if (totalNaturalWidth <= availableWidth) {
|
|
94
|
+
// Everything fits naturally
|
|
95
|
+
columnWidths = naturalWidths.map((width, index) => Math.max(width, minColumnWidths[index]));
|
|
96
|
+
} else {
|
|
97
|
+
// Need to shrink columns to fit
|
|
98
|
+
const totalGrowPotential = naturalWidths.reduce((total, width, index) => {
|
|
99
|
+
return total + Math.max(0, width - minColumnWidths[index]);
|
|
100
|
+
}, 0);
|
|
101
|
+
const extraWidth = Math.max(0, availableForCells - minCellsWidth);
|
|
102
|
+
columnWidths = minColumnWidths.map((minWidth, index) => {
|
|
103
|
+
const naturalWidth = naturalWidths[index];
|
|
104
|
+
const minWidthDelta = Math.max(0, naturalWidth - minWidth);
|
|
105
|
+
let grow = 0;
|
|
106
|
+
if (totalGrowPotential > 0) {
|
|
107
|
+
grow = Math.floor((minWidthDelta / totalGrowPotential) * extraWidth);
|
|
108
|
+
}
|
|
109
|
+
return minWidth + grow;
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Adjust for rounding errors - distribute remaining space
|
|
113
|
+
const allocated = columnWidths.reduce((a, b) => a + b, 0);
|
|
114
|
+
let remaining = availableForCells - allocated;
|
|
115
|
+
while (remaining > 0) {
|
|
116
|
+
let grew = false;
|
|
117
|
+
for (let i = 0; i < numCols && remaining > 0; i++) {
|
|
118
|
+
if (columnWidths[i] < naturalWidths[i]) {
|
|
119
|
+
columnWidths[i]++;
|
|
120
|
+
remaining--;
|
|
121
|
+
grew = true;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (!grew) {
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (columnWidths.some((width, index) => width < Math.min(6, naturalWidths[index]))) return reflow();
|
|
131
|
+
|
|
132
|
+
const t = theme.symbols;
|
|
133
|
+
const h = t.horizontal;
|
|
134
|
+
const v = t.vertical;
|
|
135
|
+
|
|
136
|
+
// Render top border
|
|
137
|
+
const topBorderCells = columnWidths.map(w => h.repeat(w));
|
|
138
|
+
lines.push(`${t.topLeft}${h}${topBorderCells.join(`${h}${t.teeDown}${h}`)}${h}${t.topRight}`);
|
|
139
|
+
|
|
140
|
+
// Render header with wrapping
|
|
141
|
+
const headerCellLines: string[][] = headers.map((text, i) => wrapTextWithAnsi(text, columnWidths[i]));
|
|
142
|
+
const headerLineCount = Math.max(...headerCellLines.map(c => c.length));
|
|
143
|
+
|
|
144
|
+
for (let lineIdx = 0; lineIdx < headerLineCount; lineIdx++) {
|
|
145
|
+
const rowParts = headerCellLines.map((cellLines, colIdx) => {
|
|
146
|
+
const text = cellLines[lineIdx] || "";
|
|
147
|
+
const padded = text + padding(Math.max(0, columnWidths[colIdx] - visibleWidth(text)));
|
|
148
|
+
return theme.bold(padded);
|
|
149
|
+
});
|
|
150
|
+
lines.push(`${v} ${rowParts.join(` ${v} `)} ${v}`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Render separator
|
|
154
|
+
const separatorCells = columnWidths.map(w => h.repeat(w));
|
|
155
|
+
const separatorLine = `${t.teeRight}${h}${separatorCells.join(`${h}${t.cross}${h}`)}${h}${t.teeLeft}`;
|
|
156
|
+
lines.push(separatorLine);
|
|
157
|
+
|
|
158
|
+
// Render rows with wrapping
|
|
159
|
+
for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) {
|
|
160
|
+
const row = rows[rowIndex];
|
|
161
|
+
const rowCellLines: string[][] = headers.map((_, i) => wrapTextWithAnsi(row[i] ?? "", columnWidths[i]));
|
|
162
|
+
const rowLineCount = Math.max(...rowCellLines.map(c => c.length));
|
|
163
|
+
|
|
164
|
+
for (let lineIdx = 0; lineIdx < rowLineCount; lineIdx++) {
|
|
165
|
+
const rowParts = rowCellLines.map((cellLines, colIdx) => {
|
|
166
|
+
const text = cellLines[lineIdx] || "";
|
|
167
|
+
const pad = padding(Math.max(0, columnWidths[colIdx] - visibleWidth(text)));
|
|
168
|
+
const numeric =
|
|
169
|
+
alignment?.[colIdx] === "right" ||
|
|
170
|
+
(alignment?.[colIdx] !== "left" &&
|
|
171
|
+
rows.every(row => !row[colIdx] || /^-?\d+(?:[.,]\d+)*%?$/.test(row[colIdx])));
|
|
172
|
+
return numeric ? pad + text : text + pad;
|
|
173
|
+
});
|
|
174
|
+
lines.push(`${v} ${rowParts.join(` ${v} `)} ${v}`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (rowIndex < rows.length - 1) {
|
|
178
|
+
lines.push(separatorLine);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Render bottom border
|
|
183
|
+
const bottomBorderCells = columnWidths.map(w => h.repeat(w));
|
|
184
|
+
lines.push(`${t.bottomLeft}${h}${bottomBorderCells.join(`${h}${t.teeUp}${h}`)}${h}${t.bottomRight}`);
|
|
185
|
+
|
|
186
|
+
return lines;
|
|
187
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -31,6 +31,7 @@ export * from "./components/select-list";
|
|
|
31
31
|
export * from "./components/settings-list";
|
|
32
32
|
export * from "./components/spacer";
|
|
33
33
|
export * from "./components/tab-bar";
|
|
34
|
+
export * from "./components/table";
|
|
34
35
|
export * from "./components/text";
|
|
35
36
|
export * from "./components/truncated-text";
|
|
36
37
|
// Editor component interface (for custom editors)
|