omk-tui 1.3.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -4
- package/dist/components/box.d.ts +5 -0
- package/dist/components/box.d.ts.map +1 -1
- package/dist/components/box.js +50 -15
- package/dist/components/box.js.map +1 -1
- package/dist/components/image.d.ts +2 -0
- package/dist/components/image.d.ts.map +1 -1
- package/dist/components/image.js +4 -0
- package/dist/components/image.js.map +1 -1
- package/dist/components/markdown.d.ts +4 -0
- package/dist/components/markdown.d.ts.map +1 -1
- package/dist/components/markdown.js +14 -0
- package/dist/components/markdown.js.map +1 -1
- package/dist/components/spacer.d.ts +2 -0
- package/dist/components/spacer.d.ts.map +1 -1
- package/dist/components/spacer.js +8 -0
- package/dist/components/spacer.js.map +1 -1
- package/dist/components/text.d.ts +4 -0
- package/dist/components/text.d.ts.map +1 -1
- package/dist/components/text.js +14 -0
- package/dist/components/text.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/line-reset-memo.d.ts +27 -0
- package/dist/line-reset-memo.d.ts.map +1 -0
- package/dist/line-reset-memo.js +101 -0
- package/dist/line-reset-memo.js.map +1 -0
- package/dist/render-generation.d.ts +24 -0
- package/dist/render-generation.d.ts.map +1 -0
- package/dist/render-generation.js +48 -0
- package/dist/render-generation.js.map +1 -0
- package/dist/terminal-output.d.ts +20 -1
- package/dist/terminal-output.d.ts.map +1 -1
- package/dist/terminal-output.js +62 -1
- package/dist/terminal-output.js.map +1 -1
- package/dist/terminal-resize-log.d.ts +39 -0
- package/dist/terminal-resize-log.d.ts.map +1 -0
- package/dist/terminal-resize-log.js +57 -0
- package/dist/terminal-resize-log.js.map +1 -0
- package/dist/terminal-resync.d.ts +74 -0
- package/dist/terminal-resync.d.ts.map +1 -0
- package/dist/terminal-resync.js +112 -0
- package/dist/terminal-resync.js.map +1 -0
- package/dist/terminal.d.ts.map +1 -1
- package/dist/terminal.js +1 -1
- package/dist/terminal.js.map +1 -1
- package/dist/tui.d.ts +29 -9
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +63 -85
- package/dist/tui.js.map +1 -1
- package/dist/undo-stack.d.ts +8 -2
- package/dist/undo-stack.d.ts.map +1 -1
- package/dist/undo-stack.js +12 -2
- package/dist/undo-stack.js.map +1 -1
- package/dist/utils.d.ts +2 -4
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +2 -128
- package/dist/utils.js.map +1 -1
- package/dist/visible-width.d.ts +14 -0
- package/dist/visible-width.d.ts.map +1 -0
- package/dist/visible-width.js +163 -0
- package/dist/visible-width.js.map +1 -0
- package/dist/windowed-container.d.ts +63 -0
- package/dist/windowed-container.d.ts.map +1 -0
- package/dist/windowed-container.js +259 -0
- package/dist/windowed-container.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Shared grapheme segmenter; `getGraphemeSegmenter()` in utils.ts returns this instance. */
|
|
2
|
+
export declare const graphemeSegmenter: Intl.Segmenter;
|
|
3
|
+
export declare function isPrintableAscii(str: string): boolean;
|
|
4
|
+
/**
|
|
5
|
+
* Calculate the terminal width of a single grapheme cluster.
|
|
6
|
+
* Based on code from the string-width library, but includes a possible-emoji
|
|
7
|
+
* check to avoid running the RGI_Emoji regex unnecessarily.
|
|
8
|
+
*/
|
|
9
|
+
export declare function graphemeWidth(segment: string): number;
|
|
10
|
+
/**
|
|
11
|
+
* Calculate the visible width of a string in terminal columns.
|
|
12
|
+
*/
|
|
13
|
+
export declare function visibleWidth(str: string): number;
|
|
14
|
+
//# sourceMappingURL=visible-width.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"visible-width.d.ts","sourceRoot":"","sources":["../src/visible-width.ts"],"names":[],"mappings":"AAIA,6FAA6F;AAC7F,eAAO,MAAM,iBAAiB,gBAA6D,CAAC;AAa5F,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAQrD;AAoBD;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CA4CrD;AAyCD;;GAEG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAuChD","sourcesContent":["import { eastAsianWidth } from \"get-east-asian-width\";\nimport { extractAnsiCode } from \"./ansi-codes.ts\";\nimport { leadingNonPrintingRegex, rgiEmojiRegex, zeroWidthRegex } from \"./unicode-regex.ts\";\n\n/** Shared grapheme segmenter; `getGraphemeSegmenter()` in utils.ts returns this instance. */\nexport const graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: \"grapheme\" });\n\n/**\n * Width cache for strings that need grapheme segmentation. Styled ASCII, Hangul and CJK\n * ideographs never reach it (see `simpleWidth`), so the entries go to emoji, combining\n * marks and other scripts. A render pass over a long transcript touches thousands of distinct lines;\n * 512 entries made such passes miss on every line (FIFO eviction under a cyclic scan).\n */\nconst WIDTH_CACHE_SIZE = 8192;\n/** Longer strings are measured without caching so the cache cannot pin large text. */\nconst WIDTH_CACHE_MAX_KEY_LENGTH = 4096;\nconst widthCache = new Map<string, number>();\n\nexport function isPrintableAscii(str: string): boolean {\n\tfor (let i = 0; i < str.length; i++) {\n\t\tconst code = str.charCodeAt(i);\n\t\tif (code < 0x20 || code > 0x7e) {\n\t\t\treturn false;\n\t\t}\n\t}\n\treturn true;\n}\n\n/**\n * Check if a grapheme cluster (after segmentation) could possibly be an RGI emoji.\n * This is a fast heuristic to avoid the expensive rgiEmojiRegex test.\n * The tested Unicode blocks are deliberately broad to account for future\n * Unicode additions.\n */\nfunction couldBeEmoji(segment: string): boolean {\n\tconst cp = segment.codePointAt(0)!;\n\treturn (\n\t\t(cp >= 0x1f000 && cp <= 0x1fbff) || // Emoji and Pictograph\n\t\t(cp >= 0x2300 && cp <= 0x23ff) || // Misc technical\n\t\t(cp >= 0x2600 && cp <= 0x27bf) || // Misc symbols, dingbats\n\t\t(cp >= 0x2b50 && cp <= 0x2b55) || // Specific stars/circles\n\t\tsegment.includes(\"\\uFE0F\") || // Contains VS16 (emoji presentation selector)\n\t\tsegment.length > 2 // Multi-codepoint sequences (ZWJ, skin tones, etc.)\n\t);\n}\n\n/**\n * Calculate the terminal width of a single grapheme cluster.\n * Based on code from the string-width library, but includes a possible-emoji\n * check to avoid running the RGI_Emoji regex unnecessarily.\n */\nexport function graphemeWidth(segment: string): number {\n\tif (segment === \"\\t\") {\n\t\treturn 3;\n\t}\n\n\t// Zero-width clusters\n\tif (zeroWidthRegex.test(segment)) {\n\t\treturn 0;\n\t}\n\n\t// Emoji check with pre-filter\n\tif (couldBeEmoji(segment) && rgiEmojiRegex.test(segment)) {\n\t\treturn 2;\n\t}\n\n\t// Get base visible codepoint\n\tconst base = segment.replace(leadingNonPrintingRegex, \"\");\n\tconst cp = base.codePointAt(0);\n\tif (cp === undefined) {\n\t\treturn 0;\n\t}\n\n\t// Regional indicator symbols (U+1F1E6..U+1F1FF) are often rendered as\n\t// full-width emoji in terminals, even when isolated during streaming.\n\t// Keep width conservative (2) to avoid terminal auto-wrap drift artifacts.\n\tif (cp >= 0x1f1e6 && cp <= 0x1f1ff) {\n\t\treturn 2;\n\t}\n\n\tlet width = eastAsianWidth(cp);\n\n\t// Trailing halfwidth/fullwidth forms and AM vowels that segment with a base.\n\tif (segment.length > 1) {\n\t\tfor (const char of segment.slice(1)) {\n\t\t\tconst c = char.codePointAt(0)!;\n\t\t\tif (c >= 0xff00 && c <= 0xffef) {\n\t\t\t\twidth += eastAsianWidth(c);\n\t\t\t} else if (c === 0x0e33 || c === 0x0eb3) {\n\t\t\t\twidth += 1;\n\t\t\t}\n\t\t}\n\t}\n\n\treturn width;\n}\n\n/**\n * Normalize for measuring: tabs become 3 spaces and supported ANSI/OSC/APC escape\n * sequences (CSI styling/cursor codes, OSC hyperlinks and prompt markers, APC sequences\n * like CURSOR_MARKER) are removed. An ESC that starts no complete sequence is kept.\n */\nfunction stripForWidth(str: string): string {\n\tconst clean = str.includes(\"\\t\") ? str.replace(/\\t/g, \" \") : str;\n\tlet esc = clean.indexOf(\"\\x1b\");\n\tif (esc === -1) return clean;\n\tlet stripped = \"\";\n\tlet copied = 0;\n\twhile (esc !== -1) {\n\t\tconst ansi = extractAnsiCode(clean, esc);\n\t\tif (ansi) {\n\t\t\tstripped += clean.slice(copied, esc);\n\t\t\tcopied = esc + ansi.length;\n\t\t}\n\t\tesc = clean.indexOf(\"\\x1b\", ansi ? copied : esc + 1);\n\t}\n\treturn stripped + clean.slice(copied);\n}\n\n/**\n * Width of text made only of printable ASCII (1 column), Hangul syllables and CJK unified\n * ideographs (2 columns each, UAX #11 Wide), or -1 when anything else appears. Each of these\n * code points is a grapheme cluster on its own: a Hangul syllable joins only trailing jamo\n * (U+1160..U+11FF) and an ideograph only variation selectors, both of which fall back here.\n */\nfunction simpleWidth(clean: string): number {\n\tlet width = 0;\n\tfor (let i = 0; i < clean.length; i++) {\n\t\tconst code = clean.charCodeAt(i);\n\t\tif (code >= 0x20 && code <= 0x7e) width += 1;\n\t\telse if ((code >= 0xac00 && code <= 0xd7a3) || (code >= 0x4e00 && code <= 0x9fff)) width += 2;\n\t\telse return -1;\n\t}\n\treturn width;\n}\n\n/**\n * Calculate the visible width of a string in terminal columns.\n */\nexport function visibleWidth(str: string): number {\n\tif (str.length === 0) {\n\t\treturn 0;\n\t}\n\n\t// Fast path: pure ASCII printable\n\tif (isPrintableAscii(str)) {\n\t\treturn str.length;\n\t}\n\n\t// Styled ASCII, Hangul and CJK (the bulk of rendered lines): once escapes are\n\t// stripped, every character is its own cluster, so segmentation is unnecessary.\n\tconst clean = stripForWidth(str);\n\tconst simple = simpleWidth(clean);\n\tif (simple >= 0) {\n\t\treturn simple;\n\t}\n\n\tconst cached = widthCache.get(str);\n\tif (cached !== undefined) {\n\t\treturn cached;\n\t}\n\n\tlet width = 0;\n\tfor (const { segment } of graphemeSegmenter.segment(clean)) {\n\t\twidth += graphemeWidth(segment);\n\t}\n\n\tif (str.length <= WIDTH_CACHE_MAX_KEY_LENGTH) {\n\t\tif (widthCache.size >= WIDTH_CACHE_SIZE) {\n\t\t\tconst firstKey = widthCache.keys().next().value;\n\t\t\tif (firstKey !== undefined) {\n\t\t\t\twidthCache.delete(firstKey);\n\t\t\t}\n\t\t}\n\t\twidthCache.set(str, width);\n\t}\n\n\treturn width;\n}\n"]}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { eastAsianWidth } from "get-east-asian-width";
|
|
2
|
+
import { extractAnsiCode } from "./ansi-codes.js";
|
|
3
|
+
import { leadingNonPrintingRegex, rgiEmojiRegex, zeroWidthRegex } from "./unicode-regex.js";
|
|
4
|
+
/** Shared grapheme segmenter; `getGraphemeSegmenter()` in utils.ts returns this instance. */
|
|
5
|
+
export const graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
6
|
+
/**
|
|
7
|
+
* Width cache for strings that need grapheme segmentation. Styled ASCII, Hangul and CJK
|
|
8
|
+
* ideographs never reach it (see `simpleWidth`), so the entries go to emoji, combining
|
|
9
|
+
* marks and other scripts. A render pass over a long transcript touches thousands of distinct lines;
|
|
10
|
+
* 512 entries made such passes miss on every line (FIFO eviction under a cyclic scan).
|
|
11
|
+
*/
|
|
12
|
+
const WIDTH_CACHE_SIZE = 8192;
|
|
13
|
+
/** Longer strings are measured without caching so the cache cannot pin large text. */
|
|
14
|
+
const WIDTH_CACHE_MAX_KEY_LENGTH = 4096;
|
|
15
|
+
const widthCache = new Map();
|
|
16
|
+
export function isPrintableAscii(str) {
|
|
17
|
+
for (let i = 0; i < str.length; i++) {
|
|
18
|
+
const code = str.charCodeAt(i);
|
|
19
|
+
if (code < 0x20 || code > 0x7e) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Check if a grapheme cluster (after segmentation) could possibly be an RGI emoji.
|
|
27
|
+
* This is a fast heuristic to avoid the expensive rgiEmojiRegex test.
|
|
28
|
+
* The tested Unicode blocks are deliberately broad to account for future
|
|
29
|
+
* Unicode additions.
|
|
30
|
+
*/
|
|
31
|
+
function couldBeEmoji(segment) {
|
|
32
|
+
const cp = segment.codePointAt(0);
|
|
33
|
+
return ((cp >= 0x1f000 && cp <= 0x1fbff) || // Emoji and Pictograph
|
|
34
|
+
(cp >= 0x2300 && cp <= 0x23ff) || // Misc technical
|
|
35
|
+
(cp >= 0x2600 && cp <= 0x27bf) || // Misc symbols, dingbats
|
|
36
|
+
(cp >= 0x2b50 && cp <= 0x2b55) || // Specific stars/circles
|
|
37
|
+
segment.includes("\uFE0F") || // Contains VS16 (emoji presentation selector)
|
|
38
|
+
segment.length > 2 // Multi-codepoint sequences (ZWJ, skin tones, etc.)
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Calculate the terminal width of a single grapheme cluster.
|
|
43
|
+
* Based on code from the string-width library, but includes a possible-emoji
|
|
44
|
+
* check to avoid running the RGI_Emoji regex unnecessarily.
|
|
45
|
+
*/
|
|
46
|
+
export function graphemeWidth(segment) {
|
|
47
|
+
if (segment === "\t") {
|
|
48
|
+
return 3;
|
|
49
|
+
}
|
|
50
|
+
// Zero-width clusters
|
|
51
|
+
if (zeroWidthRegex.test(segment)) {
|
|
52
|
+
return 0;
|
|
53
|
+
}
|
|
54
|
+
// Emoji check with pre-filter
|
|
55
|
+
if (couldBeEmoji(segment) && rgiEmojiRegex.test(segment)) {
|
|
56
|
+
return 2;
|
|
57
|
+
}
|
|
58
|
+
// Get base visible codepoint
|
|
59
|
+
const base = segment.replace(leadingNonPrintingRegex, "");
|
|
60
|
+
const cp = base.codePointAt(0);
|
|
61
|
+
if (cp === undefined) {
|
|
62
|
+
return 0;
|
|
63
|
+
}
|
|
64
|
+
// Regional indicator symbols (U+1F1E6..U+1F1FF) are often rendered as
|
|
65
|
+
// full-width emoji in terminals, even when isolated during streaming.
|
|
66
|
+
// Keep width conservative (2) to avoid terminal auto-wrap drift artifacts.
|
|
67
|
+
if (cp >= 0x1f1e6 && cp <= 0x1f1ff) {
|
|
68
|
+
return 2;
|
|
69
|
+
}
|
|
70
|
+
let width = eastAsianWidth(cp);
|
|
71
|
+
// Trailing halfwidth/fullwidth forms and AM vowels that segment with a base.
|
|
72
|
+
if (segment.length > 1) {
|
|
73
|
+
for (const char of segment.slice(1)) {
|
|
74
|
+
const c = char.codePointAt(0);
|
|
75
|
+
if (c >= 0xff00 && c <= 0xffef) {
|
|
76
|
+
width += eastAsianWidth(c);
|
|
77
|
+
}
|
|
78
|
+
else if (c === 0x0e33 || c === 0x0eb3) {
|
|
79
|
+
width += 1;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return width;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Normalize for measuring: tabs become 3 spaces and supported ANSI/OSC/APC escape
|
|
87
|
+
* sequences (CSI styling/cursor codes, OSC hyperlinks and prompt markers, APC sequences
|
|
88
|
+
* like CURSOR_MARKER) are removed. An ESC that starts no complete sequence is kept.
|
|
89
|
+
*/
|
|
90
|
+
function stripForWidth(str) {
|
|
91
|
+
const clean = str.includes("\t") ? str.replace(/\t/g, " ") : str;
|
|
92
|
+
let esc = clean.indexOf("\x1b");
|
|
93
|
+
if (esc === -1)
|
|
94
|
+
return clean;
|
|
95
|
+
let stripped = "";
|
|
96
|
+
let copied = 0;
|
|
97
|
+
while (esc !== -1) {
|
|
98
|
+
const ansi = extractAnsiCode(clean, esc);
|
|
99
|
+
if (ansi) {
|
|
100
|
+
stripped += clean.slice(copied, esc);
|
|
101
|
+
copied = esc + ansi.length;
|
|
102
|
+
}
|
|
103
|
+
esc = clean.indexOf("\x1b", ansi ? copied : esc + 1);
|
|
104
|
+
}
|
|
105
|
+
return stripped + clean.slice(copied);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Width of text made only of printable ASCII (1 column), Hangul syllables and CJK unified
|
|
109
|
+
* ideographs (2 columns each, UAX #11 Wide), or -1 when anything else appears. Each of these
|
|
110
|
+
* code points is a grapheme cluster on its own: a Hangul syllable joins only trailing jamo
|
|
111
|
+
* (U+1160..U+11FF) and an ideograph only variation selectors, both of which fall back here.
|
|
112
|
+
*/
|
|
113
|
+
function simpleWidth(clean) {
|
|
114
|
+
let width = 0;
|
|
115
|
+
for (let i = 0; i < clean.length; i++) {
|
|
116
|
+
const code = clean.charCodeAt(i);
|
|
117
|
+
if (code >= 0x20 && code <= 0x7e)
|
|
118
|
+
width += 1;
|
|
119
|
+
else if ((code >= 0xac00 && code <= 0xd7a3) || (code >= 0x4e00 && code <= 0x9fff))
|
|
120
|
+
width += 2;
|
|
121
|
+
else
|
|
122
|
+
return -1;
|
|
123
|
+
}
|
|
124
|
+
return width;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Calculate the visible width of a string in terminal columns.
|
|
128
|
+
*/
|
|
129
|
+
export function visibleWidth(str) {
|
|
130
|
+
if (str.length === 0) {
|
|
131
|
+
return 0;
|
|
132
|
+
}
|
|
133
|
+
// Fast path: pure ASCII printable
|
|
134
|
+
if (isPrintableAscii(str)) {
|
|
135
|
+
return str.length;
|
|
136
|
+
}
|
|
137
|
+
// Styled ASCII, Hangul and CJK (the bulk of rendered lines): once escapes are
|
|
138
|
+
// stripped, every character is its own cluster, so segmentation is unnecessary.
|
|
139
|
+
const clean = stripForWidth(str);
|
|
140
|
+
const simple = simpleWidth(clean);
|
|
141
|
+
if (simple >= 0) {
|
|
142
|
+
return simple;
|
|
143
|
+
}
|
|
144
|
+
const cached = widthCache.get(str);
|
|
145
|
+
if (cached !== undefined) {
|
|
146
|
+
return cached;
|
|
147
|
+
}
|
|
148
|
+
let width = 0;
|
|
149
|
+
for (const { segment } of graphemeSegmenter.segment(clean)) {
|
|
150
|
+
width += graphemeWidth(segment);
|
|
151
|
+
}
|
|
152
|
+
if (str.length <= WIDTH_CACHE_MAX_KEY_LENGTH) {
|
|
153
|
+
if (widthCache.size >= WIDTH_CACHE_SIZE) {
|
|
154
|
+
const firstKey = widthCache.keys().next().value;
|
|
155
|
+
if (firstKey !== undefined) {
|
|
156
|
+
widthCache.delete(firstKey);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
widthCache.set(str, width);
|
|
160
|
+
}
|
|
161
|
+
return width;
|
|
162
|
+
}
|
|
163
|
+
//# sourceMappingURL=visible-width.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"visible-width.js","sourceRoot":"","sources":["../src/visible-width.ts"],"names":[],"mappings":"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","sourcesContent":["import { eastAsianWidth } from \"get-east-asian-width\";\nimport { extractAnsiCode } from \"./ansi-codes.ts\";\nimport { leadingNonPrintingRegex, rgiEmojiRegex, zeroWidthRegex } from \"./unicode-regex.ts\";\n\n/** Shared grapheme segmenter; `getGraphemeSegmenter()` in utils.ts returns this instance. */\nexport const graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: \"grapheme\" });\n\n/**\n * Width cache for strings that need grapheme segmentation. Styled ASCII, Hangul and CJK\n * ideographs never reach it (see `simpleWidth`), so the entries go to emoji, combining\n * marks and other scripts. A render pass over a long transcript touches thousands of distinct lines;\n * 512 entries made such passes miss on every line (FIFO eviction under a cyclic scan).\n */\nconst WIDTH_CACHE_SIZE = 8192;\n/** Longer strings are measured without caching so the cache cannot pin large text. */\nconst WIDTH_CACHE_MAX_KEY_LENGTH = 4096;\nconst widthCache = new Map<string, number>();\n\nexport function isPrintableAscii(str: string): boolean {\n\tfor (let i = 0; i < str.length; i++) {\n\t\tconst code = str.charCodeAt(i);\n\t\tif (code < 0x20 || code > 0x7e) {\n\t\t\treturn false;\n\t\t}\n\t}\n\treturn true;\n}\n\n/**\n * Check if a grapheme cluster (after segmentation) could possibly be an RGI emoji.\n * This is a fast heuristic to avoid the expensive rgiEmojiRegex test.\n * The tested Unicode blocks are deliberately broad to account for future\n * Unicode additions.\n */\nfunction couldBeEmoji(segment: string): boolean {\n\tconst cp = segment.codePointAt(0)!;\n\treturn (\n\t\t(cp >= 0x1f000 && cp <= 0x1fbff) || // Emoji and Pictograph\n\t\t(cp >= 0x2300 && cp <= 0x23ff) || // Misc technical\n\t\t(cp >= 0x2600 && cp <= 0x27bf) || // Misc symbols, dingbats\n\t\t(cp >= 0x2b50 && cp <= 0x2b55) || // Specific stars/circles\n\t\tsegment.includes(\"\\uFE0F\") || // Contains VS16 (emoji presentation selector)\n\t\tsegment.length > 2 // Multi-codepoint sequences (ZWJ, skin tones, etc.)\n\t);\n}\n\n/**\n * Calculate the terminal width of a single grapheme cluster.\n * Based on code from the string-width library, but includes a possible-emoji\n * check to avoid running the RGI_Emoji regex unnecessarily.\n */\nexport function graphemeWidth(segment: string): number {\n\tif (segment === \"\\t\") {\n\t\treturn 3;\n\t}\n\n\t// Zero-width clusters\n\tif (zeroWidthRegex.test(segment)) {\n\t\treturn 0;\n\t}\n\n\t// Emoji check with pre-filter\n\tif (couldBeEmoji(segment) && rgiEmojiRegex.test(segment)) {\n\t\treturn 2;\n\t}\n\n\t// Get base visible codepoint\n\tconst base = segment.replace(leadingNonPrintingRegex, \"\");\n\tconst cp = base.codePointAt(0);\n\tif (cp === undefined) {\n\t\treturn 0;\n\t}\n\n\t// Regional indicator symbols (U+1F1E6..U+1F1FF) are often rendered as\n\t// full-width emoji in terminals, even when isolated during streaming.\n\t// Keep width conservative (2) to avoid terminal auto-wrap drift artifacts.\n\tif (cp >= 0x1f1e6 && cp <= 0x1f1ff) {\n\t\treturn 2;\n\t}\n\n\tlet width = eastAsianWidth(cp);\n\n\t// Trailing halfwidth/fullwidth forms and AM vowels that segment with a base.\n\tif (segment.length > 1) {\n\t\tfor (const char of segment.slice(1)) {\n\t\t\tconst c = char.codePointAt(0)!;\n\t\t\tif (c >= 0xff00 && c <= 0xffef) {\n\t\t\t\twidth += eastAsianWidth(c);\n\t\t\t} else if (c === 0x0e33 || c === 0x0eb3) {\n\t\t\t\twidth += 1;\n\t\t\t}\n\t\t}\n\t}\n\n\treturn width;\n}\n\n/**\n * Normalize for measuring: tabs become 3 spaces and supported ANSI/OSC/APC escape\n * sequences (CSI styling/cursor codes, OSC hyperlinks and prompt markers, APC sequences\n * like CURSOR_MARKER) are removed. An ESC that starts no complete sequence is kept.\n */\nfunction stripForWidth(str: string): string {\n\tconst clean = str.includes(\"\\t\") ? str.replace(/\\t/g, \" \") : str;\n\tlet esc = clean.indexOf(\"\\x1b\");\n\tif (esc === -1) return clean;\n\tlet stripped = \"\";\n\tlet copied = 0;\n\twhile (esc !== -1) {\n\t\tconst ansi = extractAnsiCode(clean, esc);\n\t\tif (ansi) {\n\t\t\tstripped += clean.slice(copied, esc);\n\t\t\tcopied = esc + ansi.length;\n\t\t}\n\t\tesc = clean.indexOf(\"\\x1b\", ansi ? copied : esc + 1);\n\t}\n\treturn stripped + clean.slice(copied);\n}\n\n/**\n * Width of text made only of printable ASCII (1 column), Hangul syllables and CJK unified\n * ideographs (2 columns each, UAX #11 Wide), or -1 when anything else appears. Each of these\n * code points is a grapheme cluster on its own: a Hangul syllable joins only trailing jamo\n * (U+1160..U+11FF) and an ideograph only variation selectors, both of which fall back here.\n */\nfunction simpleWidth(clean: string): number {\n\tlet width = 0;\n\tfor (let i = 0; i < clean.length; i++) {\n\t\tconst code = clean.charCodeAt(i);\n\t\tif (code >= 0x20 && code <= 0x7e) width += 1;\n\t\telse if ((code >= 0xac00 && code <= 0xd7a3) || (code >= 0x4e00 && code <= 0x9fff)) width += 2;\n\t\telse return -1;\n\t}\n\treturn width;\n}\n\n/**\n * Calculate the visible width of a string in terminal columns.\n */\nexport function visibleWidth(str: string): number {\n\tif (str.length === 0) {\n\t\treturn 0;\n\t}\n\n\t// Fast path: pure ASCII printable\n\tif (isPrintableAscii(str)) {\n\t\treturn str.length;\n\t}\n\n\t// Styled ASCII, Hangul and CJK (the bulk of rendered lines): once escapes are\n\t// stripped, every character is its own cluster, so segmentation is unnecessary.\n\tconst clean = stripForWidth(str);\n\tconst simple = simpleWidth(clean);\n\tif (simple >= 0) {\n\t\treturn simple;\n\t}\n\n\tconst cached = widthCache.get(str);\n\tif (cached !== undefined) {\n\t\treturn cached;\n\t}\n\n\tlet width = 0;\n\tfor (const { segment } of graphemeSegmenter.segment(clean)) {\n\t\twidth += graphemeWidth(segment);\n\t}\n\n\tif (str.length <= WIDTH_CACHE_MAX_KEY_LENGTH) {\n\t\tif (widthCache.size >= WIDTH_CACHE_SIZE) {\n\t\t\tconst firstKey = widthCache.keys().next().value;\n\t\t\tif (firstKey !== undefined) {\n\t\t\t\twidthCache.delete(firstKey);\n\t\t\t}\n\t\t}\n\t\twidthCache.set(str, width);\n\t}\n\n\treturn width;\n}\n"]}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { type Component, Container } from "./tui.ts";
|
|
2
|
+
export interface WindowedRenderStats {
|
|
3
|
+
/** Children rendered live this frame (live tail plus unsettled gaps). */
|
|
4
|
+
liveChildren: number;
|
|
5
|
+
/** Frozen children re-rendered because their generation moved. */
|
|
6
|
+
refreshedChildren: number;
|
|
7
|
+
/** Frozen segments whose line buffer was rebuilt this frame. */
|
|
8
|
+
refreshedSegments: number;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Container that freezes settled children above a live-line budget into
|
|
12
|
+
* bounded line segments and stops re-rendering them.
|
|
13
|
+
*
|
|
14
|
+
* - Settledness comes from `isRenderSettled()` (missing ⇒ settled). Unsettled
|
|
15
|
+
* children always render live; settled runs on both sides of them freeze.
|
|
16
|
+
* - Each frozen child remembers its `getRenderGeneration()`. When it moves,
|
|
17
|
+
* only that child is re-rendered and only its segment is rebuilt.
|
|
18
|
+
* - An unsettled frozen child drops just its segment. Width changes,
|
|
19
|
+
* `invalidate()`, `clear()` and foreign edits of `children` thaw everything.
|
|
20
|
+
* - Freezing releases the child's render caches (frozen lines are the only
|
|
21
|
+
* retained copy); a later change re-renders that one child from source.
|
|
22
|
+
*/
|
|
23
|
+
export declare class WindowedContainer extends Container {
|
|
24
|
+
private segments;
|
|
25
|
+
private frozenWidth;
|
|
26
|
+
private liveLineBudget;
|
|
27
|
+
private viewportRows?;
|
|
28
|
+
private lastLiveLineBudget;
|
|
29
|
+
private stats;
|
|
30
|
+
/** Fixed budget (tests, embedders); detaches any viewport set by `setViewportRows`. */
|
|
31
|
+
setLiveLineBudget(lines: number): void;
|
|
32
|
+
/**
|
|
33
|
+
* Follow the terminal height: budget = max(120, 2 × rows), re-read on every
|
|
34
|
+
* render so a resize applies on the next frame.
|
|
35
|
+
*/
|
|
36
|
+
setViewportRows(rows: () => number): void;
|
|
37
|
+
getLiveLineBudget(): number;
|
|
38
|
+
getFrozenChildCount(): number;
|
|
39
|
+
getFrozenLineCount(): number;
|
|
40
|
+
/** Diagnostics: number of frozen segments. */
|
|
41
|
+
getFrozenSegmentCount(): number;
|
|
42
|
+
/** Diagnostics: frozen child index ranges [from, to). */
|
|
43
|
+
getFrozenRanges(): ReadonlyArray<{
|
|
44
|
+
from: number;
|
|
45
|
+
to: number;
|
|
46
|
+
}>;
|
|
47
|
+
/** Diagnostics: what the most recent `render` had to redo. */
|
|
48
|
+
getLastRenderStats(): Readonly<WindowedRenderStats>;
|
|
49
|
+
/** Subclasses may add their own lifecycle knowledge (e.g. an agent_end backstop). */
|
|
50
|
+
protected isChildSettled(child: Component): boolean;
|
|
51
|
+
removeChild(component: Component): void;
|
|
52
|
+
clear(): void;
|
|
53
|
+
invalidate(): void;
|
|
54
|
+
/** Drop every frozen segment; the next render re-renders all children. */
|
|
55
|
+
thaw(): void;
|
|
56
|
+
render(width: number): string[];
|
|
57
|
+
/** Drop unsettled segments and refresh children whose generation moved. */
|
|
58
|
+
private reconcile;
|
|
59
|
+
private refreshSegment;
|
|
60
|
+
/** Freeze settled live children that sit above the live-line budget. */
|
|
61
|
+
private freezeAboveBudget;
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=windowed-container.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"windowed-container.d.ts","sourceRoot":"","sources":["../src/windowed-container.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,SAAS,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAgCrD,MAAM,WAAW,mBAAmB;IACnC,yEAAyE;IACzE,YAAY,EAAE,MAAM,CAAC;IACrB,kEAAkE;IAClE,iBAAiB,EAAE,MAAM,CAAC;IAC1B,gEAAgE;IAChE,iBAAiB,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;;;;;;;;GAYG;AACH,qBAAa,iBAAkB,SAAQ,SAAS;IAC/C,OAAO,CAAC,QAAQ,CAAuB;IACvC,OAAO,CAAC,WAAW,CAAM;IACzB,OAAO,CAAC,cAAc,CAAwB;IAC9C,OAAO,CAAC,YAAY,CAAC,CAAe;IACpC,OAAO,CAAC,kBAAkB,CAAwB;IAClD,OAAO,CAAC,KAAK,CAAwF;IAErG,uFAAuF;IACvF,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAGrC;IAED;;;OAGG;IACH,eAAe,CAAC,IAAI,EAAE,MAAM,MAAM,GAAG,IAAI,CAExC;IAED,iBAAiB,IAAI,MAAM,CAI1B;IAED,mBAAmB,IAAI,MAAM,CAI5B;IAED,kBAAkB,IAAI,MAAM,CAI3B;IAED,8CAA8C;IAC9C,qBAAqB,IAAI,MAAM,CAE9B;IAED,yDAAyD;IACzD,eAAe,IAAI,aAAa,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC,CAE7D;IAED,8DAA8D;IAC9D,kBAAkB,IAAI,QAAQ,CAAC,mBAAmB,CAAC,CAElD;IAED,qFAAqF;IACrF,SAAS,CAAC,cAAc,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAElD;IAEQ,WAAW,CAAC,SAAS,EAAE,SAAS,GAAG,IAAI,CAkB/C;IAEQ,KAAK,IAAI,IAAI,CAGrB;IAEQ,UAAU,IAAI,IAAI,CAG1B;IAED,0EAA0E;IAC1E,IAAI,IAAI,IAAI,CAGX;IAEQ,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAsCvC;IAED,2EAA2E;IAC3E,OAAO,CAAC,SAAS;IA4BjB,OAAO,CAAC,cAAc;IAwBtB,wEAAwE;IACxE,OAAO,CAAC,iBAAiB;CA8CzB","sourcesContent":["import { isRenderSettled, releaseRenderCache, renderGeneration } from \"./render-generation.ts\";\nimport { type Component, Container } from \"./tui.ts\";\n\n/**\n * A frozen segment never grows past this many lines (a single larger child\n * gets a segment of its own). Bounds the work of refreshing one changed child:\n * only its segment's line buffer is rebuilt, never the whole frozen history.\n */\nconst SEGMENT_MAX_LINES = 512;\n\n/**\n * Live-line budget: the bottom `max(120, 2 × terminal rows)` lines always\n * render live. Two viewports keep components that change without announcing\n * it (animations, stateful extension renderers) live wherever the user can\n * see them, also after scrolling back one screen; the 120 floor is the budget\n * when no viewport is attached and on small terminals.\n */\nconst MIN_LIVE_LINE_BUDGET = 120;\nconst LIVE_VIEWPORTS = 2;\n\n/** Contiguous settled children whose rendered lines are reused verbatim. */\ntype FrozenSegment = {\n\t/** Index in `children` of the first frozen child. */\n\tfrom: number;\n\t/** The frozen children themselves (identity-checked every frame). */\n\tchildren: Component[];\n\tlineCounts: number[];\n\t/** `getRenderGeneration()` of each child when its lines were taken. */\n\tgenerations: number[];\n\t/** Concatenation of the children's lines. */\n\tlines: string[];\n};\n\nexport interface WindowedRenderStats {\n\t/** Children rendered live this frame (live tail plus unsettled gaps). */\n\tliveChildren: number;\n\t/** Frozen children re-rendered because their generation moved. */\n\trefreshedChildren: number;\n\t/** Frozen segments whose line buffer was rebuilt this frame. */\n\trefreshedSegments: number;\n}\n\n/**\n * Container that freezes settled children above a live-line budget into\n * bounded line segments and stops re-rendering them.\n *\n * - Settledness comes from `isRenderSettled()` (missing ⇒ settled). Unsettled\n * children always render live; settled runs on both sides of them freeze.\n * - Each frozen child remembers its `getRenderGeneration()`. When it moves,\n * only that child is re-rendered and only its segment is rebuilt.\n * - An unsettled frozen child drops just its segment. Width changes,\n * `invalidate()`, `clear()` and foreign edits of `children` thaw everything.\n * - Freezing releases the child's render caches (frozen lines are the only\n * retained copy); a later change re-renders that one child from source.\n */\nexport class WindowedContainer extends Container {\n\tprivate segments: FrozenSegment[] = [];\n\tprivate frozenWidth = -1;\n\tprivate liveLineBudget = MIN_LIVE_LINE_BUDGET;\n\tprivate viewportRows?: () => number;\n\tprivate lastLiveLineBudget = MIN_LIVE_LINE_BUDGET;\n\tprivate stats: WindowedRenderStats = { liveChildren: 0, refreshedChildren: 0, refreshedSegments: 0 };\n\n\t/** Fixed budget (tests, embedders); detaches any viewport set by `setViewportRows`. */\n\tsetLiveLineBudget(lines: number): void {\n\t\tthis.viewportRows = undefined;\n\t\tthis.liveLineBudget = Math.max(1, Math.floor(lines));\n\t}\n\n\t/**\n\t * Follow the terminal height: budget = max(120, 2 × rows), re-read on every\n\t * render so a resize applies on the next frame.\n\t */\n\tsetViewportRows(rows: () => number): void {\n\t\tthis.viewportRows = rows;\n\t}\n\n\tgetLiveLineBudget(): number {\n\t\tif (!this.viewportRows) return this.liveLineBudget;\n\t\tconst rows = Math.floor(this.viewportRows());\n\t\treturn Math.max(MIN_LIVE_LINE_BUDGET, Number.isFinite(rows) ? rows * LIVE_VIEWPORTS : 0);\n\t}\n\n\tgetFrozenChildCount(): number {\n\t\tlet count = 0;\n\t\tfor (const segment of this.segments) count += segment.children.length;\n\t\treturn count;\n\t}\n\n\tgetFrozenLineCount(): number {\n\t\tlet count = 0;\n\t\tfor (const segment of this.segments) count += segment.lines.length;\n\t\treturn count;\n\t}\n\n\t/** Diagnostics: number of frozen segments. */\n\tgetFrozenSegmentCount(): number {\n\t\treturn this.segments.length;\n\t}\n\n\t/** Diagnostics: frozen child index ranges [from, to). */\n\tgetFrozenRanges(): ReadonlyArray<{ from: number; to: number }> {\n\t\treturn this.segments.map((segment) => ({ from: segment.from, to: segment.from + segment.children.length }));\n\t}\n\n\t/** Diagnostics: what the most recent `render` had to redo. */\n\tgetLastRenderStats(): Readonly<WindowedRenderStats> {\n\t\treturn this.stats;\n\t}\n\n\t/** Subclasses may add their own lifecycle knowledge (e.g. an agent_end backstop). */\n\tprotected isChildSettled(child: Component): boolean {\n\t\treturn isRenderSettled(child);\n\t}\n\n\toverride removeChild(component: Component): void {\n\t\tconst index = this.children.indexOf(component);\n\t\tif (index === -1) return;\n\t\tsuper.removeChild(component);\n\t\tfor (const segment of this.segments) {\n\t\t\tif (index < segment.from) {\n\t\t\t\tsegment.from -= 1;\n\t\t\t} else if (index < segment.from + segment.children.length) {\n\t\t\t\tconst j = index - segment.from;\n\t\t\t\tlet offset = 0;\n\t\t\t\tfor (let k = 0; k < j; k++) offset += segment.lineCounts[k];\n\t\t\t\tsegment.lines.splice(offset, segment.lineCounts[j]);\n\t\t\t\tsegment.children.splice(j, 1);\n\t\t\t\tsegment.lineCounts.splice(j, 1);\n\t\t\t\tsegment.generations.splice(j, 1);\n\t\t\t}\n\t\t}\n\t\tthis.segments = this.segments.filter((segment) => segment.children.length > 0);\n\t}\n\n\toverride clear(): void {\n\t\tthis.thaw();\n\t\tsuper.clear();\n\t}\n\n\toverride invalidate(): void {\n\t\tthis.thaw();\n\t\tsuper.invalidate();\n\t}\n\n\t/** Drop every frozen segment; the next render re-renders all children. */\n\tthaw(): void {\n\t\tthis.segments = [];\n\t\tthis.frozenWidth = -1;\n\t}\n\n\toverride render(width: number): string[] {\n\t\tconst budget = this.getLiveLineBudget();\n\t\t// A taller terminal widens the live window: thaw so rows now inside it render live.\n\t\tif (width !== this.frozenWidth || budget > this.lastLiveLineBudget) this.thaw();\n\t\tthis.lastLiveLineBudget = budget;\n\t\tthis.frozenWidth = width;\n\t\tthis.stats = { liveChildren: 0, refreshedChildren: 0, refreshedSegments: 0 };\n\t\tthis.reconcile(width);\n\n\t\tconst parts: string[][] = [];\n\t\tconst partStarts: number[] = [];\n\t\tconst live: number[] = [];\n\t\tconst liveLines: string[][] = [];\n\t\tlet segmentIndex = 0;\n\t\tfor (let i = 0; i < this.children.length; ) {\n\t\t\tconst segment = this.segments[segmentIndex];\n\t\t\tlet lines: string[];\n\t\t\tpartStarts.push(i);\n\t\t\tif (segment && segment.from === i) {\n\t\t\t\tlines = segment.lines;\n\t\t\t\ti += segment.children.length;\n\t\t\t\tsegmentIndex += 1;\n\t\t\t} else {\n\t\t\t\tlines = this.children[i].render(width);\n\t\t\t\tlive.push(i);\n\t\t\t\tliveLines.push(lines);\n\t\t\t\ti += 1;\n\t\t\t}\n\t\t\tparts.push(lines);\n\t\t}\n\t\tthis.stats.liveChildren = live.length;\n\n\t\t// Copy before freezing: freezing appends to segment line buffers. Native\n\t\t// concat is a bulk copy; spread arguments stay far below engine limits\n\t\t// because frozen rows arrive as a few hundred segment arrays.\n\t\tconst out = parts.length < 8192 ? ([] as string[]).concat(...parts) : parts.flat();\n\t\tthis.freezeAboveBudget(parts, partStarts, live, liveLines, budget);\n\t\treturn out;\n\t}\n\n\t/** Drop unsettled segments and refresh children whose generation moved. */\n\tprivate reconcile(width: number): void {\n\t\tconst kept: FrozenSegment[] = [];\n\t\tfor (const segment of this.segments) {\n\t\t\tlet dirty: number[] | undefined;\n\t\t\tlet settled = true;\n\t\t\tfor (let j = 0; j < segment.children.length; j++) {\n\t\t\t\tconst child = segment.children[j];\n\t\t\t\tif (this.children[segment.from + j] !== child) {\n\t\t\t\t\t// `children` was edited behind our back: nothing frozen can be trusted.\n\t\t\t\t\tthis.segments = [];\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (!this.isChildSettled(child)) {\n\t\t\t\t\tsettled = false;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tif (renderGeneration(child) !== segment.generations[j]) {\n\t\t\t\t\tdirty ??= [];\n\t\t\t\t\tdirty.push(j);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (!settled) continue;\n\t\t\tif (dirty) this.refreshSegment(segment, dirty, width);\n\t\t\tkept.push(segment);\n\t\t}\n\t\tthis.segments = kept;\n\t}\n\n\tprivate refreshSegment(segment: FrozenSegment, dirty: number[], width: number): void {\n\t\tconst lines: string[] = [];\n\t\tlet offset = 0;\n\t\tlet next = 0;\n\t\tfor (let j = 0; j < segment.children.length; j++) {\n\t\t\tconst count = segment.lineCounts[j];\n\t\t\tif (dirty[next] === j) {\n\t\t\t\tnext += 1;\n\t\t\t\tconst child = segment.children[j];\n\t\t\t\tconst fresh = child.render(width);\n\t\t\t\tfor (const line of fresh) lines.push(line);\n\t\t\t\tsegment.lineCounts[j] = fresh.length;\n\t\t\t\tsegment.generations[j] = renderGeneration(child);\n\t\t\t\treleaseRenderCache(child);\n\t\t\t} else {\n\t\t\t\tfor (let l = offset; l < offset + count; l++) lines.push(segment.lines[l]);\n\t\t\t}\n\t\t\toffset += count;\n\t\t}\n\t\tsegment.lines = lines;\n\t\tthis.stats.refreshedChildren += dirty.length;\n\t\tthis.stats.refreshedSegments += 1;\n\t}\n\n\t/** Freeze settled live children that sit above the live-line budget. */\n\tprivate freezeAboveBudget(\n\t\tparts: string[][],\n\t\tpartStarts: number[],\n\t\tlive: number[],\n\t\tliveLines: string[][],\n\t\tbudget: number,\n\t): void {\n\t\tlet acc = 0;\n\t\tlet liveStart = this.children.length;\n\t\tfor (let p = parts.length - 1; p >= 0; p--) {\n\t\t\tacc += parts[p].length;\n\t\t\tliveStart = partStarts[p];\n\t\t\tif (acc >= budget) break;\n\t\t}\n\t\tlet segmentIndex = 0;\n\t\tfor (let n = 0; n < live.length && live[n] < liveStart; n++) {\n\t\t\tconst index = live[n];\n\t\t\tconst child = this.children[index];\n\t\t\tif (!this.isChildSettled(child)) continue;\n\t\t\tconst lines = liveLines[n];\n\t\t\twhile (segmentIndex < this.segments.length && this.segments[segmentIndex].from < index) segmentIndex += 1;\n\t\t\tconst previous = this.segments[segmentIndex - 1];\n\t\t\tconst generation = renderGeneration(child);\n\t\t\tif (\n\t\t\t\tprevious &&\n\t\t\t\tprevious.from + previous.children.length === index &&\n\t\t\t\tprevious.lines.length + lines.length <= SEGMENT_MAX_LINES\n\t\t\t) {\n\t\t\t\tprevious.children.push(child);\n\t\t\t\tprevious.lineCounts.push(lines.length);\n\t\t\t\tprevious.generations.push(generation);\n\t\t\t\tfor (const line of lines) previous.lines.push(line);\n\t\t\t} else {\n\t\t\t\tconst segment: FrozenSegment = {\n\t\t\t\t\tfrom: index,\n\t\t\t\t\tchildren: [child],\n\t\t\t\t\tlineCounts: [lines.length],\n\t\t\t\t\tgenerations: [generation],\n\t\t\t\t\tlines: lines.slice(),\n\t\t\t\t};\n\t\t\t\tthis.segments.splice(segmentIndex, 0, segment);\n\t\t\t\tsegmentIndex += 1;\n\t\t\t}\n\t\t\treleaseRenderCache(child);\n\t\t}\n\t}\n}\n"]}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { isRenderSettled, releaseRenderCache, renderGeneration } from "./render-generation.js";
|
|
2
|
+
import { Container } from "./tui.js";
|
|
3
|
+
/**
|
|
4
|
+
* A frozen segment never grows past this many lines (a single larger child
|
|
5
|
+
* gets a segment of its own). Bounds the work of refreshing one changed child:
|
|
6
|
+
* only its segment's line buffer is rebuilt, never the whole frozen history.
|
|
7
|
+
*/
|
|
8
|
+
const SEGMENT_MAX_LINES = 512;
|
|
9
|
+
/**
|
|
10
|
+
* Live-line budget: the bottom `max(120, 2 × terminal rows)` lines always
|
|
11
|
+
* render live. Two viewports keep components that change without announcing
|
|
12
|
+
* it (animations, stateful extension renderers) live wherever the user can
|
|
13
|
+
* see them, also after scrolling back one screen; the 120 floor is the budget
|
|
14
|
+
* when no viewport is attached and on small terminals.
|
|
15
|
+
*/
|
|
16
|
+
const MIN_LIVE_LINE_BUDGET = 120;
|
|
17
|
+
const LIVE_VIEWPORTS = 2;
|
|
18
|
+
/**
|
|
19
|
+
* Container that freezes settled children above a live-line budget into
|
|
20
|
+
* bounded line segments and stops re-rendering them.
|
|
21
|
+
*
|
|
22
|
+
* - Settledness comes from `isRenderSettled()` (missing ⇒ settled). Unsettled
|
|
23
|
+
* children always render live; settled runs on both sides of them freeze.
|
|
24
|
+
* - Each frozen child remembers its `getRenderGeneration()`. When it moves,
|
|
25
|
+
* only that child is re-rendered and only its segment is rebuilt.
|
|
26
|
+
* - An unsettled frozen child drops just its segment. Width changes,
|
|
27
|
+
* `invalidate()`, `clear()` and foreign edits of `children` thaw everything.
|
|
28
|
+
* - Freezing releases the child's render caches (frozen lines are the only
|
|
29
|
+
* retained copy); a later change re-renders that one child from source.
|
|
30
|
+
*/
|
|
31
|
+
export class WindowedContainer extends Container {
|
|
32
|
+
segments = [];
|
|
33
|
+
frozenWidth = -1;
|
|
34
|
+
liveLineBudget = MIN_LIVE_LINE_BUDGET;
|
|
35
|
+
viewportRows;
|
|
36
|
+
lastLiveLineBudget = MIN_LIVE_LINE_BUDGET;
|
|
37
|
+
stats = { liveChildren: 0, refreshedChildren: 0, refreshedSegments: 0 };
|
|
38
|
+
/** Fixed budget (tests, embedders); detaches any viewport set by `setViewportRows`. */
|
|
39
|
+
setLiveLineBudget(lines) {
|
|
40
|
+
this.viewportRows = undefined;
|
|
41
|
+
this.liveLineBudget = Math.max(1, Math.floor(lines));
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Follow the terminal height: budget = max(120, 2 × rows), re-read on every
|
|
45
|
+
* render so a resize applies on the next frame.
|
|
46
|
+
*/
|
|
47
|
+
setViewportRows(rows) {
|
|
48
|
+
this.viewportRows = rows;
|
|
49
|
+
}
|
|
50
|
+
getLiveLineBudget() {
|
|
51
|
+
if (!this.viewportRows)
|
|
52
|
+
return this.liveLineBudget;
|
|
53
|
+
const rows = Math.floor(this.viewportRows());
|
|
54
|
+
return Math.max(MIN_LIVE_LINE_BUDGET, Number.isFinite(rows) ? rows * LIVE_VIEWPORTS : 0);
|
|
55
|
+
}
|
|
56
|
+
getFrozenChildCount() {
|
|
57
|
+
let count = 0;
|
|
58
|
+
for (const segment of this.segments)
|
|
59
|
+
count += segment.children.length;
|
|
60
|
+
return count;
|
|
61
|
+
}
|
|
62
|
+
getFrozenLineCount() {
|
|
63
|
+
let count = 0;
|
|
64
|
+
for (const segment of this.segments)
|
|
65
|
+
count += segment.lines.length;
|
|
66
|
+
return count;
|
|
67
|
+
}
|
|
68
|
+
/** Diagnostics: number of frozen segments. */
|
|
69
|
+
getFrozenSegmentCount() {
|
|
70
|
+
return this.segments.length;
|
|
71
|
+
}
|
|
72
|
+
/** Diagnostics: frozen child index ranges [from, to). */
|
|
73
|
+
getFrozenRanges() {
|
|
74
|
+
return this.segments.map((segment) => ({ from: segment.from, to: segment.from + segment.children.length }));
|
|
75
|
+
}
|
|
76
|
+
/** Diagnostics: what the most recent `render` had to redo. */
|
|
77
|
+
getLastRenderStats() {
|
|
78
|
+
return this.stats;
|
|
79
|
+
}
|
|
80
|
+
/** Subclasses may add their own lifecycle knowledge (e.g. an agent_end backstop). */
|
|
81
|
+
isChildSettled(child) {
|
|
82
|
+
return isRenderSettled(child);
|
|
83
|
+
}
|
|
84
|
+
removeChild(component) {
|
|
85
|
+
const index = this.children.indexOf(component);
|
|
86
|
+
if (index === -1)
|
|
87
|
+
return;
|
|
88
|
+
super.removeChild(component);
|
|
89
|
+
for (const segment of this.segments) {
|
|
90
|
+
if (index < segment.from) {
|
|
91
|
+
segment.from -= 1;
|
|
92
|
+
}
|
|
93
|
+
else if (index < segment.from + segment.children.length) {
|
|
94
|
+
const j = index - segment.from;
|
|
95
|
+
let offset = 0;
|
|
96
|
+
for (let k = 0; k < j; k++)
|
|
97
|
+
offset += segment.lineCounts[k];
|
|
98
|
+
segment.lines.splice(offset, segment.lineCounts[j]);
|
|
99
|
+
segment.children.splice(j, 1);
|
|
100
|
+
segment.lineCounts.splice(j, 1);
|
|
101
|
+
segment.generations.splice(j, 1);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
this.segments = this.segments.filter((segment) => segment.children.length > 0);
|
|
105
|
+
}
|
|
106
|
+
clear() {
|
|
107
|
+
this.thaw();
|
|
108
|
+
super.clear();
|
|
109
|
+
}
|
|
110
|
+
invalidate() {
|
|
111
|
+
this.thaw();
|
|
112
|
+
super.invalidate();
|
|
113
|
+
}
|
|
114
|
+
/** Drop every frozen segment; the next render re-renders all children. */
|
|
115
|
+
thaw() {
|
|
116
|
+
this.segments = [];
|
|
117
|
+
this.frozenWidth = -1;
|
|
118
|
+
}
|
|
119
|
+
render(width) {
|
|
120
|
+
const budget = this.getLiveLineBudget();
|
|
121
|
+
// A taller terminal widens the live window: thaw so rows now inside it render live.
|
|
122
|
+
if (width !== this.frozenWidth || budget > this.lastLiveLineBudget)
|
|
123
|
+
this.thaw();
|
|
124
|
+
this.lastLiveLineBudget = budget;
|
|
125
|
+
this.frozenWidth = width;
|
|
126
|
+
this.stats = { liveChildren: 0, refreshedChildren: 0, refreshedSegments: 0 };
|
|
127
|
+
this.reconcile(width);
|
|
128
|
+
const parts = [];
|
|
129
|
+
const partStarts = [];
|
|
130
|
+
const live = [];
|
|
131
|
+
const liveLines = [];
|
|
132
|
+
let segmentIndex = 0;
|
|
133
|
+
for (let i = 0; i < this.children.length;) {
|
|
134
|
+
const segment = this.segments[segmentIndex];
|
|
135
|
+
let lines;
|
|
136
|
+
partStarts.push(i);
|
|
137
|
+
if (segment && segment.from === i) {
|
|
138
|
+
lines = segment.lines;
|
|
139
|
+
i += segment.children.length;
|
|
140
|
+
segmentIndex += 1;
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
lines = this.children[i].render(width);
|
|
144
|
+
live.push(i);
|
|
145
|
+
liveLines.push(lines);
|
|
146
|
+
i += 1;
|
|
147
|
+
}
|
|
148
|
+
parts.push(lines);
|
|
149
|
+
}
|
|
150
|
+
this.stats.liveChildren = live.length;
|
|
151
|
+
// Copy before freezing: freezing appends to segment line buffers. Native
|
|
152
|
+
// concat is a bulk copy; spread arguments stay far below engine limits
|
|
153
|
+
// because frozen rows arrive as a few hundred segment arrays.
|
|
154
|
+
const out = parts.length < 8192 ? [].concat(...parts) : parts.flat();
|
|
155
|
+
this.freezeAboveBudget(parts, partStarts, live, liveLines, budget);
|
|
156
|
+
return out;
|
|
157
|
+
}
|
|
158
|
+
/** Drop unsettled segments and refresh children whose generation moved. */
|
|
159
|
+
reconcile(width) {
|
|
160
|
+
const kept = [];
|
|
161
|
+
for (const segment of this.segments) {
|
|
162
|
+
let dirty;
|
|
163
|
+
let settled = true;
|
|
164
|
+
for (let j = 0; j < segment.children.length; j++) {
|
|
165
|
+
const child = segment.children[j];
|
|
166
|
+
if (this.children[segment.from + j] !== child) {
|
|
167
|
+
// `children` was edited behind our back: nothing frozen can be trusted.
|
|
168
|
+
this.segments = [];
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (!this.isChildSettled(child)) {
|
|
172
|
+
settled = false;
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
if (renderGeneration(child) !== segment.generations[j]) {
|
|
176
|
+
dirty ??= [];
|
|
177
|
+
dirty.push(j);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (!settled)
|
|
181
|
+
continue;
|
|
182
|
+
if (dirty)
|
|
183
|
+
this.refreshSegment(segment, dirty, width);
|
|
184
|
+
kept.push(segment);
|
|
185
|
+
}
|
|
186
|
+
this.segments = kept;
|
|
187
|
+
}
|
|
188
|
+
refreshSegment(segment, dirty, width) {
|
|
189
|
+
const lines = [];
|
|
190
|
+
let offset = 0;
|
|
191
|
+
let next = 0;
|
|
192
|
+
for (let j = 0; j < segment.children.length; j++) {
|
|
193
|
+
const count = segment.lineCounts[j];
|
|
194
|
+
if (dirty[next] === j) {
|
|
195
|
+
next += 1;
|
|
196
|
+
const child = segment.children[j];
|
|
197
|
+
const fresh = child.render(width);
|
|
198
|
+
for (const line of fresh)
|
|
199
|
+
lines.push(line);
|
|
200
|
+
segment.lineCounts[j] = fresh.length;
|
|
201
|
+
segment.generations[j] = renderGeneration(child);
|
|
202
|
+
releaseRenderCache(child);
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
for (let l = offset; l < offset + count; l++)
|
|
206
|
+
lines.push(segment.lines[l]);
|
|
207
|
+
}
|
|
208
|
+
offset += count;
|
|
209
|
+
}
|
|
210
|
+
segment.lines = lines;
|
|
211
|
+
this.stats.refreshedChildren += dirty.length;
|
|
212
|
+
this.stats.refreshedSegments += 1;
|
|
213
|
+
}
|
|
214
|
+
/** Freeze settled live children that sit above the live-line budget. */
|
|
215
|
+
freezeAboveBudget(parts, partStarts, live, liveLines, budget) {
|
|
216
|
+
let acc = 0;
|
|
217
|
+
let liveStart = this.children.length;
|
|
218
|
+
for (let p = parts.length - 1; p >= 0; p--) {
|
|
219
|
+
acc += parts[p].length;
|
|
220
|
+
liveStart = partStarts[p];
|
|
221
|
+
if (acc >= budget)
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
let segmentIndex = 0;
|
|
225
|
+
for (let n = 0; n < live.length && live[n] < liveStart; n++) {
|
|
226
|
+
const index = live[n];
|
|
227
|
+
const child = this.children[index];
|
|
228
|
+
if (!this.isChildSettled(child))
|
|
229
|
+
continue;
|
|
230
|
+
const lines = liveLines[n];
|
|
231
|
+
while (segmentIndex < this.segments.length && this.segments[segmentIndex].from < index)
|
|
232
|
+
segmentIndex += 1;
|
|
233
|
+
const previous = this.segments[segmentIndex - 1];
|
|
234
|
+
const generation = renderGeneration(child);
|
|
235
|
+
if (previous &&
|
|
236
|
+
previous.from + previous.children.length === index &&
|
|
237
|
+
previous.lines.length + lines.length <= SEGMENT_MAX_LINES) {
|
|
238
|
+
previous.children.push(child);
|
|
239
|
+
previous.lineCounts.push(lines.length);
|
|
240
|
+
previous.generations.push(generation);
|
|
241
|
+
for (const line of lines)
|
|
242
|
+
previous.lines.push(line);
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
const segment = {
|
|
246
|
+
from: index,
|
|
247
|
+
children: [child],
|
|
248
|
+
lineCounts: [lines.length],
|
|
249
|
+
generations: [generation],
|
|
250
|
+
lines: lines.slice(),
|
|
251
|
+
};
|
|
252
|
+
this.segments.splice(segmentIndex, 0, segment);
|
|
253
|
+
segmentIndex += 1;
|
|
254
|
+
}
|
|
255
|
+
releaseRenderCache(child);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
//# sourceMappingURL=windowed-container.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"windowed-container.js","sourceRoot":"","sources":["../src/windowed-container.ts"],"names":[],"mappings":"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","sourcesContent":["import { isRenderSettled, releaseRenderCache, renderGeneration } from \"./render-generation.ts\";\nimport { type Component, Container } from \"./tui.ts\";\n\n/**\n * A frozen segment never grows past this many lines (a single larger child\n * gets a segment of its own). Bounds the work of refreshing one changed child:\n * only its segment's line buffer is rebuilt, never the whole frozen history.\n */\nconst SEGMENT_MAX_LINES = 512;\n\n/**\n * Live-line budget: the bottom `max(120, 2 × terminal rows)` lines always\n * render live. Two viewports keep components that change without announcing\n * it (animations, stateful extension renderers) live wherever the user can\n * see them, also after scrolling back one screen; the 120 floor is the budget\n * when no viewport is attached and on small terminals.\n */\nconst MIN_LIVE_LINE_BUDGET = 120;\nconst LIVE_VIEWPORTS = 2;\n\n/** Contiguous settled children whose rendered lines are reused verbatim. */\ntype FrozenSegment = {\n\t/** Index in `children` of the first frozen child. */\n\tfrom: number;\n\t/** The frozen children themselves (identity-checked every frame). */\n\tchildren: Component[];\n\tlineCounts: number[];\n\t/** `getRenderGeneration()` of each child when its lines were taken. */\n\tgenerations: number[];\n\t/** Concatenation of the children's lines. */\n\tlines: string[];\n};\n\nexport interface WindowedRenderStats {\n\t/** Children rendered live this frame (live tail plus unsettled gaps). */\n\tliveChildren: number;\n\t/** Frozen children re-rendered because their generation moved. */\n\trefreshedChildren: number;\n\t/** Frozen segments whose line buffer was rebuilt this frame. */\n\trefreshedSegments: number;\n}\n\n/**\n * Container that freezes settled children above a live-line budget into\n * bounded line segments and stops re-rendering them.\n *\n * - Settledness comes from `isRenderSettled()` (missing ⇒ settled). Unsettled\n * children always render live; settled runs on both sides of them freeze.\n * - Each frozen child remembers its `getRenderGeneration()`. When it moves,\n * only that child is re-rendered and only its segment is rebuilt.\n * - An unsettled frozen child drops just its segment. Width changes,\n * `invalidate()`, `clear()` and foreign edits of `children` thaw everything.\n * - Freezing releases the child's render caches (frozen lines are the only\n * retained copy); a later change re-renders that one child from source.\n */\nexport class WindowedContainer extends Container {\n\tprivate segments: FrozenSegment[] = [];\n\tprivate frozenWidth = -1;\n\tprivate liveLineBudget = MIN_LIVE_LINE_BUDGET;\n\tprivate viewportRows?: () => number;\n\tprivate lastLiveLineBudget = MIN_LIVE_LINE_BUDGET;\n\tprivate stats: WindowedRenderStats = { liveChildren: 0, refreshedChildren: 0, refreshedSegments: 0 };\n\n\t/** Fixed budget (tests, embedders); detaches any viewport set by `setViewportRows`. */\n\tsetLiveLineBudget(lines: number): void {\n\t\tthis.viewportRows = undefined;\n\t\tthis.liveLineBudget = Math.max(1, Math.floor(lines));\n\t}\n\n\t/**\n\t * Follow the terminal height: budget = max(120, 2 × rows), re-read on every\n\t * render so a resize applies on the next frame.\n\t */\n\tsetViewportRows(rows: () => number): void {\n\t\tthis.viewportRows = rows;\n\t}\n\n\tgetLiveLineBudget(): number {\n\t\tif (!this.viewportRows) return this.liveLineBudget;\n\t\tconst rows = Math.floor(this.viewportRows());\n\t\treturn Math.max(MIN_LIVE_LINE_BUDGET, Number.isFinite(rows) ? rows * LIVE_VIEWPORTS : 0);\n\t}\n\n\tgetFrozenChildCount(): number {\n\t\tlet count = 0;\n\t\tfor (const segment of this.segments) count += segment.children.length;\n\t\treturn count;\n\t}\n\n\tgetFrozenLineCount(): number {\n\t\tlet count = 0;\n\t\tfor (const segment of this.segments) count += segment.lines.length;\n\t\treturn count;\n\t}\n\n\t/** Diagnostics: number of frozen segments. */\n\tgetFrozenSegmentCount(): number {\n\t\treturn this.segments.length;\n\t}\n\n\t/** Diagnostics: frozen child index ranges [from, to). */\n\tgetFrozenRanges(): ReadonlyArray<{ from: number; to: number }> {\n\t\treturn this.segments.map((segment) => ({ from: segment.from, to: segment.from + segment.children.length }));\n\t}\n\n\t/** Diagnostics: what the most recent `render` had to redo. */\n\tgetLastRenderStats(): Readonly<WindowedRenderStats> {\n\t\treturn this.stats;\n\t}\n\n\t/** Subclasses may add their own lifecycle knowledge (e.g. an agent_end backstop). */\n\tprotected isChildSettled(child: Component): boolean {\n\t\treturn isRenderSettled(child);\n\t}\n\n\toverride removeChild(component: Component): void {\n\t\tconst index = this.children.indexOf(component);\n\t\tif (index === -1) return;\n\t\tsuper.removeChild(component);\n\t\tfor (const segment of this.segments) {\n\t\t\tif (index < segment.from) {\n\t\t\t\tsegment.from -= 1;\n\t\t\t} else if (index < segment.from + segment.children.length) {\n\t\t\t\tconst j = index - segment.from;\n\t\t\t\tlet offset = 0;\n\t\t\t\tfor (let k = 0; k < j; k++) offset += segment.lineCounts[k];\n\t\t\t\tsegment.lines.splice(offset, segment.lineCounts[j]);\n\t\t\t\tsegment.children.splice(j, 1);\n\t\t\t\tsegment.lineCounts.splice(j, 1);\n\t\t\t\tsegment.generations.splice(j, 1);\n\t\t\t}\n\t\t}\n\t\tthis.segments = this.segments.filter((segment) => segment.children.length > 0);\n\t}\n\n\toverride clear(): void {\n\t\tthis.thaw();\n\t\tsuper.clear();\n\t}\n\n\toverride invalidate(): void {\n\t\tthis.thaw();\n\t\tsuper.invalidate();\n\t}\n\n\t/** Drop every frozen segment; the next render re-renders all children. */\n\tthaw(): void {\n\t\tthis.segments = [];\n\t\tthis.frozenWidth = -1;\n\t}\n\n\toverride render(width: number): string[] {\n\t\tconst budget = this.getLiveLineBudget();\n\t\t// A taller terminal widens the live window: thaw so rows now inside it render live.\n\t\tif (width !== this.frozenWidth || budget > this.lastLiveLineBudget) this.thaw();\n\t\tthis.lastLiveLineBudget = budget;\n\t\tthis.frozenWidth = width;\n\t\tthis.stats = { liveChildren: 0, refreshedChildren: 0, refreshedSegments: 0 };\n\t\tthis.reconcile(width);\n\n\t\tconst parts: string[][] = [];\n\t\tconst partStarts: number[] = [];\n\t\tconst live: number[] = [];\n\t\tconst liveLines: string[][] = [];\n\t\tlet segmentIndex = 0;\n\t\tfor (let i = 0; i < this.children.length; ) {\n\t\t\tconst segment = this.segments[segmentIndex];\n\t\t\tlet lines: string[];\n\t\t\tpartStarts.push(i);\n\t\t\tif (segment && segment.from === i) {\n\t\t\t\tlines = segment.lines;\n\t\t\t\ti += segment.children.length;\n\t\t\t\tsegmentIndex += 1;\n\t\t\t} else {\n\t\t\t\tlines = this.children[i].render(width);\n\t\t\t\tlive.push(i);\n\t\t\t\tliveLines.push(lines);\n\t\t\t\ti += 1;\n\t\t\t}\n\t\t\tparts.push(lines);\n\t\t}\n\t\tthis.stats.liveChildren = live.length;\n\n\t\t// Copy before freezing: freezing appends to segment line buffers. Native\n\t\t// concat is a bulk copy; spread arguments stay far below engine limits\n\t\t// because frozen rows arrive as a few hundred segment arrays.\n\t\tconst out = parts.length < 8192 ? ([] as string[]).concat(...parts) : parts.flat();\n\t\tthis.freezeAboveBudget(parts, partStarts, live, liveLines, budget);\n\t\treturn out;\n\t}\n\n\t/** Drop unsettled segments and refresh children whose generation moved. */\n\tprivate reconcile(width: number): void {\n\t\tconst kept: FrozenSegment[] = [];\n\t\tfor (const segment of this.segments) {\n\t\t\tlet dirty: number[] | undefined;\n\t\t\tlet settled = true;\n\t\t\tfor (let j = 0; j < segment.children.length; j++) {\n\t\t\t\tconst child = segment.children[j];\n\t\t\t\tif (this.children[segment.from + j] !== child) {\n\t\t\t\t\t// `children` was edited behind our back: nothing frozen can be trusted.\n\t\t\t\t\tthis.segments = [];\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (!this.isChildSettled(child)) {\n\t\t\t\t\tsettled = false;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tif (renderGeneration(child) !== segment.generations[j]) {\n\t\t\t\t\tdirty ??= [];\n\t\t\t\t\tdirty.push(j);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (!settled) continue;\n\t\t\tif (dirty) this.refreshSegment(segment, dirty, width);\n\t\t\tkept.push(segment);\n\t\t}\n\t\tthis.segments = kept;\n\t}\n\n\tprivate refreshSegment(segment: FrozenSegment, dirty: number[], width: number): void {\n\t\tconst lines: string[] = [];\n\t\tlet offset = 0;\n\t\tlet next = 0;\n\t\tfor (let j = 0; j < segment.children.length; j++) {\n\t\t\tconst count = segment.lineCounts[j];\n\t\t\tif (dirty[next] === j) {\n\t\t\t\tnext += 1;\n\t\t\t\tconst child = segment.children[j];\n\t\t\t\tconst fresh = child.render(width);\n\t\t\t\tfor (const line of fresh) lines.push(line);\n\t\t\t\tsegment.lineCounts[j] = fresh.length;\n\t\t\t\tsegment.generations[j] = renderGeneration(child);\n\t\t\t\treleaseRenderCache(child);\n\t\t\t} else {\n\t\t\t\tfor (let l = offset; l < offset + count; l++) lines.push(segment.lines[l]);\n\t\t\t}\n\t\t\toffset += count;\n\t\t}\n\t\tsegment.lines = lines;\n\t\tthis.stats.refreshedChildren += dirty.length;\n\t\tthis.stats.refreshedSegments += 1;\n\t}\n\n\t/** Freeze settled live children that sit above the live-line budget. */\n\tprivate freezeAboveBudget(\n\t\tparts: string[][],\n\t\tpartStarts: number[],\n\t\tlive: number[],\n\t\tliveLines: string[][],\n\t\tbudget: number,\n\t): void {\n\t\tlet acc = 0;\n\t\tlet liveStart = this.children.length;\n\t\tfor (let p = parts.length - 1; p >= 0; p--) {\n\t\t\tacc += parts[p].length;\n\t\t\tliveStart = partStarts[p];\n\t\t\tif (acc >= budget) break;\n\t\t}\n\t\tlet segmentIndex = 0;\n\t\tfor (let n = 0; n < live.length && live[n] < liveStart; n++) {\n\t\t\tconst index = live[n];\n\t\t\tconst child = this.children[index];\n\t\t\tif (!this.isChildSettled(child)) continue;\n\t\t\tconst lines = liveLines[n];\n\t\t\twhile (segmentIndex < this.segments.length && this.segments[segmentIndex].from < index) segmentIndex += 1;\n\t\t\tconst previous = this.segments[segmentIndex - 1];\n\t\t\tconst generation = renderGeneration(child);\n\t\t\tif (\n\t\t\t\tprevious &&\n\t\t\t\tprevious.from + previous.children.length === index &&\n\t\t\t\tprevious.lines.length + lines.length <= SEGMENT_MAX_LINES\n\t\t\t) {\n\t\t\t\tprevious.children.push(child);\n\t\t\t\tprevious.lineCounts.push(lines.length);\n\t\t\t\tprevious.generations.push(generation);\n\t\t\t\tfor (const line of lines) previous.lines.push(line);\n\t\t\t} else {\n\t\t\t\tconst segment: FrozenSegment = {\n\t\t\t\t\tfrom: index,\n\t\t\t\t\tchildren: [child],\n\t\t\t\t\tlineCounts: [lines.length],\n\t\t\t\t\tgenerations: [generation],\n\t\t\t\t\tlines: lines.slice(),\n\t\t\t\t};\n\t\t\t\tthis.segments.splice(segmentIndex, 0, segment);\n\t\t\t\tsegmentIndex += 1;\n\t\t\t}\n\t\t\treleaseRenderCache(child);\n\t\t}\n\t}\n}\n"]}
|