@nuvin/ink 7.5.0-alpha → 7.7.0-alpha
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/build/allocated-seek.d.ts +21 -0
- package/build/allocated-seek.js +273 -0
- package/build/allocated-seek.js.map +1 -0
- package/build/ansi-tokenizer.js.map +1 -1
- package/build/components/App.d.ts +5 -1
- package/build/components/App.js +48 -10
- package/build/components/App.js.map +1 -1
- package/build/components/Box.d.ts +95 -0
- package/build/components/Box.js +176 -140
- package/build/components/Box.js.map +1 -1
- package/build/components/Transform.js +1 -1
- package/build/components/Transform.js.map +1 -1
- package/build/components/VLBox.d.ts +87 -0
- package/build/components/VLBox.js +5 -0
- package/build/components/VLBox.js.map +1 -0
- package/build/dom.d.ts +45 -1
- package/build/dom.js +5 -0
- package/build/dom.js.map +1 -1
- package/build/explicit-width-detection.d.ts +45 -0
- package/build/explicit-width-detection.js +172 -0
- package/build/explicit-width-detection.js.map +1 -0
- package/build/explicit-width.d.ts +1 -0
- package/build/explicit-width.js +114 -0
- package/build/explicit-width.js.map +1 -0
- package/build/hooks/use-input.js +3 -3
- package/build/hooks/use-input.js.map +1 -1
- package/build/index.d.ts +2 -0
- package/build/index.js +1 -0
- package/build/index.js.map +1 -1
- package/build/ink.d.ts +45 -2
- package/build/ink.js +691 -164
- package/build/ink.js.map +1 -1
- package/build/input-parser.d.ts +14 -1
- package/build/input-parser.js +74 -4
- package/build/input-parser.js.map +1 -1
- package/build/layout-metadata.d.ts +28 -0
- package/build/layout-metadata.js +302 -0
- package/build/layout-metadata.js.map +1 -0
- package/build/log-update.d.ts +16 -8
- package/build/log-update.js +439 -37
- package/build/log-update.js.map +1 -1
- package/build/measure-text.d.ts +1 -1
- package/build/measure-text.js +2 -2
- package/build/measure-text.js.map +1 -1
- package/build/output.d.ts +1 -0
- package/build/output.js +69 -28
- package/build/output.js.map +1 -1
- package/build/reconciler.js +84 -10
- package/build/reconciler.js.map +1 -1
- package/build/render-background.d.ts +2 -2
- package/build/render-background.js +33 -5
- package/build/render-background.js.map +1 -1
- package/build/render-border.d.ts +2 -2
- package/build/render-border.js +78 -34
- package/build/render-border.js.map +1 -1
- package/build/render-node-to-output.d.ts +7 -1
- package/build/render-node-to-output.js +118 -27
- package/build/render-node-to-output.js.map +1 -1
- package/build/render-to-string.js +2 -1
- package/build/render-to-string.js.map +1 -1
- package/build/render.d.ts +11 -0
- package/build/render.js.map +1 -1
- package/build/renderer.d.ts +1 -0
- package/build/renderer.js +10 -2
- package/build/renderer.js.map +1 -1
- package/build/styles.js.map +1 -1
- package/build/terminal-control.d.ts +6 -0
- package/build/terminal-control.js +124 -0
- package/build/terminal-control.js.map +1 -0
- package/build/terminal-paint.d.ts +38 -0
- package/build/terminal-paint.js +47 -0
- package/build/terminal-paint.js.map +1 -0
- package/build/wrap-text.d.ts +1 -1
- package/build/wrap-text.js +2 -2
- package/build/wrap-text.js.map +1 -1
- package/package.json +3 -1
- package/readme.md +98 -0
- package/build/reflow.d.ts +0 -12
- package/build/reflow.js +0 -47
- package/build/reflow.js.map +0 -1
package/build/log-update.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import process from 'node:process';
|
|
1
2
|
import ansiEscapes from 'ansi-escapes';
|
|
2
3
|
import cliCursor from 'cli-cursor';
|
|
3
4
|
import { cursorPositionChanged, getOutputCursorRow, buildCursorSuffix, buildCursorOnlySequence, buildReturnToBottomPrefix, hideCursorEscape, showCursorEscape, } from './cursor-helpers.js';
|
|
5
|
+
import { createSeekPreparer } from './allocated-seek.js';
|
|
6
|
+
import { isSeekViewport, clampSeekCursor, buildSeekCursorSequence, encodeSeekRows, wrapSeekPaint, decawmOn, eraseDisplayHome, } from './terminal-paint.js';
|
|
4
7
|
// Count visible lines in a string, ignoring the trailing empty element
|
|
5
8
|
// that `split('\n')` produces when the string ends with '\n'.
|
|
6
9
|
const visibleLineCount = (lines, str) => str.endsWith('\n') ? lines.length - 1 : lines.length;
|
|
@@ -10,7 +13,103 @@ const getEraseLineCount = (cachedLineCount, options) => {
|
|
|
10
13
|
}
|
|
11
14
|
return options?.eraseLineCount ?? cachedLineCount;
|
|
12
15
|
};
|
|
13
|
-
|
|
16
|
+
// Maximum shift distance (rows) considered by scroll-region detection. Real
|
|
17
|
+
// wheel/keyboard scrolls move a handful of rows per commit; larger jumps
|
|
18
|
+
// (PageUp/PageDown) rarely share half the frame and fall through to the
|
|
19
|
+
// ordinary diff anyway.
|
|
20
|
+
const maxScrollShift = 32;
|
|
21
|
+
// Minimum fraction of the frame that must be explained by a pure vertical
|
|
22
|
+
// shift for the scroll-region fast path to engage. Updates that merely resemble
|
|
23
|
+
// a shift (streaming appends, selection highlights, spinner ticks) stay below
|
|
24
|
+
// the threshold and use the ordinary diff.
|
|
25
|
+
const minShiftedCoreRatio = 0.5;
|
|
26
|
+
const setScrollRegion = (start, end) => `\u001B[${start + 1};${end}r`;
|
|
27
|
+
const resetScrollRegion = '\u001B[r';
|
|
28
|
+
const cursorToRow = (row) => `\u001B[${row + 1};1H`;
|
|
29
|
+
const blankLines = (count) => Array.from({ length: count }, () => '');
|
|
30
|
+
const prefixUnchanged = (previousLines, nextLines, start) => {
|
|
31
|
+
for (let index = 0; index < start; index++) {
|
|
32
|
+
if (nextLines[index] !== previousLines[index]) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return true;
|
|
37
|
+
};
|
|
38
|
+
const suffixUnchanged = (previousLines, nextLines, end) => {
|
|
39
|
+
for (let index = end; index < nextLines.length; index++) {
|
|
40
|
+
if (nextLines[index] !== previousLines[index])
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
return true;
|
|
44
|
+
};
|
|
45
|
+
// Detect whether `nextLines` is `previousLines` shifted vertically by k rows
|
|
46
|
+
// with at most a small band of changed edge lines. Only exact whole-line
|
|
47
|
+
// equality counts (same semantics as the existing positional diff).
|
|
48
|
+
const detectScrollShift = (previousLines, nextLines) => {
|
|
49
|
+
const height = nextLines.length;
|
|
50
|
+
if (height < 4 || height !== previousLines.length) {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
const required = Math.ceil(height * minShiftedCoreRatio);
|
|
54
|
+
for (let rows = 1; rows <= Math.min(maxScrollShift, height - 1); rows++) {
|
|
55
|
+
// Content moved up: nextLines[i] === previousLines[i + rows] over the
|
|
56
|
+
// overlap [0, height - rows); the bottom `rows` lines are new.
|
|
57
|
+
let up = 0;
|
|
58
|
+
let upFirst = -1;
|
|
59
|
+
let upLast = -1;
|
|
60
|
+
for (let i = 0; i < height - rows; i++) {
|
|
61
|
+
if (nextLines[i] !== previousLines[i + rows]) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
up++;
|
|
65
|
+
if (nextLines[i] === previousLines[i]) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
upFirst = upFirst < 0 ? i : upFirst;
|
|
69
|
+
upLast = i;
|
|
70
|
+
}
|
|
71
|
+
const upEnd = upLast + rows + 1;
|
|
72
|
+
if (up >= required &&
|
|
73
|
+
upFirst >= 0 &&
|
|
74
|
+
upLast - upFirst + 1 >= required &&
|
|
75
|
+
upEnd <= height &&
|
|
76
|
+
upEnd - upFirst > rows &&
|
|
77
|
+
prefixUnchanged(previousLines, nextLines, upFirst) &&
|
|
78
|
+
suffixUnchanged(previousLines, nextLines, upEnd)) {
|
|
79
|
+
return { rows, start: upFirst, end: upEnd };
|
|
80
|
+
}
|
|
81
|
+
// Content moved down: nextLines[i] === previousLines[i - rows]; the top
|
|
82
|
+
// `rows` lines of the shifted band are new.
|
|
83
|
+
let down = 0;
|
|
84
|
+
let downFirst = -1;
|
|
85
|
+
let downLast = -1;
|
|
86
|
+
for (let i = rows; i < height; i++) {
|
|
87
|
+
if (nextLines[i] !== previousLines[i - rows]) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
down++;
|
|
91
|
+
if (nextLines[i] === previousLines[i]) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
downFirst = downFirst < 0 ? i : downFirst;
|
|
95
|
+
downLast = i;
|
|
96
|
+
}
|
|
97
|
+
const downStart = downFirst - rows;
|
|
98
|
+
const downEnd = downLast + 1;
|
|
99
|
+
if (down >= required &&
|
|
100
|
+
downFirst >= 0 &&
|
|
101
|
+
downLast - downFirst + 1 >= required &&
|
|
102
|
+
downStart >= 0 &&
|
|
103
|
+
downEnd <= height &&
|
|
104
|
+
downEnd - downStart > rows &&
|
|
105
|
+
prefixUnchanged(previousLines, nextLines, downStart) &&
|
|
106
|
+
suffixUnchanged(previousLines, nextLines, downEnd)) {
|
|
107
|
+
return { rows: -rows, start: downStart, end: downEnd };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return undefined;
|
|
111
|
+
};
|
|
112
|
+
const createStandard = (stream, { showCursor = false, transformOutput } = {}) => {
|
|
14
113
|
let previousLineCount = 0;
|
|
15
114
|
let previousOutput = '';
|
|
16
115
|
let hasHiddenCursor = false;
|
|
@@ -54,7 +153,7 @@ const createStandard = (stream, { showCursor = false } = {}) => {
|
|
|
54
153
|
const returnPrefix = buildReturnToBottomPrefix(cursorWasShown, previousLineCount, previousCursorPosition);
|
|
55
154
|
stream.write(returnPrefix +
|
|
56
155
|
ansiEscapes.eraseLines(getEraseLineCount(previousLineCount, options)) +
|
|
57
|
-
str +
|
|
156
|
+
(transformOutput ? transformOutput(str) : str) +
|
|
58
157
|
cursorSuffix);
|
|
59
158
|
previousLineCount = lines.length;
|
|
60
159
|
}
|
|
@@ -62,8 +161,8 @@ const createStandard = (stream, { showCursor = false } = {}) => {
|
|
|
62
161
|
cursorWasShown = activeCursor !== undefined;
|
|
63
162
|
return true;
|
|
64
163
|
};
|
|
65
|
-
const render = (str) => writeFrame(str, false);
|
|
66
|
-
render.repaint = (str, options) => writeFrame(str, true, options);
|
|
164
|
+
const render = (str, _context) => writeFrame(str, false);
|
|
165
|
+
render.repaint = (str, options, _context) => writeFrame(str, true, options);
|
|
67
166
|
render.clear = (options) => {
|
|
68
167
|
const prefix = buildReturnToBottomPrefix(cursorWasShown, previousLineCount, previousCursorPosition);
|
|
69
168
|
stream.write(prefix +
|
|
@@ -98,7 +197,7 @@ const createStandard = (stream, { showCursor = false } = {}) => {
|
|
|
98
197
|
cursorWasShown = false;
|
|
99
198
|
cursorDirty = false;
|
|
100
199
|
};
|
|
101
|
-
render.sync = (str) => {
|
|
200
|
+
render.sync = (str, _physical) => {
|
|
102
201
|
const activeCursor = cursorPosition;
|
|
103
202
|
cursorDirty = false;
|
|
104
203
|
const lines = str.split('\n');
|
|
@@ -118,10 +217,14 @@ const createStandard = (stream, { showCursor = false } = {}) => {
|
|
|
118
217
|
cursorDirty = true;
|
|
119
218
|
};
|
|
120
219
|
render.isCursorDirty = () => cursorDirty;
|
|
121
|
-
render.willRender = (str) => hasChanges(str, getActiveCursor());
|
|
220
|
+
render.willRender = (str, _context) => hasChanges(str, getActiveCursor());
|
|
221
|
+
render.getPhysicalFrame = () => undefined;
|
|
222
|
+
render.restoreTerminalModes = () => {
|
|
223
|
+
// Standard rendering never acquires seek terminal modes.
|
|
224
|
+
};
|
|
122
225
|
return render;
|
|
123
226
|
};
|
|
124
|
-
const createIncremental = (stream, { showCursor = false } = {}) => {
|
|
227
|
+
const createIncremental = (stream, { showCursor = false, transformOutput } = {}) => {
|
|
125
228
|
let previousLines = [];
|
|
126
229
|
let previousOutput = '';
|
|
127
230
|
let hasHiddenCursor = false;
|
|
@@ -129,12 +232,221 @@ const createIncremental = (stream, { showCursor = false } = {}) => {
|
|
|
129
232
|
let cursorDirty = false;
|
|
130
233
|
let previousCursorPosition;
|
|
131
234
|
let cursorWasShown = false;
|
|
235
|
+
let physicalFrame;
|
|
236
|
+
let previousIdentities = [];
|
|
237
|
+
let ownsDecawm = false;
|
|
238
|
+
let writeGeneration = 0;
|
|
239
|
+
let paintedSeekViewport = false;
|
|
240
|
+
const seekPreparer = createSeekPreparer();
|
|
241
|
+
let streamErrorHandler;
|
|
242
|
+
let handlingAsyncWriteError = false;
|
|
132
243
|
const getActiveCursor = () => cursorPosition;
|
|
133
244
|
const hasChanges = (str, activeCursor) => {
|
|
134
245
|
const cursorChanged = cursorPositionChanged(activeCursor, previousCursorPosition);
|
|
135
246
|
return str !== previousOutput || cursorChanged;
|
|
136
247
|
};
|
|
137
|
-
const
|
|
248
|
+
const bumpGeneration = () => {
|
|
249
|
+
writeGeneration++;
|
|
250
|
+
};
|
|
251
|
+
const invalidatePhysical = () => {
|
|
252
|
+
if (physicalFrame === undefined) {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
physicalFrame = {
|
|
256
|
+
...physicalFrame,
|
|
257
|
+
valid: false,
|
|
258
|
+
ownsViewport: false,
|
|
259
|
+
};
|
|
260
|
+
};
|
|
261
|
+
const canWriteStream = () => {
|
|
262
|
+
const candidate = stream;
|
|
263
|
+
return (!candidate.destroyed &&
|
|
264
|
+
!candidate.writableEnded &&
|
|
265
|
+
(candidate.writable ?? true));
|
|
266
|
+
};
|
|
267
|
+
const restoreTerminalModes = () => {
|
|
268
|
+
if (!ownsDecawm) {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
if (canWriteStream()) {
|
|
272
|
+
try {
|
|
273
|
+
stream.write(decawmOn);
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
// Best-effort autowrap restore.
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
ownsDecawm = false;
|
|
280
|
+
};
|
|
281
|
+
const handleAsyncWriteError = () => {
|
|
282
|
+
if (handlingAsyncWriteError) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
handlingAsyncWriteError = true;
|
|
286
|
+
bumpGeneration();
|
|
287
|
+
invalidatePhysical();
|
|
288
|
+
previousIdentities = [];
|
|
289
|
+
previousOutput = '';
|
|
290
|
+
previousLines = [];
|
|
291
|
+
// A queued successful write can establish the next ordered diff base,
|
|
292
|
+
// but an async error invalidates it; it is not proof that a terminal
|
|
293
|
+
// displayed the frame.
|
|
294
|
+
if (paintedSeekViewport) {
|
|
295
|
+
ownsDecawm = true;
|
|
296
|
+
}
|
|
297
|
+
restoreTerminalModes();
|
|
298
|
+
};
|
|
299
|
+
const detachStreamErrorListener = () => {
|
|
300
|
+
if (!streamErrorHandler) {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
stream.off('error', streamErrorHandler);
|
|
304
|
+
streamErrorHandler = undefined;
|
|
305
|
+
};
|
|
306
|
+
const attachStreamErrorListener = () => {
|
|
307
|
+
if (streamErrorHandler) {
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
streamErrorHandler = () => {
|
|
311
|
+
handleAsyncWriteError();
|
|
312
|
+
};
|
|
313
|
+
stream.on('error', streamErrorHandler);
|
|
314
|
+
};
|
|
315
|
+
const copyContext = (context) => ({
|
|
316
|
+
strategy: context.strategy,
|
|
317
|
+
columns: context.columns,
|
|
318
|
+
rows: context.rows,
|
|
319
|
+
});
|
|
320
|
+
const seekHasChanges = (str, context, activeCursor) => {
|
|
321
|
+
if (physicalFrame === undefined ||
|
|
322
|
+
!physicalFrame.valid ||
|
|
323
|
+
physicalFrame.context.strategy !== context.strategy ||
|
|
324
|
+
physicalFrame.context.columns !== context.columns ||
|
|
325
|
+
physicalFrame.context.rows !== context.rows ||
|
|
326
|
+
str !== previousOutput) {
|
|
327
|
+
return true;
|
|
328
|
+
}
|
|
329
|
+
return cursorPositionChanged(activeCursor, previousCursorPosition);
|
|
330
|
+
};
|
|
331
|
+
const submitSeekWrite = (payload, { decawm, onAccept, }) => {
|
|
332
|
+
const generation = ++writeGeneration;
|
|
333
|
+
handlingAsyncWriteError = false;
|
|
334
|
+
if (decawm) {
|
|
335
|
+
ownsDecawm = true;
|
|
336
|
+
// Seek cells may already be on the stream; keep teardown ownership
|
|
337
|
+
// through throw/reentry. Physical/diff still publish only on accept.
|
|
338
|
+
paintedSeekViewport = true;
|
|
339
|
+
}
|
|
340
|
+
attachStreamErrorListener();
|
|
341
|
+
try {
|
|
342
|
+
stream.write(payload, error => {
|
|
343
|
+
if (error) {
|
|
344
|
+
handleAsyncWriteError();
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
if (decawm) {
|
|
348
|
+
ownsDecawm = false;
|
|
349
|
+
}
|
|
350
|
+
// A queued successful write can establish the next ordered diff base,
|
|
351
|
+
// but an async error invalidates it; it is not proof that a terminal
|
|
352
|
+
// displayed the frame.
|
|
353
|
+
if (generation === writeGeneration) {
|
|
354
|
+
onAccept();
|
|
355
|
+
}
|
|
356
|
+
return true;
|
|
357
|
+
}
|
|
358
|
+
catch (error) {
|
|
359
|
+
invalidatePhysical();
|
|
360
|
+
previousIdentities = [];
|
|
361
|
+
previousOutput = '';
|
|
362
|
+
previousLines = [];
|
|
363
|
+
if (decawm) {
|
|
364
|
+
ownsDecawm = true;
|
|
365
|
+
}
|
|
366
|
+
restoreTerminalModes();
|
|
367
|
+
throw error;
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
const writeSeekFrame = (str, context, force) => {
|
|
371
|
+
const activeCursor = getActiveCursor();
|
|
372
|
+
cursorDirty = false;
|
|
373
|
+
if (!force && !seekHasChanges(str, context, activeCursor)) {
|
|
374
|
+
return false;
|
|
375
|
+
}
|
|
376
|
+
const cursorChanged = cursorPositionChanged(activeCursor, previousCursorPosition);
|
|
377
|
+
const dimensionsChanged = physicalFrame === undefined ||
|
|
378
|
+
!physicalFrame.valid ||
|
|
379
|
+
physicalFrame.context.columns !== context.columns ||
|
|
380
|
+
physicalFrame.context.rows !== context.rows ||
|
|
381
|
+
physicalFrame.context.strategy !== context.strategy;
|
|
382
|
+
if (!force &&
|
|
383
|
+
!dimensionsChanged &&
|
|
384
|
+
str === previousOutput &&
|
|
385
|
+
cursorChanged) {
|
|
386
|
+
const payload = buildSeekCursorSequence(activeCursor, context.columns, context.rows, cursorWasShown);
|
|
387
|
+
return submitSeekWrite(payload, {
|
|
388
|
+
decawm: false,
|
|
389
|
+
onAccept() {
|
|
390
|
+
previousCursorPosition = activeCursor ? { ...activeCursor } : undefined;
|
|
391
|
+
cursorWasShown = activeCursor !== undefined;
|
|
392
|
+
if (physicalFrame?.valid) {
|
|
393
|
+
physicalFrame = {
|
|
394
|
+
...physicalFrame,
|
|
395
|
+
cursor: clampSeekCursor(activeCursor, context.columns, context.rows),
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
},
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
const prepared = seekPreparer.prepareFrame(str, context.columns);
|
|
402
|
+
const viewportCount = Math.min(prepared.length, context.rows);
|
|
403
|
+
const viewportPrepared = prepared.slice(0, viewportCount);
|
|
404
|
+
const paintAll = force || dimensionsChanged;
|
|
405
|
+
const indexes = [];
|
|
406
|
+
if (paintAll) {
|
|
407
|
+
for (let index = 0; index < viewportCount; index++) {
|
|
408
|
+
indexes.push(index);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
for (let index = 0; index < viewportCount; index++) {
|
|
413
|
+
if (viewportPrepared[index].identity !== previousIdentities[index]) {
|
|
414
|
+
indexes.push(index);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
const encodedRows = encodeSeekRows(viewportPrepared, indexes, context.columns);
|
|
419
|
+
const cursorSuffix = buildSeekCursorSequence(activeCursor, context.columns, context.rows, cursorWasShown);
|
|
420
|
+
const payload = encodedRows.length > 0
|
|
421
|
+
? wrapSeekPaint(encodedRows + cursorSuffix)
|
|
422
|
+
: cursorSuffix;
|
|
423
|
+
if (payload.length === 0) {
|
|
424
|
+
return false;
|
|
425
|
+
}
|
|
426
|
+
return submitSeekWrite(payload, {
|
|
427
|
+
decawm: encodedRows.length > 0,
|
|
428
|
+
onAccept() {
|
|
429
|
+
previousOutput = str;
|
|
430
|
+
previousLines = str.split('\n');
|
|
431
|
+
previousIdentities = viewportPrepared.map(row => row.identity);
|
|
432
|
+
previousCursorPosition = activeCursor ? { ...activeCursor } : undefined;
|
|
433
|
+
cursorWasShown = activeCursor !== undefined;
|
|
434
|
+
paintedSeekViewport = true;
|
|
435
|
+
physicalFrame = {
|
|
436
|
+
context: copyContext(context),
|
|
437
|
+
ownsViewport: true,
|
|
438
|
+
valid: true,
|
|
439
|
+
logicalRows: viewportPrepared.map(row => row.logical),
|
|
440
|
+
allocatedWidths: viewportPrepared.map(row => row.allocatedWidth),
|
|
441
|
+
cursor: clampSeekCursor(activeCursor, context.columns, context.rows),
|
|
442
|
+
};
|
|
443
|
+
},
|
|
444
|
+
});
|
|
445
|
+
};
|
|
446
|
+
const render = (str, context) => {
|
|
447
|
+
if (isSeekViewport(context)) {
|
|
448
|
+
return writeSeekFrame(str, context, false);
|
|
449
|
+
}
|
|
138
450
|
if (!showCursor && !hasHiddenCursor) {
|
|
139
451
|
cliCursor.hide(stream);
|
|
140
452
|
hasHiddenCursor = true;
|
|
@@ -169,7 +481,7 @@ const createIncremental = (stream, { showCursor = false } = {}) => {
|
|
|
169
481
|
const cursorSuffix = buildCursorSuffix(outputCursorRow, activeCursor);
|
|
170
482
|
stream.write(returnPrefix +
|
|
171
483
|
ansiEscapes.eraseLines(previousLines.length) +
|
|
172
|
-
str +
|
|
484
|
+
(transformOutput ? transformOutput(str) : str) +
|
|
173
485
|
cursorSuffix);
|
|
174
486
|
cursorWasShown = activeCursor !== undefined;
|
|
175
487
|
previousCursorPosition = activeCursor ? { ...activeCursor } : undefined;
|
|
@@ -181,8 +493,33 @@ const createIncremental = (stream, { showCursor = false } = {}) => {
|
|
|
181
493
|
// We aggregate all chunks for incremental rendering into a buffer, and then write them to stdout at the end.
|
|
182
494
|
const buffer = [];
|
|
183
495
|
buffer.push(returnPrefix);
|
|
184
|
-
|
|
185
|
-
|
|
496
|
+
const prevVisible = previousLines.slice(0, previousVisible);
|
|
497
|
+
const nextVisible = nextLines.slice(0, visibleCount);
|
|
498
|
+
const shift = process.env['NUVIN_INK_NO_SCROLL_OPT'] === '1' ||
|
|
499
|
+
activeCursor !== undefined ||
|
|
500
|
+
hasTrailingNewline ||
|
|
501
|
+
previousOutput.endsWith('\n') ||
|
|
502
|
+
visibleCount !== previousVisible
|
|
503
|
+
? undefined
|
|
504
|
+
: detectScrollShift(prevVisible, nextVisible);
|
|
505
|
+
let diffPrevious = previousLines;
|
|
506
|
+
let loopStart = 0;
|
|
507
|
+
if (shift) {
|
|
508
|
+
const k = Math.abs(shift.rows);
|
|
509
|
+
const { start, end } = shift;
|
|
510
|
+
const prefix = prevVisible.slice(0, start);
|
|
511
|
+
const band = prevVisible.slice(start, end);
|
|
512
|
+
const suffix = prevVisible.slice(end);
|
|
513
|
+
const shiftedBand = shift.rows > 0
|
|
514
|
+
? [...band.slice(k), ...blankLines(k)]
|
|
515
|
+
: [...blankLines(k), ...band.slice(0, band.length - k)];
|
|
516
|
+
const shiftedPrevious = [...prefix, ...shiftedBand, ...suffix];
|
|
517
|
+
buffer.push(setScrollRegion(start, end), cursorToRow(start), shift.rows > 0 ? `\u001B[${k}M` : `\u001B[${k}L`, resetScrollRegion, cursorToRow(start));
|
|
518
|
+
diffPrevious = shiftedPrevious;
|
|
519
|
+
loopStart = start;
|
|
520
|
+
}
|
|
521
|
+
else if (visibleCount < previousVisible) {
|
|
522
|
+
// Clear extra lines if the current content's line count is lower than the previous.
|
|
186
523
|
const previousHadTrailingNewline = previousOutput.endsWith('\n');
|
|
187
524
|
const extraSlot = previousHadTrailingNewline ? 1 : 0;
|
|
188
525
|
buffer.push(ansiEscapes.eraseLines(previousVisible - visibleCount + extraSlot), ansiEscapes.cursorUp(visibleCount));
|
|
@@ -190,10 +527,10 @@ const createIncremental = (stream, { showCursor = false } = {}) => {
|
|
|
190
527
|
else {
|
|
191
528
|
buffer.push(ansiEscapes.cursorUp(previousLines.length - 1));
|
|
192
529
|
}
|
|
193
|
-
for (let i =
|
|
530
|
+
for (let i = loopStart; i < visibleCount; i++) {
|
|
194
531
|
const isLastLine = i === visibleCount - 1;
|
|
195
532
|
// We do not write lines if the contents are the same. This prevents flickering during renders.
|
|
196
|
-
if (nextLines[i] ===
|
|
533
|
+
if (nextLines[i] === diffPrevious[i]) {
|
|
197
534
|
// Don't move past the last line when there's no trailing newline,
|
|
198
535
|
// otherwise the cursor overshoots the rendered block.
|
|
199
536
|
if (!isLastLine || hasTrailingNewline) {
|
|
@@ -202,7 +539,7 @@ const createIncremental = (stream, { showCursor = false } = {}) => {
|
|
|
202
539
|
continue;
|
|
203
540
|
}
|
|
204
541
|
buffer.push(ansiEscapes.cursorTo(0) +
|
|
205
|
-
nextLines[i] +
|
|
542
|
+
(transformOutput ? transformOutput(nextLines[i]) : nextLines[i]) +
|
|
206
543
|
ansiEscapes.eraseEndLine +
|
|
207
544
|
// Don't append newline after the last line when the input
|
|
208
545
|
// has no trailing newline (fullscreen mode).
|
|
@@ -217,7 +554,10 @@ const createIncremental = (stream, { showCursor = false } = {}) => {
|
|
|
217
554
|
previousLines = nextLines;
|
|
218
555
|
return true;
|
|
219
556
|
};
|
|
220
|
-
render.repaint = (str, options) => {
|
|
557
|
+
render.repaint = (str, options, context) => {
|
|
558
|
+
if (isSeekViewport(context)) {
|
|
559
|
+
return writeSeekFrame(str, context, true);
|
|
560
|
+
}
|
|
221
561
|
if (!showCursor && !hasHiddenCursor) {
|
|
222
562
|
cliCursor.hide(stream);
|
|
223
563
|
hasHiddenCursor = true;
|
|
@@ -230,7 +570,7 @@ const createIncremental = (stream, { showCursor = false } = {}) => {
|
|
|
230
570
|
const cursorSuffix = buildCursorSuffix(outputCursorRow, activeCursor);
|
|
231
571
|
stream.write(returnPrefix +
|
|
232
572
|
ansiEscapes.eraseLines(getEraseLineCount(previousLines.length, options)) +
|
|
233
|
-
str +
|
|
573
|
+
(transformOutput ? transformOutput(str) : str) +
|
|
234
574
|
cursorSuffix);
|
|
235
575
|
previousOutput = str;
|
|
236
576
|
previousLines = nextLines;
|
|
@@ -238,46 +578,101 @@ const createIncremental = (stream, { showCursor = false } = {}) => {
|
|
|
238
578
|
cursorWasShown = activeCursor !== undefined;
|
|
239
579
|
return true;
|
|
240
580
|
};
|
|
241
|
-
|
|
242
|
-
const prefix = buildReturnToBottomPrefix(cursorWasShown, previousLines.length, previousCursorPosition);
|
|
243
|
-
stream.write(prefix +
|
|
244
|
-
ansiEscapes.eraseLines(getEraseLineCount(previousLines.length, options)));
|
|
581
|
+
const clearSeekState = () => {
|
|
245
582
|
previousOutput = '';
|
|
246
583
|
previousLines = [];
|
|
584
|
+
previousIdentities = [];
|
|
247
585
|
previousCursorPosition = undefined;
|
|
248
586
|
cursorWasShown = false;
|
|
249
587
|
cursorDirty = false;
|
|
588
|
+
paintedSeekViewport = false;
|
|
589
|
+
invalidatePhysical();
|
|
590
|
+
};
|
|
591
|
+
render.clear = (options) => {
|
|
592
|
+
bumpGeneration();
|
|
593
|
+
restoreTerminalModes();
|
|
594
|
+
if (paintedSeekViewport) {
|
|
595
|
+
stream.write(eraseDisplayHome);
|
|
596
|
+
clearSeekState();
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
const prefix = buildReturnToBottomPrefix(cursorWasShown, previousLines.length, previousCursorPosition);
|
|
600
|
+
stream.write(prefix +
|
|
601
|
+
ansiEscapes.eraseLines(getEraseLineCount(previousLines.length, options)));
|
|
602
|
+
clearSeekState();
|
|
250
603
|
};
|
|
251
604
|
render.done = () => {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
605
|
+
bumpGeneration();
|
|
606
|
+
restoreTerminalModes();
|
|
607
|
+
detachStreamErrorListener();
|
|
608
|
+
try {
|
|
609
|
+
if (paintedSeekViewport) {
|
|
610
|
+
if (showCursor) {
|
|
611
|
+
stream.write(showCursorEscape);
|
|
612
|
+
}
|
|
613
|
+
clearSeekState();
|
|
614
|
+
cursorPosition = undefined;
|
|
615
|
+
if (!showCursor) {
|
|
616
|
+
cliCursor.show(stream);
|
|
617
|
+
hasHiddenCursor = false;
|
|
618
|
+
}
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
const returnPrefix = buildReturnToBottomPrefix(cursorWasShown, previousLines.length, previousCursorPosition);
|
|
622
|
+
if (returnPrefix || showCursor) {
|
|
623
|
+
stream.write(returnPrefix + (showCursor ? showCursorEscape : ''));
|
|
624
|
+
}
|
|
625
|
+
clearSeekState();
|
|
626
|
+
cursorPosition = undefined;
|
|
627
|
+
if (!showCursor) {
|
|
628
|
+
cliCursor.show(stream);
|
|
629
|
+
hasHiddenCursor = false;
|
|
630
|
+
}
|
|
255
631
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
cursorWasShown = false;
|
|
260
|
-
cursorPosition = undefined;
|
|
261
|
-
cursorDirty = false;
|
|
262
|
-
if (!showCursor) {
|
|
263
|
-
cliCursor.show(stream);
|
|
264
|
-
hasHiddenCursor = false;
|
|
632
|
+
catch {
|
|
633
|
+
clearSeekState();
|
|
634
|
+
cursorPosition = undefined;
|
|
265
635
|
}
|
|
266
636
|
};
|
|
267
637
|
render.reset = () => {
|
|
268
638
|
// Cache-only: the caller must have externally reset or replaced terminal contents.
|
|
639
|
+
bumpGeneration();
|
|
269
640
|
previousOutput = '';
|
|
270
641
|
previousLines = [];
|
|
642
|
+
previousIdentities = [];
|
|
271
643
|
previousCursorPosition = undefined;
|
|
272
644
|
cursorWasShown = false;
|
|
273
645
|
cursorDirty = false;
|
|
646
|
+
invalidatePhysical();
|
|
274
647
|
};
|
|
275
|
-
render.sync = (str) => {
|
|
648
|
+
render.sync = (str, physical) => {
|
|
276
649
|
const activeCursor = cursorPosition;
|
|
277
650
|
cursorDirty = false;
|
|
278
651
|
const lines = str.split('\n');
|
|
279
652
|
previousOutput = str;
|
|
280
653
|
previousLines = lines;
|
|
654
|
+
const matching = physical?.valid === true &&
|
|
655
|
+
physical.ownsViewport &&
|
|
656
|
+
physical.context.strategy === 'seek-viewport' &&
|
|
657
|
+
physical.logicalRows.join('\n') === str;
|
|
658
|
+
if (matching && physical) {
|
|
659
|
+
physicalFrame = {
|
|
660
|
+
context: copyContext(physical.context),
|
|
661
|
+
ownsViewport: true,
|
|
662
|
+
valid: true,
|
|
663
|
+
logicalRows: physical.logicalRows,
|
|
664
|
+
allocatedWidths: physical.allocatedWidths,
|
|
665
|
+
cursor: physical.cursor,
|
|
666
|
+
};
|
|
667
|
+
previousIdentities = [];
|
|
668
|
+
paintedSeekViewport = true;
|
|
669
|
+
previousCursorPosition = activeCursor ? { ...activeCursor } : undefined;
|
|
670
|
+
cursorWasShown = activeCursor !== undefined;
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
previousIdentities = [];
|
|
674
|
+
paintedSeekViewport = false;
|
|
675
|
+
invalidatePhysical();
|
|
281
676
|
if (!activeCursor && cursorWasShown) {
|
|
282
677
|
stream.write(hideCursorEscape);
|
|
283
678
|
}
|
|
@@ -292,14 +687,21 @@ const createIncremental = (stream, { showCursor = false } = {}) => {
|
|
|
292
687
|
cursorDirty = true;
|
|
293
688
|
};
|
|
294
689
|
render.isCursorDirty = () => cursorDirty;
|
|
295
|
-
render.willRender = (str) =>
|
|
690
|
+
render.willRender = (str, context) => {
|
|
691
|
+
if (isSeekViewport(context)) {
|
|
692
|
+
return seekHasChanges(str, context, getActiveCursor());
|
|
693
|
+
}
|
|
694
|
+
return hasChanges(str, getActiveCursor());
|
|
695
|
+
};
|
|
696
|
+
render.getPhysicalFrame = () => physicalFrame;
|
|
697
|
+
render.restoreTerminalModes = restoreTerminalModes;
|
|
296
698
|
return render;
|
|
297
699
|
};
|
|
298
|
-
const create = (stream, { showCursor = false, incremental = false } = {}) => {
|
|
700
|
+
const create = (stream, { showCursor = false, incremental = false, transformOutput, } = {}) => {
|
|
299
701
|
if (incremental) {
|
|
300
|
-
return createIncremental(stream, { showCursor });
|
|
702
|
+
return createIncremental(stream, { showCursor, transformOutput });
|
|
301
703
|
}
|
|
302
|
-
return createStandard(stream, { showCursor });
|
|
704
|
+
return createStandard(stream, { showCursor, transformOutput });
|
|
303
705
|
};
|
|
304
706
|
const logUpdate = { create };
|
|
305
707
|
export default logUpdate;
|