@oh-my-pi/pi-tui 15.7.2 → 15.7.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 +25 -0
- package/dist/types/components/select-list.d.ts +2 -0
- package/dist/types/tui.d.ts +12 -0
- package/package.json +4 -4
- package/src/components/editor.ts +7 -5
- package/src/components/select-list.ts +92 -16
- package/src/tui.ts +221 -64
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [15.7.3] - 2026-05-31
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added `overflowSearch` to `SelectListLayoutOptions` to let consumers enable or disable type-to-filter search and search-status rendering per SelectList instance
|
|
10
|
+
- Added fuzzy type-to-filter search to overflowing `SelectList` pickers, with search status and result counts.
|
|
11
|
+
- Added `TUI.setEagerNativeScrollbackRebuild(enabled)` — while enabled, live render frames rebuild native scrollback on offscreen/structural changes even when the viewport position is unobservable (POSIX), instead of deferring to a non-destructive repaint. Trades the anti-yank guarantee for clean, duplicate-free history; intended for windows where output above the fold is actively re-laying out (e.g. a tool whose result is still streaming). A terminal that reports a known-scrolled viewport still defers.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- Disabled interactive search filtering for editor autocomplete and slash-command `SelectList`s by passing `overflowSearch: false` in their layout options
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- Preserved hidden tmux overlays in the live viewport by removing overlay content from view when an overlay was hidden while keeping pane history intact
|
|
20
|
+
- Preserved native scrollback when forced TUI renders coalesce with content growth, and deferred pure tail appends while readers are scrolled into history.
|
|
21
|
+
- Preserved existing terminal scrollback during forced and structural TUI renders so preexisting shell lines remained visible after component mutations
|
|
22
|
+
- Rebuilt native scrollback for safe bottom-anchored offscreen edits and high-water preview collapses instead of repainting only the viewport, preventing stale or duplicated rows above the live viewport.
|
|
23
|
+
- Stripped internal cursor marker sentinels from all rendered lines so offscreen focus markers no longer leak into terminal output
|
|
24
|
+
- Truncated all painted lines to terminal width during viewport repaints and append-tail updates so long content no longer overflows or wraps unexpectedly
|
|
25
|
+
- Fixed `tui.select.cancel` handling in `SelectList` so pressing Escape or Ctrl+C closes the list even when no matches are currently shown
|
|
26
|
+
- Fixed native scrollback corruption when an offscreen row edit and repeated-tail append land in one render frame; ambiguous appended tails now rebuild history instead of splicing stale rows into the buffer.
|
|
27
|
+
- Fixed scrolled-up readers being yanked back to the tail whenever streaming content arrived on POSIX terminals (macOS/Linux). Native viewport position is unobservable there (`isNativeViewportAtBottom()` returns `undefined`), and the planner optimistically treated "unknown" as "at bottom", so every offscreen streaming edit ran a destructive `historyRebuild` that cleared scrollback and snapped the view to the bottom. Live render frames now treat an unknown viewport as unsafe for a destructive rebuild — they defer to a non-destructive viewport repaint and reconcile native scrollback at the next explicit checkpoint (prompt submit). Resize and checkpoint replays keep the prior behavior.
|
|
28
|
+
- Fixed native scrollback not rewrapping when the terminal widens on POSIX. A width increase reflows the transcript to fewer lines, which the shrink-across-boundary branch intercepted and (after the unknown-viewport deferral) repainted only the viewport — leaving committed history wrapped at the old width and duplicated above the live viewport. Width changes now rebuild native scrollback at the new geometry even when the viewport position is unknown (a yank is acceptable on an explicit resize); a terminal that can report a scrolled viewport still defers.
|
|
29
|
+
|
|
5
30
|
## [15.7.0] - 2026-05-31
|
|
6
31
|
|
|
7
32
|
### Fixed
|
|
@@ -26,6 +26,8 @@ export interface SelectListLayoutOptions {
|
|
|
26
26
|
minPrimaryColumnWidth?: number;
|
|
27
27
|
maxPrimaryColumnWidth?: number;
|
|
28
28
|
truncatePrimary?: (context: SelectListTruncatePrimaryContext) => string;
|
|
29
|
+
/** Enable type-to-filter search when the item count exceeds maxVisible. Defaults to true. */
|
|
30
|
+
overflowSearch?: boolean;
|
|
29
31
|
}
|
|
30
32
|
export declare class SelectList implements Component {
|
|
31
33
|
#private;
|
package/dist/types/tui.d.ts
CHANGED
|
@@ -164,6 +164,18 @@ export declare class TUI extends Container {
|
|
|
164
164
|
* When false, empty rows remain (reduces redraws on slower terminals).
|
|
165
165
|
*/
|
|
166
166
|
setClearOnShrink(enabled: boolean): void;
|
|
167
|
+
/**
|
|
168
|
+
* When enabled, live render frames rebuild native scrollback on offscreen and
|
|
169
|
+
* structural changes even when the viewport position is unobservable (POSIX,
|
|
170
|
+
* where `isNativeViewportAtBottom()` is `undefined`), instead of deferring to a
|
|
171
|
+
* non-destructive repaint. This trades the anti-yank guarantee for a clean,
|
|
172
|
+
* duplicate-free history and is meant for windows where output above the fold
|
|
173
|
+
* is actively re-rendering — e.g. a tool whose result is still streaming and
|
|
174
|
+
* re-laying-out rows that have already scrolled into history. A snap to the tail
|
|
175
|
+
* is acceptable there. A terminal that can report a *known*-scrolled viewport
|
|
176
|
+
* (Windows) still defers; only the unknown case is forced to rebuild.
|
|
177
|
+
*/
|
|
178
|
+
setEagerNativeScrollbackRebuild(enabled: boolean): void;
|
|
167
179
|
setFocus(component: Component | null): void;
|
|
168
180
|
/**
|
|
169
181
|
* Show an overlay component with configurable positioning and sizing.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-tui",
|
|
4
|
-
"version": "15.7.
|
|
4
|
+
"version": "15.7.3",
|
|
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",
|
|
@@ -32,13 +32,13 @@
|
|
|
32
32
|
"check": "biome check . && bun run check:types",
|
|
33
33
|
"check:types": "tsgo -p tsconfig.json --noEmit",
|
|
34
34
|
"lint": "biome lint .",
|
|
35
|
-
"test": "bun test test/*.test.ts",
|
|
35
|
+
"test": "bun test --parallel test/*.test.ts",
|
|
36
36
|
"fix": "biome check --write --unsafe .",
|
|
37
37
|
"fmt": "biome format --write ."
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@oh-my-pi/pi-natives": "15.7.
|
|
41
|
-
"@oh-my-pi/pi-utils": "15.7.
|
|
40
|
+
"@oh-my-pi/pi-natives": "15.7.3",
|
|
41
|
+
"@oh-my-pi/pi-utils": "15.7.3",
|
|
42
42
|
"lru-cache": "11.5.1",
|
|
43
43
|
"marked": "^18.0.4"
|
|
44
44
|
},
|
package/src/components/editor.ts
CHANGED
|
@@ -19,9 +19,14 @@ import {
|
|
|
19
19
|
} from "../utils";
|
|
20
20
|
import { SelectList, type SelectListLayoutOptions, type SelectListTheme } from "./select-list";
|
|
21
21
|
|
|
22
|
+
const AUTOCOMPLETE_SELECT_LIST_LAYOUT: SelectListLayoutOptions = {
|
|
23
|
+
overflowSearch: false,
|
|
24
|
+
};
|
|
25
|
+
|
|
22
26
|
const SLASH_COMMAND_SELECT_LIST_LAYOUT: SelectListLayoutOptions = {
|
|
23
27
|
minPrimaryColumnWidth: 12,
|
|
24
28
|
maxPrimaryColumnWidth: 32,
|
|
29
|
+
overflowSearch: false,
|
|
25
30
|
};
|
|
26
31
|
|
|
27
32
|
function sanitizeLoadedText(text: string): string {
|
|
@@ -2524,11 +2529,8 @@ export class Editor implements Component, Focusable {
|
|
|
2524
2529
|
prefix: string,
|
|
2525
2530
|
items: Array<{ value: string; label: string; description?: string }>,
|
|
2526
2531
|
): SelectList {
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
// TODO: Pass layout to SelectList when constructor is updated to support it
|
|
2530
|
-
void layout; // Use layout variable to avoid lint warnings
|
|
2531
|
-
return new SelectList(items, this.#autocompleteMaxVisible, this.#theme.selectList);
|
|
2532
|
+
const layout = prefix.startsWith("/") ? SLASH_COMMAND_SELECT_LIST_LAYOUT : AUTOCOMPLETE_SELECT_LIST_LAYOUT;
|
|
2533
|
+
return new SelectList(items, this.#autocompleteMaxVisible, this.#theme.selectList, layout);
|
|
2532
2534
|
}
|
|
2533
2535
|
|
|
2534
2536
|
#handleTabCompletion(): void {
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { fuzzyFilter } from "../fuzzy";
|
|
1
2
|
import { getKeybindings } from "../keybindings";
|
|
3
|
+
import { extractPrintableText } from "../keys";
|
|
2
4
|
import type { SymbolTheme } from "../symbols";
|
|
3
5
|
import type { Component } from "../tui";
|
|
4
6
|
import { Ellipsis, padding, replaceTabs, truncateToWidth, visibleWidth } from "../utils";
|
|
@@ -45,10 +47,13 @@ export interface SelectListLayoutOptions {
|
|
|
45
47
|
minPrimaryColumnWidth?: number;
|
|
46
48
|
maxPrimaryColumnWidth?: number;
|
|
47
49
|
truncatePrimary?: (context: SelectListTruncatePrimaryContext) => string;
|
|
50
|
+
/** Enable type-to-filter search when the item count exceeds maxVisible. Defaults to true. */
|
|
51
|
+
overflowSearch?: boolean;
|
|
48
52
|
}
|
|
49
53
|
|
|
50
54
|
export class SelectList implements Component {
|
|
51
55
|
#filteredItems: ReadonlyArray<SelectItem>;
|
|
56
|
+
#filterQuery = "";
|
|
52
57
|
#selectedIndex: number = 0;
|
|
53
58
|
|
|
54
59
|
onSelect?: (item: SelectItem) => void;
|
|
@@ -65,9 +70,7 @@ export class SelectList implements Component {
|
|
|
65
70
|
}
|
|
66
71
|
|
|
67
72
|
setFilter(filter: string): void {
|
|
68
|
-
this.#
|
|
69
|
-
// Reset selection when filter changes
|
|
70
|
-
this.#selectedIndex = 0;
|
|
73
|
+
this.#setFilter(filter, true);
|
|
71
74
|
}
|
|
72
75
|
|
|
73
76
|
setSelectedIndex(index: number): void {
|
|
@@ -80,10 +83,14 @@ export class SelectList implements Component {
|
|
|
80
83
|
|
|
81
84
|
render(width: number): string[] {
|
|
82
85
|
const lines: string[] = [];
|
|
86
|
+
const showSearchStatus = this.#shouldRenderSearchStatus();
|
|
83
87
|
|
|
84
88
|
// If no items match filter, show message
|
|
85
89
|
if (this.#filteredItems.length === 0) {
|
|
86
|
-
|
|
90
|
+
if (showSearchStatus) {
|
|
91
|
+
lines.push(this.#renderStatusLine(width));
|
|
92
|
+
}
|
|
93
|
+
lines.push(this.theme.noMatch(" No matching items"));
|
|
87
94
|
return lines;
|
|
88
95
|
}
|
|
89
96
|
|
|
@@ -106,19 +113,29 @@ export class SelectList implements Component {
|
|
|
106
113
|
lines.push(this.#renderItem(item, isSelected, width, descriptionText, primaryColumnWidth));
|
|
107
114
|
}
|
|
108
115
|
|
|
109
|
-
// Add scroll
|
|
110
|
-
if (startIndex > 0 || endIndex < this.#filteredItems.length) {
|
|
111
|
-
|
|
112
|
-
// Truncate if too long for terminal
|
|
113
|
-
lines.push(this.theme.scrollInfo(truncateToWidth(scrollText, width - 2, Ellipsis.Omit)));
|
|
116
|
+
// Add scroll/search status when needed
|
|
117
|
+
if (startIndex > 0 || endIndex < this.#filteredItems.length || showSearchStatus) {
|
|
118
|
+
lines.push(this.#renderStatusLine(width));
|
|
114
119
|
}
|
|
115
120
|
|
|
116
121
|
return lines;
|
|
117
122
|
}
|
|
118
123
|
|
|
119
124
|
handleInput(keyData: string): void {
|
|
120
|
-
if (this.#filteredItems.length === 0) return;
|
|
121
125
|
const kb = getKeybindings();
|
|
126
|
+
// Escape or Ctrl+C
|
|
127
|
+
if (kb.matches(keyData, "tui.select.cancel")) {
|
|
128
|
+
if (this.onCancel) {
|
|
129
|
+
this.onCancel();
|
|
130
|
+
}
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (this.#handleSearchInput(keyData)) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (this.#filteredItems.length === 0) return;
|
|
122
139
|
// Up arrow - wrap to bottom when at top
|
|
123
140
|
if (kb.matches(keyData, "tui.select.up")) {
|
|
124
141
|
this.#selectedIndex = this.#selectedIndex === 0 ? this.#filteredItems.length - 1 : this.#selectedIndex - 1;
|
|
@@ -146,12 +163,6 @@ export class SelectList implements Component {
|
|
|
146
163
|
this.onSelect(selectedItem);
|
|
147
164
|
}
|
|
148
165
|
}
|
|
149
|
-
// Escape or Ctrl+C
|
|
150
|
-
else if (kb.matches(keyData, "tui.select.cancel")) {
|
|
151
|
-
if (this.onCancel) {
|
|
152
|
-
this.onCancel();
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
166
|
}
|
|
156
167
|
|
|
157
168
|
#renderItem(
|
|
@@ -235,6 +246,71 @@ export class SelectList implements Component {
|
|
|
235
246
|
return sanitizeSingleLine(item.label || item.value);
|
|
236
247
|
}
|
|
237
248
|
|
|
249
|
+
#renderStatusLine(width: number): string {
|
|
250
|
+
const selectedCount = this.#filteredItems.length === 0 ? 0 : this.#selectedIndex + 1;
|
|
251
|
+
const filteredCount = this.#filteredItems.length;
|
|
252
|
+
const count =
|
|
253
|
+
this.#filterQuery.trim() && filteredCount !== this.items.length
|
|
254
|
+
? `${selectedCount}/${filteredCount} of ${this.items.length}`
|
|
255
|
+
: `${selectedCount}/${filteredCount}`;
|
|
256
|
+
const query = sanitizeSingleLine(this.#filterQuery);
|
|
257
|
+
const searchSuffix = this.#shouldRenderSearchStatus() ? (query ? ` Search: ${query}` : " Type to search") : "";
|
|
258
|
+
const statusText = ` (${count})${searchSuffix}`;
|
|
259
|
+
return this.theme.scrollInfo(truncateToWidth(statusText, Math.max(1, width - 2), Ellipsis.Omit));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
#shouldRenderSearchStatus(): boolean {
|
|
263
|
+
return (
|
|
264
|
+
this.layout.overflowSearch !== false && (this.items.length > this.maxVisible || this.#filterQuery.length > 0)
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
#canEditSearch(): boolean {
|
|
269
|
+
return this.layout.overflowSearch !== false && this.items.length > this.maxVisible;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
#handleSearchInput(keyData: string): boolean {
|
|
273
|
+
if (!this.#canEditSearch()) return false;
|
|
274
|
+
|
|
275
|
+
const kb = getKeybindings();
|
|
276
|
+
if (kb.matches(keyData, "tui.editor.deleteCharBackward")) {
|
|
277
|
+
if (this.#filterQuery.length === 0) return false;
|
|
278
|
+
const chars = [...this.#filterQuery];
|
|
279
|
+
chars.pop();
|
|
280
|
+
this.#setFilter(chars.join(""), true);
|
|
281
|
+
return true;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const printableText = extractPrintableText(keyData);
|
|
285
|
+
if (printableText === undefined) return false;
|
|
286
|
+
if (this.#filterQuery.length === 0 && printableText.trim().length === 0) return false;
|
|
287
|
+
|
|
288
|
+
this.#setFilter(this.#filterQuery + printableText, true);
|
|
289
|
+
return true;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
#setFilter(filter: string, notify: boolean): void {
|
|
293
|
+
this.#filterQuery = filter;
|
|
294
|
+
this.#filteredItems = filter.trim()
|
|
295
|
+
? fuzzyFilter([...this.items], filter, item => this.#getFilterText(item))
|
|
296
|
+
: this.items;
|
|
297
|
+
this.#selectedIndex = 0;
|
|
298
|
+
if (notify) {
|
|
299
|
+
this.#notifySelectionChange();
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
#getFilterText(item: SelectItem): string {
|
|
304
|
+
let text = `${item.label} ${item.value}`;
|
|
305
|
+
if (item.description) {
|
|
306
|
+
text += ` ${item.description}`;
|
|
307
|
+
}
|
|
308
|
+
if (item.hint) {
|
|
309
|
+
text += ` ${item.hint}`;
|
|
310
|
+
}
|
|
311
|
+
return sanitizeSingleLine(text);
|
|
312
|
+
}
|
|
313
|
+
|
|
238
314
|
#notifySelectionChange(): void {
|
|
239
315
|
const selectedItem = this.#filteredItems[this.#selectedIndex];
|
|
240
316
|
if (selectedItem && this.onSelectionChange) {
|
package/src/tui.ts
CHANGED
|
@@ -282,6 +282,7 @@ type RenderIntent =
|
|
|
282
282
|
| { kind: "initial" }
|
|
283
283
|
| { kind: "sessionReplace" }
|
|
284
284
|
| { kind: "historyRebuild" }
|
|
285
|
+
| { kind: "overlayRebuild" }
|
|
285
286
|
| { kind: "viewportRepaint"; appendFrom?: number }
|
|
286
287
|
| { kind: "deferredShrink"; paddedLength: number }
|
|
287
288
|
| { kind: "deferredMutation" }
|
|
@@ -328,7 +329,9 @@ export class TUI extends Container {
|
|
|
328
329
|
#nativeScrollbackDirty = false;
|
|
329
330
|
#fullRedrawCount = 0;
|
|
330
331
|
#clearScrollbackOnNextRender = false;
|
|
332
|
+
#forceViewportRepaintOnNextRender = false;
|
|
331
333
|
#allowUnknownViewportMutationOnNextRender = false;
|
|
334
|
+
#eagerNativeScrollbackRebuild = false;
|
|
332
335
|
#hasEverRendered = false;
|
|
333
336
|
#stopped = false;
|
|
334
337
|
|
|
@@ -376,6 +379,21 @@ export class TUI extends Container {
|
|
|
376
379
|
this.#clearOnShrink = enabled;
|
|
377
380
|
}
|
|
378
381
|
|
|
382
|
+
/**
|
|
383
|
+
* When enabled, live render frames rebuild native scrollback on offscreen and
|
|
384
|
+
* structural changes even when the viewport position is unobservable (POSIX,
|
|
385
|
+
* where `isNativeViewportAtBottom()` is `undefined`), instead of deferring to a
|
|
386
|
+
* non-destructive repaint. This trades the anti-yank guarantee for a clean,
|
|
387
|
+
* duplicate-free history and is meant for windows where output above the fold
|
|
388
|
+
* is actively re-rendering — e.g. a tool whose result is still streaming and
|
|
389
|
+
* re-laying-out rows that have already scrolled into history. A snap to the tail
|
|
390
|
+
* is acceptable there. A terminal that can report a *known*-scrolled viewport
|
|
391
|
+
* (Windows) still defers; only the unknown case is forced to rebuild.
|
|
392
|
+
*/
|
|
393
|
+
setEagerNativeScrollbackRebuild(enabled: boolean): void {
|
|
394
|
+
this.#eagerNativeScrollbackRebuild = enabled;
|
|
395
|
+
}
|
|
396
|
+
|
|
379
397
|
setFocus(component: Component | null): void {
|
|
380
398
|
// Clear focused flag on old component
|
|
381
399
|
if (isFocusable(this.#focusedComponent)) {
|
|
@@ -675,7 +693,7 @@ export class TUI extends Container {
|
|
|
675
693
|
) {
|
|
676
694
|
return false;
|
|
677
695
|
}
|
|
678
|
-
this.#prepareForcedRender(true);
|
|
696
|
+
this.#prepareForcedRender(true, options?.allowUnknownViewport === true);
|
|
679
697
|
this.#renderRequested = false;
|
|
680
698
|
this.#lastRenderAt = performance.now();
|
|
681
699
|
this.#doRender();
|
|
@@ -683,9 +701,10 @@ export class TUI extends Container {
|
|
|
683
701
|
}
|
|
684
702
|
|
|
685
703
|
requestRender(force = false, options?: RenderRequestOptions): void {
|
|
686
|
-
|
|
704
|
+
const allowUnknownViewportMutation = options?.allowUnknownViewportMutation === true;
|
|
705
|
+
this.#allowUnknownViewportMutationOnNextRender ||= allowUnknownViewportMutation;
|
|
687
706
|
if (force) {
|
|
688
|
-
this.#prepareForcedRender(options?.clearScrollback === true);
|
|
707
|
+
this.#prepareForcedRender(options?.clearScrollback === true, allowUnknownViewportMutation);
|
|
689
708
|
this.#renderRequested = true;
|
|
690
709
|
process.nextTick(() => {
|
|
691
710
|
if (this.#stopped || !this.#renderRequested) {
|
|
@@ -702,15 +721,15 @@ export class TUI extends Container {
|
|
|
702
721
|
process.nextTick(() => this.#scheduleRender());
|
|
703
722
|
}
|
|
704
723
|
|
|
705
|
-
#prepareForcedRender(clearScrollback: boolean): void {
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
this.#
|
|
713
|
-
this.#
|
|
724
|
+
#prepareForcedRender(clearScrollback: boolean, allowUnknownViewportMutation: boolean): void {
|
|
725
|
+
const geometryChanged =
|
|
726
|
+
(this.#previousWidth > 0 && this.#previousWidth !== this.terminal.columns) ||
|
|
727
|
+
(this.#previousHeight > 0 && this.#previousHeight !== this.terminal.rows);
|
|
728
|
+
const replayGeometry =
|
|
729
|
+
geometryChanged &&
|
|
730
|
+
this.#canReplayNativeScrollbackAtCheckpoint(this.#readNativeViewportAtBottom(), allowUnknownViewportMutation);
|
|
731
|
+
this.#clearScrollbackOnNextRender ||= clearScrollback || replayGeometry;
|
|
732
|
+
this.#forceViewportRepaintOnNextRender = true;
|
|
714
733
|
if (this.#renderTimer) {
|
|
715
734
|
clearTimeout(this.#renderTimer);
|
|
716
735
|
this.#renderTimer = undefined;
|
|
@@ -1090,23 +1109,27 @@ export class TUI extends Container {
|
|
|
1090
1109
|
* @returns Cursor position { row, col } or null if no marker found
|
|
1091
1110
|
*/
|
|
1092
1111
|
#extractCursorPosition(lines: string[], height: number): { row: number; col: number } | null {
|
|
1093
|
-
//
|
|
1112
|
+
// Cursor markers are internal sentinels and must never reach the terminal,
|
|
1113
|
+
// even when the focused component is above the visible viewport. Only a
|
|
1114
|
+
// visible marker becomes a hardware cursor target.
|
|
1094
1115
|
const viewportTop = Math.max(0, lines.length - height);
|
|
1095
|
-
|
|
1116
|
+
let cursor: { row: number; col: number } | null = null;
|
|
1117
|
+
for (let row = lines.length - 1; row >= 0; row--) {
|
|
1096
1118
|
const line = lines[row];
|
|
1097
|
-
|
|
1098
|
-
if (markerIndex
|
|
1099
|
-
|
|
1119
|
+
let markerIndex = line.indexOf(CURSOR_MARKER);
|
|
1120
|
+
if (markerIndex === -1) continue;
|
|
1121
|
+
if (cursor === null && row >= viewportTop) {
|
|
1100
1122
|
const beforeMarker = line.slice(0, markerIndex);
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1123
|
+
cursor = { row, col: visibleWidth(beforeMarker) };
|
|
1124
|
+
}
|
|
1125
|
+
let stripped = line;
|
|
1126
|
+
while (markerIndex !== -1) {
|
|
1127
|
+
stripped = stripped.slice(0, markerIndex) + stripped.slice(markerIndex + CURSOR_MARKER.length);
|
|
1128
|
+
markerIndex = stripped.indexOf(CURSOR_MARKER, markerIndex);
|
|
1107
1129
|
}
|
|
1130
|
+
lines[row] = stripped;
|
|
1108
1131
|
}
|
|
1109
|
-
return
|
|
1132
|
+
return cursor;
|
|
1110
1133
|
}
|
|
1111
1134
|
|
|
1112
1135
|
/**
|
|
@@ -1140,19 +1163,25 @@ export class TUI extends Container {
|
|
|
1140
1163
|
const height = this.terminal.rows;
|
|
1141
1164
|
|
|
1142
1165
|
// 1. Compose the frame.
|
|
1143
|
-
let
|
|
1166
|
+
let baseLines = this.render(width);
|
|
1167
|
+
let lines = baseLines;
|
|
1144
1168
|
if (this.overlayStack.length > 0) {
|
|
1145
|
-
lines = this.#compositeOverlays(
|
|
1169
|
+
lines = this.#compositeOverlays(baseLines, width, height);
|
|
1146
1170
|
}
|
|
1147
1171
|
const cursorPos = this.#extractCursorPosition(lines, height);
|
|
1148
|
-
lines = this.#applyLineResets(lines);
|
|
1172
|
+
lines = this.#fitLinesToWidth(this.#applyLineResets(lines), width);
|
|
1173
|
+
if (lines !== baseLines) {
|
|
1174
|
+
this.#extractCursorPosition(baseLines, height);
|
|
1175
|
+
baseLines = this.#fitLinesToWidth(this.#applyLineResets(baseLines), width);
|
|
1176
|
+
}
|
|
1149
1177
|
|
|
1150
1178
|
// 2. Capture transition + pre-render state before any emitter runs.
|
|
1151
1179
|
const prevViewportTop = this.#viewportTopRow;
|
|
1152
1180
|
const prevHardwareCursorRow = this.#hardwareCursorRow;
|
|
1153
1181
|
const widthChanged = this.#previousWidth > 0 && this.#previousWidth !== width;
|
|
1154
1182
|
const heightChanged = this.#previousHeight > 0 && this.#previousHeight !== height;
|
|
1155
|
-
const allowUnknownViewportMutation =
|
|
1183
|
+
const allowUnknownViewportMutation =
|
|
1184
|
+
this.#allowUnknownViewportMutationOnNextRender || this.#eagerNativeScrollbackRebuild;
|
|
1156
1185
|
this.#allowUnknownViewportMutationOnNextRender = false;
|
|
1157
1186
|
|
|
1158
1187
|
// 3. Classify intent.
|
|
@@ -1192,9 +1221,17 @@ export class TUI extends Container {
|
|
|
1192
1221
|
clearScrollback: !isMultiplexerSession(),
|
|
1193
1222
|
});
|
|
1194
1223
|
return;
|
|
1224
|
+
case "overlayRebuild":
|
|
1225
|
+
this.#clearNativeScrollbackDirty();
|
|
1226
|
+
this.#emitFullPaint(baseLines, width, height, null, {
|
|
1227
|
+
clearViewport: true,
|
|
1228
|
+
clearScrollback: !isMultiplexerSession(),
|
|
1229
|
+
});
|
|
1230
|
+
this.#emitViewportRepaint(lines, width, height, cursorPos);
|
|
1231
|
+
return;
|
|
1195
1232
|
case "viewportRepaint":
|
|
1196
1233
|
if (intent.appendFrom !== undefined) {
|
|
1197
|
-
this.#emitAppendTail(lines, intent.appendFrom, height, prevViewportTop, prevHardwareCursorRow);
|
|
1234
|
+
this.#emitAppendTail(lines, intent.appendFrom, height, width, prevViewportTop, prevHardwareCursorRow);
|
|
1198
1235
|
}
|
|
1199
1236
|
this.#emitViewportRepaint(lines, width, height, cursorPos);
|
|
1200
1237
|
return;
|
|
@@ -1251,23 +1288,31 @@ export class TUI extends Container {
|
|
|
1251
1288
|
// Caller opted into a scrollback wipe via requestRender(true, { clearScrollback: true }).
|
|
1252
1289
|
if (this.#clearScrollbackOnNextRender) return { kind: "sessionReplace" };
|
|
1253
1290
|
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1291
|
+
const forceViewportRepaint = this.#forceViewportRepaintOnNextRender;
|
|
1292
|
+
if (this.hasOverlay()) {
|
|
1293
|
+
const nativeViewportAtBottom = this.#readNativeViewportAtBottom();
|
|
1294
|
+
if (
|
|
1295
|
+
this.#nativeScrollbackDirty &&
|
|
1296
|
+
this.#canRebuildNativeScrollbackLive(nativeViewportAtBottom, allowUnknownViewportMutation)
|
|
1297
|
+
) {
|
|
1298
|
+
return { kind: "overlayRebuild" };
|
|
1299
|
+
}
|
|
1300
|
+
this.#markNativeScrollbackDirty();
|
|
1301
|
+
return { kind: "viewportRepaint" };
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1258
1304
|
if (this.#nativeScrollbackDirty && this.#nativeViewportIsAtBottom(this.#readNativeViewportAtBottom())) {
|
|
1259
1305
|
return { kind: "historyRebuild" };
|
|
1260
1306
|
}
|
|
1261
1307
|
|
|
1262
1308
|
const diff = this.#diffLines(newLines);
|
|
1263
1309
|
// Shrink across the viewport boundary: the new transcript would re-expose
|
|
1264
|
-
// rows already committed to native scrollback.
|
|
1265
|
-
//
|
|
1266
|
-
//
|
|
1267
|
-
//
|
|
1268
|
-
//
|
|
1269
|
-
//
|
|
1270
|
-
// which looks like a jump upward.
|
|
1310
|
+
// rows already committed to native scrollback. Rebuild immediately when the
|
|
1311
|
+
// viewport is known/allowed to be at the tail; otherwise defer the rewrite
|
|
1312
|
+
// and repaint against the previous row count so users scrolled into history
|
|
1313
|
+
// are not yanked. A viewport-only repaint for a bottom-anchored shrink leaves
|
|
1314
|
+
// stale high-water rows in native scrollback and duplicates the new tail above
|
|
1315
|
+
// the viewport.
|
|
1271
1316
|
const naturalViewportTop = Math.max(0, newLines.length - height);
|
|
1272
1317
|
if (
|
|
1273
1318
|
diff.firstChanged !== -1 &&
|
|
@@ -1275,17 +1320,22 @@ export class TUI extends Container {
|
|
|
1275
1320
|
naturalViewportTop < this.#scrollbackHighWater &&
|
|
1276
1321
|
!isMultiplexerSession()
|
|
1277
1322
|
) {
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1323
|
+
const nativeViewportAtBottom = this.#readNativeViewportAtBottom();
|
|
1324
|
+
if (this.#nativeViewportIsScrolled(nativeViewportAtBottom, allowUnknownViewportMutation)) {
|
|
1325
|
+
this.#markNativeScrollbackDirty();
|
|
1326
|
+
return { kind: "deferredShrink", paddedLength: this.#previousLines.length };
|
|
1327
|
+
}
|
|
1328
|
+
// A width change rewraps the whole transcript, so committed scrollback is
|
|
1329
|
+
// mis-wrapped at the old width. Yank is acceptable on an explicit resize, so
|
|
1330
|
+
// rebuild even when the viewport position is unknown (POSIX); the
|
|
1331
|
+
// known-scrolled case already deferred above.
|
|
1332
|
+
if (
|
|
1333
|
+
widthChanged ||
|
|
1334
|
+
this.#canRebuildNativeScrollbackLive(nativeViewportAtBottom, allowUnknownViewportMutation)
|
|
1335
|
+
) {
|
|
1283
1336
|
return { kind: "historyRebuild" };
|
|
1284
1337
|
}
|
|
1285
1338
|
this.#markNativeScrollbackDirty();
|
|
1286
|
-
if (this.#nativeViewportIsScrolled(this.#readNativeViewportAtBottom(), allowUnknownViewportMutation)) {
|
|
1287
|
-
return { kind: "deferredShrink", paddedLength: this.#previousLines.length };
|
|
1288
|
-
}
|
|
1289
1339
|
return { kind: "viewportRepaint" };
|
|
1290
1340
|
}
|
|
1291
1341
|
|
|
@@ -1297,13 +1347,36 @@ export class TUI extends Container {
|
|
|
1297
1347
|
diff.firstChanged < this.#previousLines.length &&
|
|
1298
1348
|
!isMultiplexerSession()
|
|
1299
1349
|
) {
|
|
1350
|
+
// A checkpoint replay is followed by one frame where transient live chrome
|
|
1351
|
+
// (status/footer rows) may be inserted inside the visible suffix and then
|
|
1352
|
+
// disappear; repaint it in place so it never enters scrollback. If the
|
|
1353
|
+
// insertion grows the overflow boundary, native history would lose rows
|
|
1354
|
+
// while the viewport looks correct, so rebuild instead.
|
|
1355
|
+
const appendedTailStart = this.#findAppendedTailStart(newLines);
|
|
1356
|
+
const overflowBefore = Math.max(0, this.#previousLines.length - height);
|
|
1357
|
+
const overflowAfter = Math.max(0, newLines.length - height);
|
|
1358
|
+
if (
|
|
1359
|
+
appendedTailStart === newLines.length &&
|
|
1360
|
+
diff.firstChanged >= prevViewportTop &&
|
|
1361
|
+
overflowAfter <= overflowBefore
|
|
1362
|
+
) {
|
|
1363
|
+
return { kind: "viewportRepaint" };
|
|
1364
|
+
}
|
|
1365
|
+
const nativeViewportAtBottom = this.#readNativeViewportAtBottom();
|
|
1366
|
+
if (this.#canRebuildNativeScrollbackLive(nativeViewportAtBottom, allowUnknownViewportMutation)) {
|
|
1367
|
+
return { kind: "historyRebuild" };
|
|
1368
|
+
}
|
|
1369
|
+
this.#markNativeScrollbackDirty();
|
|
1300
1370
|
return { kind: "viewportRepaint" };
|
|
1301
1371
|
}
|
|
1302
1372
|
|
|
1303
1373
|
if (diff.firstChanged === -1) {
|
|
1304
|
-
// Content unchanged.
|
|
1305
|
-
//
|
|
1306
|
-
//
|
|
1374
|
+
// Content unchanged. A forced render still needs to refresh the visible
|
|
1375
|
+
// viewport, but it must keep the existing diff basis so later coalesced
|
|
1376
|
+
// content mutations can still update native scrollback correctly.
|
|
1377
|
+
if (forceViewportRepaint) return { kind: "viewportRepaint" };
|
|
1378
|
+
// Width change still alters wrapping geometry; height change shifts the
|
|
1379
|
+
// visible window. Either needs a repaint (outside hostile environments).
|
|
1307
1380
|
if (widthChanged) return { kind: "viewportRepaint" };
|
|
1308
1381
|
if (heightChanged && !isTermuxSession() && !isMultiplexerSession()) return { kind: "viewportRepaint" };
|
|
1309
1382
|
return { kind: "noop" };
|
|
@@ -1328,29 +1401,47 @@ export class TUI extends Container {
|
|
|
1328
1401
|
const contentGrew = newLines.length > this.#previousLines.length;
|
|
1329
1402
|
const pureAppend = diff.appendedLines && diff.firstChanged === this.#previousLines.length;
|
|
1330
1403
|
const structuralMutation = newLines.length !== this.#previousLines.length || diff.firstChanged < prevViewportTop;
|
|
1404
|
+
if (pureAppend && contentGrew && this.#previousLines.length > height && !isMultiplexerSession()) {
|
|
1405
|
+
const nativeViewportAtBottom = this.#readNativeViewportAtBottom();
|
|
1406
|
+
if (this.#nativeViewportIsScrolled(nativeViewportAtBottom, allowUnknownViewportMutation)) {
|
|
1407
|
+
this.#markNativeScrollbackDirty();
|
|
1408
|
+
return { kind: "deferredMutation" };
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1331
1411
|
if (!pureAppend && structuralMutation && !isMultiplexerSession()) {
|
|
1332
1412
|
const nativeViewportAtBottom = this.#readNativeViewportAtBottom();
|
|
1333
1413
|
if (this.#nativeViewportIsScrolled(nativeViewportAtBottom, allowUnknownViewportMutation)) {
|
|
1334
1414
|
this.#markNativeScrollbackDirty();
|
|
1335
1415
|
return { kind: "deferredMutation" };
|
|
1336
1416
|
}
|
|
1337
|
-
//
|
|
1338
|
-
//
|
|
1339
|
-
//
|
|
1340
|
-
//
|
|
1341
|
-
//
|
|
1417
|
+
// The append-tail path can only scroll a clean pure-tail append over an
|
|
1418
|
+
// offscreen edit into history: the rows it pushes must equal the net
|
|
1419
|
+
// growth, i.e. `#findAppendedTailStart` must land on `previousLines.length`
|
|
1420
|
+
// (`tailAppendCount === addedCount`). Any mismatch is structurally
|
|
1421
|
+
// ambiguous — more added than the matched tail means offscreen rows were
|
|
1422
|
+
// inserted (a collapsed cell expanding); fewer means the previous last
|
|
1423
|
+
// line repeats earlier so the tail is mis-located. Under-counting splices
|
|
1424
|
+
// stale history; over-counting scrolls an extra row and duplicates the
|
|
1425
|
+
// line at the viewport top. Rebuild whenever the replay checkpoint allows.
|
|
1342
1426
|
if (
|
|
1343
1427
|
contentGrew &&
|
|
1344
1428
|
diff.firstChanged < prevViewportTop &&
|
|
1345
|
-
this.#
|
|
1429
|
+
this.#canRebuildNativeScrollbackLive(nativeViewportAtBottom, false)
|
|
1346
1430
|
) {
|
|
1347
1431
|
const appendedTailStart = diff.appendedLines ? this.#findAppendedTailStart(newLines) : newLines.length;
|
|
1348
1432
|
const tailAppendCount = newLines.length - appendedTailStart;
|
|
1349
1433
|
const addedCount = newLines.length - this.#previousLines.length;
|
|
1350
|
-
if (addedCount
|
|
1434
|
+
if (addedCount !== tailAppendCount) {
|
|
1351
1435
|
return { kind: "historyRebuild" };
|
|
1352
1436
|
}
|
|
1353
1437
|
}
|
|
1438
|
+
if (
|
|
1439
|
+
newLines.length !== this.#previousLines.length &&
|
|
1440
|
+
this.#scrollbackHighWater > 0 &&
|
|
1441
|
+
this.#canRebuildNativeScrollbackLive(nativeViewportAtBottom, allowUnknownViewportMutation)
|
|
1442
|
+
) {
|
|
1443
|
+
return { kind: "historyRebuild" };
|
|
1444
|
+
}
|
|
1354
1445
|
}
|
|
1355
1446
|
|
|
1356
1447
|
// Height changes shift the visible window. Repaint when content didn't
|
|
@@ -1360,6 +1451,17 @@ export class TUI extends Container {
|
|
|
1360
1451
|
return { kind: "viewportRepaint" };
|
|
1361
1452
|
}
|
|
1362
1453
|
|
|
1454
|
+
// A height change that also grew the content into a frame that now fits
|
|
1455
|
+
// entirely on screen cannot use the diff or append-tail emitters below:
|
|
1456
|
+
// both position scrolled rows against the previous viewport top and
|
|
1457
|
+
// hardware cursor row, which the reflow just invalidated, so the appended
|
|
1458
|
+
// tail lands `height`-delta rows too low. With no overflow there is no
|
|
1459
|
+
// native scrollback to preserve, so repaint the viewport at the new
|
|
1460
|
+
// geometry. (Height changes with overflow keep the existing deferral.)
|
|
1461
|
+
if (heightChanged && newLines.length <= height && !isTermuxSession() && !isMultiplexerSession()) {
|
|
1462
|
+
return { kind: "viewportRepaint" };
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1363
1465
|
// Configurable shrink-clear: opt-in path that repaints to wipe rows the
|
|
1364
1466
|
// diff path would leave behind.
|
|
1365
1467
|
if (this.#clearOnShrink && newLines.length < this.#previousLines.length && this.overlayStack.length === 0) {
|
|
@@ -1371,11 +1473,33 @@ export class TUI extends Container {
|
|
|
1371
1473
|
return { kind: "shrink" };
|
|
1372
1474
|
}
|
|
1373
1475
|
|
|
1374
|
-
// Offscreen edit:
|
|
1375
|
-
//
|
|
1476
|
+
// Offscreen edit: repainting only the viewport leaves native history stale
|
|
1477
|
+
// while the user is bottom-anchored. Rebuild whenever replay is safe. If
|
|
1478
|
+
// replay is not safe, keep the viewport stable, mark history dirty, and only
|
|
1479
|
+
// scroll a clean appended tail so newly streamed rows remain reachable until
|
|
1480
|
+
// the next checkpoint rebuild.
|
|
1376
1481
|
if (diff.firstChanged < prevViewportTop) {
|
|
1377
|
-
const
|
|
1378
|
-
|
|
1482
|
+
const nativeViewportAtBottom = this.#readNativeViewportAtBottom();
|
|
1483
|
+
const cleanTailAppend =
|
|
1484
|
+
diff.appendedLines && this.#findAppendedTailStart(newLines) === this.#previousLines.length;
|
|
1485
|
+
if (
|
|
1486
|
+
!isMultiplexerSession() &&
|
|
1487
|
+
this.#canRebuildNativeScrollbackLive(nativeViewportAtBottom, allowUnknownViewportMutation)
|
|
1488
|
+
) {
|
|
1489
|
+
return { kind: "historyRebuild" };
|
|
1490
|
+
}
|
|
1491
|
+
this.#markNativeScrollbackDirty();
|
|
1492
|
+
return { kind: "viewportRepaint", appendFrom: cleanTailAppend ? this.#previousLines.length : undefined };
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
if (forceViewportRepaint) {
|
|
1496
|
+
if (isMultiplexerSession()) return { kind: "viewportRepaint" };
|
|
1497
|
+
if (pureAppend && contentGrew && this.#previousLines.length >= height) {
|
|
1498
|
+
return { kind: "viewportRepaint", appendFrom: this.#previousLines.length };
|
|
1499
|
+
}
|
|
1500
|
+
if (newLines.length === this.#previousLines.length && diff.firstChanged >= prevViewportTop) {
|
|
1501
|
+
return { kind: "viewportRepaint" };
|
|
1502
|
+
}
|
|
1379
1503
|
}
|
|
1380
1504
|
|
|
1381
1505
|
return {
|
|
@@ -1477,6 +1601,30 @@ export class TUI extends Container {
|
|
|
1477
1601
|
);
|
|
1478
1602
|
}
|
|
1479
1603
|
|
|
1604
|
+
/**
|
|
1605
|
+
* Live-frame counterpart to {@link #canReplayNativeScrollbackAtCheckpoint}.
|
|
1606
|
+
* Decides whether a destructive native scrollback rebuild
|
|
1607
|
+
* (`historyRebuild`/`overlayRebuild`, which clear scrollback and snap the
|
|
1608
|
+
* viewport to the tail) is safe to emit *during ordinary rendering*. POSIX
|
|
1609
|
+
* terminals cannot report whether the user has scrolled up
|
|
1610
|
+
* (`isNativeViewportAtBottom()` is `undefined`), so an unknown position is
|
|
1611
|
+
* treated as unsafe: defer to a non-destructive viewport repaint, mark
|
|
1612
|
+
* scrollback dirty, and reconcile history at the next explicit checkpoint
|
|
1613
|
+
* ({@link refreshNativeScrollbackIfDirty} on prompt submit) where the
|
|
1614
|
+
* editor keystroke has already pinned the terminal to the bottom. Without
|
|
1615
|
+
* this, every offscreen transcript edit while streaming wiped scrollback and
|
|
1616
|
+
* yanked a scrolled-up reader back down. `allowUnknownViewportMutation`
|
|
1617
|
+
* (autocomplete/IME) opts directly user-driven frames back into the rebuild.
|
|
1618
|
+
* Unlike the checkpoint predicate this carries no `process.platform`
|
|
1619
|
+
* optimism — resize and checkpoint replays keep using that one.
|
|
1620
|
+
*/
|
|
1621
|
+
#canRebuildNativeScrollbackLive(
|
|
1622
|
+
nativeViewportAtBottom: boolean | undefined,
|
|
1623
|
+
allowUnknownViewportMutation: boolean,
|
|
1624
|
+
): boolean {
|
|
1625
|
+
return nativeViewportAtBottom === true || (nativeViewportAtBottom === undefined && allowUnknownViewportMutation);
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1480
1628
|
#padDeferredShrinkLines(lines: string[], paddedLength: number): string[] {
|
|
1481
1629
|
if (lines.length >= paddedLength) return lines;
|
|
1482
1630
|
return [...lines, ...new Array<string>(paddedLength - lines.length).fill("")];
|
|
@@ -1488,6 +1636,13 @@ export class TUI extends Container {
|
|
|
1488
1636
|
* `truncateToWidth` drops the trailing bytes appended by
|
|
1489
1637
|
* {@link #applyLineResets}.
|
|
1490
1638
|
*/
|
|
1639
|
+
#fitLinesToWidth(lines: string[], width: number): string[] {
|
|
1640
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1641
|
+
lines[i] = this.#fitLineToWidth(lines[i], width);
|
|
1642
|
+
}
|
|
1643
|
+
return lines;
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1491
1646
|
#fitLineToWidth(line: string, width: number): string {
|
|
1492
1647
|
if (TERMINAL.isImageLine(line)) return line;
|
|
1493
1648
|
if (visibleWidth(line) <= width) return line;
|
|
@@ -1501,6 +1656,7 @@ export class TUI extends Container {
|
|
|
1501
1656
|
*/
|
|
1502
1657
|
#commit(lines: string[], width: number, height: number, viewportTop: number, hardwareCursorRow: number): void {
|
|
1503
1658
|
this.#previousLines = lines;
|
|
1659
|
+
this.#forceViewportRepaintOnNextRender = false;
|
|
1504
1660
|
this.#previousWidth = width;
|
|
1505
1661
|
this.#previousHeight = height;
|
|
1506
1662
|
this.#cursorRow = Math.max(0, lines.length - 1);
|
|
@@ -1526,7 +1682,7 @@ export class TUI extends Container {
|
|
|
1526
1682
|
}
|
|
1527
1683
|
for (let i = 0; i < lines.length; i++) {
|
|
1528
1684
|
if (i > 0) buffer += "\r\n";
|
|
1529
|
-
buffer += lines[i];
|
|
1685
|
+
buffer += this.#fitLineToWidth(lines[i], width);
|
|
1530
1686
|
}
|
|
1531
1687
|
const finalRow = Math.max(0, lines.length - 1);
|
|
1532
1688
|
const { seq, toRow } = this.#cursorControlSequence(cursorPos, lines.length, finalRow);
|
|
@@ -1592,6 +1748,7 @@ export class TUI extends Container {
|
|
|
1592
1748
|
lines: string[],
|
|
1593
1749
|
start: number,
|
|
1594
1750
|
height: number,
|
|
1751
|
+
width: number,
|
|
1595
1752
|
prevViewportTop: number,
|
|
1596
1753
|
prevHardwareCursorRow: number,
|
|
1597
1754
|
): void {
|
|
@@ -1606,7 +1763,7 @@ export class TUI extends Container {
|
|
|
1606
1763
|
if (moveToBottom > 0) buffer += `\x1b[${moveToBottom}B`;
|
|
1607
1764
|
for (let i = start; i < lines.length; i++) {
|
|
1608
1765
|
buffer += "\r\n";
|
|
1609
|
-
buffer += lines[i];
|
|
1766
|
+
buffer += this.#fitLineToWidth(lines[i], width);
|
|
1610
1767
|
}
|
|
1611
1768
|
buffer += PAINT_END;
|
|
1612
1769
|
this.terminal.write(buffer);
|