@gajae-code/tui 0.17.1 → 0.17.4
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
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.17.4] - 2026-09-23
|
|
6
|
+
|
|
7
|
+
## [0.17.3] - 2026-09-22
|
|
8
|
+
|
|
9
|
+
### Performance
|
|
10
|
+
|
|
11
|
+
- Avoid unsupported Kitty placement extraction on ordinary non-Kitty input frames.
|
|
12
|
+
- Reuse unchanged editor logical-line layouts, including keyboard shrink/join paths that previously retained deleted-line cache entries.
|
|
13
|
+
- Reuse the byte/line admission decision for exact cached Markdown highlights instead of rescanning unchanged fenced code.
|
|
14
|
+
- Add a native-highlight input-to-synchronized-write benchmark with same-frame visibility checks. Scheduling, preparation, force precedence, and output revisions are unchanged.
|
|
15
|
+
|
|
16
|
+
- Avoid allocating fallback arrays while attributing Kitty placements during frame assembly. Emitted terminal bytes are unchanged.
|
|
17
|
+
|
|
18
|
+
- Hold the raw frame by reference instead of copying every transcript line during frame assembly. Emitted terminal bytes are unchanged.
|
|
19
|
+
|
|
20
|
+
## [0.17.2] - 2026-09-18
|
|
21
|
+
|
|
5
22
|
## [0.17.1] - 2026-09-17
|
|
6
23
|
|
|
7
24
|
## [0.17.0] - 2026-09-17
|
|
@@ -98,6 +98,8 @@ export declare class Editor implements Component, Focusable {
|
|
|
98
98
|
invalidate(): void;
|
|
99
99
|
render(width: number): string[];
|
|
100
100
|
handleInput(data: string): void;
|
|
101
|
+
/** Test-only seam: retained per-logical-line layout entries. */
|
|
102
|
+
get logicalLayoutCacheSize(): number;
|
|
101
103
|
/** Test-only seam: current wrap-cache entry count (memory-bound assertions). */
|
|
102
104
|
get wrappedLineCacheSize(): number;
|
|
103
105
|
getText(): string;
|
|
@@ -21,6 +21,7 @@ export declare function resetMarkdownHighlightCallCount(): void;
|
|
|
21
21
|
export declare const __markdownPerfCounters: {
|
|
22
22
|
lexerInvocations: number;
|
|
23
23
|
lexedBytes: number;
|
|
24
|
+
highlightAdmissionScans: number;
|
|
24
25
|
reset(): void;
|
|
25
26
|
};
|
|
26
27
|
/** Drop all L2 cache entries. Call on theme change to prevent stale styled output. */
|
package/dist/types/tui.d.ts
CHANGED
|
@@ -364,6 +364,7 @@ type TuiRenderCounterSnapshot = {
|
|
|
364
364
|
widthReflowScanRows: number;
|
|
365
365
|
widthReflowVisibleWidthCalls: number;
|
|
366
366
|
kittyPlacementScanRows: number;
|
|
367
|
+
kittyPlacementReferenceRows: number;
|
|
367
368
|
};
|
|
368
369
|
/**
|
|
369
370
|
* TUI - Main class for managing terminal UI with differential rendering
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@gajae-code/tui",
|
|
4
|
-
"version": "0.17.
|
|
4
|
+
"version": "0.17.4",
|
|
5
5
|
"description": "Terminal User Interface library with differential rendering for efficient text-based applications",
|
|
6
6
|
"homepage": "https://gajae-code.com",
|
|
7
7
|
"author": "Yeachan-Heo and Gajae Code Contributors",
|
|
@@ -36,8 +36,8 @@
|
|
|
36
36
|
"fmt": "biome format --write ."
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@gajae-code/natives": "0.17.
|
|
40
|
-
"@gajae-code/utils": "0.17.
|
|
39
|
+
"@gajae-code/natives": "0.17.4",
|
|
40
|
+
"@gajae-code/utils": "0.17.4",
|
|
41
41
|
"lru-cache": "11.3.6",
|
|
42
42
|
"marked": "18.0.6"
|
|
43
43
|
},
|
package/src/components/editor.ts
CHANGED
|
@@ -443,6 +443,7 @@ export class Editor implements Component, Focusable {
|
|
|
443
443
|
#wrappedLineCache: CachedWrappedLine[] = [];
|
|
444
444
|
#docVersion = 0;
|
|
445
445
|
#layoutCache: LayoutCache | undefined;
|
|
446
|
+
#logicalLayoutCache: Array<{ text: string; width: number; cursorCol: number | undefined; lines: LayoutLine[] }> = [];
|
|
446
447
|
|
|
447
448
|
// Emacs-style kill ring
|
|
448
449
|
#killRing = new KillRing();
|
|
@@ -754,11 +755,20 @@ export class Editor implements Component, Focusable {
|
|
|
754
755
|
}
|
|
755
756
|
|
|
756
757
|
invalidate(): void {
|
|
758
|
+
this.#logicalLayoutCache.length = 0;
|
|
757
759
|
this.#wrappedLineCache.length = 0;
|
|
758
760
|
this.#layoutCache = undefined;
|
|
759
761
|
}
|
|
760
762
|
|
|
763
|
+
#trimLogicalLayoutCache(): void {
|
|
764
|
+
// Retain at most one layout per current logical line, never deleted tails.
|
|
765
|
+
if (this.#logicalLayoutCache.length > this.#state.lines.length) {
|
|
766
|
+
this.#logicalLayoutCache.length = this.#state.lines.length;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
761
770
|
#bumpDocumentVersion(): void {
|
|
771
|
+
this.#trimLogicalLayoutCache();
|
|
762
772
|
this.#docVersion += 1;
|
|
763
773
|
this.#layoutCache = undefined;
|
|
764
774
|
}
|
|
@@ -1163,6 +1173,16 @@ export class Editor implements Component, Focusable {
|
|
|
1163
1173
|
}
|
|
1164
1174
|
|
|
1165
1175
|
handleInput(data: string): void {
|
|
1176
|
+
try {
|
|
1177
|
+
this.#handleInput(data);
|
|
1178
|
+
} finally {
|
|
1179
|
+
// Undo snapshots invalidate before mutation; trim after all input paths,
|
|
1180
|
+
// including early returns and nested bracketed-paste handling.
|
|
1181
|
+
this.#trimLogicalLayoutCache();
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
#handleInput(data: string): void {
|
|
1166
1186
|
const kb = getKeybindings();
|
|
1167
1187
|
|
|
1168
1188
|
// Handle character jump mode (awaiting next character to jump to)
|
|
@@ -1585,6 +1605,11 @@ export class Editor implements Component, Focusable {
|
|
|
1585
1605
|
return wrapped;
|
|
1586
1606
|
}
|
|
1587
1607
|
|
|
1608
|
+
/** Test-only seam: retained per-logical-line layout entries. */
|
|
1609
|
+
get logicalLayoutCacheSize(): number {
|
|
1610
|
+
return this.#logicalLayoutCache.length;
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1588
1613
|
/** Test-only seam: current wrap-cache entry count (memory-bound assertions). */
|
|
1589
1614
|
get wrappedLineCacheSize(): number {
|
|
1590
1615
|
return this.#wrappedLineCache.length;
|
|
@@ -1624,6 +1649,16 @@ export class Editor implements Component, Focusable {
|
|
|
1624
1649
|
}
|
|
1625
1650
|
|
|
1626
1651
|
#layoutLogicalLine(lineIndex: number, contentWidth: number): LayoutLine[] {
|
|
1652
|
+
const text = this.#state.lines[lineIndex] || "";
|
|
1653
|
+
const cursorCol = lineIndex === this.#state.cursorLine ? this.#state.cursorCol : undefined;
|
|
1654
|
+
const cached = this.#logicalLayoutCache[lineIndex];
|
|
1655
|
+
if (cached?.text === text && cached.width === contentWidth && cached.cursorCol === cursorCol) return cached.lines;
|
|
1656
|
+
const lines = this.#buildLogicalLine(lineIndex, contentWidth);
|
|
1657
|
+
this.#logicalLayoutCache[lineIndex] = { text, width: contentWidth, cursorCol, lines };
|
|
1658
|
+
return lines;
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
#buildLogicalLine(lineIndex: number, contentWidth: number): LayoutLine[] {
|
|
1627
1662
|
__editorPerfCounters.layoutLogicalLinesProcessed += 1;
|
|
1628
1663
|
const line = this.#state.lines[lineIndex] || "";
|
|
1629
1664
|
const isCurrentLine = lineIndex === this.#state.cursorLine;
|
|
@@ -1722,6 +1757,7 @@ export class Editor implements Component, Focusable {
|
|
|
1722
1757
|
}
|
|
1723
1758
|
|
|
1724
1759
|
#layoutText(contentWidth: number): LayoutLine[] {
|
|
1760
|
+
this.#trimLogicalLayoutCache();
|
|
1725
1761
|
__editorPerfCounters.layoutTextInvocations += 1;
|
|
1726
1762
|
const key = this.#makeLayoutCacheKey(contentWidth);
|
|
1727
1763
|
const cached = this.#layoutCache;
|
|
@@ -214,9 +214,11 @@ export function resetMarkdownHighlightCallCount(): void {
|
|
|
214
214
|
export const __markdownPerfCounters = {
|
|
215
215
|
lexerInvocations: 0,
|
|
216
216
|
lexedBytes: 0,
|
|
217
|
+
highlightAdmissionScans: 0,
|
|
217
218
|
reset(): void {
|
|
218
219
|
this.lexerInvocations = 0;
|
|
219
220
|
this.lexedBytes = 0;
|
|
221
|
+
this.highlightAdmissionScans = 0;
|
|
220
222
|
},
|
|
221
223
|
};
|
|
222
224
|
|
|
@@ -452,6 +454,7 @@ export class Markdown implements Component {
|
|
|
452
454
|
}
|
|
453
455
|
|
|
454
456
|
#exceedsHighlightCap(code: string): boolean {
|
|
457
|
+
__markdownPerfCounters.highlightAdmissionScans += 1;
|
|
455
458
|
// UTF-8 requires at least one byte per UTF-16 code unit, including lone
|
|
456
459
|
// surrogates. Reject obviously oversized input without scanning or hashing it.
|
|
457
460
|
if (code.length > MAX_HIGHLIGHT_BYTES) return true;
|
|
@@ -467,10 +470,13 @@ export class Markdown implements Component {
|
|
|
467
470
|
|
|
468
471
|
#highlightCodeBlock(code: string, lang: string): string[] | null {
|
|
469
472
|
if (!this.#theme.highlightCode) return null;
|
|
470
|
-
|
|
473
|
+
// Avoid building/hashing keys for obviously oversized input. A cache hit
|
|
474
|
+
// already passed the immutable byte/line caps; only misses need a scan.
|
|
475
|
+
if (code.length > MAX_HIGHLIGHT_BYTES) return null;
|
|
471
476
|
const key = `${objectId(this.#theme)}\x00${lang}\x00${code}`;
|
|
472
477
|
const cached = highlightCache.get(key);
|
|
473
478
|
if (cached?.lang === lang && cached.code === code) return cached.lines;
|
|
479
|
+
if (this.#exceedsHighlightCap(code)) return null;
|
|
474
480
|
highlightCallCount += 1;
|
|
475
481
|
const result = this.#theme.highlightCode(code, lang || undefined);
|
|
476
482
|
highlightCache.set(key, { lang, code, lines: result });
|
package/src/tui.ts
CHANGED
|
@@ -821,6 +821,7 @@ type TuiRenderCounterSnapshot = {
|
|
|
821
821
|
widthReflowScanRows: number;
|
|
822
822
|
widthReflowVisibleWidthCalls: number;
|
|
823
823
|
kittyPlacementScanRows: number;
|
|
824
|
+
kittyPlacementReferenceRows: number;
|
|
824
825
|
};
|
|
825
826
|
type RenderCommitWaiter = {
|
|
826
827
|
resolve: (committed: boolean) => void;
|
|
@@ -1187,6 +1188,7 @@ export class TUI extends Container {
|
|
|
1187
1188
|
widthReflowScanRows: 0,
|
|
1188
1189
|
widthReflowVisibleWidthCalls: 0,
|
|
1189
1190
|
kittyPlacementScanRows: 0,
|
|
1191
|
+
kittyPlacementReferenceRows: 0,
|
|
1190
1192
|
};
|
|
1191
1193
|
|
|
1192
1194
|
static resetRenderCountersForTest(): void {
|
|
@@ -1197,6 +1199,7 @@ export class TUI extends Container {
|
|
|
1197
1199
|
widthReflowScanRows: 0,
|
|
1198
1200
|
widthReflowVisibleWidthCalls: 0,
|
|
1199
1201
|
kittyPlacementScanRows: 0,
|
|
1202
|
+
kittyPlacementReferenceRows: 0,
|
|
1200
1203
|
};
|
|
1201
1204
|
}
|
|
1202
1205
|
|
|
@@ -4624,7 +4627,12 @@ export class TUI extends Container {
|
|
|
4624
4627
|
const safeLines = reuseCached ? cached.safeLines : rendered.lines.map(stripTerminalEraseControls);
|
|
4625
4628
|
const kittyPlacements = reuseCached
|
|
4626
4629
|
? cached.kittyPlacements
|
|
4627
|
-
:
|
|
4630
|
+
: TERMINAL.imageProtocol === ImageProtocol.Kitty
|
|
4631
|
+
? rendered.lines.map(line => {
|
|
4632
|
+
TUI.#renderCounters.kittyPlacementReferenceRows++;
|
|
4633
|
+
return [...extractKittyPlacementReferences(line)];
|
|
4634
|
+
})
|
|
4635
|
+
: [];
|
|
4628
4636
|
if (!reuseCached && componentRevision !== undefined && source !== null) {
|
|
4629
4637
|
this.#viewportAnchorRenderCache = {
|
|
4630
4638
|
component: child,
|
|
@@ -4644,8 +4652,11 @@ export class TUI extends Container {
|
|
|
4644
4652
|
}
|
|
4645
4653
|
const owner: KittyPlacementOwner = hasStickySuffix && childIndex >= pinnedChildIndex ? "suffix" : "transcript";
|
|
4646
4654
|
for (let lineIndex = 0; lineIndex < rendered.lines.length; lineIndex++) {
|
|
4647
|
-
|
|
4648
|
-
|
|
4655
|
+
const placements = kittyPlacements[lineIndex];
|
|
4656
|
+
if (placements !== undefined) {
|
|
4657
|
+
for (const placement of placements) {
|
|
4658
|
+
placementOwners.set(this.#kittyPlacementKey(placement), owner);
|
|
4659
|
+
}
|
|
4649
4660
|
}
|
|
4650
4661
|
renderedLines.push(safeLines[lineIndex] ?? rendered.lines[lineIndex]!);
|
|
4651
4662
|
}
|
|
@@ -4705,7 +4716,8 @@ export class TUI extends Container {
|
|
|
4705
4716
|
// re-normalized and the diff starts at the window. Output is byte-identical to the
|
|
4706
4717
|
// full path (reused entries are deterministic normalizations of identical raw lines).
|
|
4707
4718
|
const VIEWPORT_NORMALIZE_OVERSCAN = 8;
|
|
4708
|
-
|
|
4719
|
+
// Reassigned below, never mutated in place -- a reference is the snapshot.
|
|
4720
|
+
const rawLines = newLines;
|
|
4709
4721
|
const total = rawLines.length;
|
|
4710
4722
|
let diffStart = 0;
|
|
4711
4723
|
let usedWindowNormalize = false;
|