@gajae-code/tui 0.13.2 → 0.13.3
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/CHANGELOG.md +14 -6
- package/dist/types/components/gajae-pet.d.ts +39 -21
- package/dist/types/components/ouroboros-pet.d.ts +23 -0
- package/dist/types/components/select-list.d.ts +3 -2
- package/package.json +3 -3
- package/src/components/gajae-pet.ts +284 -43
- package/src/components/ouroboros-pet-frames.json +488 -0
- package/src/components/ouroboros-pet.ts +150 -0
- package/src/components/select-list.ts +17 -11
- package/src/tui.ts +37 -29
|
@@ -39,6 +39,8 @@ export interface SelectListTheme {
|
|
|
39
39
|
symbols: SymbolTheme;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
export type SelectListThemeSource = SelectListTheme | (() => SelectListTheme);
|
|
43
|
+
|
|
42
44
|
export interface SelectListTruncatePrimaryContext {
|
|
43
45
|
text: string;
|
|
44
46
|
maxWidth: number;
|
|
@@ -67,7 +69,7 @@ export class SelectList implements Component {
|
|
|
67
69
|
constructor(
|
|
68
70
|
private readonly items: ReadonlyArray<SelectItem>,
|
|
69
71
|
private readonly maxVisible: number,
|
|
70
|
-
private readonly
|
|
72
|
+
private readonly themeSource: SelectListThemeSource,
|
|
71
73
|
private readonly layout: SelectListLayoutOptions = {},
|
|
72
74
|
) {
|
|
73
75
|
this.#filteredItems = items;
|
|
@@ -114,11 +116,12 @@ export class SelectList implements Component {
|
|
|
114
116
|
}
|
|
115
117
|
|
|
116
118
|
render(width: number): string[] {
|
|
119
|
+
const theme = this.#theme();
|
|
117
120
|
const lines: string[] = [];
|
|
118
121
|
|
|
119
122
|
// If no items match filter, show message
|
|
120
123
|
if (this.#filteredItems.length === 0) {
|
|
121
|
-
lines.push(truncateToWidth(
|
|
124
|
+
lines.push(truncateToWidth(theme.noMatch(" No matching commands"), Math.max(0, width), Ellipsis.Omit));
|
|
122
125
|
return lines;
|
|
123
126
|
}
|
|
124
127
|
|
|
@@ -146,12 +149,16 @@ export class SelectList implements Component {
|
|
|
146
149
|
const position = this.#selectedIndex >= 0 ? `${this.#selectedIndex + 1}` : "-";
|
|
147
150
|
const scrollText = ` (${position}/${this.#filteredItems.length})`;
|
|
148
151
|
// Truncate if too long for terminal
|
|
149
|
-
lines.push(
|
|
152
|
+
lines.push(theme.scrollInfo(truncateToWidth(scrollText, width - 2, Ellipsis.Omit)));
|
|
150
153
|
}
|
|
151
154
|
|
|
152
155
|
return lines;
|
|
153
156
|
}
|
|
154
157
|
|
|
158
|
+
#theme(): SelectListTheme {
|
|
159
|
+
return typeof this.themeSource === "function" ? this.themeSource() : this.themeSource;
|
|
160
|
+
}
|
|
161
|
+
|
|
155
162
|
handleInput(keyData: string): void {
|
|
156
163
|
const kb = getKeybindings();
|
|
157
164
|
if (this.#filteredItems.length === 0) {
|
|
@@ -193,9 +200,8 @@ export class SelectList implements Component {
|
|
|
193
200
|
descriptionSingleLine: string | undefined,
|
|
194
201
|
primaryColumnWidth: number,
|
|
195
202
|
): string {
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
: padding(visibleWidth(this.theme.symbols.cursor) + 1);
|
|
203
|
+
const theme = this.#theme();
|
|
204
|
+
const prefix = isSelected ? `${theme.symbols.cursor} ` : padding(visibleWidth(theme.symbols.cursor) + 1);
|
|
199
205
|
const prefixWidth = visibleWidth(prefix);
|
|
200
206
|
|
|
201
207
|
if (descriptionSingleLine && width > 40) {
|
|
@@ -210,13 +216,13 @@ export class SelectList implements Component {
|
|
|
210
216
|
if (remainingWidth > MIN_DESCRIPTION_WIDTH) {
|
|
211
217
|
const truncatedDesc = truncateToWidth(descriptionSingleLine, remainingWidth, Ellipsis.Omit);
|
|
212
218
|
if (item.disabled) {
|
|
213
|
-
return
|
|
219
|
+
return theme.description(`${prefix}${truncatedValue}${spacing}${truncatedDesc}`);
|
|
214
220
|
}
|
|
215
221
|
if (isSelected) {
|
|
216
|
-
return
|
|
222
|
+
return theme.selectedText(`${prefix}${truncatedValue}${spacing}${truncatedDesc}`);
|
|
217
223
|
}
|
|
218
224
|
|
|
219
|
-
const descText =
|
|
225
|
+
const descText = theme.description(spacing + truncatedDesc);
|
|
220
226
|
return prefix + truncatedValue + descText;
|
|
221
227
|
}
|
|
222
228
|
}
|
|
@@ -224,10 +230,10 @@ export class SelectList implements Component {
|
|
|
224
230
|
const maxWidth = width - prefixWidth - 2;
|
|
225
231
|
const truncatedValue = this.#truncatePrimary(item, isSelected, maxWidth, maxWidth);
|
|
226
232
|
if (item.disabled) {
|
|
227
|
-
return
|
|
233
|
+
return theme.description(`${prefix}${truncatedValue}`);
|
|
228
234
|
}
|
|
229
235
|
if (isSelected) {
|
|
230
|
-
return
|
|
236
|
+
return theme.selectedText(`${prefix}${truncatedValue}`);
|
|
231
237
|
}
|
|
232
238
|
|
|
233
239
|
return prefix + truncatedValue;
|
package/src/tui.ts
CHANGED
|
@@ -349,15 +349,26 @@ export type SizeValue = number | `${number}%`;
|
|
|
349
349
|
/** Parse a SizeValue into absolute value given a reference size */
|
|
350
350
|
function parseSizeValue(value: SizeValue | undefined, referenceSize: number): number | undefined {
|
|
351
351
|
if (value === undefined) return undefined;
|
|
352
|
-
if (typeof value === "number") return value;
|
|
352
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : undefined;
|
|
353
353
|
// Parse percentage string like "50%"
|
|
354
354
|
const match = value.match(/^(\d+(?:\.\d+)?)%$/);
|
|
355
355
|
if (match) {
|
|
356
|
-
|
|
356
|
+
const percent = Number.parseFloat(match[1]);
|
|
357
|
+
if (!Number.isFinite(percent)) return undefined;
|
|
358
|
+
const parsed = Math.floor((referenceSize * percent) / 100);
|
|
359
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
357
360
|
}
|
|
358
361
|
return undefined;
|
|
359
362
|
}
|
|
360
363
|
|
|
364
|
+
function finiteNumber(value: number | undefined, fallback: number): number {
|
|
365
|
+
return value !== undefined && Number.isFinite(value) ? value : fallback;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function finiteNonNegative(value: number | undefined, fallback = 0): number {
|
|
369
|
+
return Math.max(0, finiteNumber(value, fallback));
|
|
370
|
+
}
|
|
371
|
+
|
|
361
372
|
const DISABLED_ENV_VALUES = new Set(["0", "false", "off", "no"]);
|
|
362
373
|
|
|
363
374
|
function envIsEnabled(value: string | undefined): boolean {
|
|
@@ -2480,10 +2491,10 @@ export class TUI extends Container {
|
|
|
2480
2491
|
typeof opt.margin === "number"
|
|
2481
2492
|
? { top: opt.margin, right: opt.margin, bottom: opt.margin, left: opt.margin }
|
|
2482
2493
|
: (opt.margin ?? {});
|
|
2483
|
-
const marginTop = Math.max(0,
|
|
2484
|
-
const marginRight = Math.max(0,
|
|
2485
|
-
const marginBottom = Math.max(0,
|
|
2486
|
-
const marginLeft = Math.max(0,
|
|
2494
|
+
const marginTop = Math.min(finiteNonNegative(margin.top), Math.max(0, termHeight - 1));
|
|
2495
|
+
const marginRight = Math.min(finiteNonNegative(margin.right), Math.max(0, termWidth - 1));
|
|
2496
|
+
const marginBottom = Math.min(finiteNonNegative(margin.bottom), Math.max(0, termHeight - 1 - marginTop));
|
|
2497
|
+
const marginLeft = Math.min(finiteNonNegative(margin.left), Math.max(0, termWidth - 1 - marginRight));
|
|
2487
2498
|
|
|
2488
2499
|
// Available space after margins
|
|
2489
2500
|
const availWidth = Math.max(1, termWidth - marginLeft - marginRight);
|
|
@@ -2492,14 +2503,15 @@ export class TUI extends Container {
|
|
|
2492
2503
|
// === Resolve width ===
|
|
2493
2504
|
let width = parseSizeValue(opt.width, termWidth) ?? Math.min(80, availWidth);
|
|
2494
2505
|
// Apply minWidth
|
|
2495
|
-
if (opt.minWidth !== undefined) {
|
|
2506
|
+
if (opt.minWidth !== undefined && Number.isFinite(opt.minWidth)) {
|
|
2496
2507
|
width = Math.max(width, opt.minWidth);
|
|
2497
2508
|
}
|
|
2498
2509
|
// Clamp to available space
|
|
2499
2510
|
width = Math.max(1, Math.min(width, availWidth));
|
|
2500
2511
|
|
|
2501
2512
|
// === Resolve maxHeight ===
|
|
2502
|
-
|
|
2513
|
+
const parsedMaxHeight = parseSizeValue(opt.maxHeight, termHeight);
|
|
2514
|
+
let maxHeight = opt.maxHeight !== undefined && parsedMaxHeight === undefined ? availHeight : parsedMaxHeight;
|
|
2503
2515
|
// Clamp to available space
|
|
2504
2516
|
if (maxHeight !== undefined) {
|
|
2505
2517
|
maxHeight = Math.max(1, Math.min(maxHeight, availHeight));
|
|
@@ -2519,14 +2531,18 @@ export class TUI extends Container {
|
|
|
2519
2531
|
if (match) {
|
|
2520
2532
|
const maxRow = Math.max(0, availHeight - effectiveHeight);
|
|
2521
2533
|
const percent = parseFloat(match[1]) / 100;
|
|
2522
|
-
row =
|
|
2534
|
+
row = Number.isFinite(percent)
|
|
2535
|
+
? marginTop + Math.floor(maxRow * percent)
|
|
2536
|
+
: this.#resolveAnchorRow(opt.anchor ?? "center", effectiveHeight, availHeight, marginTop);
|
|
2523
2537
|
} else {
|
|
2524
2538
|
// Invalid format, fall back to center
|
|
2525
2539
|
row = this.#resolveAnchorRow("center", effectiveHeight, availHeight, marginTop);
|
|
2526
2540
|
}
|
|
2527
|
-
} else {
|
|
2541
|
+
} else if (Number.isFinite(opt.row)) {
|
|
2528
2542
|
// Absolute row position
|
|
2529
2543
|
row = opt.row;
|
|
2544
|
+
} else {
|
|
2545
|
+
row = this.#resolveAnchorRow(opt.anchor ?? "center", effectiveHeight, availHeight, marginTop);
|
|
2530
2546
|
}
|
|
2531
2547
|
} else {
|
|
2532
2548
|
// Anchor-based (default: center)
|
|
@@ -2541,14 +2557,18 @@ export class TUI extends Container {
|
|
|
2541
2557
|
if (match) {
|
|
2542
2558
|
const maxCol = Math.max(0, availWidth - width);
|
|
2543
2559
|
const percent = parseFloat(match[1]) / 100;
|
|
2544
|
-
col =
|
|
2560
|
+
col = Number.isFinite(percent)
|
|
2561
|
+
? marginLeft + Math.floor(maxCol * percent)
|
|
2562
|
+
: this.#resolveAnchorCol(opt.anchor ?? "center", width, availWidth, marginLeft);
|
|
2545
2563
|
} else {
|
|
2546
2564
|
// Invalid format, fall back to center
|
|
2547
2565
|
col = this.#resolveAnchorCol("center", width, availWidth, marginLeft);
|
|
2548
2566
|
}
|
|
2549
|
-
} else {
|
|
2567
|
+
} else if (Number.isFinite(opt.col)) {
|
|
2550
2568
|
// Absolute column position
|
|
2551
2569
|
col = opt.col;
|
|
2570
|
+
} else {
|
|
2571
|
+
col = this.#resolveAnchorCol(opt.anchor ?? "center", width, availWidth, marginLeft);
|
|
2552
2572
|
}
|
|
2553
2573
|
} else {
|
|
2554
2574
|
// Anchor-based (default: center)
|
|
@@ -2557,8 +2577,8 @@ export class TUI extends Container {
|
|
|
2557
2577
|
}
|
|
2558
2578
|
|
|
2559
2579
|
// Apply offsets
|
|
2560
|
-
|
|
2561
|
-
|
|
2580
|
+
row += finiteNumber(opt.offsetY, 0);
|
|
2581
|
+
col += finiteNumber(opt.offsetX, 0);
|
|
2562
2582
|
|
|
2563
2583
|
// Clamp to terminal bounds (respecting margins)
|
|
2564
2584
|
row = Math.max(marginTop, Math.min(row, termHeight - marginBottom - effectiveHeight));
|
|
@@ -2648,6 +2668,9 @@ export class TUI extends Container {
|
|
|
2648
2668
|
// than the current content. Padding to it can cause the renderer to output hundreds/thousands of blank
|
|
2649
2669
|
// lines, effectively scrolling the terminal when an overlay is shown.
|
|
2650
2670
|
const workingHeight = Math.max(result.length, minLinesNeeded);
|
|
2671
|
+
if (!Number.isFinite(workingHeight)) {
|
|
2672
|
+
throw new Error("Overlay layout produced a non-finite working height");
|
|
2673
|
+
}
|
|
2651
2674
|
|
|
2652
2675
|
// Extend result with empty lines if content is too short for overlay placement
|
|
2653
2676
|
while (result.length < workingHeight) {
|
|
@@ -4040,21 +4063,6 @@ export class TUI extends Container {
|
|
|
4040
4063
|
viewportRepaint(`content contraction changed viewport top (${prevViewportTop} -> ${nextLiveViewportTop})`);
|
|
4041
4064
|
return;
|
|
4042
4065
|
}
|
|
4043
|
-
if (
|
|
4044
|
-
appendedLines &&
|
|
4045
|
-
nextLiveViewportTop > prevViewportTop &&
|
|
4046
|
-
previousKittyPlacementSpans.some(placement =>
|
|
4047
|
-
this.#kittyPlacementIntersectsRegion(placement, {
|
|
4048
|
-
top: prevViewportTop,
|
|
4049
|
-
bottom: prevViewportTop + height,
|
|
4050
|
-
}),
|
|
4051
|
-
)
|
|
4052
|
-
) {
|
|
4053
|
-
viewportRepaint(
|
|
4054
|
-
`content append moved a Kitty placement viewport (${prevViewportTop} -> ${nextLiveViewportTop})`,
|
|
4055
|
-
);
|
|
4056
|
-
return;
|
|
4057
|
-
}
|
|
4058
4066
|
if (distinctPostContractionRows) this.#scrollbackResumeViewportTop = undefined;
|
|
4059
4067
|
if (
|
|
4060
4068
|
appendedLines &&
|