@code-yeongyu/senpi-tui 2026.10.1 → 2026.10.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/dist/autocomplete.d.ts +3 -1
- package/dist/autocomplete.js +7 -6
- package/dist/components/box.d.ts +5 -0
- package/dist/components/box.js +17 -2
- package/dist/components/markdown.d.ts +6 -1
- package/dist/components/markdown.js +13 -1
- package/dist/components/mouse-region.d.ts +2 -0
- package/dist/components/mouse-region.js +4 -0
- package/dist/components/spacer.d.ts +5 -1
- package/dist/components/spacer.js +11 -1
- package/dist/components/text.d.ts +8 -1
- package/dist/components/text.js +20 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/slash-command-autocomplete.js +3 -1
- package/dist/terminal.d.ts +1 -0
- package/dist/terminal.js +8 -5
- package/dist/tui-alt-screen.d.ts +2 -0
- package/dist/tui-alt-screen.js +4 -0
- package/dist/tui-main-screen.d.ts +6 -3
- package/dist/tui-main-screen.js +19 -13
- package/dist/tui.d.ts +84 -1
- package/dist/tui.js +301 -13
- package/dist/utils.d.ts +6 -0
- package/dist/utils.js +14 -2
- package/package.json +1 -1
package/dist/autocomplete.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export interface AutocompleteItem {
|
|
|
3
3
|
label: string;
|
|
4
4
|
description?: string;
|
|
5
5
|
/**
|
|
6
|
-
* The command
|
|
6
|
+
* The command requires arguments: confirming the row completes `/name ` and waits for
|
|
7
7
|
* arguments instead of submitting.
|
|
8
8
|
*/
|
|
9
9
|
awaitsArguments?: boolean;
|
|
@@ -13,6 +13,8 @@ export interface SlashCommand {
|
|
|
13
13
|
name: string;
|
|
14
14
|
description?: string;
|
|
15
15
|
argumentHint?: string;
|
|
16
|
+
/** Whether picker Enter must wait for arguments. Omitted means: wait only when `argumentHint` is set. */
|
|
17
|
+
requiresArguments?: boolean;
|
|
16
18
|
getArgumentCompletions?(argumentPrefix: string): Awaitable<AutocompleteItem[] | null>;
|
|
17
19
|
}
|
|
18
20
|
export interface AutocompleteSuggestions {
|
package/dist/autocomplete.js
CHANGED
|
@@ -240,16 +240,17 @@ export class CombinedAutocompleteProvider {
|
|
|
240
240
|
prefix: dollarContext.prefix,
|
|
241
241
|
};
|
|
242
242
|
}
|
|
243
|
-
|
|
244
|
-
|
|
243
|
+
const commandText = textBeforeCursor.trimStart();
|
|
244
|
+
if (!options.force && commandText.startsWith("/")) {
|
|
245
|
+
const spaceIndex = commandText.indexOf(" ");
|
|
245
246
|
if (spaceIndex === -1) {
|
|
246
|
-
const prefix =
|
|
247
|
+
const prefix = commandText.slice(1);
|
|
247
248
|
const filtered = getSlashCommandSuggestions(this.commands, prefix);
|
|
248
249
|
if (filtered.length === 0)
|
|
249
250
|
return null;
|
|
250
251
|
return {
|
|
251
252
|
items: filtered,
|
|
252
|
-
prefix:
|
|
253
|
+
prefix: commandText,
|
|
253
254
|
};
|
|
254
255
|
}
|
|
255
256
|
const tokenStart = textBeforeCursor.search(/\S+$/);
|
|
@@ -268,8 +269,8 @@ export class CombinedAutocompleteProvider {
|
|
|
268
269
|
prefix: currentToken,
|
|
269
270
|
};
|
|
270
271
|
}
|
|
271
|
-
const commandName =
|
|
272
|
-
const argumentText =
|
|
272
|
+
const commandName = commandText.slice(1, spaceIndex);
|
|
273
|
+
const argumentText = commandText.slice(spaceIndex + 1);
|
|
273
274
|
const command = this.commands.find((cmd) => {
|
|
274
275
|
const name = "name" in cmd ? cmd.name : cmd.value;
|
|
275
276
|
return name === commandName;
|
package/dist/components/box.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export declare class Box implements Component {
|
|
|
10
10
|
private disposed;
|
|
11
11
|
private cache?;
|
|
12
12
|
private mouseLayout?;
|
|
13
|
+
private readonly composite;
|
|
13
14
|
constructor(paddingX?: number, paddingY?: number, bgFn?: (text: string) => string);
|
|
14
15
|
addChild(component: Component): void;
|
|
15
16
|
removeChild(component: Component): void;
|
|
@@ -20,6 +21,10 @@ export declare class Box implements Component {
|
|
|
20
21
|
private invalidateCache;
|
|
21
22
|
private matchCache;
|
|
22
23
|
invalidate(): void;
|
|
24
|
+
/** Padding plus background over the children: exact `Box` instances change only with them (see `Container`). */
|
|
25
|
+
getRenderRevision(): number | undefined;
|
|
26
|
+
protected childRenderRevision(): number | undefined;
|
|
27
|
+
protected bumpRenderRevision(): void;
|
|
23
28
|
handleMouse(event: TuiMouseEvent): TuiMouseDispatchResult | undefined;
|
|
24
29
|
render(width: number): string[];
|
|
25
30
|
private applyBg;
|
package/dist/components/box.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { dispatchMouseEvent } from "../tui.js";
|
|
2
|
-
import { applyBackgroundToLine, visibleWidth } from "../utils.js";
|
|
1
|
+
import { CompositeRevision, dispatchMouseEvent, } from "../tui.js";
|
|
2
|
+
import { applyBackgroundToLine, flattenLines, visibleWidth } from "../utils.js";
|
|
3
3
|
/**
|
|
4
4
|
* Box component - a container that applies padding and background to all children
|
|
5
5
|
*/
|
|
@@ -7,6 +7,7 @@ export class Box {
|
|
|
7
7
|
constructor(paddingX = 1, paddingY = 1, bgFn) {
|
|
8
8
|
this.children = [];
|
|
9
9
|
this.disposed = false;
|
|
10
|
+
this.composite = new CompositeRevision();
|
|
10
11
|
this.paddingX = paddingX;
|
|
11
12
|
this.paddingY = paddingY;
|
|
12
13
|
this.bgFn = bgFn;
|
|
@@ -45,10 +46,12 @@ export class Box {
|
|
|
45
46
|
}
|
|
46
47
|
setBgFn(bgFn) {
|
|
47
48
|
this.bgFn = bgFn;
|
|
49
|
+
this.composite.bump();
|
|
48
50
|
// Don't invalidate here - we'll detect bgFn changes by sampling output
|
|
49
51
|
}
|
|
50
52
|
invalidateCache() {
|
|
51
53
|
this.cache = undefined;
|
|
54
|
+
this.composite.bump();
|
|
52
55
|
}
|
|
53
56
|
matchCache(width, childLines, bgSample) {
|
|
54
57
|
const cache = this.cache;
|
|
@@ -60,10 +63,21 @@ export class Box {
|
|
|
60
63
|
}
|
|
61
64
|
invalidate() {
|
|
62
65
|
this.invalidateCache();
|
|
66
|
+
this.composite.bump();
|
|
63
67
|
for (const child of this.children) {
|
|
64
68
|
child.invalidate?.();
|
|
65
69
|
}
|
|
66
70
|
}
|
|
71
|
+
/** Padding plus background over the children: exact `Box` instances change only with them (see `Container`). */
|
|
72
|
+
getRenderRevision() {
|
|
73
|
+
return Object.getPrototypeOf(this) === Box.prototype ? this.childRenderRevision() : undefined;
|
|
74
|
+
}
|
|
75
|
+
childRenderRevision() {
|
|
76
|
+
return this.composite.read(this.children);
|
|
77
|
+
}
|
|
78
|
+
bumpRenderRevision() {
|
|
79
|
+
this.composite.bump();
|
|
80
|
+
}
|
|
67
81
|
handleMouse(event) {
|
|
68
82
|
const contentWidth = Math.max(1, event.width - this.paddingX * 2);
|
|
69
83
|
const contentY = event.y - this.paddingY;
|
|
@@ -131,6 +145,7 @@ export class Box {
|
|
|
131
145
|
result.push(this.applyBg("", width));
|
|
132
146
|
}
|
|
133
147
|
// Update cache
|
|
148
|
+
flattenLines(result);
|
|
134
149
|
this.cache = { childLines, width, bgSample, lines: result };
|
|
135
150
|
return result;
|
|
136
151
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Component } from "../tui.ts";
|
|
2
2
|
export declare function clearRenderCache(): void;
|
|
3
3
|
export declare function getMarkdownHighlightCallCount(): number;
|
|
4
4
|
export declare function resetMarkdownHighlightCallCount(): void;
|
|
@@ -64,9 +64,14 @@ export declare class Markdown implements Component {
|
|
|
64
64
|
private cachedText?;
|
|
65
65
|
private cachedWidth?;
|
|
66
66
|
private cachedLines?;
|
|
67
|
+
private revision;
|
|
67
68
|
constructor(text: string, paddingX: number, paddingY: number, theme: MarkdownTheme, defaultTextStyle?: DefaultTextStyle, options?: MarkdownOptions);
|
|
68
69
|
setText(text: string): void;
|
|
69
70
|
invalidate(): void;
|
|
71
|
+
/** Instances that expose a revision advance the shared clock; others (e.g. an animated subclass) stay local. */
|
|
72
|
+
private markRevision;
|
|
73
|
+
/** Exact `Markdown` instances only; a subclass may render more than its source text. */
|
|
74
|
+
getRenderRevision(): number | undefined;
|
|
70
75
|
render(width: number): string[];
|
|
71
76
|
/**
|
|
72
77
|
* Apply default text style to a string.
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// allow: SIZE_OK - existing markdown renderer is oversized; this merge only preserves behavior and cache-key correctness.
|
|
2
2
|
import { Marked, Tokenizer } from "marked";
|
|
3
3
|
import { getCapabilities, hyperlink, isImageLine } from "../terminal-image.js";
|
|
4
|
-
import {
|
|
4
|
+
import { nextRenderRevision } from "../tui.js";
|
|
5
|
+
import { applyBackgroundToLine, flattenLines, visibleWidth, wrapTextWithAnsi } from "../utils.js";
|
|
5
6
|
import { latexToUnicode } from "./latex.js";
|
|
6
7
|
const STRICT_STRIKETHROUGH_REGEX = /^(~~)(?=[^\s~])((?:\\.|[^\\])*?(?:\\.|[^\s~\\]))\1(?=[^~]|$)/;
|
|
7
8
|
class StrictStrikethroughTokenizer extends Tokenizer {
|
|
@@ -326,6 +327,7 @@ export function resetMarkdownHighlightCallCount() {
|
|
|
326
327
|
}
|
|
327
328
|
export class Markdown {
|
|
328
329
|
constructor(text, paddingX, paddingY, theme, defaultTextStyle, options) {
|
|
330
|
+
this.revision = nextRenderRevision();
|
|
329
331
|
this.text = text;
|
|
330
332
|
this.paddingX = paddingX;
|
|
331
333
|
this.paddingY = paddingY;
|
|
@@ -341,6 +343,15 @@ export class Markdown {
|
|
|
341
343
|
this.cachedText = undefined;
|
|
342
344
|
this.cachedWidth = undefined;
|
|
343
345
|
this.cachedLines = undefined;
|
|
346
|
+
this.markRevision();
|
|
347
|
+
}
|
|
348
|
+
/** Instances that expose a revision advance the shared clock; others (e.g. an animated subclass) stay local. */
|
|
349
|
+
markRevision() {
|
|
350
|
+
this.revision = this.getRenderRevision() === undefined ? this.revision + 1 : nextRenderRevision();
|
|
351
|
+
}
|
|
352
|
+
/** Exact `Markdown` instances only; a subclass may render more than its source text. */
|
|
353
|
+
getRenderRevision() {
|
|
354
|
+
return Object.getPrototypeOf(this) === Markdown.prototype ? this.revision : undefined;
|
|
344
355
|
}
|
|
345
356
|
render(width) {
|
|
346
357
|
// Check cache
|
|
@@ -448,6 +459,7 @@ export class Markdown {
|
|
|
448
459
|
}
|
|
449
460
|
// Combine top padding, content, and bottom padding
|
|
450
461
|
const result = emptyLines.concat(contentLines, emptyLines);
|
|
462
|
+
flattenLines(result);
|
|
451
463
|
// Update cache
|
|
452
464
|
this.cachedText = this.text;
|
|
453
465
|
this.cachedWidth = width;
|
|
@@ -6,6 +6,8 @@ export declare class MouseRegion implements Component {
|
|
|
6
6
|
private readonly onMouse;
|
|
7
7
|
constructor(child: Component, onMouse: MouseRegionHandler);
|
|
8
8
|
render(width: number): string[];
|
|
9
|
+
/** Exact regions render their child unchanged; a subclass may add state and opts in itself. */
|
|
10
|
+
getRenderRevision(): number | undefined;
|
|
9
11
|
handleMouse(event: TuiMouseEvent): TuiMouseDispatchResult | TuiMouseEventResult | undefined;
|
|
10
12
|
invalidate(): void;
|
|
11
13
|
}
|
|
@@ -8,6 +8,10 @@ export class MouseRegion {
|
|
|
8
8
|
render(width) {
|
|
9
9
|
return this.child.render(width);
|
|
10
10
|
}
|
|
11
|
+
/** Exact regions render their child unchanged; a subclass may add state and opts in itself. */
|
|
12
|
+
getRenderRevision() {
|
|
13
|
+
return Object.getPrototypeOf(this) === MouseRegion.prototype ? this.child.getRenderRevision?.() : undefined;
|
|
14
|
+
}
|
|
11
15
|
handleMouse(event) {
|
|
12
16
|
const childResult = dispatchMouseEvent(this.child, event);
|
|
13
17
|
return childResult ?? this.onMouse(event);
|
|
@@ -1,12 +1,16 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Component } from "../tui.ts";
|
|
2
2
|
/**
|
|
3
3
|
* Spacer component that renders empty lines
|
|
4
4
|
*/
|
|
5
5
|
export declare class Spacer implements Component {
|
|
6
6
|
private lines;
|
|
7
|
+
private revision;
|
|
7
8
|
constructor(lines?: number);
|
|
8
9
|
setLines(lines: number): void;
|
|
9
10
|
invalidate(): void;
|
|
11
|
+
/** Instances that expose a revision advance the shared clock; others (e.g. an animated subclass) stay local. */
|
|
12
|
+
private markRevision;
|
|
13
|
+
getRenderRevision(): number | undefined;
|
|
10
14
|
render(_width: number): string[];
|
|
11
15
|
}
|
|
12
16
|
//# sourceMappingURL=spacer.d.ts.map
|
|
@@ -1,15 +1,25 @@
|
|
|
1
|
+
import { nextRenderRevision } from "../tui.js";
|
|
1
2
|
/**
|
|
2
3
|
* Spacer component that renders empty lines
|
|
3
4
|
*/
|
|
4
5
|
export class Spacer {
|
|
5
6
|
constructor(lines = 1) {
|
|
7
|
+
this.revision = nextRenderRevision();
|
|
6
8
|
this.lines = lines;
|
|
7
9
|
}
|
|
8
10
|
setLines(lines) {
|
|
9
11
|
this.lines = lines;
|
|
12
|
+
this.markRevision();
|
|
10
13
|
}
|
|
11
14
|
invalidate() {
|
|
12
|
-
|
|
15
|
+
this.markRevision();
|
|
16
|
+
}
|
|
17
|
+
/** Instances that expose a revision advance the shared clock; others (e.g. an animated subclass) stay local. */
|
|
18
|
+
markRevision() {
|
|
19
|
+
this.revision = this.getRenderRevision() === undefined ? this.revision + 1 : nextRenderRevision();
|
|
20
|
+
}
|
|
21
|
+
getRenderRevision() {
|
|
22
|
+
return Object.getPrototypeOf(this) === Spacer.prototype ? this.revision : undefined;
|
|
13
23
|
}
|
|
14
24
|
render(_width) {
|
|
15
25
|
const result = [];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Component } from "../tui.ts";
|
|
2
2
|
/**
|
|
3
3
|
* Text component - displays multi-line text with word wrapping
|
|
4
4
|
*/
|
|
@@ -10,10 +10,17 @@ export declare class Text implements Component {
|
|
|
10
10
|
private cachedText?;
|
|
11
11
|
private cachedWidth?;
|
|
12
12
|
private cachedLines?;
|
|
13
|
+
private revision;
|
|
13
14
|
constructor(text?: string, paddingX?: number, paddingY?: number, customBgFn?: (text: string) => string);
|
|
14
15
|
setText(text: string): void;
|
|
15
16
|
setCustomBgFn(customBgFn?: (text: string) => string): void;
|
|
16
17
|
invalidate(): void;
|
|
18
|
+
/** Instances that expose a revision advance the shared clock; others (e.g. an animated subclass) stay local. */
|
|
19
|
+
private markRevision;
|
|
20
|
+
/** Exact `Text` instances only: a subclass may render more than its text and opts in with {@link textRenderRevision}. */
|
|
21
|
+
getRenderRevision(): number | undefined;
|
|
22
|
+
/** Revision of the text, padding and background state this class renders. */
|
|
23
|
+
protected textRenderRevision(): number;
|
|
17
24
|
render(width: number): string[];
|
|
18
25
|
}
|
|
19
26
|
//# sourceMappingURL=text.d.ts.map
|
package/dist/components/text.js
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { nextRenderRevision } from "../tui.js";
|
|
2
|
+
import { applyBackgroundToLine, flattenLines, visibleWidth, wrapTextWithAnsi } from "../utils.js";
|
|
2
3
|
/**
|
|
3
4
|
* Text component - displays multi-line text with word wrapping
|
|
4
5
|
*/
|
|
5
6
|
export class Text {
|
|
6
7
|
constructor(text = "", paddingX = 1, paddingY = 1, customBgFn) {
|
|
8
|
+
this.revision = nextRenderRevision();
|
|
7
9
|
this.text = text;
|
|
8
10
|
this.paddingX = paddingX;
|
|
9
11
|
this.paddingY = paddingY;
|
|
10
12
|
this.customBgFn = customBgFn;
|
|
11
13
|
}
|
|
12
14
|
setText(text) {
|
|
15
|
+
if (text !== this.text)
|
|
16
|
+
this.markRevision();
|
|
13
17
|
this.text = text;
|
|
14
18
|
this.cachedText = undefined;
|
|
15
19
|
this.cachedWidth = undefined;
|
|
@@ -17,6 +21,7 @@ export class Text {
|
|
|
17
21
|
}
|
|
18
22
|
setCustomBgFn(customBgFn) {
|
|
19
23
|
this.customBgFn = customBgFn;
|
|
24
|
+
this.markRevision();
|
|
20
25
|
this.cachedText = undefined;
|
|
21
26
|
this.cachedWidth = undefined;
|
|
22
27
|
this.cachedLines = undefined;
|
|
@@ -25,6 +30,19 @@ export class Text {
|
|
|
25
30
|
this.cachedText = undefined;
|
|
26
31
|
this.cachedWidth = undefined;
|
|
27
32
|
this.cachedLines = undefined;
|
|
33
|
+
this.markRevision();
|
|
34
|
+
}
|
|
35
|
+
/** Instances that expose a revision advance the shared clock; others (e.g. an animated subclass) stay local. */
|
|
36
|
+
markRevision() {
|
|
37
|
+
this.revision = this.getRenderRevision() === undefined ? this.revision + 1 : nextRenderRevision();
|
|
38
|
+
}
|
|
39
|
+
/** Exact `Text` instances only: a subclass may render more than its text and opts in with {@link textRenderRevision}. */
|
|
40
|
+
getRenderRevision() {
|
|
41
|
+
return Object.getPrototypeOf(this) === Text.prototype ? this.revision : undefined;
|
|
42
|
+
}
|
|
43
|
+
/** Revision of the text, padding and background state this class renders. */
|
|
44
|
+
textRenderRevision() {
|
|
45
|
+
return this.revision;
|
|
28
46
|
}
|
|
29
47
|
render(width) {
|
|
30
48
|
// Check cache
|
|
@@ -72,6 +90,7 @@ export class Text {
|
|
|
72
90
|
emptyLines.push(line);
|
|
73
91
|
}
|
|
74
92
|
const result = [...emptyLines, ...contentLines, ...emptyLines];
|
|
93
|
+
flattenLines(result);
|
|
75
94
|
// Update cache
|
|
76
95
|
this.cachedText = this.text;
|
|
77
96
|
this.cachedWidth = width;
|
package/dist/index.d.ts
CHANGED
|
@@ -29,12 +29,12 @@ export { getNativeClipboard, type NativeClipboard } from "./native-platform.ts";
|
|
|
29
29
|
export { oklabToOkhslLightness } from "./oklab.ts";
|
|
30
30
|
export { type EditorPasteState, expandPasteMarkers } from "./paste-markers.ts";
|
|
31
31
|
export { StdinBuffer, type StdinBufferEventMap, type StdinBufferOptions } from "./stdin-buffer.ts";
|
|
32
|
-
export { type CursorPosition, ProcessTerminal, type ProcessTerminalOptions, type Terminal } from "./terminal.ts";
|
|
32
|
+
export { type CursorPosition, isAppleTerminalSession, isWarpWslSession, ProcessTerminal, type ProcessTerminalOptions, type Terminal, } from "./terminal.ts";
|
|
33
33
|
export { parseTerminalColorSchemeReport, type RgbColor, type TerminalColorScheme, type TerminalColors, } from "./terminal-colors.ts";
|
|
34
34
|
export declare function calculateImageRows(imageDimensions: ImageDimensions, targetWidthCells: number, cellDimensions?: CellDimensions): number;
|
|
35
35
|
export { allocateImageId, buildKittyPlaceholderRow, type CellDimensions, deleteAllKittyImages, deleteKittyImage, detectCapabilities, encodeITerm2, encodeKitty, getCapabilities, getCellDimensions, getGifDimensions, getImageDimensions, getJpegDimensions, getPngDimensions, getTerminalColorMode, getWebpDimensions, hyperlink, type ImageDimensions, type ImageProtocol, type ImageRenderOptions, imageFallback, KITTY_PLACEHOLDER_MAX, outerKittyGraphicsMode, renderImage, resetCapabilitiesCache, setCapabilities, setCapabilityOverrides, setCellDimensions, type TerminalCapabilities, type TmuxPassthroughState, wrapTmuxPassthrough, } from "./terminal-image.ts";
|
|
36
36
|
export { sanitizeTerminalLabel, shortenImagePath } from "./terminal-text.ts";
|
|
37
|
-
export { type Component, Container, CURSOR_MARKER, compositeTuiLine, type Focusable, isFocusable, isViewportTUI, type OverlayAnchor, type OverlayBounds, type OverlayHandle, type OverlayMargin, type OverlayOptions, type OverlayUnfocusOptions, type SizeValue, TUI, type TuiInputListener, type TuiInputListenerResult, type TuiMode, type TuiMouseButton, type TuiMouseEvent, type TuiMouseEventResult, type TuiMouseEventType, type TuiStopOptions, type ViewportTUI, } from "./tui.ts";
|
|
37
|
+
export { type Component, CompositeRevision, Container, CURSOR_MARKER, claimFrameRow, compositeTuiLine, currentRenderRevision, dispatchMouseEvent, type Focusable, frameMode, frameScrollbackRows, isFocusable, isViewportTUI, joinLineArrays, mainScreenHistoryLines, nextRenderRevision, type OverlayAnchor, type OverlayBounds, type OverlayHandle, type OverlayMargin, type OverlayOptions, type OverlayUnfocusOptions, renderAtFrameRow, resetMainScreenHistoryLines, type SizeValue, TUI, type TuiInputListener, type TuiInputListenerResult, type TuiMode, type TuiMouseButton, type TuiMouseDispatchResult, type TuiMouseEvent, type TuiMouseEventResult, type TuiMouseEventType, type TuiStopOptions, type ViewportTUI, } from "./tui.ts";
|
|
38
38
|
export { TuiAltScreen, type TuiAltScreenOptions } from "./tui-alt-screen.ts";
|
|
39
39
|
export { TuiMainScreen, type TuiMainScreenRenderState } from "./tui-main-screen.ts";
|
|
40
40
|
export { getGraphemeSegmenter, getOsc8LinkAtColumn, getWordSegmenter, sliceByColumn, stripTerminalSequences, truncateToWidth, visibleWidth, wrapTextWithAnsi, } from "./utils.ts";
|
package/dist/index.js
CHANGED
|
@@ -40,7 +40,7 @@ export { expandPasteMarkers } from "./paste-markers.js";
|
|
|
40
40
|
// Input buffering for batch splitting
|
|
41
41
|
export { StdinBuffer } from "./stdin-buffer.js";
|
|
42
42
|
// Terminal interface and implementations
|
|
43
|
-
export { ProcessTerminal } from "./terminal.js";
|
|
43
|
+
export { isAppleTerminalSession, isWarpWslSession, ProcessTerminal, } from "./terminal.js";
|
|
44
44
|
// Terminal colors
|
|
45
45
|
export { parseTerminalColorSchemeReport, } from "./terminal-colors.js";
|
|
46
46
|
// Terminal image support
|
|
@@ -49,7 +49,7 @@ export function calculateImageRows(imageDimensions, targetWidthCells, cellDimens
|
|
|
49
49
|
}
|
|
50
50
|
export { allocateImageId, buildKittyPlaceholderRow, deleteAllKittyImages, deleteKittyImage, detectCapabilities, encodeITerm2, encodeKitty, getCapabilities, getCellDimensions, getGifDimensions, getImageDimensions, getJpegDimensions, getPngDimensions, getTerminalColorMode, getWebpDimensions, hyperlink, imageFallback, KITTY_PLACEHOLDER_MAX, outerKittyGraphicsMode, renderImage, resetCapabilitiesCache, setCapabilities, setCapabilityOverrides, setCellDimensions, wrapTmuxPassthrough, } from "./terminal-image.js";
|
|
51
51
|
export { sanitizeTerminalLabel, shortenImagePath } from "./terminal-text.js";
|
|
52
|
-
export { Container, CURSOR_MARKER, compositeTuiLine, isFocusable, isViewportTUI, TUI, } from "./tui.js";
|
|
52
|
+
export { CompositeRevision, Container, CURSOR_MARKER, claimFrameRow, compositeTuiLine, currentRenderRevision, dispatchMouseEvent, frameMode, frameScrollbackRows, isFocusable, isViewportTUI, joinLineArrays, mainScreenHistoryLines, nextRenderRevision, renderAtFrameRow, resetMainScreenHistoryLines, TUI, } from "./tui.js";
|
|
53
53
|
export { TuiAltScreen } from "./tui-alt-screen.js";
|
|
54
54
|
export { TuiMainScreen } from "./tui-main-screen.js";
|
|
55
55
|
// Utilities
|
|
@@ -38,6 +38,8 @@ export function getSlashCommandSuggestions(commands, prefix) {
|
|
|
38
38
|
return [];
|
|
39
39
|
}
|
|
40
40
|
const hint = "argumentHint" in cmd && cmd.argumentHint ? cmd.argumentHint : undefined;
|
|
41
|
+
// A hint without an explicit `requiresArguments` means the command expects input.
|
|
42
|
+
const requiresArguments = ("requiresArguments" in cmd ? cmd.requiresArguments : undefined) ?? hint !== undefined;
|
|
41
43
|
const desc = cmd.description ?? "";
|
|
42
44
|
const fullDesc = hint ? (desc ? `${hint} — ${desc}` : hint) : desc;
|
|
43
45
|
return [
|
|
@@ -46,7 +48,7 @@ export function getSlashCommandSuggestions(commands, prefix) {
|
|
|
46
48
|
label: name,
|
|
47
49
|
description: fullDesc || undefined,
|
|
48
50
|
searchText: isSkill && !explicitSkillNamespace ? skillName : name,
|
|
49
|
-
awaitsArguments:
|
|
51
|
+
awaitsArguments: requiresArguments || ("awaitsArguments" in cmd && cmd.awaitsArguments === true),
|
|
50
52
|
},
|
|
51
53
|
];
|
|
52
54
|
});
|
package/dist/terminal.d.ts
CHANGED
|
@@ -23,6 +23,7 @@ export declare function isAppleTerminalSession(): boolean;
|
|
|
23
23
|
export declare function refreshTerminalDimensions(): void;
|
|
24
24
|
export declare function normalizeNativeShiftEnterInput(data: string, shouldDetectNativeShiftEnter: boolean, isShiftPressed: boolean): string;
|
|
25
25
|
export declare function normalizeAppleTerminalInput(data: string, isAppleTerminal: boolean, isShiftPressed: boolean): string;
|
|
26
|
+
export declare function isWarpWslSession(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform, socketExists?: (socketPath: string) => boolean): boolean;
|
|
26
27
|
export declare function normalizeWarpWslShiftEnterInput(data: string, env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform, socketExists?: (socketPath: string) => boolean): string;
|
|
27
28
|
export declare function keyboardEnhancementEnabled(): boolean;
|
|
28
29
|
export declare function __stdinErrorSubscriberCountForTests(): number;
|
package/dist/terminal.js
CHANGED
|
@@ -77,7 +77,7 @@ export function normalizeNativeShiftEnterInput(data, shouldDetectNativeShiftEnte
|
|
|
77
77
|
export function normalizeAppleTerminalInput(data, isAppleTerminal, isShiftPressed) {
|
|
78
78
|
return normalizeNativeShiftEnterInput(data, isAppleTerminal, isShiftPressed);
|
|
79
79
|
}
|
|
80
|
-
export function
|
|
80
|
+
export function isWarpWslSession(env = process.env, platform = process.platform, socketExists = (socketPath) => {
|
|
81
81
|
try {
|
|
82
82
|
return fs.statSync(socketPath).isSocket();
|
|
83
83
|
}
|
|
@@ -85,15 +85,18 @@ export function normalizeWarpWslShiftEnterInput(data, env = process.env, platfor
|
|
|
85
85
|
return false;
|
|
86
86
|
}
|
|
87
87
|
}) {
|
|
88
|
-
if (
|
|
89
|
-
return
|
|
88
|
+
if (platform !== "linux")
|
|
89
|
+
return false;
|
|
90
90
|
if (isMultiplexerSession(env) || env.SSH_CONNECTION?.trim() || env.SSH_CLIENT?.trim() || env.SSH_TTY?.trim()) {
|
|
91
|
-
return
|
|
91
|
+
return false;
|
|
92
92
|
}
|
|
93
93
|
const isWarp = Boolean(env.WARP_SESSION_ID?.trim() || env.WARP_TERMINAL_SESSION_UUID?.trim());
|
|
94
94
|
const interopPath = env.WSL_INTEROP?.trim();
|
|
95
95
|
const isWsl = isWarp && interopPath !== undefined && /^\/run\/WSL\/\d+_interop$/.test(interopPath) && socketExists(interopPath);
|
|
96
|
-
return isWarp && isWsl
|
|
96
|
+
return isWarp && isWsl;
|
|
97
|
+
}
|
|
98
|
+
export function normalizeWarpWslShiftEnterInput(data, env = process.env, platform = process.platform, socketExists) {
|
|
99
|
+
return data === "\n" && isWarpWslSession(env, platform, socketExists) ? NATIVE_SHIFT_ENTER_SEQUENCE : data;
|
|
97
100
|
}
|
|
98
101
|
export function keyboardEnhancementEnabled() {
|
|
99
102
|
const value = process.env.PI_TUI_KEYBOARD_PROTOCOL;
|
package/dist/tui-alt-screen.d.ts
CHANGED
|
@@ -90,6 +90,8 @@ export declare class TuiAltScreen extends TuiBase implements ViewportTUI {
|
|
|
90
90
|
hasActiveSelection(): boolean;
|
|
91
91
|
/** Copy the active fullscreen text selection, if any, using the configured selection clipboard path. */
|
|
92
92
|
copyActiveSelectionToClipboard(): Promise<boolean>;
|
|
93
|
+
/** The lines of the last rendered frame, one per terminal row, as written to the terminal. */
|
|
94
|
+
getScreenLines(): string[];
|
|
93
95
|
setLayoutRoot(component: Component | undefined): void;
|
|
94
96
|
render(width: number): string[];
|
|
95
97
|
protected getMountedRoots(): readonly Component[];
|
package/dist/tui-alt-screen.js
CHANGED
|
@@ -102,6 +102,10 @@ export class TuiAltScreen extends TuiBase {
|
|
|
102
102
|
return false;
|
|
103
103
|
return this.copyTextToClipboard(text);
|
|
104
104
|
}
|
|
105
|
+
/** The lines of the last rendered frame, one per terminal row, as written to the terminal. */
|
|
106
|
+
getScreenLines() {
|
|
107
|
+
return [...this.previousScreen];
|
|
108
|
+
}
|
|
105
109
|
setLayoutRoot(component) {
|
|
106
110
|
if (this.layoutRoot === component)
|
|
107
111
|
return;
|
|
@@ -13,14 +13,17 @@ export declare class TuiMainScreen extends TuiBase {
|
|
|
13
13
|
readonly mode: "regular";
|
|
14
14
|
private trackingEnabled;
|
|
15
15
|
private readonly clicks;
|
|
16
|
+
/**
|
|
17
|
+
* A press stays clickable only while the committed frame and component tree it hit are unchanged.
|
|
18
|
+
* Both are captured at press time and compared at release, so ordinary frames pay nothing for it.
|
|
19
|
+
*/
|
|
16
20
|
private mousePress?;
|
|
17
|
-
private layoutRevision;
|
|
18
|
-
private committedMouseLines;
|
|
19
|
-
private committedMouseComponents;
|
|
20
21
|
constructor(...args: ConstructorParameters<typeof TuiBase>);
|
|
21
22
|
protected applyMouseTracking(enabled: boolean): void;
|
|
22
23
|
protected beforeTerminalStop(): void;
|
|
23
24
|
protected doRender(): void;
|
|
25
|
+
private collectMouseComponents;
|
|
26
|
+
private pressLayoutUnchanged;
|
|
24
27
|
private applyMouseResult;
|
|
25
28
|
private handleMouseInput;
|
|
26
29
|
captureRenderState(): TuiMainScreenRenderState;
|
package/dist/tui-main-screen.js
CHANGED
|
@@ -8,9 +8,6 @@ export class TuiMainScreen extends TuiBase {
|
|
|
8
8
|
this.mode = "regular";
|
|
9
9
|
this.trackingEnabled = false;
|
|
10
10
|
this.clicks = new MouseClickSynthesizer();
|
|
11
|
-
this.layoutRevision = 0;
|
|
12
|
-
this.committedMouseLines = [];
|
|
13
|
-
this.committedMouseComponents = [];
|
|
14
11
|
this.addInputListener((data) => this.handleMouseInput(data));
|
|
15
12
|
}
|
|
16
13
|
applyMouseTracking(enabled) {
|
|
@@ -51,6 +48,9 @@ export class TuiMainScreen extends TuiBase {
|
|
|
51
48
|
}
|
|
52
49
|
super.doRender();
|
|
53
50
|
this.noteCommittedMouseFrame();
|
|
51
|
+
this.calibrateMouseAnchor();
|
|
52
|
+
}
|
|
53
|
+
collectMouseComponents() {
|
|
54
54
|
const components = [];
|
|
55
55
|
const visit = (component) => {
|
|
56
56
|
components.push(component);
|
|
@@ -60,15 +60,20 @@ export class TuiMainScreen extends TuiBase {
|
|
|
60
60
|
};
|
|
61
61
|
for (const root of this.getMouseLayoutRoots())
|
|
62
62
|
visit(root);
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
63
|
+
return components;
|
|
64
|
+
}
|
|
65
|
+
pressLayoutUnchanged(press) {
|
|
66
|
+
const lines = this.previousLines;
|
|
67
|
+
if (press.frame !== lines) {
|
|
68
|
+
if (press.frame.length !== lines.length)
|
|
69
|
+
return false;
|
|
70
|
+
for (let index = 0; index < lines.length; index++)
|
|
71
|
+
if (press.frame[index] !== lines[index])
|
|
72
|
+
return false;
|
|
68
73
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
74
|
+
const components = this.collectMouseComponents();
|
|
75
|
+
return (components.length === press.components.length &&
|
|
76
|
+
components.every((component, index) => component === press.components[index]));
|
|
72
77
|
}
|
|
73
78
|
applyMouseResult(result) {
|
|
74
79
|
if (!result?.focus)
|
|
@@ -105,7 +110,8 @@ export class TuiMainScreen extends TuiBase {
|
|
|
105
110
|
this.mousePress = {
|
|
106
111
|
target: result.target,
|
|
107
112
|
epoch: this.placementEpoch,
|
|
108
|
-
|
|
113
|
+
frame: this.previousLines,
|
|
114
|
+
components: this.collectMouseComponents(),
|
|
109
115
|
x: raw.x,
|
|
110
116
|
y: raw.y,
|
|
111
117
|
};
|
|
@@ -120,7 +126,7 @@ export class TuiMainScreen extends TuiBase {
|
|
|
120
126
|
this.mousePress = undefined;
|
|
121
127
|
if (!press ||
|
|
122
128
|
press.epoch !== this.placementEpoch ||
|
|
123
|
-
|
|
129
|
+
!this.pressLayoutUnchanged(press) ||
|
|
124
130
|
press.x !== raw.x ||
|
|
125
131
|
press.y !== raw.y) {
|
|
126
132
|
this.clicks.cancel();
|
package/dist/tui.d.ts
CHANGED
|
@@ -86,6 +86,15 @@ export interface Component {
|
|
|
86
86
|
* Called when theme changes or when component needs to re-render from scratch.
|
|
87
87
|
*/
|
|
88
88
|
invalidate(): void;
|
|
89
|
+
/**
|
|
90
|
+
* Optional render revision for containers that cache child output.
|
|
91
|
+
*
|
|
92
|
+
* A number promises that `render(width)` returns the same lines for the same width, terminal
|
|
93
|
+
* capabilities and theme until the number changes; the component must change it whenever its
|
|
94
|
+
* state changes (including in `invalidate()`). `undefined` means the output may change at any
|
|
95
|
+
* time (streaming, animation, unknown dependencies), so the component is rendered every frame.
|
|
96
|
+
*/
|
|
97
|
+
getRenderRevision?(): number | undefined;
|
|
89
98
|
dispose?(): void;
|
|
90
99
|
}
|
|
91
100
|
export type TuiInputListenerResult = {
|
|
@@ -217,12 +226,55 @@ type TuiConstructorOptions = {
|
|
|
217
226
|
showHardwareCursor?: boolean;
|
|
218
227
|
muxDetector?: () => boolean;
|
|
219
228
|
};
|
|
229
|
+
/** Mode of the renderer drawing the current frame, or `undefined` outside a frame. */
|
|
230
|
+
export declare function frameMode(): TuiMode | undefined;
|
|
220
231
|
/**
|
|
221
|
-
*
|
|
232
|
+
* Lines of transcript history a main-screen frame keeps above the live area: the terminal's own
|
|
233
|
+
* scrollback size where it can be read (tmux `history-limit`, or `PI_TUI_HISTORY_LINES`), else
|
|
234
|
+
* 2,000; never less than two screens, never more than 5,000 so a resume or repaint stays instant.
|
|
222
235
|
*/
|
|
236
|
+
export declare function mainScreenHistoryLines(rows?: number): number;
|
|
237
|
+
/** Forget the measured terminal scrollback size, e.g. after the environment changed in a test. */
|
|
238
|
+
export declare function resetMainScreenHistoryLines(): void;
|
|
239
|
+
/**
|
|
240
|
+
* Rows at the top of the last committed frame that now live in the terminal's native scrollback
|
|
241
|
+
* (main-screen renderer, same terminal size). Changing any of them forces a full scrollback replay.
|
|
242
|
+
* 0 outside such a frame.
|
|
243
|
+
*/
|
|
244
|
+
export declare function frameScrollbackRows(): number;
|
|
245
|
+
/**
|
|
246
|
+
* The absolute frame row where `component` starts, when its parent rendered it through
|
|
247
|
+
* {@link renderAtFrameRow}; `undefined` when the position is unknown (any other parent).
|
|
248
|
+
*/
|
|
249
|
+
export declare function claimFrameRow(component: Component): number | undefined;
|
|
250
|
+
/** Render `child` as starting at absolute frame row `row`, so it can {@link claimFrameRow} it. */
|
|
251
|
+
export declare function renderAtFrameRow(child: Component, width: number, row: number | undefined): string[];
|
|
252
|
+
/**
|
|
253
|
+
* Draw a new value from the process-wide render revision clock. Every revisioned state change uses
|
|
254
|
+
* one, so "the clock has not moved" proves no revisioned component changed and caches may skip
|
|
255
|
+
* re-reading their children's revisions.
|
|
256
|
+
*/
|
|
257
|
+
export declare function nextRenderRevision(): number;
|
|
258
|
+
/** Current value of the render revision clock (see {@link nextRenderRevision}). */
|
|
259
|
+
export declare function currentRenderRevision(): number;
|
|
260
|
+
/**
|
|
261
|
+
* Render revision of a component whose output is a pure function of its own state and its children's
|
|
262
|
+
* output. `bump()` records an own-state change; `read(children)` returns a revision that also changes
|
|
263
|
+
* whenever a child is replaced or a child's revision changes, and `undefined` while any child is live.
|
|
264
|
+
* Child revisions are re-read only after the clock moved, so an unchanged subtree costs one identity pass.
|
|
265
|
+
*/
|
|
266
|
+
export declare class CompositeRevision {
|
|
267
|
+
private revision;
|
|
268
|
+
private children;
|
|
269
|
+
private childRevisions;
|
|
270
|
+
private checkedAt;
|
|
271
|
+
bump(): void;
|
|
272
|
+
read(children: readonly Component[]): number | undefined;
|
|
273
|
+
}
|
|
223
274
|
export declare class Container implements Component {
|
|
224
275
|
children: Component[];
|
|
225
276
|
private disposed;
|
|
277
|
+
private readonly composite;
|
|
226
278
|
private mouseLayout?;
|
|
227
279
|
addChild(component: Component): void;
|
|
228
280
|
removeChild(component: Component): void;
|
|
@@ -231,9 +283,25 @@ export declare class Container implements Component {
|
|
|
231
283
|
detachAll(): void;
|
|
232
284
|
dispose(): void;
|
|
233
285
|
invalidate(): void;
|
|
286
|
+
/**
|
|
287
|
+
* A plain `Container` only concatenates its children, so its output changes exactly when a child
|
|
288
|
+
* changes. Subclasses may render more than their children and therefore opt in explicitly by
|
|
289
|
+
* overriding this (usually via {@link childRenderRevision}); an inherited revision would let a
|
|
290
|
+
* cache keep their stale output.
|
|
291
|
+
*/
|
|
292
|
+
getRenderRevision(): number | undefined;
|
|
293
|
+
/** Revision of this container's children, for subclasses whose output depends only on them and `bump()`ed state. */
|
|
294
|
+
protected childRenderRevision(): number | undefined;
|
|
295
|
+
/** Record an own-state change for {@link childRenderRevision}. */
|
|
296
|
+
protected bumpRenderRevision(): void;
|
|
234
297
|
handleMouse(event: TuiMouseEvent): TuiMouseDispatchResult | undefined;
|
|
235
298
|
render(width: number): string[];
|
|
236
299
|
}
|
|
300
|
+
/**
|
|
301
|
+
* Concatenate rendered line arrays into one new array. Native `concat` copies whole arrays at once,
|
|
302
|
+
* which keeps a frame over a long transcript from paying a per-line iterator and push.
|
|
303
|
+
*/
|
|
304
|
+
export declare function joinLineArrays(chunks: readonly (readonly string[])[]): string[];
|
|
237
305
|
/** Composite overlay content into a terminal line at a fixed column. */
|
|
238
306
|
export declare function compositeTuiLine(baseLine: string, overlayLine: string, startCol: number, overlayWidth: number, totalWidth: number): string;
|
|
239
307
|
export type TuiMode = "regular" | "fullscreen";
|
|
@@ -260,6 +328,9 @@ export declare abstract class TuiBase extends Container {
|
|
|
260
328
|
terminal: Terminal;
|
|
261
329
|
protected previousLines: string[];
|
|
262
330
|
private previousRawLines;
|
|
331
|
+
private previousImageScan;
|
|
332
|
+
/** Image presence the normalization pass already measured for the array it produced. */
|
|
333
|
+
private normalizedImageHint;
|
|
263
334
|
private normalizeMemo;
|
|
264
335
|
protected previousKittyImageIds: Set<number>;
|
|
265
336
|
protected previousWidth: number;
|
|
@@ -419,6 +490,7 @@ export declare abstract class TuiBase extends Container {
|
|
|
419
490
|
/** Composite all overlays into content lines (sorted by focusOrder, higher = on top). */
|
|
420
491
|
protected compositeOverlays(lines: string[], termWidth: number, termHeight: number): string[];
|
|
421
492
|
private static readonly SEGMENT_RESET;
|
|
493
|
+
private static readonly NORMALIZE_MEMO_MIN;
|
|
422
494
|
/**
|
|
423
495
|
* Every frame write is bracketed by synchronized output (DECSET 2026) and
|
|
424
496
|
* disables autowrap (DECAWM, DECRST 7) while rows are painted. Differential
|
|
@@ -433,10 +505,21 @@ export declare abstract class TuiBase extends Container {
|
|
|
433
505
|
private static readonly FRAME_BEGIN;
|
|
434
506
|
private static readonly FRAME_END;
|
|
435
507
|
private setPreviousLines;
|
|
508
|
+
/** Image presence of the committed frame, measured once per frame array instead of once per check. */
|
|
509
|
+
protected previousLinesHaveImage(): boolean;
|
|
510
|
+
/** Record image presence for a produced array; `undefined` leaves it to a scan when it is committed. */
|
|
511
|
+
private hintNormalizedImages;
|
|
436
512
|
private normalizeLine;
|
|
437
513
|
protected applyLineResets(lines: string[]): string[];
|
|
438
514
|
private applyLineResetResult;
|
|
439
515
|
private applyViewportLineResets;
|
|
516
|
+
/**
|
|
517
|
+
* A frame whose line count changed (an append, a growing editor, a removed row) keeps every
|
|
518
|
+
* leading line that is unchanged since the last frame. Normalization is a pure function of the
|
|
519
|
+
* raw line, so the previous normalized prefix is reused and only the changed tail is normalized;
|
|
520
|
+
* the diff then starts where the raw lines first differ.
|
|
521
|
+
*/
|
|
522
|
+
private applyResizedLineResets;
|
|
440
523
|
private collectKittyImageIds;
|
|
441
524
|
private deleteKittyImages;
|
|
442
525
|
private getKittyImageReservedRows;
|
package/dist/tui.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Minimal TUI implementation with differential rendering
|
|
3
3
|
*/
|
|
4
|
+
import { execFile } from "node:child_process";
|
|
4
5
|
import * as fs from "node:fs";
|
|
5
6
|
import * as os from "node:os";
|
|
6
7
|
import * as path from "node:path";
|
|
@@ -274,18 +275,179 @@ function parseSizeValue(value, referenceSize) {
|
|
|
274
275
|
/**
|
|
275
276
|
* Container - a component that contains other components
|
|
276
277
|
*/
|
|
278
|
+
/**
|
|
279
|
+
* Facts about the frame being rendered, published by the main-screen renderer for containers that
|
|
280
|
+
* can skip work for rows the terminal cannot repaint cheaply.
|
|
281
|
+
*/
|
|
282
|
+
const renderFrame = {
|
|
283
|
+
scrollbackRows: 0,
|
|
284
|
+
offset: 0,
|
|
285
|
+
next: undefined,
|
|
286
|
+
mode: undefined,
|
|
287
|
+
rows: 0,
|
|
288
|
+
};
|
|
289
|
+
/** Mode of the renderer drawing the current frame, or `undefined` outside a frame. */
|
|
290
|
+
export function frameMode() {
|
|
291
|
+
return renderFrame.mode;
|
|
292
|
+
}
|
|
293
|
+
const DEFAULT_HISTORY_LINES = 2000;
|
|
294
|
+
/** Writing this many lines into a terminal takes ~0.2 s, the most a resume or repaint may spend on history. */
|
|
295
|
+
const MAX_HISTORY_LINES = 5000;
|
|
296
|
+
let terminalScrollbackLines;
|
|
297
|
+
/** Bumped by {@link resetMainScreenHistoryLines}, so a tmux answer for an older environment is dropped. */
|
|
298
|
+
let scrollbackLookupGeneration = 0;
|
|
299
|
+
/**
|
|
300
|
+
* The override is read at once; tmux is asked in the background (a process run inside the first
|
|
301
|
+
* render would hold input and painting while tmux answers), and the default applies until it does.
|
|
302
|
+
*/
|
|
303
|
+
function readTerminalScrollbackLines() {
|
|
304
|
+
const override = Number(process.env.PI_TUI_HISTORY_LINES);
|
|
305
|
+
if (Number.isFinite(override) && override > 0)
|
|
306
|
+
return Math.floor(override);
|
|
307
|
+
if (!process.env.TMUX)
|
|
308
|
+
return null;
|
|
309
|
+
const generation = scrollbackLookupGeneration;
|
|
310
|
+
try {
|
|
311
|
+
execFile("tmux", ["display-message", "-p", "#{history_limit}"], { encoding: "utf8", timeout: 500 }, (error, stdout) => {
|
|
312
|
+
if (generation !== scrollbackLookupGeneration)
|
|
313
|
+
return;
|
|
314
|
+
const limit = Number(stdout.trim());
|
|
315
|
+
if (!error && Number.isFinite(limit) && limit > 0)
|
|
316
|
+
terminalScrollbackLines = limit;
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
// tmux is not runnable: keep the default.
|
|
321
|
+
}
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Lines of transcript history a main-screen frame keeps above the live area: the terminal's own
|
|
326
|
+
* scrollback size where it can be read (tmux `history-limit`, or `PI_TUI_HISTORY_LINES`), else
|
|
327
|
+
* 2,000; never less than two screens, never more than 5,000 so a resume or repaint stays instant.
|
|
328
|
+
*/
|
|
329
|
+
export function mainScreenHistoryLines(rows = renderFrame.rows) {
|
|
330
|
+
if (terminalScrollbackLines === undefined)
|
|
331
|
+
terminalScrollbackLines = readTerminalScrollbackLines();
|
|
332
|
+
const preferred = Math.min(MAX_HISTORY_LINES, terminalScrollbackLines ?? DEFAULT_HISTORY_LINES);
|
|
333
|
+
return Math.min(MAX_HISTORY_LINES, Math.max(2 * Math.max(1, rows), preferred));
|
|
334
|
+
}
|
|
335
|
+
/** Forget the measured terminal scrollback size, e.g. after the environment changed in a test. */
|
|
336
|
+
export function resetMainScreenHistoryLines() {
|
|
337
|
+
terminalScrollbackLines = undefined;
|
|
338
|
+
scrollbackLookupGeneration += 1;
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Rows at the top of the last committed frame that now live in the terminal's native scrollback
|
|
342
|
+
* (main-screen renderer, same terminal size). Changing any of them forces a full scrollback replay.
|
|
343
|
+
* 0 outside such a frame.
|
|
344
|
+
*/
|
|
345
|
+
export function frameScrollbackRows() {
|
|
346
|
+
return renderFrame.scrollbackRows;
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* The absolute frame row where `component` starts, when its parent rendered it through
|
|
350
|
+
* {@link renderAtFrameRow}; `undefined` when the position is unknown (any other parent).
|
|
351
|
+
*/
|
|
352
|
+
export function claimFrameRow(component) {
|
|
353
|
+
if (renderFrame.next !== component)
|
|
354
|
+
return undefined;
|
|
355
|
+
renderFrame.next = undefined;
|
|
356
|
+
return renderFrame.offset;
|
|
357
|
+
}
|
|
358
|
+
/** Render `child` as starting at absolute frame row `row`, so it can {@link claimFrameRow} it. */
|
|
359
|
+
export function renderAtFrameRow(child, width, row) {
|
|
360
|
+
if (row === undefined)
|
|
361
|
+
return child.render(width);
|
|
362
|
+
const previousOffset = renderFrame.offset;
|
|
363
|
+
const previousNext = renderFrame.next;
|
|
364
|
+
renderFrame.offset = row;
|
|
365
|
+
renderFrame.next = child;
|
|
366
|
+
try {
|
|
367
|
+
return child.render(width);
|
|
368
|
+
}
|
|
369
|
+
finally {
|
|
370
|
+
renderFrame.offset = previousOffset;
|
|
371
|
+
renderFrame.next = previousNext;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
let renderRevisionClock = 0;
|
|
375
|
+
/**
|
|
376
|
+
* Draw a new value from the process-wide render revision clock. Every revisioned state change uses
|
|
377
|
+
* one, so "the clock has not moved" proves no revisioned component changed and caches may skip
|
|
378
|
+
* re-reading their children's revisions.
|
|
379
|
+
*/
|
|
380
|
+
export function nextRenderRevision() {
|
|
381
|
+
renderRevisionClock += 1;
|
|
382
|
+
return renderRevisionClock;
|
|
383
|
+
}
|
|
384
|
+
/** Current value of the render revision clock (see {@link nextRenderRevision}). */
|
|
385
|
+
export function currentRenderRevision() {
|
|
386
|
+
return renderRevisionClock;
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Render revision of a component whose output is a pure function of its own state and its children's
|
|
390
|
+
* output. `bump()` records an own-state change; `read(children)` returns a revision that also changes
|
|
391
|
+
* whenever a child is replaced or a child's revision changes, and `undefined` while any child is live.
|
|
392
|
+
* Child revisions are re-read only after the clock moved, so an unchanged subtree costs one identity pass.
|
|
393
|
+
*/
|
|
394
|
+
export class CompositeRevision {
|
|
395
|
+
constructor() {
|
|
396
|
+
this.revision = nextRenderRevision();
|
|
397
|
+
this.children = [];
|
|
398
|
+
this.childRevisions = [];
|
|
399
|
+
this.checkedAt = -1;
|
|
400
|
+
}
|
|
401
|
+
bump() {
|
|
402
|
+
this.revision = nextRenderRevision();
|
|
403
|
+
}
|
|
404
|
+
read(children) {
|
|
405
|
+
let replaced = children.length !== this.children.length;
|
|
406
|
+
for (let index = 0; !replaced && index < children.length; index++) {
|
|
407
|
+
if (children[index] !== this.children[index])
|
|
408
|
+
replaced = true;
|
|
409
|
+
}
|
|
410
|
+
if (!replaced && this.checkedAt === renderRevisionClock)
|
|
411
|
+
return this.revision;
|
|
412
|
+
const revisions = [];
|
|
413
|
+
for (const child of children) {
|
|
414
|
+
const revision = child.getRenderRevision?.();
|
|
415
|
+
if (revision === undefined) {
|
|
416
|
+
// Unrevisioned: nothing to compare against next time, and removed children must not stay referenced.
|
|
417
|
+
this.children = [];
|
|
418
|
+
this.childRevisions = [];
|
|
419
|
+
this.checkedAt = -1;
|
|
420
|
+
return undefined;
|
|
421
|
+
}
|
|
422
|
+
revisions.push(revision);
|
|
423
|
+
}
|
|
424
|
+
const changed = replaced || revisions.some((revision, index) => revision !== this.childRevisions[index]);
|
|
425
|
+
if (changed) {
|
|
426
|
+
this.revision = nextRenderRevision();
|
|
427
|
+
this.children = [...children];
|
|
428
|
+
}
|
|
429
|
+
this.childRevisions = revisions;
|
|
430
|
+
this.checkedAt = renderRevisionClock;
|
|
431
|
+
return this.revision;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
277
434
|
export class Container {
|
|
278
435
|
constructor() {
|
|
279
436
|
this.children = [];
|
|
280
437
|
this.disposed = false;
|
|
438
|
+
this.composite = new CompositeRevision();
|
|
281
439
|
}
|
|
440
|
+
// Every structural change moves the render revision clock, so a cache that saw the clock stand
|
|
441
|
+
// still may trust that no revisioned subtree gained, lost or swapped a child.
|
|
282
442
|
addChild(component) {
|
|
283
443
|
this.children.push(component);
|
|
444
|
+
this.composite.bump();
|
|
284
445
|
}
|
|
285
446
|
removeChild(component) {
|
|
286
447
|
const index = this.children.indexOf(component);
|
|
287
448
|
if (index !== -1) {
|
|
288
449
|
this.children.splice(index, 1);
|
|
450
|
+
this.composite.bump();
|
|
289
451
|
component.dispose?.();
|
|
290
452
|
}
|
|
291
453
|
}
|
|
@@ -293,6 +455,7 @@ export class Container {
|
|
|
293
455
|
const index = this.children.indexOf(component);
|
|
294
456
|
if (index !== -1) {
|
|
295
457
|
this.children.splice(index, 1);
|
|
458
|
+
this.composite.bump();
|
|
296
459
|
}
|
|
297
460
|
}
|
|
298
461
|
clear() {
|
|
@@ -300,9 +463,11 @@ export class Container {
|
|
|
300
463
|
child.dispose?.();
|
|
301
464
|
}
|
|
302
465
|
this.children = [];
|
|
466
|
+
this.composite.bump();
|
|
303
467
|
}
|
|
304
468
|
detachAll() {
|
|
305
469
|
this.children = [];
|
|
470
|
+
this.composite.bump();
|
|
306
471
|
}
|
|
307
472
|
dispose() {
|
|
308
473
|
if (this.disposed)
|
|
@@ -313,10 +478,28 @@ export class Container {
|
|
|
313
478
|
}
|
|
314
479
|
}
|
|
315
480
|
invalidate() {
|
|
481
|
+
this.composite.bump();
|
|
316
482
|
for (const child of this.children) {
|
|
317
483
|
child.invalidate?.();
|
|
318
484
|
}
|
|
319
485
|
}
|
|
486
|
+
/**
|
|
487
|
+
* A plain `Container` only concatenates its children, so its output changes exactly when a child
|
|
488
|
+
* changes. Subclasses may render more than their children and therefore opt in explicitly by
|
|
489
|
+
* overriding this (usually via {@link childRenderRevision}); an inherited revision would let a
|
|
490
|
+
* cache keep their stale output.
|
|
491
|
+
*/
|
|
492
|
+
getRenderRevision() {
|
|
493
|
+
return Object.getPrototypeOf(this) === Container.prototype ? this.childRenderRevision() : undefined;
|
|
494
|
+
}
|
|
495
|
+
/** Revision of this container's children, for subclasses whose output depends only on them and `bump()`ed state. */
|
|
496
|
+
childRenderRevision() {
|
|
497
|
+
return this.composite.read(this.children);
|
|
498
|
+
}
|
|
499
|
+
/** Record an own-state change for {@link childRenderRevision}. */
|
|
500
|
+
bumpRenderRevision() {
|
|
501
|
+
this.composite.bump();
|
|
502
|
+
}
|
|
320
503
|
handleMouse(event) {
|
|
321
504
|
if (event.y < 0 || event.y >= event.height)
|
|
322
505
|
return undefined;
|
|
@@ -340,12 +523,13 @@ export class Container {
|
|
|
340
523
|
return undefined;
|
|
341
524
|
}
|
|
342
525
|
render(width) {
|
|
343
|
-
const
|
|
526
|
+
const chunks = [];
|
|
344
527
|
const mouseChildren = [];
|
|
528
|
+
let row = claimFrameRow(this);
|
|
345
529
|
for (const child of this.children) {
|
|
346
530
|
let childLines;
|
|
347
531
|
try {
|
|
348
|
-
childLines = child
|
|
532
|
+
childLines = renderAtFrameRow(child, width, row);
|
|
349
533
|
}
|
|
350
534
|
catch (error) {
|
|
351
535
|
logRenderErrorOnce(child, error);
|
|
@@ -354,14 +538,28 @@ export class Container {
|
|
|
354
538
|
childLines = [`[render error: ${componentName}]`];
|
|
355
539
|
}
|
|
356
540
|
mouseChildren.push({ component: child, height: childLines.length });
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
541
|
+
chunks.push(childLines);
|
|
542
|
+
if (row !== undefined)
|
|
543
|
+
row += childLines.length;
|
|
360
544
|
}
|
|
361
545
|
this.mouseLayout = { width, children: mouseChildren };
|
|
362
|
-
return
|
|
546
|
+
return joinLineArrays(chunks);
|
|
363
547
|
}
|
|
364
548
|
}
|
|
549
|
+
const JOIN_BATCH = 1024;
|
|
550
|
+
/**
|
|
551
|
+
* Concatenate rendered line arrays into one new array. Native `concat` copies whole arrays at once,
|
|
552
|
+
* which keeps a frame over a long transcript from paying a per-line iterator and push.
|
|
553
|
+
*/
|
|
554
|
+
export function joinLineArrays(chunks) {
|
|
555
|
+
if (chunks.length <= JOIN_BATCH)
|
|
556
|
+
return [].concat(...chunks);
|
|
557
|
+
const batches = [];
|
|
558
|
+
for (let start = 0; start < chunks.length; start += JOIN_BATCH) {
|
|
559
|
+
batches.push([].concat(...chunks.slice(start, start + JOIN_BATCH)));
|
|
560
|
+
}
|
|
561
|
+
return [].concat(...batches);
|
|
562
|
+
}
|
|
365
563
|
/**
|
|
366
564
|
* TUI - Main class for managing terminal UI with differential rendering
|
|
367
565
|
*/
|
|
@@ -528,7 +726,7 @@ export class TuiBase extends Container {
|
|
|
528
726
|
if (this.mouseCommittedLineCount !== this.previousLines.length)
|
|
529
727
|
this.placementEpoch++;
|
|
530
728
|
this.mouseCommittedLineCount = this.previousLines.length;
|
|
531
|
-
if (this.
|
|
729
|
+
if (this.previousLinesHaveImage()) {
|
|
532
730
|
this.placementEpoch++;
|
|
533
731
|
this.anchor.kind = "unknown";
|
|
534
732
|
return;
|
|
@@ -560,7 +758,7 @@ export class TuiBase extends Container {
|
|
|
560
758
|
this.mouseExternalWritePending ||
|
|
561
759
|
!this.terminal.queryCursorPosition ||
|
|
562
760
|
this.previousLines.length === 0 ||
|
|
563
|
-
this.
|
|
761
|
+
this.previousLinesHaveImage())
|
|
564
762
|
return;
|
|
565
763
|
if (this.anchor.kind !== "unknown" &&
|
|
566
764
|
this.anchor.epoch === this.placementEpoch &&
|
|
@@ -1542,6 +1740,7 @@ export class TuiBase extends Container {
|
|
|
1542
1740
|
return result;
|
|
1543
1741
|
}
|
|
1544
1742
|
static { this.SEGMENT_RESET = "\x1b[0m\x1b]8;;\x07"; }
|
|
1743
|
+
static { this.NORMALIZE_MEMO_MIN = 4096; }
|
|
1545
1744
|
/**
|
|
1546
1745
|
* Every frame write is bracketed by synchronized output (DECSET 2026) and
|
|
1547
1746
|
* disables autowrap (DECAWM, DECRST 7) while rows are painted. Differential
|
|
@@ -1559,6 +1758,19 @@ export class TuiBase extends Container {
|
|
|
1559
1758
|
this.previousLines = lines;
|
|
1560
1759
|
this.previousRawLines = rawLines;
|
|
1561
1760
|
}
|
|
1761
|
+
/** Image presence of the committed frame, measured once per frame array instead of once per check. */
|
|
1762
|
+
previousLinesHaveImage() {
|
|
1763
|
+
const lines = this.previousLines;
|
|
1764
|
+
if (this.previousImageScan?.lines !== lines) {
|
|
1765
|
+
const hint = this.normalizedImageHint?.lines === lines ? this.normalizedImageHint.hasImage : undefined;
|
|
1766
|
+
this.previousImageScan = { lines, hasImage: hint ?? lines.some(isImageLine) };
|
|
1767
|
+
}
|
|
1768
|
+
return this.previousImageScan.hasImage;
|
|
1769
|
+
}
|
|
1770
|
+
/** Record image presence for a produced array; `undefined` leaves it to a scan when it is committed. */
|
|
1771
|
+
hintNormalizedImages(lines, hasImage) {
|
|
1772
|
+
this.normalizedImageHint = hasImage === undefined ? undefined : { lines, hasImage };
|
|
1773
|
+
}
|
|
1562
1774
|
normalizeLine(line) {
|
|
1563
1775
|
if (isImageLine(line)) {
|
|
1564
1776
|
return { line, normalized: false };
|
|
@@ -1568,6 +1780,16 @@ export class TuiBase extends Container {
|
|
|
1568
1780
|
return { line: cached, normalized: false };
|
|
1569
1781
|
}
|
|
1570
1782
|
const normalized = normalizeTerminalOutput(line) + TUI.SEGMENT_RESET;
|
|
1783
|
+
// The windowed path only ever adds: a long run of distinct lines (spinners, streamed text)
|
|
1784
|
+
// grew the memo without bound. Past a bound the oldest half goes; full passes rebuild it.
|
|
1785
|
+
if (this.normalizeMemo.size >= Math.max(TUI.NORMALIZE_MEMO_MIN, this.previousRawLines.length * 2)) {
|
|
1786
|
+
let drop = this.normalizeMemo.size >> 1;
|
|
1787
|
+
for (const key of this.normalizeMemo.keys()) {
|
|
1788
|
+
if (drop-- <= 0)
|
|
1789
|
+
break;
|
|
1790
|
+
this.normalizeMemo.delete(key);
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1571
1793
|
this.normalizeMemo.set(line, normalized);
|
|
1572
1794
|
return { line: normalized, normalized: true };
|
|
1573
1795
|
}
|
|
@@ -1579,9 +1801,11 @@ export class TuiBase extends Container {
|
|
|
1579
1801
|
const nextMemo = new Map();
|
|
1580
1802
|
const normalizedLines = [];
|
|
1581
1803
|
let normalizedCount = 0;
|
|
1804
|
+
let hasImage = false;
|
|
1582
1805
|
for (let i = 0; i < lines.length; i++) {
|
|
1583
1806
|
const line = lines[i];
|
|
1584
1807
|
if (isImageLine(line)) {
|
|
1808
|
+
hasImage = true;
|
|
1585
1809
|
normalizedLines.push(line);
|
|
1586
1810
|
continue;
|
|
1587
1811
|
}
|
|
@@ -1594,6 +1818,7 @@ export class TuiBase extends Container {
|
|
|
1594
1818
|
normalizedLines.push(normalized);
|
|
1595
1819
|
}
|
|
1596
1820
|
this.normalizeMemo = nextMemo;
|
|
1821
|
+
this.hintNormalizedImages(normalizedLines, hasImage);
|
|
1597
1822
|
recordViewportRenderStats(normalizedCount, mode);
|
|
1598
1823
|
return {
|
|
1599
1824
|
lines: normalizedLines,
|
|
@@ -1606,10 +1831,12 @@ export class TuiBase extends Container {
|
|
|
1606
1831
|
if (!viewportRenderEnabled() ||
|
|
1607
1832
|
!stableDimensions ||
|
|
1608
1833
|
this.previousLines.length === 0 ||
|
|
1609
|
-
this.
|
|
1610
|
-
this.previousRawLines.length !== rawLines.length) {
|
|
1834
|
+
this.previousRawLines.length !== this.previousLines.length) {
|
|
1611
1835
|
return this.applyLineResetResult(rawLines);
|
|
1612
1836
|
}
|
|
1837
|
+
if (this.previousRawLines.length !== rawLines.length) {
|
|
1838
|
+
return this.applyResizedLineResets(rawLines);
|
|
1839
|
+
}
|
|
1613
1840
|
const windowStart = Math.max(0, viewportTop - VIEWPORT_RENDER_OVERSCAN);
|
|
1614
1841
|
const windowEnd = Math.min(rawLines.length, viewportTop + height + VIEWPORT_RENDER_OVERSCAN);
|
|
1615
1842
|
let firstRawChanged = -1;
|
|
@@ -1624,20 +1851,27 @@ export class TuiBase extends Container {
|
|
|
1624
1851
|
return this.applyLineResetResult(rawLines, "escaped");
|
|
1625
1852
|
}
|
|
1626
1853
|
}
|
|
1854
|
+
const previousHadImage = this.previousLinesHaveImage();
|
|
1627
1855
|
const lines = this.previousLines.slice();
|
|
1628
1856
|
let normalizedCount = 0;
|
|
1857
|
+
let changedImage = false;
|
|
1629
1858
|
if (firstRawChanged !== -1) {
|
|
1630
1859
|
for (let i = windowStart; i < windowEnd; i++) {
|
|
1631
1860
|
if (rawLines[i] === this.previousRawLines[i]) {
|
|
1632
1861
|
continue;
|
|
1633
1862
|
}
|
|
1634
|
-
const
|
|
1863
|
+
const raw = rawLines[i] ?? "";
|
|
1864
|
+
if (isImageLine(raw))
|
|
1865
|
+
changedImage = true;
|
|
1866
|
+
const normalized = this.normalizeLine(raw);
|
|
1635
1867
|
lines[i] = normalized.line;
|
|
1636
1868
|
if (normalized.normalized) {
|
|
1637
1869
|
normalizedCount += 1;
|
|
1638
1870
|
}
|
|
1639
1871
|
}
|
|
1640
1872
|
}
|
|
1873
|
+
// A frame that had an image may have replaced it; only an image-free frame can be updated in place.
|
|
1874
|
+
this.hintNormalizedImages(lines, previousHadImage ? undefined : changedImage);
|
|
1641
1875
|
recordViewportRenderStats(normalizedCount, "bounded");
|
|
1642
1876
|
return {
|
|
1643
1877
|
lines,
|
|
@@ -1646,6 +1880,46 @@ export class TuiBase extends Container {
|
|
|
1646
1880
|
bounded: true,
|
|
1647
1881
|
};
|
|
1648
1882
|
}
|
|
1883
|
+
/**
|
|
1884
|
+
* A frame whose line count changed (an append, a growing editor, a removed row) keeps every
|
|
1885
|
+
* leading line that is unchanged since the last frame. Normalization is a pure function of the
|
|
1886
|
+
* raw line, so the previous normalized prefix is reused and only the changed tail is normalized;
|
|
1887
|
+
* the diff then starts where the raw lines first differ.
|
|
1888
|
+
*/
|
|
1889
|
+
applyResizedLineResets(rawLines) {
|
|
1890
|
+
const previousRaw = this.previousRawLines;
|
|
1891
|
+
const sharedLength = Math.min(rawLines.length, previousRaw.length);
|
|
1892
|
+
let firstRawChanged = 0;
|
|
1893
|
+
while (firstRawChanged < sharedLength && rawLines[firstRawChanged] === previousRaw[firstRawChanged]) {
|
|
1894
|
+
firstRawChanged++;
|
|
1895
|
+
}
|
|
1896
|
+
const previousHadImage = this.previousLinesHaveImage();
|
|
1897
|
+
const lines = this.previousLines.slice(0, firstRawChanged);
|
|
1898
|
+
let normalizedCount = 0;
|
|
1899
|
+
let tailImage = false;
|
|
1900
|
+
for (let i = firstRawChanged; i < rawLines.length; i++) {
|
|
1901
|
+
const line = rawLines[i] ?? "";
|
|
1902
|
+
if (isImageLine(line)) {
|
|
1903
|
+
tailImage = true;
|
|
1904
|
+
lines.push(line);
|
|
1905
|
+
continue;
|
|
1906
|
+
}
|
|
1907
|
+
let normalized = this.normalizeMemo.get(line);
|
|
1908
|
+
if (normalized === undefined) {
|
|
1909
|
+
normalized = normalizeTerminalOutput(line) + TUI.SEGMENT_RESET;
|
|
1910
|
+
normalizedCount += 1;
|
|
1911
|
+
}
|
|
1912
|
+
lines.push(normalized);
|
|
1913
|
+
}
|
|
1914
|
+
this.hintNormalizedImages(lines, previousHadImage ? undefined : tailImage);
|
|
1915
|
+
recordViewportRenderStats(normalizedCount, "bounded");
|
|
1916
|
+
return {
|
|
1917
|
+
lines,
|
|
1918
|
+
firstRawChanged,
|
|
1919
|
+
compareEndExclusive: Math.max(rawLines.length, this.previousLines.length),
|
|
1920
|
+
bounded: true,
|
|
1921
|
+
};
|
|
1922
|
+
}
|
|
1649
1923
|
collectKittyImageIds(lines) {
|
|
1650
1924
|
recordKittyImageScanStats(lines.length);
|
|
1651
1925
|
const ids = new Set();
|
|
@@ -1960,8 +2234,22 @@ export class TuiBase extends Container {
|
|
|
1960
2234
|
const targetScreenRow = targetRow - viewportTop;
|
|
1961
2235
|
return targetScreenRow - currentScreenRow;
|
|
1962
2236
|
};
|
|
1963
|
-
// Render all components to get new lines
|
|
1964
|
-
|
|
2237
|
+
// Render all components to get new lines. The main screen tells containers which rows of the
|
|
2238
|
+
// last frame are in native scrollback, so live content there can stay as the terminal shows it.
|
|
2239
|
+
renderFrame.scrollbackRows =
|
|
2240
|
+
this.mode === "regular" && !widthChanged && !heightChanged && this.previousLines.length > 0
|
|
2241
|
+
? prevViewportTop
|
|
2242
|
+
: 0;
|
|
2243
|
+
renderFrame.mode = this.mode;
|
|
2244
|
+
renderFrame.rows = height;
|
|
2245
|
+
let newLines;
|
|
2246
|
+
try {
|
|
2247
|
+
newLines = renderAtFrameRow(this, width, 0);
|
|
2248
|
+
}
|
|
2249
|
+
finally {
|
|
2250
|
+
renderFrame.scrollbackRows = 0;
|
|
2251
|
+
renderFrame.mode = undefined;
|
|
2252
|
+
}
|
|
1965
2253
|
// Composite overlays into the rendered lines (before differential compare)
|
|
1966
2254
|
if (this.overlayStack.length > 0) {
|
|
1967
2255
|
newLines = this.compositeOverlays(newLines, width, height);
|
package/dist/utils.d.ts
CHANGED
|
@@ -54,6 +54,12 @@ export declare function getActiveBackgroundAnsi(text: string): string;
|
|
|
54
54
|
* @param width - Maximum visible width per line
|
|
55
55
|
* @returns Array of wrapped lines (NOT padded to width)
|
|
56
56
|
*/
|
|
57
|
+
/**
|
|
58
|
+
* Flatten cached lines. V8 keeps a string built by concatenation as a tree of its parts until something reads it
|
|
59
|
+
* whole, and a cached line kept as such a tree retains several times its own size. Converting a string to a number
|
|
60
|
+
* reads it whole, so V8 flattens it in place; the strings' values do not change.
|
|
61
|
+
*/
|
|
62
|
+
export declare function flattenLines(lines: readonly string[]): void;
|
|
57
63
|
export declare function wrapTextWithAnsi(text: string, width: number): string[];
|
|
58
64
|
export declare const PUNCTUATION_REGEX: RegExp;
|
|
59
65
|
/**
|
package/dist/utils.js
CHANGED
|
@@ -872,6 +872,15 @@ function splitIntoTokensWithAnsi(text) {
|
|
|
872
872
|
* @param width - Maximum visible width per line
|
|
873
873
|
* @returns Array of wrapped lines (NOT padded to width)
|
|
874
874
|
*/
|
|
875
|
+
/**
|
|
876
|
+
* Flatten cached lines. V8 keeps a string built by concatenation as a tree of its parts until something reads it
|
|
877
|
+
* whole, and a cached line kept as such a tree retains several times its own size. Converting a string to a number
|
|
878
|
+
* reads it whole, so V8 flattens it in place; the strings' values do not change.
|
|
879
|
+
*/
|
|
880
|
+
export function flattenLines(lines) {
|
|
881
|
+
for (const line of lines)
|
|
882
|
+
Number(line);
|
|
883
|
+
}
|
|
875
884
|
export function wrapTextWithAnsi(text, width) {
|
|
876
885
|
if (!text) {
|
|
877
886
|
return [""];
|
|
@@ -1260,8 +1269,11 @@ export function sliceWithWidth(line, startCol, length, strict = false) {
|
|
|
1260
1269
|
while (i < line.length) {
|
|
1261
1270
|
const ansi = extractAnsiCode(line, i);
|
|
1262
1271
|
if (ansi) {
|
|
1263
|
-
if (currentCol >= startCol && currentCol < endCol)
|
|
1264
|
-
|
|
1272
|
+
if (currentCol >= startCol && currentCol < endCol) {
|
|
1273
|
+
// Keep original order: codes from before the range must precede codes at the boundary
|
|
1274
|
+
result += pendingAnsi + ansi.code;
|
|
1275
|
+
pendingAnsi = "";
|
|
1276
|
+
}
|
|
1265
1277
|
else if (currentCol < startCol)
|
|
1266
1278
|
pendingAnsi += ansi.code;
|
|
1267
1279
|
i += ansi.length;
|
package/package.json
CHANGED