@owloops/claude-powerline 1.24.4 → 1.25.1

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.
Files changed (46) hide show
  1. package/dist/browser.d.ts +676 -0
  2. package/dist/browser.js +3 -0
  3. package/dist/index.mjs +10 -10
  4. package/package.json +9 -1
  5. package/plugin/templates/config-tui-compact.json +4 -4
  6. package/plugin/templates/config-tui-full.json +5 -5
  7. package/plugin/templates/config-tui-standard.json +5 -5
  8. package/src/browser.ts +203 -0
  9. package/src/config/defaults.ts +79 -0
  10. package/src/config/loader.ts +462 -0
  11. package/src/index.ts +90 -0
  12. package/src/powerline.ts +904 -0
  13. package/src/segments/block.ts +31 -0
  14. package/src/segments/context.ts +221 -0
  15. package/src/segments/git.ts +492 -0
  16. package/src/segments/index.ts +25 -0
  17. package/src/segments/metrics.ts +175 -0
  18. package/src/segments/pricing.ts +454 -0
  19. package/src/segments/renderer.ts +796 -0
  20. package/src/segments/session.ts +207 -0
  21. package/src/segments/tmux.ts +35 -0
  22. package/src/segments/today.ts +191 -0
  23. package/src/themes/dark.ts +52 -0
  24. package/src/themes/gruvbox.ts +52 -0
  25. package/src/themes/index.ts +131 -0
  26. package/src/themes/light.ts +52 -0
  27. package/src/themes/nord.ts +52 -0
  28. package/src/themes/rose-pine.ts +52 -0
  29. package/src/themes/tokyo-night.ts +52 -0
  30. package/src/tui/grid.ts +712 -0
  31. package/src/tui/index.ts +4 -0
  32. package/src/tui/layouts.ts +285 -0
  33. package/src/tui/primitives.ts +175 -0
  34. package/src/tui/renderer.ts +206 -0
  35. package/src/tui/sections.ts +1080 -0
  36. package/src/tui/types.ts +181 -0
  37. package/src/utils/budget.ts +47 -0
  38. package/src/utils/cache.ts +247 -0
  39. package/src/utils/claude.ts +489 -0
  40. package/src/utils/color-support.ts +118 -0
  41. package/src/utils/colors.ts +120 -0
  42. package/src/utils/constants.ts +176 -0
  43. package/src/utils/formatters.ts +160 -0
  44. package/src/utils/logger.ts +5 -0
  45. package/src/utils/terminal-width.ts +117 -0
  46. package/src/utils/terminal.ts +11 -0
@@ -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
+ }
@@ -0,0 +1,4 @@
1
+ export type { BoxChars, TuiData } from "./types";
2
+
3
+ export { renderTuiPanel } from "./renderer";
4
+ export type { TuiPanelOptions } from "./renderer";