tinky 1.5.0 → 1.7.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/README.ja-JP.md +40 -0
- package/README.md +39 -0
- package/README.zh-CN.md +38 -0
- package/lib/components/Text.js +10 -3
- package/lib/core/cell-buffer.d.ts +91 -0
- package/lib/core/cell-buffer.js +346 -0
- package/lib/core/cell-log-update-run.d.ts +40 -0
- package/lib/core/cell-log-update-run.js +526 -0
- package/lib/core/cell-output.d.ts +20 -0
- package/lib/core/cell-output.js +231 -0
- package/lib/core/cell-renderer.d.ts +25 -0
- package/lib/core/cell-renderer.js +60 -0
- package/lib/core/incremental-rendering.d.ts +43 -0
- package/lib/core/incremental-rendering.js +22 -0
- package/lib/core/output.d.ts +21 -6
- package/lib/core/output.js +73 -8
- package/lib/core/render-border.d.ts +2 -12
- package/lib/core/render-border.js +78 -69
- package/lib/core/render-node-to-output.d.ts +2 -2
- package/lib/core/render.d.ts +17 -4
- package/lib/core/renderer.js +9 -6
- package/lib/core/tinky.d.ts +27 -3
- package/lib/core/tinky.js +125 -5
- package/lib/index.d.ts +1 -0
- package/lib/types/ansi.d.ts +5 -0
- package/lib/types/ansi.js +1 -0
- package/lib/utils/ansi-escapes.d.ts +2 -0
- package/lib/utils/ansi-escapes.js +2 -0
- package/lib/utils/colorize.d.ts +5 -0
- package/lib/utils/colorize.js +42 -20
- package/lib/utils/render-background.d.ts +2 -2
- package/lib/utils/render-background.js +4 -3
- package/lib/utils/squash-text-nodes.d.ts +5 -0
- package/lib/utils/squash-text-nodes.js +18 -2
- package/package.json +3 -1
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
import ansiEscapes from "../utils/ansi-escapes.js";
|
|
2
|
+
import * as cliCursor from "../utils/cli-cursor.js";
|
|
3
|
+
const cursorToColumn = (x) => ansiEscapes.cursorTo(x);
|
|
4
|
+
const cursorForward = (count) => ansiEscapes.cursorForward(count);
|
|
5
|
+
/**
|
|
6
|
+
* Compares two cells at an absolute row/column, including style and width.
|
|
7
|
+
*/
|
|
8
|
+
const cellsEqualAt = (prevBuffer, nextBuffer, row, x) => {
|
|
9
|
+
const idx = row * nextBuffer.width + x;
|
|
10
|
+
return (prevBuffer.styleIds[idx] === nextBuffer.styleIds[idx] &&
|
|
11
|
+
prevBuffer.widths[idx] === nextBuffer.widths[idx] &&
|
|
12
|
+
prevBuffer.chars[idx] === nextBuffer.chars[idx]);
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Rewinds to the first cell of a glyph when `x` lands in a continuation cell.
|
|
16
|
+
*/
|
|
17
|
+
const findGlyphStart = (buffer, row, x) => {
|
|
18
|
+
if (x <= 0) {
|
|
19
|
+
return 0;
|
|
20
|
+
}
|
|
21
|
+
const rowOffset = row * buffer.width;
|
|
22
|
+
let currentX = x;
|
|
23
|
+
while (currentX > 0 && buffer.widths[rowOffset + currentX] === 0) {
|
|
24
|
+
currentX--;
|
|
25
|
+
}
|
|
26
|
+
return currentX;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Extends an end position so we never split a wide character.
|
|
30
|
+
*/
|
|
31
|
+
const alignEndToGlyphBoundary = (buffer, row, end) => {
|
|
32
|
+
const width = buffer.width;
|
|
33
|
+
let safeEnd = Math.max(0, Math.min(width, end));
|
|
34
|
+
if (safeEnd <= 0 || safeEnd >= width) {
|
|
35
|
+
return safeEnd <= 0 ? 0 : width;
|
|
36
|
+
}
|
|
37
|
+
const rowOffset = row * width;
|
|
38
|
+
let x = safeEnd - 1;
|
|
39
|
+
while (x > 0 && buffer.widths[rowOffset + x] === 0) {
|
|
40
|
+
x--;
|
|
41
|
+
}
|
|
42
|
+
const cellWidth = buffer.widths[rowOffset + x] || 1;
|
|
43
|
+
const glyphEnd = x + cellWidth;
|
|
44
|
+
if (glyphEnd > safeEnd) {
|
|
45
|
+
safeEnd = Math.min(width, glyphEnd);
|
|
46
|
+
}
|
|
47
|
+
return safeEnd;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Finds changed cell runs for one row and normalizes them to glyph boundaries.
|
|
51
|
+
*/
|
|
52
|
+
const computeSegments = (prevBuffer, nextBuffer, row, scanEnd) => {
|
|
53
|
+
const width = nextBuffer.width;
|
|
54
|
+
const end = Math.max(0, Math.min(width, scanEnd));
|
|
55
|
+
const segments = [];
|
|
56
|
+
let inSegment = false;
|
|
57
|
+
let segmentStart = 0;
|
|
58
|
+
for (let x = 0; x < end; x++) {
|
|
59
|
+
const isDiff = !cellsEqualAt(prevBuffer, nextBuffer, row, x);
|
|
60
|
+
if (isDiff && !inSegment) {
|
|
61
|
+
inSegment = true;
|
|
62
|
+
segmentStart = x;
|
|
63
|
+
}
|
|
64
|
+
else if (!isDiff && inSegment) {
|
|
65
|
+
segments.push({ start: segmentStart, end: x });
|
|
66
|
+
inSegment = false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (inSegment) {
|
|
70
|
+
segments.push({ start: segmentStart, end });
|
|
71
|
+
}
|
|
72
|
+
for (const segment of segments) {
|
|
73
|
+
segment.start = findGlyphStart(nextBuffer, row, segment.start);
|
|
74
|
+
segment.end = alignEndToGlyphBoundary(nextBuffer, row, segment.end);
|
|
75
|
+
}
|
|
76
|
+
segments.sort((a, b) => a.start - b.start);
|
|
77
|
+
const merged = [];
|
|
78
|
+
for (const segment of segments) {
|
|
79
|
+
if (merged.length === 0) {
|
|
80
|
+
merged.push({ ...segment });
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const last = merged.at(-1);
|
|
84
|
+
if (!last) {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (segment.start <= last.end) {
|
|
88
|
+
last.end = Math.max(last.end, segment.end);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
merged.push({ ...segment });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return merged;
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* Merges nearby segments using a fixed gap threshold.
|
|
98
|
+
*/
|
|
99
|
+
const mergeByGapThreshold = (segments, mergeThreshold) => {
|
|
100
|
+
if (segments.length <= 1) {
|
|
101
|
+
return segments;
|
|
102
|
+
}
|
|
103
|
+
const merged = [];
|
|
104
|
+
for (const segment of segments) {
|
|
105
|
+
if (merged.length === 0) {
|
|
106
|
+
merged.push({ ...segment });
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const last = merged.at(-1);
|
|
110
|
+
if (!last) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const gap = segment.start - last.end;
|
|
114
|
+
if (gap <= mergeThreshold) {
|
|
115
|
+
last.end = Math.max(last.end, segment.end);
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
merged.push({ ...segment });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return merged;
|
|
122
|
+
};
|
|
123
|
+
const asciiByteLength = (value) => value.length;
|
|
124
|
+
const utf8ByteLength = (value) => {
|
|
125
|
+
let bytes = 0;
|
|
126
|
+
for (const char of value) {
|
|
127
|
+
const codePoint = char.codePointAt(0) ?? 0;
|
|
128
|
+
if (codePoint <= 0x7f) {
|
|
129
|
+
bytes += 1;
|
|
130
|
+
}
|
|
131
|
+
else if (codePoint <= 0x7ff) {
|
|
132
|
+
bytes += 2;
|
|
133
|
+
}
|
|
134
|
+
else if (codePoint <= 0xffff) {
|
|
135
|
+
bytes += 3;
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
bytes += 4;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return bytes;
|
|
142
|
+
};
|
|
143
|
+
const digits10 = (input) => {
|
|
144
|
+
let value = input;
|
|
145
|
+
let digits = 1;
|
|
146
|
+
while (value >= 10) {
|
|
147
|
+
value = Math.floor(value / 10);
|
|
148
|
+
digits++;
|
|
149
|
+
}
|
|
150
|
+
return digits;
|
|
151
|
+
};
|
|
152
|
+
const bytesCursorForward = (delta) => {
|
|
153
|
+
if (delta <= 0) {
|
|
154
|
+
return 0;
|
|
155
|
+
}
|
|
156
|
+
return 3 + digits10(delta);
|
|
157
|
+
};
|
|
158
|
+
const bytesCursorToColumn = (column0) => {
|
|
159
|
+
const column = Math.max(0, column0) + 1;
|
|
160
|
+
return 3 + digits10(column);
|
|
161
|
+
};
|
|
162
|
+
/**
|
|
163
|
+
* Chooses between relative forward movement and absolute column movement based
|
|
164
|
+
* on encoded byte cost.
|
|
165
|
+
*/
|
|
166
|
+
const chooseHorizontalMove = (fromX, toX) => {
|
|
167
|
+
if (toX <= fromX) {
|
|
168
|
+
return { bytes: bytesCursorToColumn(toX), method: "abs" };
|
|
169
|
+
}
|
|
170
|
+
const delta = toX - fromX;
|
|
171
|
+
const forwardBytes = bytesCursorForward(delta);
|
|
172
|
+
const absoluteBytes = bytesCursorToColumn(toX);
|
|
173
|
+
if (forwardBytes <= absoluteBytes) {
|
|
174
|
+
return { bytes: forwardBytes, method: "forward" };
|
|
175
|
+
}
|
|
176
|
+
return { bytes: absoluteBytes, method: "abs" };
|
|
177
|
+
};
|
|
178
|
+
const getStyleLens = (buffer, styleLenCache, styleId) => {
|
|
179
|
+
const cached = styleLenCache.get(styleId);
|
|
180
|
+
if (cached) {
|
|
181
|
+
return cached;
|
|
182
|
+
}
|
|
183
|
+
const styles = buffer.styleRegistry.getStyles(styleId);
|
|
184
|
+
let open = 0;
|
|
185
|
+
let close = 0;
|
|
186
|
+
for (const style of styles) {
|
|
187
|
+
open += asciiByteLength(style.code);
|
|
188
|
+
close += asciiByteLength(style.endCode);
|
|
189
|
+
}
|
|
190
|
+
const lens = { open, close };
|
|
191
|
+
styleLenCache.set(styleId, lens);
|
|
192
|
+
return lens;
|
|
193
|
+
};
|
|
194
|
+
const transitionBytes = (buffer, styleLenCache, fromId, toId) => {
|
|
195
|
+
if (fromId === toId) {
|
|
196
|
+
return 0;
|
|
197
|
+
}
|
|
198
|
+
const from = getStyleLens(buffer, styleLenCache, fromId);
|
|
199
|
+
const to = getStyleLens(buffer, styleLenCache, toId);
|
|
200
|
+
return from.close + to.open;
|
|
201
|
+
};
|
|
202
|
+
const closeBytes = (buffer, styleLenCache, activeStyleId) => {
|
|
203
|
+
if (activeStyleId === 0) {
|
|
204
|
+
return 0;
|
|
205
|
+
}
|
|
206
|
+
return getStyleLens(buffer, styleLenCache, activeStyleId).close;
|
|
207
|
+
};
|
|
208
|
+
/**
|
|
209
|
+
* Estimates bytes written for a styled range and returns resulting cursor/style.
|
|
210
|
+
*/
|
|
211
|
+
const measureStyledRange = (buffer, styleLenCache, row, start, end, activeStyleId, cursorX) => {
|
|
212
|
+
const width = buffer.width;
|
|
213
|
+
const safeStart = Math.max(0, Math.min(width, start));
|
|
214
|
+
const safeEnd = Math.max(0, Math.min(width, end));
|
|
215
|
+
if (safeEnd <= safeStart) {
|
|
216
|
+
return { bytes: 0, activeStyleId, cursorX };
|
|
217
|
+
}
|
|
218
|
+
let bytes = 0;
|
|
219
|
+
const rowOffset = row * width;
|
|
220
|
+
let x = safeStart;
|
|
221
|
+
while (x < safeEnd) {
|
|
222
|
+
const index = rowOffset + x;
|
|
223
|
+
const cellWidth = buffer.widths[index];
|
|
224
|
+
if (cellWidth === 0) {
|
|
225
|
+
x++;
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
const styleId = buffer.styleIds[index] ?? 0;
|
|
229
|
+
if (styleId !== activeStyleId) {
|
|
230
|
+
bytes += transitionBytes(buffer, styleLenCache, activeStyleId, styleId);
|
|
231
|
+
activeStyleId = styleId;
|
|
232
|
+
}
|
|
233
|
+
const char = buffer.chars[index] ?? " ";
|
|
234
|
+
bytes += utf8ByteLength(char);
|
|
235
|
+
const advance = cellWidth || 1;
|
|
236
|
+
cursorX += advance;
|
|
237
|
+
x += advance;
|
|
238
|
+
}
|
|
239
|
+
return { bytes, activeStyleId, cursorX };
|
|
240
|
+
};
|
|
241
|
+
/**
|
|
242
|
+
* Builds row operations and compares patch cost versus full-row redraw cost.
|
|
243
|
+
*/
|
|
244
|
+
const planRowOpsWithCostModel = (nextBuffer, styleLenCache, row, segments, nextEnd, tailClear, overwriteGapPenaltyBytes) => {
|
|
245
|
+
const width = nextBuffer.width;
|
|
246
|
+
const ops = [];
|
|
247
|
+
let patchBytes = 0;
|
|
248
|
+
let cursorX = 0;
|
|
249
|
+
let activeStyleId = 0;
|
|
250
|
+
for (const segment of segments) {
|
|
251
|
+
let segStart = segment.start;
|
|
252
|
+
const segEnd = Math.min(width, segment.end);
|
|
253
|
+
if (segStart < cursorX) {
|
|
254
|
+
segStart = cursorX;
|
|
255
|
+
}
|
|
256
|
+
if (segStart >= segEnd) {
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
if (segStart > cursorX) {
|
|
260
|
+
const move = chooseHorizontalMove(cursorX, segStart);
|
|
261
|
+
const split = measureStyledRange(nextBuffer, styleLenCache, row, segStart, segEnd, activeStyleId, segStart);
|
|
262
|
+
const splitCost = move.bytes + split.bytes;
|
|
263
|
+
const gap = measureStyledRange(nextBuffer, styleLenCache, row, cursorX, segStart, activeStyleId, cursorX);
|
|
264
|
+
const mergedSegment = measureStyledRange(nextBuffer, styleLenCache, row, segStart, segEnd, gap.activeStyleId, segStart);
|
|
265
|
+
const mergeCost = gap.bytes + mergedSegment.bytes + overwriteGapPenaltyBytes;
|
|
266
|
+
if (mergeCost <= splitCost) {
|
|
267
|
+
ops.push({ kind: "write", start: cursorX, end: segStart });
|
|
268
|
+
patchBytes += gap.bytes;
|
|
269
|
+
cursorX = gap.cursorX;
|
|
270
|
+
activeStyleId = gap.activeStyleId;
|
|
271
|
+
ops.push({ kind: "write", start: segStart, end: segEnd });
|
|
272
|
+
patchBytes += mergedSegment.bytes;
|
|
273
|
+
cursorX = mergedSegment.cursorX;
|
|
274
|
+
activeStyleId = mergedSegment.activeStyleId;
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
ops.push({ kind: "move", toX: segStart, method: move.method });
|
|
278
|
+
patchBytes += move.bytes;
|
|
279
|
+
cursorX = segStart;
|
|
280
|
+
ops.push({ kind: "write", start: segStart, end: segEnd });
|
|
281
|
+
patchBytes += split.bytes;
|
|
282
|
+
cursorX = split.cursorX;
|
|
283
|
+
activeStyleId = split.activeStyleId;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
const fullSegment = measureStyledRange(nextBuffer, styleLenCache, row, segStart, segEnd, activeStyleId, cursorX);
|
|
288
|
+
ops.push({ kind: "write", start: segStart, end: segEnd });
|
|
289
|
+
patchBytes += fullSegment.bytes;
|
|
290
|
+
cursorX = fullSegment.cursorX;
|
|
291
|
+
activeStyleId = fullSegment.activeStyleId;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
patchBytes += closeBytes(nextBuffer, styleLenCache, activeStyleId);
|
|
295
|
+
activeStyleId = 0;
|
|
296
|
+
if (tailClear) {
|
|
297
|
+
if (cursorX !== nextEnd) {
|
|
298
|
+
patchBytes += chooseHorizontalMove(cursorX, nextEnd).bytes;
|
|
299
|
+
cursorX = nextEnd;
|
|
300
|
+
}
|
|
301
|
+
patchBytes += asciiByteLength(ansiEscapes.eraseEndLine);
|
|
302
|
+
}
|
|
303
|
+
patchBytes += asciiByteLength(ansiEscapes.cursorNextLine);
|
|
304
|
+
let fullBytes = 0;
|
|
305
|
+
const fullRange = measureStyledRange(nextBuffer, styleLenCache, row, 0, nextEnd, 0, 0);
|
|
306
|
+
fullBytes += fullRange.bytes;
|
|
307
|
+
fullBytes += closeBytes(nextBuffer, styleLenCache, fullRange.activeStyleId);
|
|
308
|
+
if (tailClear) {
|
|
309
|
+
fullBytes += asciiByteLength(ansiEscapes.eraseEndLine);
|
|
310
|
+
}
|
|
311
|
+
fullBytes += asciiByteLength(ansiEscapes.cursorNextLine);
|
|
312
|
+
if (patchBytes >= fullBytes) {
|
|
313
|
+
return { useFullRow: true, ops: [] };
|
|
314
|
+
}
|
|
315
|
+
return { useFullRow: false, ops };
|
|
316
|
+
};
|
|
317
|
+
/**
|
|
318
|
+
* Creates a run-diff renderer bound to one output stream.
|
|
319
|
+
*/
|
|
320
|
+
const create = (stream, options = {}) => {
|
|
321
|
+
const incremental = options.incremental ?? true;
|
|
322
|
+
const showCursor = options.showCursor ?? false;
|
|
323
|
+
const mergeStrategy = options.mergeStrategy ?? "cost";
|
|
324
|
+
const mergeThreshold = Math.max(0, options.mergeThreshold ?? 2);
|
|
325
|
+
const maxSegmentsBeforeFullRow = Math.max(1, options.maxSegmentsBeforeFullRow ?? 12);
|
|
326
|
+
const overwriteGapPenaltyBytes = Math.max(0, options.overwriteGapPenaltyBytes ?? 0);
|
|
327
|
+
const styleLenCache = new Map();
|
|
328
|
+
let previousHeight = 0;
|
|
329
|
+
let hasHiddenCursor = false;
|
|
330
|
+
const previousFrameLineCount = () => previousHeight > 0 ? previousHeight + 1 : 0;
|
|
331
|
+
const render = ((prevBuffer, nextBuffer, renderOptions = {}) => {
|
|
332
|
+
const forceFull = renderOptions.forceFull ?? false;
|
|
333
|
+
if (!showCursor && !hasHiddenCursor) {
|
|
334
|
+
cliCursor.hide(stream);
|
|
335
|
+
hasHiddenCursor = true;
|
|
336
|
+
}
|
|
337
|
+
const nextHeight = nextBuffer.height;
|
|
338
|
+
const dimsChanged = previousHeight === 0 ||
|
|
339
|
+
prevBuffer.width !== nextBuffer.width ||
|
|
340
|
+
prevBuffer.height !== nextBuffer.height;
|
|
341
|
+
// Full redraw path: used on first frame, dimension changes, or forced mode.
|
|
342
|
+
if (forceFull || !incremental || dimsChanged) {
|
|
343
|
+
const output = nextBuffer.toString();
|
|
344
|
+
stream.write(ansiEscapes.eraseLines(previousFrameLineCount()) + output + "\n");
|
|
345
|
+
previousHeight = nextHeight;
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
const prevCount = previousHeight + 1;
|
|
349
|
+
const nextCount = nextHeight + 1;
|
|
350
|
+
const visibleCount = nextHeight;
|
|
351
|
+
const rowChunks = [];
|
|
352
|
+
let hasDiff = false;
|
|
353
|
+
const width = nextBuffer.width;
|
|
354
|
+
// Incremental path: process one row at a time and emit minimal writes.
|
|
355
|
+
for (let row = 0; row < visibleCount; row++) {
|
|
356
|
+
if (prevBuffer.isRowEqual(nextBuffer, row)) {
|
|
357
|
+
rowChunks.push(ansiEscapes.cursorNextLine);
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
hasDiff = true;
|
|
361
|
+
rowChunks.push(cursorToColumn(0));
|
|
362
|
+
const nextEnd = nextBuffer.getRowRightEdge(row);
|
|
363
|
+
const prevEnd = prevBuffer.getRowRightEdge(row);
|
|
364
|
+
const tailClear = prevEnd > nextEnd;
|
|
365
|
+
const scanEnd = tailClear ? nextEnd : Math.max(nextEnd, prevEnd);
|
|
366
|
+
if (scanEnd === 0 && tailClear) {
|
|
367
|
+
rowChunks.push(ansiEscapes.eraseEndLine);
|
|
368
|
+
rowChunks.push(ansiEscapes.cursorNextLine);
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
let segments = computeSegments(prevBuffer, nextBuffer, row, scanEnd);
|
|
372
|
+
if (mergeStrategy === "threshold") {
|
|
373
|
+
segments = mergeByGapThreshold(segments, mergeThreshold);
|
|
374
|
+
}
|
|
375
|
+
if (segments.length > maxSegmentsBeforeFullRow) {
|
|
376
|
+
const full = nextBuffer.appendStyledRange(row, 0, nextEnd, rowChunks, 0, 0);
|
|
377
|
+
let activeStyleId = full.activeStyleId;
|
|
378
|
+
activeStyleId = nextBuffer.appendCloseActiveStyle(rowChunks, activeStyleId);
|
|
379
|
+
if (tailClear) {
|
|
380
|
+
rowChunks.push(ansiEscapes.eraseEndLine);
|
|
381
|
+
}
|
|
382
|
+
rowChunks.push(ansiEscapes.cursorNextLine);
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
if (segments.length === 0) {
|
|
386
|
+
if (tailClear) {
|
|
387
|
+
rowChunks.push(ansiEscapes.eraseEndLine);
|
|
388
|
+
}
|
|
389
|
+
rowChunks.push(ansiEscapes.cursorNextLine);
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
let cursorX = 0;
|
|
393
|
+
let activeStyleId = 0;
|
|
394
|
+
if (mergeStrategy === "cost") {
|
|
395
|
+
const plan = planRowOpsWithCostModel(nextBuffer, styleLenCache, row, segments, nextEnd, tailClear, overwriteGapPenaltyBytes);
|
|
396
|
+
if (plan.useFullRow) {
|
|
397
|
+
const full = nextBuffer.appendStyledRange(row, 0, nextEnd, rowChunks, activeStyleId, cursorX);
|
|
398
|
+
cursorX = full.cursorX;
|
|
399
|
+
activeStyleId = full.activeStyleId;
|
|
400
|
+
activeStyleId = nextBuffer.appendCloseActiveStyle(rowChunks, activeStyleId);
|
|
401
|
+
if (tailClear) {
|
|
402
|
+
rowChunks.push(ansiEscapes.eraseEndLine);
|
|
403
|
+
}
|
|
404
|
+
rowChunks.push(ansiEscapes.cursorNextLine);
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
for (const op of plan.ops) {
|
|
408
|
+
if (op.kind === "move") {
|
|
409
|
+
const toX = Math.max(0, Math.min(width, op.toX));
|
|
410
|
+
const delta = toX - cursorX;
|
|
411
|
+
if (delta === 0) {
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
if (delta > 0) {
|
|
415
|
+
if (op.method === "abs") {
|
|
416
|
+
rowChunks.push(cursorToColumn(toX));
|
|
417
|
+
}
|
|
418
|
+
else {
|
|
419
|
+
rowChunks.push(cursorForward(delta));
|
|
420
|
+
}
|
|
421
|
+
cursorX = toX;
|
|
422
|
+
}
|
|
423
|
+
else {
|
|
424
|
+
rowChunks.push(cursorToColumn(toX));
|
|
425
|
+
cursorX = toX;
|
|
426
|
+
}
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
let start = op.start;
|
|
430
|
+
const end = op.end;
|
|
431
|
+
if (start < cursorX) {
|
|
432
|
+
start = cursorX;
|
|
433
|
+
}
|
|
434
|
+
if (start >= end) {
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
if (start !== cursorX) {
|
|
438
|
+
const move = chooseHorizontalMove(cursorX, start);
|
|
439
|
+
if (move.method === "abs") {
|
|
440
|
+
rowChunks.push(cursorToColumn(start));
|
|
441
|
+
}
|
|
442
|
+
else {
|
|
443
|
+
rowChunks.push(cursorForward(start - cursorX));
|
|
444
|
+
}
|
|
445
|
+
cursorX = start;
|
|
446
|
+
}
|
|
447
|
+
const result = nextBuffer.appendStyledRange(row, start, end, rowChunks, activeStyleId, cursorX);
|
|
448
|
+
cursorX = result.cursorX;
|
|
449
|
+
activeStyleId = result.activeStyleId;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
else {
|
|
453
|
+
for (const segment of segments) {
|
|
454
|
+
let segStart = segment.start;
|
|
455
|
+
const segEnd = Math.min(width, segment.end);
|
|
456
|
+
if (segStart < cursorX) {
|
|
457
|
+
segStart = cursorX;
|
|
458
|
+
}
|
|
459
|
+
if (segStart >= segEnd) {
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
const delta = segStart - cursorX;
|
|
463
|
+
if (delta > 0) {
|
|
464
|
+
rowChunks.push(cursorForward(delta));
|
|
465
|
+
cursorX = segStart;
|
|
466
|
+
}
|
|
467
|
+
else if (delta < 0) {
|
|
468
|
+
rowChunks.push(cursorToColumn(segStart));
|
|
469
|
+
cursorX = segStart;
|
|
470
|
+
}
|
|
471
|
+
const result = nextBuffer.appendStyledRange(row, segStart, segEnd, rowChunks, activeStyleId, cursorX);
|
|
472
|
+
cursorX = result.cursorX;
|
|
473
|
+
activeStyleId = result.activeStyleId;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
activeStyleId = nextBuffer.appendCloseActiveStyle(rowChunks, activeStyleId);
|
|
477
|
+
if (tailClear) {
|
|
478
|
+
if (cursorX !== nextEnd) {
|
|
479
|
+
const move = chooseHorizontalMove(cursorX, nextEnd);
|
|
480
|
+
if (move.method === "abs") {
|
|
481
|
+
rowChunks.push(cursorToColumn(nextEnd));
|
|
482
|
+
}
|
|
483
|
+
else {
|
|
484
|
+
rowChunks.push(cursorForward(Math.max(0, nextEnd - cursorX)));
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
rowChunks.push(ansiEscapes.eraseEndLine);
|
|
488
|
+
}
|
|
489
|
+
rowChunks.push(ansiEscapes.cursorNextLine);
|
|
490
|
+
}
|
|
491
|
+
if (!hasDiff && nextCount === prevCount) {
|
|
492
|
+
previousHeight = nextHeight;
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
const chunks = [];
|
|
496
|
+
if (nextCount < prevCount) {
|
|
497
|
+
chunks.push(ansiEscapes.eraseLines(prevCount - nextCount + 1));
|
|
498
|
+
chunks.push(ansiEscapes.cursorUp(visibleCount));
|
|
499
|
+
}
|
|
500
|
+
else {
|
|
501
|
+
chunks.push(ansiEscapes.cursorUp(prevCount - 1));
|
|
502
|
+
}
|
|
503
|
+
chunks.push(...rowChunks);
|
|
504
|
+
stream.write(chunks.join(""));
|
|
505
|
+
previousHeight = nextHeight;
|
|
506
|
+
});
|
|
507
|
+
render.clear = () => {
|
|
508
|
+
stream.write(ansiEscapes.eraseLines(previousFrameLineCount()));
|
|
509
|
+
previousHeight = 0;
|
|
510
|
+
};
|
|
511
|
+
render.reset = () => {
|
|
512
|
+
previousHeight = 0;
|
|
513
|
+
};
|
|
514
|
+
render.sync = (buffer) => {
|
|
515
|
+
previousHeight = buffer.height;
|
|
516
|
+
};
|
|
517
|
+
render.done = () => {
|
|
518
|
+
previousHeight = 0;
|
|
519
|
+
if (!showCursor && hasHiddenCursor) {
|
|
520
|
+
cliCursor.show(stream);
|
|
521
|
+
hasHiddenCursor = false;
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
return render;
|
|
525
|
+
};
|
|
526
|
+
export const cellLogUpdateRun = { create };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output implementation that writes directly into CellBuffer.
|
|
3
|
+
*/
|
|
4
|
+
import { type CellBuffer } from "./cell-buffer.js";
|
|
5
|
+
import { type Clip, type OutputLike, type OutputWriteOptions } from "./output.js";
|
|
6
|
+
import { type AnsiCode } from "../types/ansi.js";
|
|
7
|
+
export declare class CellOutput implements OutputLike {
|
|
8
|
+
private readonly clips;
|
|
9
|
+
private readonly styleIdByRef;
|
|
10
|
+
private lastStyleRef;
|
|
11
|
+
private lastStyleId;
|
|
12
|
+
readonly buffer: CellBuffer;
|
|
13
|
+
constructor(buffer: CellBuffer);
|
|
14
|
+
clip(clip: Clip): void;
|
|
15
|
+
unclip(): void;
|
|
16
|
+
fill(x: number, y: number, length: number, char: string, charWidth: number, styles: AnsiCode[]): void;
|
|
17
|
+
private resolveStyleId;
|
|
18
|
+
private writePlainLine;
|
|
19
|
+
write(x: number, y: number, text: string, options: OutputWriteOptions): void;
|
|
20
|
+
}
|