@f5-sales-demo/pi-tui 21.45.3 → 21.46.2
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 -0
- package/package.json +3 -3
- package/src/components/editor.ts +3 -1
- package/src/components/select-list.ts +66 -10
- package/src/terminal.ts +17 -0
package/README.md
CHANGED
|
@@ -346,6 +346,7 @@ const list = new SelectList(
|
|
|
346
346
|
],
|
|
347
347
|
5, // maxVisible
|
|
348
348
|
theme, // SelectListTheme
|
|
349
|
+
{ presentation: "compact-with-selected-detail" },
|
|
349
350
|
);
|
|
350
351
|
|
|
351
352
|
list.onSelect = (item) => console.log("Selected:", item);
|
|
@@ -354,6 +355,10 @@ list.onSelectionChange = (item) => console.log("Highlighted:", item);
|
|
|
354
355
|
list.setFilter("opt"); // Filter items
|
|
355
356
|
```
|
|
356
357
|
|
|
358
|
+
Every list chooses a presentation contract. Use `compact-with-selected-detail` for dense one-line rows; when a
|
|
359
|
+
selected label or description does not fit, `SelectList` adds its complete wrapped value below the rows. Use
|
|
360
|
+
`wrapped-prose` when every visible item should wrap instead of truncating.
|
|
361
|
+
|
|
357
362
|
**Controls:**
|
|
358
363
|
|
|
359
364
|
- Arrow keys: Navigate
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@f5-sales-demo/pi-tui",
|
|
4
|
-
"version": "21.
|
|
4
|
+
"version": "21.46.2",
|
|
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": "21.
|
|
41
|
-
"@f5-sales-demo/pi-utils": "21.
|
|
40
|
+
"@f5-sales-demo/pi-natives": "21.46.2",
|
|
41
|
+
"@f5-sales-demo/pi-utils": "21.46.2",
|
|
42
42
|
"marked": "18.0.11"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
package/src/components/editor.ts
CHANGED
|
@@ -24,9 +24,11 @@ import {
|
|
|
24
24
|
import { SelectList, type SelectListLayoutOptions, type SelectListTheme } from "./select-list";
|
|
25
25
|
|
|
26
26
|
const SLASH_COMMAND_SELECT_LIST_LAYOUT: SelectListLayoutOptions = {
|
|
27
|
+
presentation: "compact-with-selected-detail",
|
|
27
28
|
minPrimaryColumnWidth: 12,
|
|
28
29
|
maxPrimaryColumnWidth: 32,
|
|
29
30
|
};
|
|
31
|
+
const FILE_SELECT_LIST_LAYOUT: SelectListLayoutOptions = { presentation: "compact-with-selected-detail" };
|
|
30
32
|
|
|
31
33
|
function sanitizeLoadedText(text: string): string {
|
|
32
34
|
return replaceTabs(text.replace(/\r\n/g, "\n").replace(/\r/g, "\n"))
|
|
@@ -2442,7 +2444,7 @@ export class Editor implements Component, Focusable {
|
|
|
2442
2444
|
prefix: string,
|
|
2443
2445
|
items: Array<{ value: string; label: string; description?: string }>,
|
|
2444
2446
|
): SelectList {
|
|
2445
|
-
const layout = prefix.startsWith("/") ? SLASH_COMMAND_SELECT_LIST_LAYOUT :
|
|
2447
|
+
const layout = prefix.startsWith("/") ? SLASH_COMMAND_SELECT_LIST_LAYOUT : FILE_SELECT_LIST_LAYOUT;
|
|
2446
2448
|
return new SelectList(items, this.#autocompleteMaxVisible, this.#theme.selectList, layout);
|
|
2447
2449
|
}
|
|
2448
2450
|
|
|
@@ -2,7 +2,7 @@ import { getKeybindings } from "../keybindings";
|
|
|
2
2
|
import { type MouseRoutable, routeSelectListMouse, type SgrMouseEvent } from "../mouse";
|
|
3
3
|
import type { SymbolTheme } from "../symbols";
|
|
4
4
|
import type { Component } from "../tui";
|
|
5
|
-
import { Ellipsis, padding, replaceTabs, truncateToWidth, visibleWidth } from "../utils";
|
|
5
|
+
import { Ellipsis, padding, replaceTabs, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "../utils";
|
|
6
6
|
|
|
7
7
|
const DEFAULT_PRIMARY_COLUMN_WIDTH = 32;
|
|
8
8
|
const PRIMARY_COLUMN_GAP = 2;
|
|
@@ -44,6 +44,7 @@ export interface SelectListTruncatePrimaryContext {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
export interface SelectListLayoutOptions {
|
|
47
|
+
presentation: "wrapped-prose" | "compact-with-selected-detail";
|
|
47
48
|
minPrimaryColumnWidth?: number;
|
|
48
49
|
maxPrimaryColumnWidth?: number;
|
|
49
50
|
truncatePrimary?: (context: SelectListTruncatePrimaryContext) => string;
|
|
@@ -63,8 +64,11 @@ export class SelectList implements Component, MouseRoutable {
|
|
|
63
64
|
private readonly items: ReadonlyArray<SelectItem>,
|
|
64
65
|
private readonly maxVisible: number,
|
|
65
66
|
private readonly theme: SelectListTheme,
|
|
66
|
-
private readonly layout: SelectListLayoutOptions
|
|
67
|
+
private readonly layout: SelectListLayoutOptions,
|
|
67
68
|
) {
|
|
69
|
+
if (layout.presentation !== "wrapped-prose" && layout.presentation !== "compact-with-selected-detail") {
|
|
70
|
+
throw new TypeError("SelectList requires an explicit wrapped-prose or compact-with-selected-detail policy.");
|
|
71
|
+
}
|
|
68
72
|
this.#filteredItems = items;
|
|
69
73
|
}
|
|
70
74
|
|
|
@@ -114,6 +118,7 @@ export class SelectList implements Component, MouseRoutable {
|
|
|
114
118
|
|
|
115
119
|
render(width: number): string[] {
|
|
116
120
|
const lines: string[] = [];
|
|
121
|
+
let selectedDetailLines: string[] = [];
|
|
117
122
|
this.#hitRows = [];
|
|
118
123
|
|
|
119
124
|
// If no items match filter, show message
|
|
@@ -138,11 +143,37 @@ export class SelectList implements Component, MouseRoutable {
|
|
|
138
143
|
|
|
139
144
|
const isSelected = i === this.#selectedIndex;
|
|
140
145
|
const descriptionText = item.description ? sanitizeSingleLine(item.description) : undefined;
|
|
141
|
-
|
|
146
|
+
if (this.layout.presentation === "wrapped-prose") {
|
|
147
|
+
for (const rendered of this.#renderWrappedItem(item, isSelected, width, descriptionText)) {
|
|
148
|
+
this.#hitRows[lines.length] = i;
|
|
149
|
+
lines.push(
|
|
150
|
+
!isSelected && i === this.#hoveredIndex && this.theme.hovered
|
|
151
|
+
? this.theme.hovered(rendered)
|
|
152
|
+
: rendered,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const rendered = this.#renderCompactItem(item, isSelected, width, descriptionText, primaryColumnWidth);
|
|
142
159
|
this.#hitRows[lines.length] = i;
|
|
143
160
|
lines.push(
|
|
144
|
-
!isSelected && i === this.#hoveredIndex && this.theme.hovered
|
|
161
|
+
!isSelected && i === this.#hoveredIndex && this.theme.hovered
|
|
162
|
+
? this.theme.hovered(rendered.line)
|
|
163
|
+
: rendered.line,
|
|
145
164
|
);
|
|
165
|
+
if (isSelected && rendered.omitted) {
|
|
166
|
+
const detail = descriptionText
|
|
167
|
+
? `${this.#getDisplayValue(item)} — ${descriptionText}`
|
|
168
|
+
: this.#getDisplayValue(item);
|
|
169
|
+
selectedDetailLines = wrapTextWithAnsi(detail, Math.max(1, width - 2)).map(detailLine =>
|
|
170
|
+
this.theme.description(` ${detailLine}`),
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
for (const detailLine of selectedDetailLines) {
|
|
175
|
+
this.#hitRows[lines.length] = undefined;
|
|
176
|
+
lines.push(detailLine);
|
|
146
177
|
}
|
|
147
178
|
|
|
148
179
|
// Add scroll indicators if needed
|
|
@@ -193,13 +224,13 @@ export class SelectList implements Component, MouseRoutable {
|
|
|
193
224
|
}
|
|
194
225
|
}
|
|
195
226
|
|
|
196
|
-
#
|
|
227
|
+
#renderCompactItem(
|
|
197
228
|
item: SelectItem,
|
|
198
229
|
isSelected: boolean,
|
|
199
230
|
width: number,
|
|
200
231
|
descriptionSingleLine: string | undefined,
|
|
201
232
|
primaryColumnWidth: number,
|
|
202
|
-
): string {
|
|
233
|
+
): { line: string; omitted: boolean } {
|
|
203
234
|
const prefix = isSelected
|
|
204
235
|
? `${this.theme.symbols.cursor} `
|
|
205
236
|
: padding(visibleWidth(this.theme.symbols.cursor) + 1);
|
|
@@ -216,22 +247,47 @@ export class SelectList implements Component, MouseRoutable {
|
|
|
216
247
|
|
|
217
248
|
if (remainingWidth > MIN_DESCRIPTION_WIDTH) {
|
|
218
249
|
const truncatedDesc = truncateToWidth(descriptionSingleLine, remainingWidth, Ellipsis.Omit);
|
|
250
|
+
const omitted =
|
|
251
|
+
visibleWidth(this.#getDisplayValue(item)) > maxPrimaryWidth ||
|
|
252
|
+
visibleWidth(descriptionSingleLine) > remainingWidth;
|
|
219
253
|
if (isSelected) {
|
|
220
|
-
return
|
|
254
|
+
return {
|
|
255
|
+
line: this.theme.selectedText(`${prefix}${truncatedValue}${spacing}${truncatedDesc}`),
|
|
256
|
+
omitted,
|
|
257
|
+
};
|
|
221
258
|
}
|
|
222
259
|
|
|
223
260
|
const descText = this.theme.description(spacing + truncatedDesc);
|
|
224
|
-
return prefix + truncatedValue + descText;
|
|
261
|
+
return { line: prefix + truncatedValue + descText, omitted };
|
|
225
262
|
}
|
|
226
263
|
}
|
|
227
264
|
|
|
228
265
|
const maxWidth = width - prefixWidth - 2;
|
|
229
266
|
const truncatedValue = this.#truncatePrimary(item, isSelected, maxWidth, maxWidth);
|
|
267
|
+
const omitted = visibleWidth(this.#getDisplayValue(item)) > maxWidth || descriptionSingleLine !== undefined;
|
|
230
268
|
if (isSelected) {
|
|
231
|
-
return this.theme.selectedText(`${prefix}${truncatedValue}`);
|
|
269
|
+
return { line: this.theme.selectedText(`${prefix}${truncatedValue}`), omitted };
|
|
232
270
|
}
|
|
233
271
|
|
|
234
|
-
return prefix + truncatedValue;
|
|
272
|
+
return { line: prefix + truncatedValue, omitted };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
#renderWrappedItem(
|
|
276
|
+
item: SelectItem,
|
|
277
|
+
isSelected: boolean,
|
|
278
|
+
width: number,
|
|
279
|
+
descriptionSingleLine: string | undefined,
|
|
280
|
+
): string[] {
|
|
281
|
+
const cursor = `${this.theme.symbols.cursor} `;
|
|
282
|
+
const prefixWidth = visibleWidth(cursor);
|
|
283
|
+
const content = descriptionSingleLine
|
|
284
|
+
? `${this.#getDisplayValue(item)} — ${descriptionSingleLine}`
|
|
285
|
+
: this.#getDisplayValue(item);
|
|
286
|
+
return wrapTextWithAnsi(content, Math.max(1, width - prefixWidth - 2)).map((wrapped, index) => {
|
|
287
|
+
const prefix = index === 0 && isSelected ? cursor : padding(prefixWidth);
|
|
288
|
+
const line = `${prefix}${wrapped}`;
|
|
289
|
+
return isSelected ? this.theme.selectedText(line) : line;
|
|
290
|
+
});
|
|
235
291
|
}
|
|
236
292
|
|
|
237
293
|
#getPrimaryColumnWidth(): number {
|
package/src/terminal.ts
CHANGED
|
@@ -106,6 +106,7 @@ export interface Terminal {
|
|
|
106
106
|
* Fires when the detected appearance changes, including the initial detection.
|
|
107
107
|
*/
|
|
108
108
|
onAppearanceChange(callback: (appearance: TerminalAppearance) => void): void;
|
|
109
|
+
onFocusChange?(callback: (focused: boolean) => void): void;
|
|
109
110
|
|
|
110
111
|
/** The last detected terminal appearance, or undefined if not yet known. */
|
|
111
112
|
get appearance(): TerminalAppearance | undefined;
|
|
@@ -132,6 +133,8 @@ export class ProcessTerminal implements Terminal {
|
|
|
132
133
|
#writeLogPath = $env.PI_TUI_WRITE_LOG || "";
|
|
133
134
|
#windowsVTInputRestore?: () => void;
|
|
134
135
|
#appearanceCallbacks: Array<(appearance: TerminalAppearance) => void> = [];
|
|
136
|
+
#focusCallbacks: Array<(focused: boolean) => void> = [];
|
|
137
|
+
#focused = true;
|
|
135
138
|
#appearance: TerminalAppearance | undefined;
|
|
136
139
|
#osc11Pending = false;
|
|
137
140
|
#osc11QueryQueued = false;
|
|
@@ -162,6 +165,10 @@ export class ProcessTerminal implements Terminal {
|
|
|
162
165
|
this.#appearanceCallbacks.push(callback);
|
|
163
166
|
}
|
|
164
167
|
|
|
168
|
+
onFocusChange(callback: (focused: boolean) => void): void {
|
|
169
|
+
this.#focusCallbacks.push(callback);
|
|
170
|
+
}
|
|
171
|
+
|
|
165
172
|
start(onInput: (data: string) => void, onResize: () => void, onDisconnect?: () => void): void {
|
|
166
173
|
this.#resizeHandler = onResize;
|
|
167
174
|
this.#disconnectHandler = onDisconnect;
|
|
@@ -184,6 +191,7 @@ export class ProcessTerminal implements Terminal {
|
|
|
184
191
|
|
|
185
192
|
// Enable bracketed paste mode - terminal will wrap pastes in \x1b[200~ ... \x1b[201~
|
|
186
193
|
this.#safeWrite("\x1b[?2004h");
|
|
194
|
+
this.#safeWrite("\x1b[?1004h");
|
|
187
195
|
|
|
188
196
|
// Set up resize handler immediately
|
|
189
197
|
process.stdout.on("resize", this.#resizeHandler);
|
|
@@ -319,6 +327,14 @@ export class ProcessTerminal implements Terminal {
|
|
|
319
327
|
|
|
320
328
|
// Forward individual sequences to the input handler
|
|
321
329
|
this.#stdinBuffer.on("data", (sequence: string) => {
|
|
330
|
+
if (sequence === "\x1b[I" || sequence === "\x1b[O") {
|
|
331
|
+
const focused = sequence === "\x1b[I";
|
|
332
|
+
if (focused !== this.#focused) {
|
|
333
|
+
this.#focused = focused;
|
|
334
|
+
for (const callback of this.#focusCallbacks) callback(focused);
|
|
335
|
+
}
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
322
338
|
// Kitty protocol response — always swallow, enable protocol on first match
|
|
323
339
|
const kittyMatch = sequence.match(kittyResponsePattern);
|
|
324
340
|
if (kittyMatch) {
|
|
@@ -593,6 +609,7 @@ export class ProcessTerminal implements Terminal {
|
|
|
593
609
|
|
|
594
610
|
// Disable bracketed paste mode
|
|
595
611
|
this.#safeWrite("\x1b[?2004l");
|
|
612
|
+
this.#safeWrite("\x1b[?1004l");
|
|
596
613
|
|
|
597
614
|
// Disable Mode 2031 appearance change notifications
|
|
598
615
|
this.#safeWrite("\x1b[?2031l");
|