@kolisachint/hoocode-tui 0.4.111 → 0.4.113
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/components/loader.d.ts.map +1 -1
- package/dist/components/loader.js +3 -1
- package/dist/components/loader.js.map +1 -1
- package/dist/components/markdown.d.ts +5 -3
- package/dist/components/markdown.d.ts.map +1 -1
- package/dist/components/markdown.js +45 -23
- package/dist/components/markdown.js.map +1 -1
- package/dist/components/spacer.d.ts +1 -0
- package/dist/components/spacer.d.ts.map +1 -1
- package/dist/components/spacer.js +11 -5
- package/dist/components/spacer.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/tui.d.ts +43 -1
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +277 -35
- package/dist/tui.js.map +1 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +6 -2
- package/dist/utils.js.map +1 -1
- package/package.json +1 -1
package/dist/tui.js
CHANGED
|
@@ -62,31 +62,54 @@ function isTermuxSession() {
|
|
|
62
62
|
*/
|
|
63
63
|
export class Container {
|
|
64
64
|
children = [];
|
|
65
|
+
// Flatten memo: children are always render()ed (side effects and their own
|
|
66
|
+
// caches must run), but when every child returns the same array reference as
|
|
67
|
+
// last time, the previously flattened array is returned as-is. Unchanged
|
|
68
|
+
// subtrees thus stay reference-stable all the way up, which lets the TUI
|
|
69
|
+
// root diff whole regions by identity instead of re-flattening the world.
|
|
70
|
+
renderMemo;
|
|
65
71
|
addChild(component) {
|
|
66
72
|
this.children.push(component);
|
|
73
|
+
this.renderMemo = undefined;
|
|
67
74
|
}
|
|
68
75
|
removeChild(component) {
|
|
69
76
|
const index = this.children.indexOf(component);
|
|
70
77
|
if (index !== -1) {
|
|
71
78
|
this.children.splice(index, 1);
|
|
79
|
+
this.renderMemo = undefined;
|
|
72
80
|
}
|
|
73
81
|
}
|
|
74
82
|
clear() {
|
|
75
83
|
this.children = [];
|
|
84
|
+
this.renderMemo = undefined;
|
|
76
85
|
}
|
|
77
86
|
invalidate() {
|
|
87
|
+
this.renderMemo = undefined;
|
|
78
88
|
for (const child of this.children) {
|
|
79
89
|
child.invalidate?.();
|
|
80
90
|
}
|
|
81
91
|
}
|
|
82
92
|
render(width) {
|
|
93
|
+
const n = this.children.length;
|
|
94
|
+
const memo = this.renderMemo;
|
|
95
|
+
const refs = new Array(n);
|
|
96
|
+
let unchanged = memo !== undefined && memo.width === width && memo.refs.length === n;
|
|
97
|
+
for (let i = 0; i < n; i++) {
|
|
98
|
+
refs[i] = this.children[i].render(width);
|
|
99
|
+
if (unchanged && refs[i] !== memo.refs[i]) {
|
|
100
|
+
unchanged = false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (unchanged) {
|
|
104
|
+
return memo.lines;
|
|
105
|
+
}
|
|
83
106
|
const lines = [];
|
|
84
|
-
for (const
|
|
85
|
-
const childLines = child.render(width);
|
|
107
|
+
for (const childLines of refs) {
|
|
86
108
|
for (const line of childLines) {
|
|
87
109
|
lines.push(line);
|
|
88
110
|
}
|
|
89
111
|
}
|
|
112
|
+
this.renderMemo = { width, refs, lines };
|
|
90
113
|
return lines;
|
|
91
114
|
}
|
|
92
115
|
}
|
|
@@ -96,7 +119,28 @@ export class Container {
|
|
|
96
119
|
export class TUI extends Container {
|
|
97
120
|
terminal;
|
|
98
121
|
previousLines = [];
|
|
122
|
+
// Root flat-line cache (see the render() override): per-child line arrays,
|
|
123
|
+
// their offsets into the flat buffer, and the flat buffer itself. Active
|
|
124
|
+
// only when no overlays are up and no image has been drawn; otherwise the
|
|
125
|
+
// legacy full-flatten + full-diff path runs.
|
|
126
|
+
flatCache;
|
|
127
|
+
flatLines;
|
|
128
|
+
/** What the last render() call changed: "full" = unknown (legacy diff must
|
|
129
|
+
* scan), null = nothing, otherwise the dirty row range + previous length. */
|
|
130
|
+
lastPatch = "full";
|
|
131
|
+
/** Cursor position extracted on the last frame; reused when the dirty range
|
|
132
|
+
* shows the marker's row untouched (the marker was already stripped). */
|
|
133
|
+
lastCursorPos = undefined;
|
|
134
|
+
/** Set by render() when this frame's patches invalidate lastCursorPos: the
|
|
135
|
+
* marker's row was overwritten, or a patched-in line carries a marker. */
|
|
136
|
+
cursorRowOverwritten = false;
|
|
99
137
|
previousKittyImageIds = new Set();
|
|
138
|
+
/** Flips true the first time an image line is emitted. While false no image
|
|
139
|
+
* has ever been drawn, so there are no kitty ids on screen to track and the
|
|
140
|
+
* per-frame full-buffer scan (collectKittyImageIds) is skipped entirely —
|
|
141
|
+
* the common case for a pure-text session. */
|
|
142
|
+
sawImageLine = false;
|
|
143
|
+
static EMPTY_KITTY_IDS = new Set();
|
|
100
144
|
previousWidth = 0;
|
|
101
145
|
previousHeight = 0;
|
|
102
146
|
focusedComponent = null;
|
|
@@ -424,7 +468,24 @@ export class TUI extends Container {
|
|
|
424
468
|
}
|
|
425
469
|
this.focusedComponent.handleInput(data);
|
|
426
470
|
this.requestRender();
|
|
471
|
+
// Keystroke echo should not queue behind the animation coalescing
|
|
472
|
+
// window: render the input's effect immediately instead of waiting out
|
|
473
|
+
// MIN_RENDER_INTERVAL_MS behind spinner/streaming frames.
|
|
474
|
+
this.expediteRender();
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
/** Run a requested render now, bypassing the coalescing delay. Used for
|
|
478
|
+
* input-driven frames where echo latency matters more than batching. */
|
|
479
|
+
expediteRender() {
|
|
480
|
+
if (this.stopped || !this.renderRequested)
|
|
481
|
+
return;
|
|
482
|
+
if (this.renderTimer) {
|
|
483
|
+
clearTimeout(this.renderTimer);
|
|
484
|
+
this.renderTimer = undefined;
|
|
427
485
|
}
|
|
486
|
+
this.renderRequested = false;
|
|
487
|
+
this.lastRenderAt = performance.now();
|
|
488
|
+
this.doRender();
|
|
428
489
|
}
|
|
429
490
|
consumeCellSizeResponse(data) {
|
|
430
491
|
// Response format: ESC [ 6 ; height ; width t
|
|
@@ -619,17 +680,29 @@ export class TUI extends Container {
|
|
|
619
680
|
return result;
|
|
620
681
|
}
|
|
621
682
|
static SEGMENT_RESET = "\x1b[0m\x1b]8;;\x07";
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
683
|
+
/**
|
|
684
|
+
* Append the per-line style/hyperlink reset (and normalize Thai/Lao AM
|
|
685
|
+
* vowels) at the moment a line is written to the terminal. This is
|
|
686
|
+
* deliberately kept OFF the cached/diffed line arrays: leaf components cache
|
|
687
|
+
* their lines without the reset, so leaving `newLines`/`previousLines`
|
|
688
|
+
* un-reset keeps unchanged lines reference-stable frame to frame. The
|
|
689
|
+
* differential compare then short-circuits on identity for every unchanged
|
|
690
|
+
* line instead of allocating a fresh reset-appended string per line and
|
|
691
|
+
* doing a full content compare across the whole transcript every frame.
|
|
692
|
+
* Image lines carry no trailing style and are emitted verbatim.
|
|
693
|
+
*/
|
|
694
|
+
emitLine(line) {
|
|
695
|
+
if (isImageLine(line)) {
|
|
696
|
+
this.sawImageLine = true;
|
|
697
|
+
return line;
|
|
629
698
|
}
|
|
630
|
-
return
|
|
699
|
+
return normalizeTerminalOutput(line) + TUI.SEGMENT_RESET;
|
|
631
700
|
}
|
|
632
701
|
collectKittyImageIds(lines) {
|
|
702
|
+
// No image has ever been drawn: nothing on screen carries a kitty id, so
|
|
703
|
+
// skip the full-buffer scan and the Set allocation.
|
|
704
|
+
if (!this.sawImageLine)
|
|
705
|
+
return TUI.EMPTY_KITTY_IDS;
|
|
633
706
|
const ids = new Set();
|
|
634
707
|
for (const line of lines) {
|
|
635
708
|
for (const id of extractKittyImageIds(line)) {
|
|
@@ -646,6 +719,10 @@ export class TUI extends Container {
|
|
|
646
719
|
return buffer;
|
|
647
720
|
}
|
|
648
721
|
expandLastChangedForKittyImages(firstChanged, lastChanged) {
|
|
722
|
+
// No image ever drawn: nothing to expand over, skip the scan (also,
|
|
723
|
+
// on patched frames previousLines is not the previous content).
|
|
724
|
+
if (!this.sawImageLine)
|
|
725
|
+
return lastChanged;
|
|
649
726
|
let expandedLastChanged = lastChanged;
|
|
650
727
|
for (let i = firstChanged; i < this.previousLines.length; i++) {
|
|
651
728
|
if (extractKittyImageIds(this.previousLines[i]).length > 0) {
|
|
@@ -730,6 +807,124 @@ export class TUI extends Container {
|
|
|
730
807
|
}
|
|
731
808
|
return null;
|
|
732
809
|
}
|
|
810
|
+
/**
|
|
811
|
+
* Root flatten with patch tracking. Children stay memoized (Container), so
|
|
812
|
+
* a frame where only one region changed patches that region into the
|
|
813
|
+
* persistent flat buffer and reports the dirty row range via lastPatch —
|
|
814
|
+
* doRender then skips the whole-transcript diff. Falls back to a fresh
|
|
815
|
+
* flatten (lastPatch = "full") when overlays are up, an image has been
|
|
816
|
+
* drawn (kitty bookkeeping needs true previous content), the width changed,
|
|
817
|
+
* or the child list changed.
|
|
818
|
+
*/
|
|
819
|
+
render(width) {
|
|
820
|
+
const cacheAllowed = this.overlayStack.length === 0 && !this.sawImageLine;
|
|
821
|
+
const cache = this.flatCache;
|
|
822
|
+
if (!cacheAllowed || !cache || cache.width !== width || cache.refs.length !== this.children.length) {
|
|
823
|
+
const n = this.children.length;
|
|
824
|
+
const refs = new Array(n);
|
|
825
|
+
const offsets = new Array(n);
|
|
826
|
+
const flat = [];
|
|
827
|
+
for (let i = 0; i < n; i++) {
|
|
828
|
+
refs[i] = this.children[i].render(width);
|
|
829
|
+
offsets[i] = flat.length;
|
|
830
|
+
for (const line of refs[i])
|
|
831
|
+
flat.push(line);
|
|
832
|
+
}
|
|
833
|
+
if (cacheAllowed) {
|
|
834
|
+
this.flatCache = { width, refs, offsets };
|
|
835
|
+
this.flatLines = flat;
|
|
836
|
+
}
|
|
837
|
+
else {
|
|
838
|
+
this.flatCache = undefined;
|
|
839
|
+
this.flatLines = undefined;
|
|
840
|
+
}
|
|
841
|
+
this.lastPatch = "full";
|
|
842
|
+
return flat;
|
|
843
|
+
}
|
|
844
|
+
let flat = this.flatLines;
|
|
845
|
+
const prevLength = flat.length;
|
|
846
|
+
let low = Infinity;
|
|
847
|
+
let high = -1;
|
|
848
|
+
let delta = 0;
|
|
849
|
+
// Cursor bookkeeping: the marker was stripped out of the persistent flat
|
|
850
|
+
// when last extracted, so the cached position stays valid until the row
|
|
851
|
+
// it lives on is overwritten by re-imported child content — and it
|
|
852
|
+
// shifts when content above it grows or shrinks.
|
|
853
|
+
let cp = this.lastCursorPos ?? null;
|
|
854
|
+
this.cursorRowOverwritten = false;
|
|
855
|
+
for (let i = 0; i < this.children.length; i++) {
|
|
856
|
+
const r = this.children[i].render(width);
|
|
857
|
+
const old = cache.refs[i];
|
|
858
|
+
if (r === old)
|
|
859
|
+
continue;
|
|
860
|
+
const off = cache.offsets[i] + delta;
|
|
861
|
+
if (r.length === old.length) {
|
|
862
|
+
for (let k = 0; k < r.length; k++) {
|
|
863
|
+
if (old[k] !== r[k]) {
|
|
864
|
+
const row = off + k;
|
|
865
|
+
flat[row] = r[k];
|
|
866
|
+
if (row < low)
|
|
867
|
+
low = row;
|
|
868
|
+
if (row > high)
|
|
869
|
+
high = row;
|
|
870
|
+
// Overwrote the marker's row, or imported a line carrying a
|
|
871
|
+
// (possibly relocated) marker: position must be re-extracted.
|
|
872
|
+
if (cp && cp.row === row)
|
|
873
|
+
this.cursorRowOverwritten = true;
|
|
874
|
+
if (r[k].includes(CURSOR_MARKER))
|
|
875
|
+
this.cursorRowOverwritten = true;
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
else {
|
|
880
|
+
// Length changed: find the first differing line, then splice the
|
|
881
|
+
// child's new lines in. Everything from there down shifts rows, so
|
|
882
|
+
// the dirty range extends to the end (positional diff semantics).
|
|
883
|
+
let p = 0;
|
|
884
|
+
const minLen = Math.min(old.length, r.length);
|
|
885
|
+
while (p < minLen && old[p] === r[p])
|
|
886
|
+
p++;
|
|
887
|
+
flat = flat.slice(0, off + p).concat(r.slice(p), flat.slice(off + old.length));
|
|
888
|
+
if (off + p < low)
|
|
889
|
+
low = off + p;
|
|
890
|
+
if (cp) {
|
|
891
|
+
if (cp.row >= off + old.length) {
|
|
892
|
+
// Below the replaced region: shifts with it.
|
|
893
|
+
cp = { row: cp.row + (r.length - old.length), col: cp.col };
|
|
894
|
+
}
|
|
895
|
+
else if (cp.row >= off + p) {
|
|
896
|
+
// Inside the replaced region: fresh content, re-extract.
|
|
897
|
+
this.cursorRowOverwritten = true;
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
if (!this.cursorRowOverwritten) {
|
|
901
|
+
for (let k = p; k < r.length; k++) {
|
|
902
|
+
if (r[k].includes(CURSOR_MARKER)) {
|
|
903
|
+
this.cursorRowOverwritten = true;
|
|
904
|
+
break;
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
delta += r.length - old.length;
|
|
909
|
+
}
|
|
910
|
+
cache.refs[i] = r;
|
|
911
|
+
}
|
|
912
|
+
this.lastCursorPos = cp;
|
|
913
|
+
const spliced = flat !== this.flatLines;
|
|
914
|
+
if (spliced) {
|
|
915
|
+
let acc = 0;
|
|
916
|
+
for (let i = 0; i < cache.refs.length; i++) {
|
|
917
|
+
cache.offsets[i] = acc;
|
|
918
|
+
acc += cache.refs[i].length;
|
|
919
|
+
}
|
|
920
|
+
this.flatLines = flat;
|
|
921
|
+
// Rows below the first splice all shifted; positional diff semantics
|
|
922
|
+
// mean everything from there to the end must be treated as dirty.
|
|
923
|
+
high = Math.max(prevLength - 1, flat.length - 1);
|
|
924
|
+
}
|
|
925
|
+
this.lastPatch = low === Infinity && high === -1 ? null : { low: low === Infinity ? 0 : low, high, prevLength };
|
|
926
|
+
return flat;
|
|
927
|
+
}
|
|
733
928
|
doRender() {
|
|
734
929
|
if (this.stopped)
|
|
735
930
|
return;
|
|
@@ -746,15 +941,39 @@ export class TUI extends Container {
|
|
|
746
941
|
const targetScreenRow = targetRow - viewportTop;
|
|
747
942
|
return targetScreenRow - currentScreenRow;
|
|
748
943
|
};
|
|
749
|
-
// Render all components to get new lines
|
|
944
|
+
// Render all components to get new lines. The root render() reports what
|
|
945
|
+
// it changed via lastPatch; consume it here (it is per-frame state).
|
|
750
946
|
let newLines = this.render(width);
|
|
947
|
+
const patch = this.lastPatch;
|
|
948
|
+
this.lastPatch = "full";
|
|
751
949
|
// Composite overlays into the rendered lines (before differential compare)
|
|
752
950
|
if (this.overlayStack.length > 0) {
|
|
753
951
|
newLines = this.compositeOverlays(newLines, width, height);
|
|
754
952
|
}
|
|
755
|
-
// Extract cursor position before
|
|
756
|
-
|
|
757
|
-
|
|
953
|
+
// Extract cursor position before the marker could be obscured. The reset
|
|
954
|
+
// is applied per-line at write time (see emitLine), so newLines stays the
|
|
955
|
+
// un-reset, reference-stable output of the component tree from here on.
|
|
956
|
+
// On patched frames the persistent flat buffer already had the marker
|
|
957
|
+
// stripped; the cached position (row-shifted by render()) stays valid
|
|
958
|
+
// unless its row was overwritten by re-imported child content, or a
|
|
959
|
+
// marker could have newly appeared in changed content.
|
|
960
|
+
let cursorPos;
|
|
961
|
+
if (patch !== "full" && this.lastCursorPos !== undefined) {
|
|
962
|
+
const cp = this.lastCursorPos;
|
|
963
|
+
if (cp !== null && !this.cursorRowOverwritten) {
|
|
964
|
+
cursorPos = cp;
|
|
965
|
+
}
|
|
966
|
+
else if (patch === null) {
|
|
967
|
+
cursorPos = cp;
|
|
968
|
+
}
|
|
969
|
+
else {
|
|
970
|
+
cursorPos = this.extractCursorPosition(newLines, height);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
else {
|
|
974
|
+
cursorPos = this.extractCursorPosition(newLines, height);
|
|
975
|
+
}
|
|
976
|
+
this.lastCursorPos = cursorPos;
|
|
758
977
|
// Helper to clear scrollback and viewport and render all new lines
|
|
759
978
|
const fullRender = (clear) => {
|
|
760
979
|
this.fullRedrawCount += 1;
|
|
@@ -766,7 +985,7 @@ export class TUI extends Container {
|
|
|
766
985
|
for (let i = 0; i < newLines.length; i++) {
|
|
767
986
|
if (i > 0)
|
|
768
987
|
buffer += "\r\n";
|
|
769
|
-
buffer += newLines[i];
|
|
988
|
+
buffer += this.emitLine(newLines[i]);
|
|
770
989
|
}
|
|
771
990
|
buffer += "\x1b[?2026l"; // End synchronized output
|
|
772
991
|
this.terminal.write(buffer);
|
|
@@ -824,31 +1043,52 @@ export class TUI extends Container {
|
|
|
824
1043
|
fullRender(true);
|
|
825
1044
|
return;
|
|
826
1045
|
}
|
|
827
|
-
// Find first and last changed lines
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
1046
|
+
// Find first and last changed lines. When the root render() produced a
|
|
1047
|
+
// patch report the dirty range is already known and the whole-buffer scan
|
|
1048
|
+
// is skipped. On patched frames previousLines is the same in-place-updated
|
|
1049
|
+
// array as newLines, so the previous length must come from the report.
|
|
1050
|
+
let firstChanged;
|
|
1051
|
+
let lastChanged;
|
|
1052
|
+
let prevLineCount;
|
|
1053
|
+
if (patch !== "full") {
|
|
1054
|
+
if (patch === null) {
|
|
1055
|
+
prevLineCount = newLines.length;
|
|
1056
|
+
firstChanged = -1;
|
|
1057
|
+
lastChanged = -1;
|
|
1058
|
+
}
|
|
1059
|
+
else {
|
|
1060
|
+
prevLineCount = patch.prevLength;
|
|
1061
|
+
firstChanged = patch.low;
|
|
1062
|
+
lastChanged = patch.high;
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
else {
|
|
1066
|
+
prevLineCount = this.previousLines.length;
|
|
1067
|
+
firstChanged = -1;
|
|
1068
|
+
lastChanged = -1;
|
|
1069
|
+
const maxLines = Math.max(newLines.length, prevLineCount);
|
|
1070
|
+
for (let i = 0; i < maxLines; i++) {
|
|
1071
|
+
const oldLine = i < prevLineCount ? this.previousLines[i] : "";
|
|
1072
|
+
const newLine = i < newLines.length ? newLines[i] : "";
|
|
1073
|
+
if (oldLine !== newLine) {
|
|
1074
|
+
if (firstChanged === -1) {
|
|
1075
|
+
firstChanged = i;
|
|
1076
|
+
}
|
|
1077
|
+
lastChanged = i;
|
|
837
1078
|
}
|
|
838
|
-
lastChanged = i;
|
|
839
1079
|
}
|
|
840
1080
|
}
|
|
841
|
-
const appendedLines = newLines.length >
|
|
1081
|
+
const appendedLines = newLines.length > prevLineCount;
|
|
842
1082
|
if (appendedLines) {
|
|
843
1083
|
if (firstChanged === -1) {
|
|
844
|
-
firstChanged =
|
|
1084
|
+
firstChanged = prevLineCount;
|
|
845
1085
|
}
|
|
846
1086
|
lastChanged = newLines.length - 1;
|
|
847
1087
|
}
|
|
848
1088
|
if (firstChanged !== -1) {
|
|
849
1089
|
lastChanged = this.expandLastChangedForKittyImages(firstChanged, lastChanged);
|
|
850
1090
|
}
|
|
851
|
-
const appendStart = appendedLines && firstChanged ===
|
|
1091
|
+
const appendStart = appendedLines && firstChanged === prevLineCount && firstChanged > 0;
|
|
852
1092
|
// No changes - but still need to update hardware cursor position if it moved
|
|
853
1093
|
if (firstChanged === -1) {
|
|
854
1094
|
this.positionHardwareCursor(cursorPos, newLines.length);
|
|
@@ -858,7 +1098,7 @@ export class TUI extends Container {
|
|
|
858
1098
|
}
|
|
859
1099
|
// All changes are in deleted lines (nothing to render, just clear)
|
|
860
1100
|
if (firstChanged >= newLines.length) {
|
|
861
|
-
if (
|
|
1101
|
+
if (prevLineCount > newLines.length) {
|
|
862
1102
|
let buffer = "\x1b[?2026h";
|
|
863
1103
|
buffer += this.deleteChangedKittyImages(firstChanged, lastChanged);
|
|
864
1104
|
// Move to end of new content (clamp to 0 for empty content)
|
|
@@ -875,7 +1115,7 @@ export class TUI extends Container {
|
|
|
875
1115
|
buffer += `\x1b[${-lineDiff}A`;
|
|
876
1116
|
buffer += "\r";
|
|
877
1117
|
// Clear extra lines without scrolling
|
|
878
|
-
const extraLines =
|
|
1118
|
+
const extraLines = prevLineCount - newLines.length;
|
|
879
1119
|
if (extraLines > height) {
|
|
880
1120
|
logRedraw(`extraLines > height (${extraLines} > ${height})`);
|
|
881
1121
|
fullRender(true);
|
|
@@ -975,20 +1215,22 @@ export class TUI extends Container {
|
|
|
975
1215
|
].join("\n");
|
|
976
1216
|
throw new Error(errorMsg);
|
|
977
1217
|
}
|
|
978
|
-
|
|
1218
|
+
if (isImage)
|
|
1219
|
+
this.sawImageLine = true;
|
|
1220
|
+
buffer += isImage ? line : normalizeTerminalOutput(line) + TUI.SEGMENT_RESET;
|
|
979
1221
|
}
|
|
980
1222
|
// Track where cursor ended up after rendering
|
|
981
1223
|
let finalCursorRow = renderEnd;
|
|
982
1224
|
// If we had more lines before, clear them and move cursor back
|
|
983
|
-
if (
|
|
1225
|
+
if (prevLineCount > newLines.length) {
|
|
984
1226
|
// Move to end of new content first if we stopped before it
|
|
985
1227
|
if (renderEnd < newLines.length - 1) {
|
|
986
1228
|
const moveDown = newLines.length - 1 - renderEnd;
|
|
987
1229
|
buffer += `\x1b[${moveDown}B`;
|
|
988
1230
|
finalCursorRow = newLines.length - 1;
|
|
989
1231
|
}
|
|
990
|
-
const extraLines =
|
|
991
|
-
for (let i = newLines.length; i <
|
|
1232
|
+
const extraLines = prevLineCount - newLines.length;
|
|
1233
|
+
for (let i = newLines.length; i < prevLineCount; i++) {
|
|
992
1234
|
buffer += "\r\n\x1b[2K";
|
|
993
1235
|
}
|
|
994
1236
|
// Move cursor back to end of new content
|