@oh-my-pi/pi-tui 17.0.8 → 17.1.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/CHANGELOG.md +16 -0
- package/dist/types/components/input.d.ts +2 -0
- package/dist/types/components/select-list.d.ts +2 -1
- package/dist/types/components/text.d.ts +14 -1
- package/package.json +3 -3
- package/src/components/input.ts +10 -2
- package/src/components/select-list.ts +13 -6
- package/src/components/text.ts +24 -2
- package/src/terminal.ts +17 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [17.1.0] - 2026-07-24
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added Text.setStyleFn() to apply foreground stylers at render time, allowing components to dynamically re-resolve colors after invalidation instead of baking in the palette active at construction.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Fixed an issue where exiting or tearing down the TUI left the terminal in cursor-key/keypad application mode (DECCKM), which broke arrow keys in the parent shell. Both stop() and emergencyTerminalRestore() now correctly emit standard rmkx resets.
|
|
14
|
+
|
|
15
|
+
## [17.0.9] - 2026-07-23
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- Added `SelectList.setMaxVisible()` so hosts can refit the visible row budget to available height after construction.
|
|
20
|
+
|
|
5
21
|
## [17.0.8] - 2026-07-22
|
|
6
22
|
|
|
7
23
|
### Fixed
|
|
@@ -6,6 +6,8 @@ export declare class Input implements Component, Focusable {
|
|
|
6
6
|
#private;
|
|
7
7
|
/** Rendered before the editable area; set to "" for chrome-less embedding. */
|
|
8
8
|
prompt: string;
|
|
9
|
+
/** Render the editable value as bullets while retaining the real value internally. */
|
|
10
|
+
mask: boolean;
|
|
9
11
|
onSubmit?: (value: string) => void;
|
|
10
12
|
onEscape?: () => void;
|
|
11
13
|
/** Focusable interface - set by TUI when focus changes */
|
|
@@ -43,13 +43,14 @@ export interface SelectListLayoutOptions {
|
|
|
43
43
|
export declare class SelectList implements Component, MouseRoutable {
|
|
44
44
|
#private;
|
|
45
45
|
private readonly items;
|
|
46
|
-
private readonly maxVisible;
|
|
47
46
|
private readonly theme;
|
|
48
47
|
private readonly layout;
|
|
49
48
|
onSelect?: (item: SelectItem) => void;
|
|
50
49
|
onCancel?: () => void;
|
|
51
50
|
onSelectionChange?: (item: SelectItem) => void;
|
|
52
51
|
constructor(items: ReadonlyArray<SelectItem>, maxVisible: number, theme: SelectListTheme, layout?: SelectListLayoutOptions);
|
|
52
|
+
/** Refit the visible row budget (hosts clamp the list to available height). */
|
|
53
|
+
setMaxVisible(rows: number): void;
|
|
53
54
|
setFilter(filter: string): void;
|
|
54
55
|
setSelectedIndex(index: number): void;
|
|
55
56
|
/** Resolve a 0-based rendered-line index to a filtered-item index. */
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import type { Component } from "../tui.js";
|
|
2
2
|
/**
|
|
3
|
-
* Text component - displays multi-line text with word wrapping
|
|
3
|
+
* Text component - displays multi-line text with word wrapping.
|
|
4
|
+
*
|
|
5
|
+
* Foreground colors may be supplied lazily via {@link setStyleFn} instead of
|
|
6
|
+
* baked into `text`: the styler runs at render time, so a caller that
|
|
7
|
+
* invalidates the component on a theme change (see the coding-agent's
|
|
8
|
+
* `onThemeChange` handler) re-resolves the color against the now-active theme
|
|
9
|
+
* rather than replaying the palette active when the component was constructed.
|
|
4
10
|
*/
|
|
5
11
|
export declare class Text implements Component {
|
|
6
12
|
#private;
|
|
@@ -9,6 +15,13 @@ export declare class Text implements Component {
|
|
|
9
15
|
getText(): string;
|
|
10
16
|
setText(text: string): boolean;
|
|
11
17
|
setCustomBgFn(customBgFn?: (text: string) => string): void;
|
|
18
|
+
/**
|
|
19
|
+
* Supply a foreground styler applied to the text at render time (e.g. a
|
|
20
|
+
* theme color resolver). Unlike baking the color into `text`, the styler
|
|
21
|
+
* re-runs on every render, so invalidating the component after a theme
|
|
22
|
+
* change re-resolves the color against the active theme.
|
|
23
|
+
*/
|
|
24
|
+
setStyleFn(styleFn?: (text: string) => string): this;
|
|
12
25
|
invalidate(): void;
|
|
13
26
|
render(width: number): readonly string[];
|
|
14
27
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-tui",
|
|
4
|
-
"version": "17.0
|
|
4
|
+
"version": "17.1.0",
|
|
5
5
|
"description": "Terminal User Interface library with differential rendering for efficient text-based applications",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
"fmt": "biome format --write ."
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@oh-my-pi/pi-natives": "17.0
|
|
41
|
-
"@oh-my-pi/pi-utils": "17.0
|
|
40
|
+
"@oh-my-pi/pi-natives": "17.1.0",
|
|
41
|
+
"@oh-my-pi/pi-utils": "17.1.0",
|
|
42
42
|
"lru-cache": "11.5.2",
|
|
43
43
|
"marked": "^18.0.6"
|
|
44
44
|
},
|
package/src/components/input.ts
CHANGED
|
@@ -30,6 +30,8 @@ export class Input implements Component, Focusable {
|
|
|
30
30
|
#useTerminalCursor = false;
|
|
31
31
|
/** Rendered before the editable area; set to "" for chrome-less embedding. */
|
|
32
32
|
prompt = "> ";
|
|
33
|
+
/** Render the editable value as bullets while retaining the real value internally. */
|
|
34
|
+
mask = false;
|
|
33
35
|
onSubmit?: (value: string) => void;
|
|
34
36
|
onEscape?: () => void;
|
|
35
37
|
|
|
@@ -415,9 +417,15 @@ export class Input implements Component, Focusable {
|
|
|
415
417
|
return [prompt];
|
|
416
418
|
}
|
|
417
419
|
|
|
418
|
-
|
|
420
|
+
let cursorIndex = this.#cursor;
|
|
419
421
|
// Ensure we always have a grapheme to invert at the cursor (space at end).
|
|
420
|
-
|
|
422
|
+
let visibleValue = this.#value;
|
|
423
|
+
if (this.mask) {
|
|
424
|
+
const graphemes = [...segmenter.segment(this.#value)];
|
|
425
|
+
visibleValue = "•".repeat(graphemes.length);
|
|
426
|
+
cursorIndex = graphemes.filter(grapheme => grapheme.index < this.#cursor).length;
|
|
427
|
+
}
|
|
428
|
+
const displayValue = this.#cursor >= this.#value.length ? `${visibleValue} ` : visibleValue;
|
|
421
429
|
|
|
422
430
|
const totalCols = visibleWidth(displayValue);
|
|
423
431
|
const cursorCols = visibleWidth(displayValue.slice(0, cursorIndex));
|
|
@@ -84,6 +84,7 @@ type SelectItemLayout =
|
|
|
84
84
|
};
|
|
85
85
|
|
|
86
86
|
export class SelectList implements Component, MouseRoutable {
|
|
87
|
+
#maxVisible: number;
|
|
87
88
|
#filteredItems: ReadonlyArray<SelectItem>;
|
|
88
89
|
#filterQuery = "";
|
|
89
90
|
#selectedIndex: number = 0;
|
|
@@ -97,13 +98,19 @@ export class SelectList implements Component, MouseRoutable {
|
|
|
97
98
|
|
|
98
99
|
constructor(
|
|
99
100
|
private readonly items: ReadonlyArray<SelectItem>,
|
|
100
|
-
|
|
101
|
+
maxVisible: number,
|
|
101
102
|
private readonly theme: SelectListTheme,
|
|
102
103
|
private readonly layout: SelectListLayoutOptions = {},
|
|
103
104
|
) {
|
|
105
|
+
this.#maxVisible = Math.max(1, Math.trunc(maxVisible));
|
|
104
106
|
this.#filteredItems = items;
|
|
105
107
|
}
|
|
106
108
|
|
|
109
|
+
/** Refit the visible row budget (hosts clamp the list to available height). */
|
|
110
|
+
setMaxVisible(rows: number): void {
|
|
111
|
+
this.#maxVisible = Math.max(1, Math.trunc(rows));
|
|
112
|
+
}
|
|
113
|
+
|
|
107
114
|
setFilter(filter: string): void {
|
|
108
115
|
this.#setFilter(filter, true);
|
|
109
116
|
}
|
|
@@ -168,7 +175,7 @@ export class SelectList implements Component, MouseRoutable {
|
|
|
168
175
|
const wrapEnabled = this.layout.wrapDescription === true;
|
|
169
176
|
// `maxVisible` is the picker's visual row budget. For non-wrap layouts
|
|
170
177
|
// every item is one row, so the budget matches the original item count.
|
|
171
|
-
const visualBudget = this
|
|
178
|
+
const visualBudget = this.#maxVisible;
|
|
172
179
|
|
|
173
180
|
// Compute per-item visual row counts at the conservative width (i.e.
|
|
174
181
|
// assume the scrollbar column might be reserved). For non-wrap layouts
|
|
@@ -256,12 +263,12 @@ export class SelectList implements Component, MouseRoutable {
|
|
|
256
263
|
}
|
|
257
264
|
// PageUp - jump up by one visible page
|
|
258
265
|
else if (kb.matches(keyData, "tui.select.pageUp")) {
|
|
259
|
-
this.#selectedIndex = Math.max(0, this.#selectedIndex - this
|
|
266
|
+
this.#selectedIndex = Math.max(0, this.#selectedIndex - this.#maxVisible);
|
|
260
267
|
this.#notifySelectionChange();
|
|
261
268
|
}
|
|
262
269
|
// PageDown - jump down by one visible page
|
|
263
270
|
else if (kb.matches(keyData, "tui.select.pageDown")) {
|
|
264
|
-
this.#selectedIndex = Math.min(this.#filteredItems.length - 1, this.#selectedIndex + this
|
|
271
|
+
this.#selectedIndex = Math.min(this.#filteredItems.length - 1, this.#selectedIndex + this.#maxVisible);
|
|
265
272
|
this.#notifySelectionChange();
|
|
266
273
|
}
|
|
267
274
|
// Enter
|
|
@@ -459,12 +466,12 @@ export class SelectList implements Component, MouseRoutable {
|
|
|
459
466
|
|
|
460
467
|
#shouldRenderSearchStatus(): boolean {
|
|
461
468
|
return (
|
|
462
|
-
this.layout.overflowSearch !== false && (this.items.length > this
|
|
469
|
+
this.layout.overflowSearch !== false && (this.items.length > this.#maxVisible || this.#filterQuery.length > 0)
|
|
463
470
|
);
|
|
464
471
|
}
|
|
465
472
|
|
|
466
473
|
#canEditSearch(): boolean {
|
|
467
|
-
return this.layout.overflowSearch !== false && this.items.length > this
|
|
474
|
+
return this.layout.overflowSearch !== false && this.items.length > this.#maxVisible;
|
|
468
475
|
}
|
|
469
476
|
|
|
470
477
|
#handleSearchInput(keyData: string): boolean {
|
package/src/components/text.ts
CHANGED
|
@@ -11,13 +11,20 @@ import {
|
|
|
11
11
|
} from "../utils";
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
* Text component - displays multi-line text with word wrapping
|
|
14
|
+
* Text component - displays multi-line text with word wrapping.
|
|
15
|
+
*
|
|
16
|
+
* Foreground colors may be supplied lazily via {@link setStyleFn} instead of
|
|
17
|
+
* baked into `text`: the styler runs at render time, so a caller that
|
|
18
|
+
* invalidates the component on a theme change (see the coding-agent's
|
|
19
|
+
* `onThemeChange` handler) re-resolves the color against the now-active theme
|
|
20
|
+
* rather than replaying the palette active when the component was constructed.
|
|
15
21
|
*/
|
|
16
22
|
export class Text implements Component {
|
|
17
23
|
#text: string;
|
|
18
24
|
#paddingX: number; // Left/right padding
|
|
19
25
|
#paddingY: number; // Top/bottom padding
|
|
20
26
|
#customBgFn?: (text: string) => string;
|
|
27
|
+
#styleFn?: (text: string) => string;
|
|
21
28
|
|
|
22
29
|
#ignoreTight = false;
|
|
23
30
|
|
|
@@ -64,6 +71,21 @@ export class Text implements Component {
|
|
|
64
71
|
this.#cachedLines = undefined;
|
|
65
72
|
}
|
|
66
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Supply a foreground styler applied to the text at render time (e.g. a
|
|
76
|
+
* theme color resolver). Unlike baking the color into `text`, the styler
|
|
77
|
+
* re-runs on every render, so invalidating the component after a theme
|
|
78
|
+
* change re-resolves the color against the active theme.
|
|
79
|
+
*/
|
|
80
|
+
setStyleFn(styleFn?: (text: string) => string): this {
|
|
81
|
+
this.#styleFn = styleFn;
|
|
82
|
+
this.#cachedText = undefined;
|
|
83
|
+
this.#cachedWidth = undefined;
|
|
84
|
+
this.#cachedWidthEpoch = undefined;
|
|
85
|
+
this.#cachedLines = undefined;
|
|
86
|
+
return this;
|
|
87
|
+
}
|
|
88
|
+
|
|
67
89
|
invalidate(): void {
|
|
68
90
|
this.#cachedText = undefined;
|
|
69
91
|
this.#cachedWidth = undefined;
|
|
@@ -93,7 +115,7 @@ export class Text implements Component {
|
|
|
93
115
|
}
|
|
94
116
|
|
|
95
117
|
// Replace tabs with 3 spaces
|
|
96
|
-
const normalizedText = replaceTabs(this.#text);
|
|
118
|
+
const normalizedText = replaceTabs(this.#styleFn ? this.#styleFn(this.#text) : this.#text);
|
|
97
119
|
|
|
98
120
|
// Calculate content width (subtract left/right margins)
|
|
99
121
|
const paddingX = this.#ignoreTight ? this.#paddingX : getPaddingX(this.#paddingX);
|
package/src/terminal.ts
CHANGED
|
@@ -312,6 +312,7 @@ export function emergencyTerminalRestore(): void {
|
|
|
312
312
|
process.stdout.write(
|
|
313
313
|
"\x1b[?2026l" + // End synchronized output
|
|
314
314
|
"\x1b[?7h" + // Restore autowrap
|
|
315
|
+
"\x1b[?1l\x1b>" + // Restore normal cursor-key + keypad mode (rmkx, #6374)
|
|
315
316
|
"\x1b[?2004l" + // Disable bracketed paste
|
|
316
317
|
"\x1b[?2031l" + // Disable Mode 2031 appearance notifications
|
|
317
318
|
"\x1b[?2048l" + // Disable in-band resize notifications
|
|
@@ -625,6 +626,15 @@ export class ProcessTerminal implements Terminal {
|
|
|
625
626
|
// Enable bracketed paste mode - terminal will wrap pastes in \x1b[200~ ... \x1b[201~
|
|
626
627
|
this.#safeWrite("\x1b[?2004h");
|
|
627
628
|
|
|
629
|
+
// Force normal cursor-key (DECCKM) and numeric-keypad mode (terminfo
|
|
630
|
+
// `rmkx` = "\x1b[?1l\x1b>"). omp decodes both CSI ("\x1b[A") and SS3
|
|
631
|
+
// ("\x1bOA") arrow encodings, so it never enables application mode
|
|
632
|
+
// itself — but a prior program that left the TTY in application-cursor-
|
|
633
|
+
// keys mode makes arrows arrive as SS3. Normalizing on entry keeps input
|
|
634
|
+
// in the predictable default state; stop() restores the same on exit.
|
|
635
|
+
// See #6374.
|
|
636
|
+
this.#safeWrite("\x1b[?1l\x1b>");
|
|
637
|
+
|
|
628
638
|
// Set up resize handler immediately. The OS refreshes process.stdout
|
|
629
639
|
// dimensions before firing `resize`, so it is authoritative for geometry:
|
|
630
640
|
// reconcile any stale cached DEC 2048 report before notifying the renderer.
|
|
@@ -1376,6 +1386,13 @@ export class ProcessTerminal implements Terminal {
|
|
|
1376
1386
|
// begin/end halves of a frame. Safe no-ops on terminals that ignored them.
|
|
1377
1387
|
this.#safeWrite("\x1b[?2026l\x1b[?7h");
|
|
1378
1388
|
|
|
1389
|
+
// Restore normal cursor-key (DECCKM) and numeric-keypad mode (terminfo
|
|
1390
|
+
// `rmkx`). Symmetric with the normalize in start(): a TTY-sharing child
|
|
1391
|
+
// can leave the terminal in application-cursor-keys mode, and without
|
|
1392
|
+
// this reset the parent shell inherits SS3 arrows so Up/Down history
|
|
1393
|
+
// navigation stays broken after omp exits (#6374).
|
|
1394
|
+
this.#safeWrite("\x1b[?1l\x1b>");
|
|
1395
|
+
|
|
1379
1396
|
// Disable bracketed paste mode
|
|
1380
1397
|
this.#safeWrite("\x1b[?2004l");
|
|
1381
1398
|
this.#safeWrite("\x1b[?5522l");
|