@owloops/claude-powerline 1.24.3 → 1.25.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/README.md +5 -43
- package/dist/browser.d.ts +676 -0
- package/dist/browser.js +3 -0
- package/dist/index.mjs +12 -12
- package/package.json +9 -1
- package/plugin/templates/config-full.json +1 -1
- package/plugin/templates/config-tui-compact.json +3 -3
- package/plugin/templates/config-tui-full.json +4 -4
- package/plugin/templates/config-tui-standard.json +4 -4
- package/src/browser.ts +203 -0
- package/src/config/defaults.ts +79 -0
- package/src/config/loader.ts +462 -0
- package/src/index.ts +90 -0
- package/src/powerline.ts +904 -0
- package/src/segments/block.ts +31 -0
- package/src/segments/context.ts +221 -0
- package/src/segments/git.ts +492 -0
- package/src/segments/index.ts +25 -0
- package/src/segments/metrics.ts +175 -0
- package/src/segments/pricing.ts +454 -0
- package/src/segments/renderer.ts +796 -0
- package/src/segments/session.ts +207 -0
- package/src/segments/tmux.ts +35 -0
- package/src/segments/today.ts +191 -0
- package/src/themes/dark.ts +52 -0
- package/src/themes/gruvbox.ts +52 -0
- package/src/themes/index.ts +131 -0
- package/src/themes/light.ts +52 -0
- package/src/themes/nord.ts +52 -0
- package/src/themes/rose-pine.ts +52 -0
- package/src/themes/tokyo-night.ts +52 -0
- package/src/tui/grid.ts +712 -0
- package/src/tui/index.ts +4 -0
- package/src/tui/layouts.ts +285 -0
- package/src/tui/primitives.ts +175 -0
- package/src/tui/renderer.ts +206 -0
- package/src/tui/sections.ts +1080 -0
- package/src/tui/types.ts +181 -0
- package/src/utils/budget.ts +47 -0
- package/src/utils/cache.ts +247 -0
- package/src/utils/claude.ts +489 -0
- package/src/utils/color-support.ts +118 -0
- package/src/utils/colors.ts +120 -0
- package/src/utils/constants.ts +176 -0
- package/src/utils/formatters.ts +160 -0
- package/src/utils/logger.ts +5 -0
- package/src/utils/terminal-width.ts +117 -0
- package/src/utils/terminal.ts +11 -0
package/src/tui/grid.ts
ADDED
|
@@ -0,0 +1,712 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
GridCell,
|
|
3
|
+
AlignValue,
|
|
4
|
+
TuiGridBreakpoint,
|
|
5
|
+
TuiGridConfig,
|
|
6
|
+
BoxChars,
|
|
7
|
+
} from "./types";
|
|
8
|
+
import { visibleLength } from "../utils/terminal";
|
|
9
|
+
import { truncateAnsi, padRight, padLeft, padCenter } from "./primitives";
|
|
10
|
+
|
|
11
|
+
export const DIVIDER = "---";
|
|
12
|
+
export const EMPTY_CELL = ".";
|
|
13
|
+
|
|
14
|
+
// Segments whose content is resolved after column widths are known (lateResolve).
|
|
15
|
+
// Auto-width measurement must skip these to avoid locking columns to placeholder widths.
|
|
16
|
+
export const LATE_RESOLVE_SEGMENTS = new Set([
|
|
17
|
+
"context",
|
|
18
|
+
"context.bar",
|
|
19
|
+
"block.bar",
|
|
20
|
+
"weekly.bar",
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
function isDividerRow(row: GridCell[]): boolean {
|
|
24
|
+
return row.length === 1 && row[0]!.segment === DIVIDER;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function parseFr(colDef: string): number {
|
|
28
|
+
if (!colDef.endsWith("fr")) return 0;
|
|
29
|
+
const fr = parseInt(colDef.replace("fr", ""), 10);
|
|
30
|
+
return !isNaN(fr) && fr > 0 ? fr : 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function distributeExact(
|
|
34
|
+
total: number,
|
|
35
|
+
targets: number[],
|
|
36
|
+
widths: number[],
|
|
37
|
+
): void {
|
|
38
|
+
const base = Math.floor(total / targets.length);
|
|
39
|
+
let extra = total - base * targets.length;
|
|
40
|
+
for (const idx of targets) {
|
|
41
|
+
widths[idx] = widths[idx]! + base + (extra > 0 ? 1 : 0);
|
|
42
|
+
if (extra > 0) extra--;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function spanCellWidth(
|
|
47
|
+
colWidths: number[],
|
|
48
|
+
startIdx: number,
|
|
49
|
+
spanSize: number,
|
|
50
|
+
sepWidth: number,
|
|
51
|
+
): number {
|
|
52
|
+
let width = 0;
|
|
53
|
+
for (let j = 0; j < spanSize; j++) {
|
|
54
|
+
width += colWidths[startIdx + j] ?? 0;
|
|
55
|
+
}
|
|
56
|
+
if (spanSize > 1) {
|
|
57
|
+
width += (spanSize - 1) * sepWidth;
|
|
58
|
+
}
|
|
59
|
+
return width;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface GridResult {
|
|
63
|
+
lines: string[];
|
|
64
|
+
panelWidth: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// --- Breakpoint Selection ---
|
|
68
|
+
|
|
69
|
+
export function selectBreakpoint(
|
|
70
|
+
breakpoints: TuiGridBreakpoint[],
|
|
71
|
+
panelWidth: number,
|
|
72
|
+
): TuiGridBreakpoint {
|
|
73
|
+
let best: TuiGridBreakpoint | undefined;
|
|
74
|
+
for (const bp of breakpoints) {
|
|
75
|
+
if (panelWidth >= bp.minWidth) {
|
|
76
|
+
if (!best || bp.minWidth > best.minWidth) {
|
|
77
|
+
best = bp;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (best) return best;
|
|
82
|
+
|
|
83
|
+
// Fallback to smallest minWidth
|
|
84
|
+
let smallest = breakpoints[0]!;
|
|
85
|
+
for (let i = 1; i < breakpoints.length; i++) {
|
|
86
|
+
if (breakpoints[i]!.minWidth < smallest.minWidth) {
|
|
87
|
+
smallest = breakpoints[i]!;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return smallest;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// --- Area Parsing ---
|
|
94
|
+
|
|
95
|
+
export function parseAreas(areas: string[]): GridCell[][] {
|
|
96
|
+
const matrix: GridCell[][] = [];
|
|
97
|
+
|
|
98
|
+
for (const row of areas) {
|
|
99
|
+
const trimmed = row.trim();
|
|
100
|
+
|
|
101
|
+
// Divider row
|
|
102
|
+
if (trimmed === DIVIDER) {
|
|
103
|
+
matrix.push([{ segment: DIVIDER, spanStart: true, spanSize: 1 }]);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const cells = trimmed.split(/\s+/);
|
|
108
|
+
const rowCells: GridCell[] = [];
|
|
109
|
+
|
|
110
|
+
let i = 0;
|
|
111
|
+
while (i < cells.length) {
|
|
112
|
+
const name = cells[i]!;
|
|
113
|
+
let spanSize = 1;
|
|
114
|
+
|
|
115
|
+
// Count adjacent cells with the same name
|
|
116
|
+
while (i + spanSize < cells.length && cells[i + spanSize] === name) {
|
|
117
|
+
spanSize++;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// First cell of the span
|
|
121
|
+
rowCells.push({ segment: name, spanStart: true, spanSize });
|
|
122
|
+
|
|
123
|
+
// Continuation cells
|
|
124
|
+
for (let j = 1; j < spanSize; j++) {
|
|
125
|
+
rowCells.push({ segment: name, spanStart: false, spanSize: 0 });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
i += spanSize;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
matrix.push(rowCells);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return matrix;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// --- Matrix Culling ---
|
|
138
|
+
|
|
139
|
+
export function cullMatrix(
|
|
140
|
+
matrix: GridCell[][],
|
|
141
|
+
resolvedData: Record<string, string>,
|
|
142
|
+
): GridCell[][] {
|
|
143
|
+
// Phase 1: Replace cells whose segment has no data with "."
|
|
144
|
+
const processed = matrix.map((row) => {
|
|
145
|
+
if (isDividerRow(row)) return row;
|
|
146
|
+
|
|
147
|
+
return row.map((cell) => {
|
|
148
|
+
if (cell.segment === EMPTY_CELL || cell.segment === DIVIDER) return cell;
|
|
149
|
+
|
|
150
|
+
const data = resolvedData[cell.segment];
|
|
151
|
+
if (!data) {
|
|
152
|
+
return { segment: EMPTY_CELL, spanStart: true, spanSize: 1 };
|
|
153
|
+
}
|
|
154
|
+
return cell;
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Phase 2: Re-calculate spans after emptying cells
|
|
159
|
+
// When a span-start cell was emptied, all its continuation cells are already individual "." cells.
|
|
160
|
+
// But when continuation cells were emptied, the span-start needs fixing.
|
|
161
|
+
const respanned = processed.map((row) => {
|
|
162
|
+
if (isDividerRow(row)) return row;
|
|
163
|
+
|
|
164
|
+
// Rebuild spans from scratch
|
|
165
|
+
const cells = row.map((c) => c.segment);
|
|
166
|
+
const rebuilt: GridCell[] = [];
|
|
167
|
+
|
|
168
|
+
let i = 0;
|
|
169
|
+
while (i < cells.length) {
|
|
170
|
+
const name = cells[i]!;
|
|
171
|
+
let spanSize = 1;
|
|
172
|
+
|
|
173
|
+
while (i + spanSize < cells.length && cells[i + spanSize] === name) {
|
|
174
|
+
spanSize++;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
rebuilt.push({ segment: name, spanStart: true, spanSize });
|
|
178
|
+
for (let j = 1; j < spanSize; j++) {
|
|
179
|
+
rebuilt.push({ segment: name, spanStart: false, spanSize: 0 });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
i += spanSize;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return rebuilt;
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// Phase 3: Remove rows that are entirely "."
|
|
189
|
+
const nonEmpty = respanned.filter((row) => {
|
|
190
|
+
if (isDividerRow(row)) return true;
|
|
191
|
+
return row.some((cell) => cell.segment !== EMPTY_CELL);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// Phase 4: Collapse adjacent dividers into one, remove leading/trailing dividers
|
|
195
|
+
const cleaned: GridCell[][] = [];
|
|
196
|
+
for (let i = 0; i < nonEmpty.length; i++) {
|
|
197
|
+
const row = nonEmpty[i]!;
|
|
198
|
+
if (!isDividerRow(row)) {
|
|
199
|
+
cleaned.push(row);
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Skip dividers at top
|
|
204
|
+
if (cleaned.length === 0) continue;
|
|
205
|
+
|
|
206
|
+
// Collapse adjacent dividers: skip if last pushed row is already a divider
|
|
207
|
+
if (isDividerRow(cleaned[cleaned.length - 1]!)) continue;
|
|
208
|
+
|
|
209
|
+
cleaned.push(row);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Remove trailing divider
|
|
213
|
+
if (cleaned.length > 0 && isDividerRow(cleaned[cleaned.length - 1]!)) {
|
|
214
|
+
cleaned.pop();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return cleaned;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// --- Column Width Distribution ---
|
|
221
|
+
|
|
222
|
+
function measureAutoWidths(
|
|
223
|
+
colCount: number,
|
|
224
|
+
matrix: GridCell[][],
|
|
225
|
+
resolvedData: Record<string, string>,
|
|
226
|
+
lateResolveNames?: ReadonlySet<string>,
|
|
227
|
+
): number[] {
|
|
228
|
+
const widths = Array.from<number>({ length: colCount }).fill(0);
|
|
229
|
+
for (const row of matrix) {
|
|
230
|
+
if (isDividerRow(row)) continue;
|
|
231
|
+
for (let colIdx = 0; colIdx < row.length; colIdx++) {
|
|
232
|
+
const cell = row[colIdx]!;
|
|
233
|
+
if (!cell.spanStart || cell.spanSize !== 1) continue;
|
|
234
|
+
if (cell.segment === EMPTY_CELL) continue;
|
|
235
|
+
if (colIdx >= colCount) continue;
|
|
236
|
+
if (lateResolveNames?.has(cell.segment)) continue;
|
|
237
|
+
const content = resolvedData[cell.segment] || "";
|
|
238
|
+
const len = visibleLength(content);
|
|
239
|
+
if (len > widths[colIdx]!) {
|
|
240
|
+
widths[colIdx] = len;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return widths;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function calculateColumnWidths(
|
|
248
|
+
columns: string[],
|
|
249
|
+
matrix: GridCell[][],
|
|
250
|
+
resolvedData: Record<string, string>,
|
|
251
|
+
contentWidth: number,
|
|
252
|
+
separatorWidth: number,
|
|
253
|
+
lateResolveNames?: ReadonlySet<string>,
|
|
254
|
+
): number[] {
|
|
255
|
+
const colCount = columns.length;
|
|
256
|
+
const autoWidths = measureAutoWidths(
|
|
257
|
+
colCount,
|
|
258
|
+
matrix,
|
|
259
|
+
resolvedData,
|
|
260
|
+
lateResolveNames,
|
|
261
|
+
);
|
|
262
|
+
const widths = Array.from<number>({ length: colCount }).fill(0);
|
|
263
|
+
|
|
264
|
+
// Phase 1: Apply auto widths
|
|
265
|
+
for (let i = 0; i < colCount; i++) {
|
|
266
|
+
if (columns[i] === "auto") {
|
|
267
|
+
widths[i] = autoWidths[i]!;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Phase 2: Apply fixed widths
|
|
272
|
+
for (let i = 0; i < colCount; i++) {
|
|
273
|
+
const colDef = columns[i]!;
|
|
274
|
+
if (colDef === "auto") continue;
|
|
275
|
+
if (colDef.endsWith("fr")) continue;
|
|
276
|
+
|
|
277
|
+
const fixed = parseInt(colDef, 10);
|
|
278
|
+
if (!isNaN(fixed) && fixed > 0) {
|
|
279
|
+
widths[i] = fixed;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Clamp auto/fixed widths to >= 1 BEFORE computing fr remaining,
|
|
284
|
+
// so fr columns account for the clamped minimums in their budget.
|
|
285
|
+
for (let i = 0; i < colCount; i++) {
|
|
286
|
+
if (widths[i]! < 1 && !columns[i]!.endsWith("fr")) {
|
|
287
|
+
widths[i] = 1;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const totalSepWidth = Math.max(0, colCount - 1) * separatorWidth;
|
|
292
|
+
const usedWidth = widths.reduce((sum, w) => sum + w, 0);
|
|
293
|
+
const remaining = Math.max(0, contentWidth - usedWidth - totalSepWidth);
|
|
294
|
+
|
|
295
|
+
let totalFr = 0;
|
|
296
|
+
for (const colDef of columns) totalFr += parseFr(colDef);
|
|
297
|
+
|
|
298
|
+
if (totalFr > 0) {
|
|
299
|
+
const perFr = remaining / totalFr;
|
|
300
|
+
const frCols: number[] = [];
|
|
301
|
+
let allocatedFr = 0;
|
|
302
|
+
for (let i = 0; i < colCount; i++) {
|
|
303
|
+
const fr = parseFr(columns[i]!);
|
|
304
|
+
if (fr > 0) {
|
|
305
|
+
const w = Math.floor(perFr * fr);
|
|
306
|
+
widths[i] = w;
|
|
307
|
+
allocatedFr += w;
|
|
308
|
+
frCols.push(i);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
let leftover = remaining - allocatedFr;
|
|
312
|
+
for (let k = 0; leftover > 0 && k < frCols.length; k++) {
|
|
313
|
+
widths[frCols[k]!]! += 1;
|
|
314
|
+
leftover--;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return widths;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export function solveFitContentLayout(
|
|
322
|
+
columns: string[],
|
|
323
|
+
matrix: GridCell[][],
|
|
324
|
+
resolvedData: Record<string, string>,
|
|
325
|
+
separatorWidth: number,
|
|
326
|
+
horizontalPadding: number,
|
|
327
|
+
lateResolveNames?: ReadonlySet<string>,
|
|
328
|
+
): { panelWidth: number; colWidths: number[] } {
|
|
329
|
+
const colCount = columns.length;
|
|
330
|
+
const autoWidths = measureAutoWidths(
|
|
331
|
+
colCount,
|
|
332
|
+
matrix,
|
|
333
|
+
resolvedData,
|
|
334
|
+
lateResolveNames,
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
// Seed from intrinsic non-spanning content and fixed widths
|
|
338
|
+
const widths = Array.from<number>({ length: colCount });
|
|
339
|
+
for (let i = 0; i < colCount; i++) {
|
|
340
|
+
const colDef = columns[i]!;
|
|
341
|
+
if (colDef !== "auto" && !colDef.endsWith("fr")) {
|
|
342
|
+
const fixed = parseInt(colDef, 10);
|
|
343
|
+
widths[i] = !isNaN(fixed) && fixed > 0 ? fixed : autoWidths[i]!;
|
|
344
|
+
} else {
|
|
345
|
+
widths[i] = autoWidths[i]!;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Expand columns to fit spanning cells
|
|
350
|
+
for (const row of matrix) {
|
|
351
|
+
if (isDividerRow(row)) continue;
|
|
352
|
+
for (let i = 0; i < row.length; i++) {
|
|
353
|
+
const cell = row[i]!;
|
|
354
|
+
if (!cell.spanStart || cell.spanSize <= 1 || cell.segment === EMPTY_CELL)
|
|
355
|
+
continue;
|
|
356
|
+
|
|
357
|
+
const content = resolvedData[cell.segment] || "";
|
|
358
|
+
const contentLen = visibleLength(content);
|
|
359
|
+
const sw = spanCellWidth(widths, i, cell.spanSize, separatorWidth);
|
|
360
|
+
|
|
361
|
+
if (contentLen > sw) {
|
|
362
|
+
const deficit = contentLen - sw;
|
|
363
|
+
const frCols: number[] = [];
|
|
364
|
+
for (let j = 0; j < cell.spanSize; j++) {
|
|
365
|
+
if (parseFr(columns[i + j]!) > 0) frCols.push(i + j);
|
|
366
|
+
}
|
|
367
|
+
if (frCols.length > 0) {
|
|
368
|
+
distributeExact(deficit, frCols, widths);
|
|
369
|
+
} else {
|
|
370
|
+
const allCols: number[] = [];
|
|
371
|
+
for (let j = 0; j < cell.spanSize; j++) allCols.push(i + j);
|
|
372
|
+
distributeExact(deficit, allCols, widths);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Clamp all widths to >= 1
|
|
379
|
+
for (let i = 0; i < colCount; i++) {
|
|
380
|
+
if (widths[i]! < 1) widths[i] = 1;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
let naturalWidth = 0;
|
|
384
|
+
for (let i = 0; i < colCount; i++) {
|
|
385
|
+
naturalWidth += widths[i]!;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const totalSepWidth = Math.max(0, colCount - 1) * separatorWidth;
|
|
389
|
+
const extraWallPad = Math.max(0, 1 - horizontalPadding);
|
|
390
|
+
const borders = 2 + extraWallPad * 2; // 2 box chars + extra wall padding
|
|
391
|
+
const cellPadding = colCount * horizontalPadding * 2;
|
|
392
|
+
return {
|
|
393
|
+
panelWidth: naturalWidth + totalSepWidth + borders + cellPadding,
|
|
394
|
+
colWidths: widths,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// --- Cell Rendering ---
|
|
399
|
+
|
|
400
|
+
function alignContent(text: string, width: number, align: AlignValue): string {
|
|
401
|
+
switch (align) {
|
|
402
|
+
case "right":
|
|
403
|
+
return padLeft(text, width);
|
|
404
|
+
case "center":
|
|
405
|
+
return padCenter(text, width);
|
|
406
|
+
case "left":
|
|
407
|
+
default:
|
|
408
|
+
return padRight(text, width);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
export function renderGridRow(
|
|
413
|
+
row: GridCell[],
|
|
414
|
+
colWidths: number[],
|
|
415
|
+
align: AlignValue[],
|
|
416
|
+
resolvedData: Record<string, string>,
|
|
417
|
+
separator: string,
|
|
418
|
+
horizontalPadding = 0,
|
|
419
|
+
padShrink?: number[],
|
|
420
|
+
): string {
|
|
421
|
+
const parts: string[] = [];
|
|
422
|
+
const sepWidth = visibleLength(separator);
|
|
423
|
+
const hPad = horizontalPadding;
|
|
424
|
+
|
|
425
|
+
for (let i = 0; i < row.length; i++) {
|
|
426
|
+
const cell = row[i]!;
|
|
427
|
+
if (!cell.spanStart) continue;
|
|
428
|
+
|
|
429
|
+
const cellWidth = spanCellWidth(colWidths, i, cell.spanSize, sepWidth);
|
|
430
|
+
|
|
431
|
+
// Compute per-cell padding from column shrink values
|
|
432
|
+
const lastCol = i + cell.spanSize - 1;
|
|
433
|
+
const leftShrink = align[i] === "right" ? (padShrink?.[i] ?? 0) : 0;
|
|
434
|
+
const rightShrink =
|
|
435
|
+
align[lastCol] === "left" ? (padShrink?.[lastCol] ?? 0) : 0;
|
|
436
|
+
const leftPad = hPad - leftShrink;
|
|
437
|
+
const rightPad = hPad - rightShrink;
|
|
438
|
+
|
|
439
|
+
// Inner padding for spanning cells (accounts for shrink of internal columns)
|
|
440
|
+
let innerPad = 0;
|
|
441
|
+
for (let j = i; j < lastCol; j++) {
|
|
442
|
+
const rShrink = align[j] === "left" ? (padShrink?.[j] ?? 0) : 0;
|
|
443
|
+
const lShrink = align[j + 1] === "right" ? (padShrink?.[j + 1] ?? 0) : 0;
|
|
444
|
+
innerPad += hPad - rShrink + (hPad - lShrink);
|
|
445
|
+
}
|
|
446
|
+
const contentWidth = cellWidth + innerPad;
|
|
447
|
+
|
|
448
|
+
if (cell.segment === EMPTY_CELL) {
|
|
449
|
+
parts.push(" ".repeat(contentWidth + leftPad + rightPad));
|
|
450
|
+
} else {
|
|
451
|
+
const content = resolvedData[cell.segment] || "";
|
|
452
|
+
const truncated = truncateAnsi(content, contentWidth);
|
|
453
|
+
const cellAlign = align[i] || "left";
|
|
454
|
+
const aligned = alignContent(truncated, contentWidth, cellAlign);
|
|
455
|
+
const lp = leftPad > 0 ? " ".repeat(leftPad) : "";
|
|
456
|
+
const rp = rightPad > 0 ? " ".repeat(rightPad) : "";
|
|
457
|
+
parts.push(lp + aligned + rp);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return parts.join(separator);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// --- Divider Rendering ---
|
|
465
|
+
|
|
466
|
+
export function renderGridDivider(
|
|
467
|
+
box: BoxChars,
|
|
468
|
+
innerWidth: number,
|
|
469
|
+
dividerChar?: string,
|
|
470
|
+
): string {
|
|
471
|
+
const ch = dividerChar || box.horizontal;
|
|
472
|
+
return box.teeLeft + ch.repeat(innerWidth) + box.teeRight;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// --- Main Grid Render ---
|
|
476
|
+
|
|
477
|
+
export function renderGrid(
|
|
478
|
+
gridConfig: TuiGridConfig,
|
|
479
|
+
resolvedData: Record<string, string>,
|
|
480
|
+
box: BoxChars,
|
|
481
|
+
rawTerminalWidth: number,
|
|
482
|
+
lateResolve?: (segment: string, cellWidth: number) => string | undefined,
|
|
483
|
+
): GridResult {
|
|
484
|
+
const minWidth = gridConfig.minWidth ?? 32;
|
|
485
|
+
const maxWidth = gridConfig.maxWidth ?? Infinity;
|
|
486
|
+
const colSep = gridConfig.separator?.column ?? " ";
|
|
487
|
+
const dividerChar = gridConfig.separator?.divider;
|
|
488
|
+
const sepWidth = visibleLength(colSep);
|
|
489
|
+
const fitContent = gridConfig.fitContent ?? false;
|
|
490
|
+
const hPad = gridConfig.padding?.horizontal ?? 0;
|
|
491
|
+
|
|
492
|
+
// Breakpoint selection always uses available width (terminal - reserve)
|
|
493
|
+
const widthReserve = gridConfig.widthReserve ?? 45;
|
|
494
|
+
const availableWidth = Math.min(
|
|
495
|
+
maxWidth,
|
|
496
|
+
Math.max(minWidth, rawTerminalWidth - widthReserve),
|
|
497
|
+
);
|
|
498
|
+
const bp = selectBreakpoint(gridConfig.breakpoints, availableWidth);
|
|
499
|
+
|
|
500
|
+
// Panel width for rendering
|
|
501
|
+
let panelWidth: number;
|
|
502
|
+
if (fitContent) {
|
|
503
|
+
panelWidth =
|
|
504
|
+
maxWidth !== Infinity
|
|
505
|
+
? Math.min(rawTerminalWidth, maxWidth)
|
|
506
|
+
: rawTerminalWidth;
|
|
507
|
+
} else {
|
|
508
|
+
panelWidth = availableWidth;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Parse areas
|
|
512
|
+
const rawMatrix = parseAreas(bp.areas);
|
|
513
|
+
|
|
514
|
+
// Cull empty cells/rows
|
|
515
|
+
const matrix = cullMatrix(rawMatrix, resolvedData);
|
|
516
|
+
|
|
517
|
+
if (matrix.length === 0) {
|
|
518
|
+
return { lines: [], panelWidth };
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
let colWidths: number[];
|
|
522
|
+
|
|
523
|
+
// Collect late-resolve segment names (including user-defined templates)
|
|
524
|
+
const lateNames = new Set(LATE_RESOLVE_SEGMENTS);
|
|
525
|
+
if (gridConfig.segments) {
|
|
526
|
+
for (const key of Object.keys(gridConfig.segments)) {
|
|
527
|
+
lateNames.add(key);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
if (fitContent) {
|
|
532
|
+
const solved = solveFitContentLayout(
|
|
533
|
+
bp.columns,
|
|
534
|
+
matrix,
|
|
535
|
+
resolvedData,
|
|
536
|
+
sepWidth,
|
|
537
|
+
hPad,
|
|
538
|
+
lateNames,
|
|
539
|
+
);
|
|
540
|
+
panelWidth = Math.min(maxWidth, Math.max(minWidth, solved.panelWidth));
|
|
541
|
+
colWidths = solved.colWidths;
|
|
542
|
+
|
|
543
|
+
// Redistribute surplus (from minWidth or maxWidth clamping) into fr columns
|
|
544
|
+
const surplus = panelWidth - solved.panelWidth;
|
|
545
|
+
if (surplus > 0) {
|
|
546
|
+
let totalFr = 0;
|
|
547
|
+
for (const colDef of bp.columns) totalFr += parseFr(colDef);
|
|
548
|
+
if (totalFr > 0) {
|
|
549
|
+
const frCols: number[] = [];
|
|
550
|
+
let allocated = 0;
|
|
551
|
+
for (let i = 0; i < colWidths.length; i++) {
|
|
552
|
+
const fr = parseFr(bp.columns[i]!);
|
|
553
|
+
if (fr > 0) {
|
|
554
|
+
const add = Math.floor((surplus * fr) / totalFr);
|
|
555
|
+
colWidths[i]! += add;
|
|
556
|
+
allocated += add;
|
|
557
|
+
frCols.push(i);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
let leftover = surplus - allocated;
|
|
561
|
+
for (let k = 0; leftover > 0 && k < frCols.length; k++) {
|
|
562
|
+
colWidths[frCols[k]!]! += 1;
|
|
563
|
+
leftover--;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
} else {
|
|
568
|
+
const innerW = panelWidth - 2;
|
|
569
|
+
const ewp = Math.max(0, 1 - hPad);
|
|
570
|
+
const contentW = innerW - ewp * 2 - bp.columns.length * hPad * 2;
|
|
571
|
+
colWidths = calculateColumnWidths(
|
|
572
|
+
bp.columns,
|
|
573
|
+
matrix,
|
|
574
|
+
resolvedData,
|
|
575
|
+
contentW,
|
|
576
|
+
sepWidth,
|
|
577
|
+
lateNames,
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
const innerWidth = panelWidth - 2;
|
|
582
|
+
// When hPad >= 1, cell padding replaces the base 1-space wall padding
|
|
583
|
+
const wallPad = Math.max(1, hPad);
|
|
584
|
+
const extraWallPad = wallPad - hPad; // 1 when hPad=0, 0 when hPad>=1
|
|
585
|
+
const wallPadStr = extraWallPad > 0 ? " ".repeat(extraWallPad) : "";
|
|
586
|
+
const contentWidth = innerWidth - extraWallPad * 2;
|
|
587
|
+
|
|
588
|
+
// Alignment defaults
|
|
589
|
+
const align: AlignValue[] =
|
|
590
|
+
bp.align || bp.columns.map(() => "left" as AlignValue);
|
|
591
|
+
|
|
592
|
+
// Adaptive padding: absorb alignment gaps into padding, redistribute savings to fr columns.
|
|
593
|
+
// padShrink[col] = how much of hPad is absorbed by existing alignment gap on the aligned side.
|
|
594
|
+
const padShrink = new Array<number>(bp.columns.length).fill(0);
|
|
595
|
+
if (hPad > 0) {
|
|
596
|
+
const maxContent = new Array<number>(bp.columns.length).fill(0);
|
|
597
|
+
for (const row of matrix) {
|
|
598
|
+
if (isDividerRow(row)) continue;
|
|
599
|
+
for (let ci = 0; ci < row.length; ci++) {
|
|
600
|
+
const cell = row[ci]!;
|
|
601
|
+
if (!cell.spanStart || cell.spanSize !== 1) continue;
|
|
602
|
+
if (cell.segment === EMPTY_CELL) continue;
|
|
603
|
+
if (lateNames.has(cell.segment)) continue;
|
|
604
|
+
const len = visibleLength(resolvedData[cell.segment] || "");
|
|
605
|
+
if (len > maxContent[ci]!) maxContent[ci] = len;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
let totalSavings = 0;
|
|
610
|
+
for (let ci = 0; ci < bp.columns.length; ci++) {
|
|
611
|
+
if (parseFr(bp.columns[ci]!) > 0) continue;
|
|
612
|
+
if (maxContent[ci]! <= 0) continue;
|
|
613
|
+
const gap = colWidths[ci]! - maxContent[ci]!;
|
|
614
|
+
if (gap <= 0) continue;
|
|
615
|
+
padShrink[ci] = Math.min(hPad, gap);
|
|
616
|
+
totalSavings += padShrink[ci]!;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
if (totalSavings > 0) {
|
|
620
|
+
let totalFr = 0;
|
|
621
|
+
for (const colDef of bp.columns) totalFr += parseFr(colDef);
|
|
622
|
+
if (totalFr > 0) {
|
|
623
|
+
const frCols: number[] = [];
|
|
624
|
+
let allocated = 0;
|
|
625
|
+
for (let ci = 0; ci < colWidths.length; ci++) {
|
|
626
|
+
const fr = parseFr(bp.columns[ci]!);
|
|
627
|
+
if (fr > 0) {
|
|
628
|
+
const add = Math.floor((totalSavings * fr) / totalFr);
|
|
629
|
+
colWidths[ci]! += add;
|
|
630
|
+
allocated += add;
|
|
631
|
+
frCols.push(ci);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
let leftover = totalSavings - allocated;
|
|
635
|
+
for (let k = 0; leftover > 0 && k < frCols.length; k++) {
|
|
636
|
+
colWidths[frCols[k]!]! += 1;
|
|
637
|
+
leftover--;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// Compute span inner padding accounting for per-column shrink
|
|
644
|
+
function spanInnerPad(colIdx: number, spanSize: number): number {
|
|
645
|
+
let pad = 0;
|
|
646
|
+
for (let j = colIdx; j < colIdx + spanSize - 1; j++) {
|
|
647
|
+
const rShrink = align[j] === "left" ? (padShrink[j] ?? 0) : 0;
|
|
648
|
+
const lShrink = align[j + 1] === "right" ? (padShrink[j + 1] ?? 0) : 0;
|
|
649
|
+
pad += hPad - rShrink + (hPad - lShrink);
|
|
650
|
+
}
|
|
651
|
+
return pad;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// Late resolve: re-resolve width-dependent segments now that cell widths are known
|
|
655
|
+
if (lateResolve) {
|
|
656
|
+
const seen = new Set<string>();
|
|
657
|
+
for (const row of matrix) {
|
|
658
|
+
if (isDividerRow(row)) continue;
|
|
659
|
+
for (let i = 0; i < row.length; i++) {
|
|
660
|
+
const cell = row[i]!;
|
|
661
|
+
if (
|
|
662
|
+
!cell.spanStart ||
|
|
663
|
+
cell.segment === EMPTY_CELL ||
|
|
664
|
+
cell.segment === DIVIDER
|
|
665
|
+
)
|
|
666
|
+
continue;
|
|
667
|
+
if (seen.has(cell.segment)) continue;
|
|
668
|
+
seen.add(cell.segment);
|
|
669
|
+
|
|
670
|
+
const cellWidth = spanCellWidth(colWidths, i, cell.spanSize, sepWidth);
|
|
671
|
+
const innerPad = spanInnerPad(i, cell.spanSize);
|
|
672
|
+
const content = lateResolve(cell.segment, cellWidth + innerPad);
|
|
673
|
+
if (content !== undefined) {
|
|
674
|
+
resolvedData[cell.segment] = content;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// Post-lateResolve culling: segments that resolved to empty after lateResolve
|
|
681
|
+
// can leave orphaned rows and dividers. Re-cull the matrix.
|
|
682
|
+
const finalMatrix = cullMatrix(matrix, resolvedData);
|
|
683
|
+
|
|
684
|
+
if (finalMatrix.length === 0) {
|
|
685
|
+
return { lines: [], panelWidth };
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// Render rows
|
|
689
|
+
const lines: string[] = [];
|
|
690
|
+
for (const row of finalMatrix) {
|
|
691
|
+
if (isDividerRow(row)) {
|
|
692
|
+
lines.push(renderGridDivider(box, innerWidth, dividerChar));
|
|
693
|
+
} else {
|
|
694
|
+
const rowStr = renderGridRow(
|
|
695
|
+
row,
|
|
696
|
+
colWidths,
|
|
697
|
+
align,
|
|
698
|
+
resolvedData,
|
|
699
|
+
colSep,
|
|
700
|
+
hPad,
|
|
701
|
+
padShrink,
|
|
702
|
+
);
|
|
703
|
+
const truncated = truncateAnsi(rowStr, contentWidth);
|
|
704
|
+
const padded = padRight(truncated, contentWidth);
|
|
705
|
+
lines.push(
|
|
706
|
+
box.vertical + wallPadStr + padded + wallPadStr + box.vertical,
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
return { lines, panelWidth };
|
|
712
|
+
}
|
package/src/tui/index.ts
ADDED