@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/ink.js
CHANGED
|
@@ -16,72 +16,19 @@ import render from './renderer.js';
|
|
|
16
16
|
import * as dom from './dom.js';
|
|
17
17
|
import { hideCursorEscape, showCursorEscape } from './cursor-helpers.js';
|
|
18
18
|
import logUpdate from './log-update.js';
|
|
19
|
+
import { createExplicitWidthEncoder } from './explicit-width.js';
|
|
20
|
+
import { ExplicitWidthDetection, canOverwriteFirstCell, } from './explicit-width-detection.js';
|
|
19
21
|
import { bsu, esu, shouldSynchronize } from './write-synchronized.js';
|
|
22
|
+
import { isOwnedSeekViewport, isSeekViewport, seekViewportReset, selectPaintStrategy, } from './terminal-paint.js';
|
|
20
23
|
import instances from './instances.js';
|
|
21
24
|
import { TextSelectionController } from './text-selection-controller.js';
|
|
22
25
|
import App from './components/App.js';
|
|
23
26
|
import { accessibilityContext as AccessibilityContext } from './components/AccessibilityContext.js';
|
|
24
27
|
import { resolveFlags, } from './kitty-keyboard.js';
|
|
25
28
|
const noop = () => { };
|
|
26
|
-
const textEncoder = new TextEncoder();
|
|
27
29
|
const yieldImmediate = async () => new Promise(resolve => {
|
|
28
30
|
setImmediate(resolve);
|
|
29
31
|
});
|
|
30
|
-
const kittyQueryEscapeByte = 0x1b;
|
|
31
|
-
const kittyQueryOpenBracketByte = 0x5b;
|
|
32
|
-
const kittyQueryQuestionMarkByte = 0x3f;
|
|
33
|
-
const kittyQueryLetterByte = 0x75;
|
|
34
|
-
const zeroByte = 0x30;
|
|
35
|
-
const nineByte = 0x39;
|
|
36
|
-
const isDigitByte = (byte) => byte >= zeroByte && byte <= nineByte;
|
|
37
|
-
const matchKittyQueryResponse = (buffer, startIndex) => {
|
|
38
|
-
if (buffer[startIndex] !== kittyQueryEscapeByte ||
|
|
39
|
-
buffer[startIndex + 1] !== kittyQueryOpenBracketByte ||
|
|
40
|
-
buffer[startIndex + 2] !== kittyQueryQuestionMarkByte) {
|
|
41
|
-
return undefined;
|
|
42
|
-
}
|
|
43
|
-
let index = startIndex + 3;
|
|
44
|
-
const digitsStartIndex = index;
|
|
45
|
-
while (index < buffer.length && isDigitByte(buffer[index])) {
|
|
46
|
-
index++;
|
|
47
|
-
}
|
|
48
|
-
if (index === digitsStartIndex) {
|
|
49
|
-
return undefined;
|
|
50
|
-
}
|
|
51
|
-
if (index === buffer.length) {
|
|
52
|
-
return { state: 'partial' };
|
|
53
|
-
}
|
|
54
|
-
if (buffer[index] === kittyQueryLetterByte) {
|
|
55
|
-
return { state: 'complete', endIndex: index };
|
|
56
|
-
}
|
|
57
|
-
return undefined;
|
|
58
|
-
};
|
|
59
|
-
const hasCompleteKittyQueryResponse = (buffer) => {
|
|
60
|
-
for (let index = 0; index < buffer.length; index++) {
|
|
61
|
-
const match = matchKittyQueryResponse(buffer, index);
|
|
62
|
-
if (match?.state === 'complete') {
|
|
63
|
-
return true;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
return false;
|
|
67
|
-
};
|
|
68
|
-
const stripKittyQueryResponsesAndTrailingPartial = (buffer) => {
|
|
69
|
-
const keptBytes = [];
|
|
70
|
-
let index = 0;
|
|
71
|
-
while (index < buffer.length) {
|
|
72
|
-
const match = matchKittyQueryResponse(buffer, index);
|
|
73
|
-
if (match?.state === 'complete') {
|
|
74
|
-
index = match.endIndex + 1;
|
|
75
|
-
continue;
|
|
76
|
-
}
|
|
77
|
-
if (match?.state === 'partial') {
|
|
78
|
-
break;
|
|
79
|
-
}
|
|
80
|
-
keptBytes.push(buffer[index]);
|
|
81
|
-
index++;
|
|
82
|
-
}
|
|
83
|
-
return keptBytes;
|
|
84
|
-
};
|
|
85
32
|
const shouldClearTerminalForFrame = ({ isTty, viewportRows, previousViewportRows, previousOutputHeight, nextOutputHeight, isUnmounting, }) => {
|
|
86
33
|
if (!isTty) {
|
|
87
34
|
return false;
|
|
@@ -116,7 +63,15 @@ const getReflowedLineCount = (output, columns) => {
|
|
|
116
63
|
wordWrap: false,
|
|
117
64
|
}).split('\n').length;
|
|
118
65
|
};
|
|
119
|
-
const isOutputSoftWrapped = (output, columns) =>
|
|
66
|
+
const isOutputSoftWrapped = (output, columns, maxVisualWidth) => {
|
|
67
|
+
if (output === '') {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
if (maxVisualWidth !== undefined) {
|
|
71
|
+
return maxVisualWidth > columns;
|
|
72
|
+
}
|
|
73
|
+
return widestLine(output) > columns;
|
|
74
|
+
};
|
|
120
75
|
const isErrorInput = (value) => {
|
|
121
76
|
return (value instanceof Error ||
|
|
122
77
|
Object.prototype.toString.call(value) === '[object Error]');
|
|
@@ -158,6 +113,7 @@ export default class Ink {
|
|
|
158
113
|
// Ignore last render after unmounting a tree to prevent empty output before exit
|
|
159
114
|
isUnmounted;
|
|
160
115
|
isUnmounting;
|
|
116
|
+
outputError;
|
|
161
117
|
lastOutput;
|
|
162
118
|
lastOutputToRender;
|
|
163
119
|
lastOutputHeight;
|
|
@@ -181,6 +137,22 @@ export default class Ink {
|
|
|
181
137
|
cancelKittyDetection;
|
|
182
138
|
nextRenderCommit;
|
|
183
139
|
textSelection;
|
|
140
|
+
lastLayoutWidth;
|
|
141
|
+
lastLayoutRows;
|
|
142
|
+
pendingStaticOutput = '';
|
|
143
|
+
pendingHistoryAdvance = false;
|
|
144
|
+
transformOutput;
|
|
145
|
+
paintEligible = false;
|
|
146
|
+
paintCapability = 'raw';
|
|
147
|
+
widthDetection;
|
|
148
|
+
pendingFrame;
|
|
149
|
+
reservedFrame;
|
|
150
|
+
deferredWidthRenders = [];
|
|
151
|
+
deferredWidthLifecycleActions = [];
|
|
152
|
+
rawInputReady = false;
|
|
153
|
+
resumePendingInput;
|
|
154
|
+
kittyAutoFlags;
|
|
155
|
+
kittyQuerySent = false;
|
|
184
156
|
constructor(options) {
|
|
185
157
|
autoBind(this);
|
|
186
158
|
this.options = options;
|
|
@@ -199,6 +171,41 @@ export default class Ink {
|
|
|
199
171
|
// Using Boolean(isTTY) (rather than an 'in' guard) correctly handles piped streams
|
|
200
172
|
// where the property is absent (e.g. `node app.js | cat`).
|
|
201
173
|
this.interactive = this.resolveInteractiveOption(options.interactive);
|
|
174
|
+
const explicitWidth = process.env['INK_EXPLICIT_WIDTH'];
|
|
175
|
+
const canUseExplicitWidth = options.explicitWidth !== 'disabled' &&
|
|
176
|
+
this.interactive &&
|
|
177
|
+
Boolean(options.stdout.isTTY) &&
|
|
178
|
+
!options.debug &&
|
|
179
|
+
!this.isScreenReaderEnabled;
|
|
180
|
+
this.paintEligible = canUseExplicitWidth && explicitWidth !== '0';
|
|
181
|
+
if (canUseExplicitWidth && explicitWidth === '1') {
|
|
182
|
+
this.transformOutput = createExplicitWidthEncoder();
|
|
183
|
+
this.paintCapability = 'osc66';
|
|
184
|
+
}
|
|
185
|
+
else if (canUseExplicitWidth &&
|
|
186
|
+
explicitWidth !== '0' &&
|
|
187
|
+
options.stdin.isTTY &&
|
|
188
|
+
options.stdin.readable &&
|
|
189
|
+
typeof options.stdin.setRawMode === 'function' &&
|
|
190
|
+
typeof options.stdin.ref === 'function' &&
|
|
191
|
+
typeof options.stdin.unref === 'function') {
|
|
192
|
+
this.widthDetection = new ExplicitWidthDetection({
|
|
193
|
+
getSize: () => getWindowSize(this.options.stdout),
|
|
194
|
+
write: text => {
|
|
195
|
+
if (!getWritableStreamState(this.options.stdout)
|
|
196
|
+
.canWriteToStdout) {
|
|
197
|
+
throw new Error('Terminal output is unavailable');
|
|
198
|
+
}
|
|
199
|
+
this.options.stdout.write(text);
|
|
200
|
+
},
|
|
201
|
+
reserve: this.reserveExplicitWidthFrame,
|
|
202
|
+
onResult: this.settleExplicitWidth,
|
|
203
|
+
});
|
|
204
|
+
this.widthDetection.onSettled(this.finishExplicitWidthSettlement);
|
|
205
|
+
}
|
|
206
|
+
else if (this.paintEligible) {
|
|
207
|
+
this.paintCapability = 'fallback';
|
|
208
|
+
}
|
|
202
209
|
this.alternateScreen = false;
|
|
203
210
|
const unthrottled = options.debug || this.isScreenReaderEnabled;
|
|
204
211
|
const maxFps = options.maxFps ?? 30;
|
|
@@ -225,27 +232,24 @@ export default class Ink {
|
|
|
225
232
|
this.rootNode.onImmediateRender = this.onRender;
|
|
226
233
|
this.log = logUpdate.create(options.stdout, {
|
|
227
234
|
incremental: options.incrementalRendering,
|
|
235
|
+
transformOutput: this.widthDetection
|
|
236
|
+
? this.encodeTerminalOutput
|
|
237
|
+
: this.transformOutput,
|
|
228
238
|
});
|
|
229
239
|
this.cursorPosition = undefined;
|
|
240
|
+
const writePaintRequest = (request) => {
|
|
241
|
+
this.writePaintRequest(request);
|
|
242
|
+
};
|
|
230
243
|
this.throttledLog = unthrottled
|
|
231
|
-
?
|
|
232
|
-
: throttle(
|
|
233
|
-
const shouldWrite = this.log.willRender(output);
|
|
234
|
-
const sync = this.shouldSync();
|
|
235
|
-
if (sync && shouldWrite) {
|
|
236
|
-
this.options.stdout.write(bsu);
|
|
237
|
-
}
|
|
238
|
-
this.log(output);
|
|
239
|
-
if (sync && shouldWrite) {
|
|
240
|
-
this.options.stdout.write(esu);
|
|
241
|
-
}
|
|
242
|
-
}, undefined, {
|
|
244
|
+
? writePaintRequest
|
|
245
|
+
: throttle(writePaintRequest, undefined, {
|
|
243
246
|
leading: true,
|
|
244
247
|
trailing: true,
|
|
245
248
|
});
|
|
246
249
|
// Ignore last render after unmounting a tree to prevent empty output before exit
|
|
247
250
|
this.isUnmounted = false;
|
|
248
251
|
this.isUnmounting = false;
|
|
252
|
+
this.outputError = undefined;
|
|
249
253
|
// Store concurrent mode setting
|
|
250
254
|
this.isConcurrent = options.concurrent ?? false;
|
|
251
255
|
// Store last output to only rerender when needed
|
|
@@ -256,6 +260,7 @@ export default class Ink {
|
|
|
256
260
|
this.lastPhysicalFrameWasSoftWrapped = false;
|
|
257
261
|
this.lastViewportRows = getWindowSize(this.options.stdout).rows;
|
|
258
262
|
this.lastTerminalWidth = getWindowSize(this.options.stdout).columns;
|
|
263
|
+
this.lastLayoutRows = this.lastViewportRows;
|
|
259
264
|
// This variable is used only in debug mode to store full static output
|
|
260
265
|
// so that it's rerendered every time, not just new static parts, like in non-debug mode
|
|
261
266
|
this.fullStaticOutput = '';
|
|
@@ -279,6 +284,7 @@ export default class Ink {
|
|
|
279
284
|
options.stdout.off('resize', this.resized);
|
|
280
285
|
};
|
|
281
286
|
}
|
|
287
|
+
options.stdout.on('error', this.handleStdoutError);
|
|
282
288
|
this.initKittyKeyboard();
|
|
283
289
|
this.exitPromise = new Promise((resolve, reject) => {
|
|
284
290
|
this.resolveExitPromise = resolve;
|
|
@@ -289,6 +295,9 @@ export default class Ink {
|
|
|
289
295
|
void this.exitPromise.catch(noop);
|
|
290
296
|
}
|
|
291
297
|
resized = () => {
|
|
298
|
+
if (this.deferUntilExplicitWidthSettlement(this.resized))
|
|
299
|
+
return;
|
|
300
|
+
settleThrottle(this.throttledLog, false);
|
|
292
301
|
const currentWidth = getWindowSize(this.options.stdout).columns;
|
|
293
302
|
this.calculateLayout();
|
|
294
303
|
this.onRender();
|
|
@@ -308,6 +317,24 @@ export default class Ink {
|
|
|
308
317
|
this.exitResult = errorOrResult;
|
|
309
318
|
this.unmount();
|
|
310
319
|
};
|
|
320
|
+
handleStdoutError = (error) => {
|
|
321
|
+
this.surfaceOutputError(error);
|
|
322
|
+
};
|
|
323
|
+
surfaceOutputError(error) {
|
|
324
|
+
this.log.restoreTerminalModes();
|
|
325
|
+
this.hasPhysicalFrame = false;
|
|
326
|
+
this.pendingHistoryAdvance = false;
|
|
327
|
+
const err = isErrorInput(error)
|
|
328
|
+
? error
|
|
329
|
+
: new Error('Terminal output failed');
|
|
330
|
+
if (!this.outputError) {
|
|
331
|
+
this.outputError = err;
|
|
332
|
+
}
|
|
333
|
+
if (this.isUnmounted || this.isUnmounting) {
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
this.unmount(err);
|
|
337
|
+
}
|
|
311
338
|
setCursorPosition = (position) => {
|
|
312
339
|
this.cursorPosition = position;
|
|
313
340
|
this.log.setCursorPosition(position);
|
|
@@ -320,16 +347,49 @@ export default class Ink {
|
|
|
320
347
|
// before restoring output after external stdout/stderr writes.
|
|
321
348
|
this.log.setCursorPosition(this.cursorPosition);
|
|
322
349
|
const outputToRestore = this.lastOutputToRender || this.lastOutput + '\n';
|
|
350
|
+
const size = getWindowSize(this.options.stdout);
|
|
351
|
+
const layoutStale = this.lastLayoutWidth !== size.columns ||
|
|
352
|
+
this.lastLayoutRows !== size.rows;
|
|
353
|
+
const context = this.getSeekPaintContext({
|
|
354
|
+
output: this.lastOutput,
|
|
355
|
+
outputHeight: this.lastOutputHeight,
|
|
356
|
+
staticOutput: '',
|
|
357
|
+
layoutColumns: this.lastLayoutWidth ?? size.columns,
|
|
358
|
+
layoutRows: this.lastLayoutRows ?? size.rows,
|
|
359
|
+
});
|
|
360
|
+
if (layoutStale && (context || this.pendingHistoryAdvance)) {
|
|
361
|
+
this.consumeHistoryAdvance(size.rows);
|
|
362
|
+
this.scheduleLayoutRender();
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
if (context) {
|
|
366
|
+
this.consumeHistoryAdvance(context.rows);
|
|
367
|
+
this.log(outputToRestore, context);
|
|
368
|
+
const physical = this.log.getPhysicalFrame();
|
|
369
|
+
this.hasPhysicalFrame = Boolean(physical?.valid);
|
|
370
|
+
this.lastPhysicalFrameWasSoftWrapped = false;
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
this.pendingHistoryAdvance = false;
|
|
323
374
|
this.log(outputToRestore);
|
|
324
375
|
this.hasPhysicalFrame = true;
|
|
325
376
|
this.lastPhysicalFrameWasSoftWrapped =
|
|
326
377
|
Boolean(this.options.stdout.isTTY) &&
|
|
327
|
-
isOutputSoftWrapped(outputToRestore,
|
|
378
|
+
isOutputSoftWrapped(outputToRestore, size.columns);
|
|
328
379
|
};
|
|
329
380
|
calculateLayout = () => {
|
|
330
|
-
const terminalWidth = getWindowSize(this.options.stdout)
|
|
331
|
-
this.rootNode.yogaNode
|
|
332
|
-
this.
|
|
381
|
+
const { columns: terminalWidth, rows: terminalRows } = getWindowSize(this.options.stdout);
|
|
382
|
+
const yoga = this.rootNode.yogaNode;
|
|
383
|
+
const widthChanged = this.lastLayoutWidth !== terminalWidth;
|
|
384
|
+
if (widthChanged) {
|
|
385
|
+
yoga.setWidth(terminalWidth);
|
|
386
|
+
this.lastLayoutWidth = terminalWidth;
|
|
387
|
+
}
|
|
388
|
+
this.lastLayoutRows = terminalRows;
|
|
389
|
+
if (!widthChanged && !yoga.isDirty())
|
|
390
|
+
return;
|
|
391
|
+
yoga.calculateLayout(undefined, undefined, Yoga.DIRECTION_LTR);
|
|
392
|
+
dom.incrementLayoutEpoch(this.rootNode);
|
|
333
393
|
};
|
|
334
394
|
onRender = () => {
|
|
335
395
|
this.hasPendingThrottledRender = false;
|
|
@@ -341,7 +401,7 @@ export default class Ink {
|
|
|
341
401
|
this.nextRenderCommit = undefined;
|
|
342
402
|
}
|
|
343
403
|
const startTime = performance.now();
|
|
344
|
-
const { output, outputHeight, staticOutput } = render(this.rootNode, this.isScreenReaderEnabled,
|
|
404
|
+
const { output, outputHeight, staticOutput, maxVisualWidth } = render(this.rootNode, this.isScreenReaderEnabled,
|
|
345
405
|
// Screen-reader rendering bypasses Output, so selection is skipped.
|
|
346
406
|
this.isScreenReaderEnabled ? undefined : this.textSelection);
|
|
347
407
|
this.options.onRender?.({
|
|
@@ -414,14 +474,52 @@ export default class Ink {
|
|
|
414
474
|
}
|
|
415
475
|
return;
|
|
416
476
|
}
|
|
417
|
-
|
|
418
|
-
|
|
477
|
+
const size = getWindowSize(this.options.stdout);
|
|
478
|
+
const frame = {
|
|
479
|
+
output,
|
|
480
|
+
outputHeight,
|
|
481
|
+
staticOutput: hasStaticOutput ? staticOutput : '',
|
|
482
|
+
maxVisualWidth,
|
|
483
|
+
layoutColumns: this.lastLayoutWidth ?? size.columns,
|
|
484
|
+
layoutRows: this.lastLayoutRows ?? size.rows,
|
|
485
|
+
};
|
|
486
|
+
if (this.widthDetection && !this.widthDetection.settled) {
|
|
487
|
+
this.pendingFrame = {
|
|
488
|
+
...frame,
|
|
489
|
+
staticOutput: (this.pendingFrame?.staticOutput ?? '') + frame.staticOutput,
|
|
490
|
+
};
|
|
491
|
+
if (!this.widthDetection.pending &&
|
|
492
|
+
!frame.output &&
|
|
493
|
+
!frame.staticOutput) {
|
|
494
|
+
// No physical frame exists yet; keep the first nonempty frame eligible.
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
else {
|
|
498
|
+
this.widthDetection.start();
|
|
499
|
+
}
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
try {
|
|
503
|
+
this.commitInteractiveFrame(frame);
|
|
504
|
+
}
|
|
505
|
+
catch (error) {
|
|
506
|
+
this.surfaceOutputError(error);
|
|
419
507
|
}
|
|
420
|
-
this.renderInteractiveFrame(output, outputHeight, hasStaticOutput ? staticOutput : '');
|
|
421
508
|
};
|
|
422
509
|
render(node) {
|
|
510
|
+
if (this.widthDetection?.publicationBlocked) {
|
|
511
|
+
this.deferredWidthRenders.push(node);
|
|
512
|
+
this.widthDetection.cancel();
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
423
515
|
const tree = (React.createElement(AccessibilityContext.Provider, { value: { isScreenReaderEnabled: this.isScreenReaderEnabled } },
|
|
424
|
-
React.createElement(App, { stdin: this.options.stdin, stdout: this.options.stdout, stderr: this.options.stderr, exitOnCtrlC: this.options.exitOnCtrlC, interactive: this.interactive, renderThrottleMs: this.renderThrottleMs, writeToStdout: this.writeToStdout, writeToStderr: this.writeToStderr, setCursorPosition: this.setCursorPosition, textSelection: this.textSelection, onExit: this.handleAppExit, onWaitUntilRenderFlush: this.waitUntilRenderFlush
|
|
516
|
+
React.createElement(App, { stdin: this.options.stdin, stdout: this.options.stdout, stderr: this.options.stderr, exitOnCtrlC: this.options.exitOnCtrlC, interactive: this.interactive, renderThrottleMs: this.renderThrottleMs, writeToStdout: this.writeToStdout, writeToStderr: this.writeToStderr, setCursorPosition: this.setCursorPosition, textSelection: this.textSelection, onExit: this.handleAppExit, onWaitUntilRenderFlush: this.waitUntilRenderFlush, onTerminalResponse: this.widthDetection || this.kittyAutoFlags
|
|
517
|
+
? this.handleTerminalResponse
|
|
518
|
+
: undefined, isTerminalResponsePending: this.widthDetection || this.kittyAutoFlags
|
|
519
|
+
? this.isTerminalResponsePending
|
|
520
|
+
: undefined, onInputReady: this.widthDetection || this.kittyAutoFlags
|
|
521
|
+
? this.handleInputReady
|
|
522
|
+
: undefined }, node)));
|
|
425
523
|
if (this.options.concurrent) {
|
|
426
524
|
// Concurrent mode: use updateContainer (async scheduling)
|
|
427
525
|
reconciler.updateContainer(tree, this.container, null, noop);
|
|
@@ -436,6 +534,8 @@ export default class Ink {
|
|
|
436
534
|
if (this.isUnmounted) {
|
|
437
535
|
return;
|
|
438
536
|
}
|
|
537
|
+
if (this.deferUntilExplicitWidthSettlement(() => this.writeToStdout(data)))
|
|
538
|
+
return;
|
|
439
539
|
if (this.options.debug) {
|
|
440
540
|
this.options.stdout.write(data + this.fullStaticOutput + this.lastOutput);
|
|
441
541
|
return;
|
|
@@ -444,21 +544,14 @@ export default class Ink {
|
|
|
444
544
|
this.options.stdout.write(data);
|
|
445
545
|
return;
|
|
446
546
|
}
|
|
447
|
-
|
|
448
|
-
if (sync) {
|
|
449
|
-
this.options.stdout.write(bsu);
|
|
450
|
-
}
|
|
451
|
-
this.log.clear(this.getPhysicalEraseOptions());
|
|
452
|
-
this.options.stdout.write(data);
|
|
453
|
-
this.restoreLastOutput();
|
|
454
|
-
if (sync) {
|
|
455
|
-
this.options.stdout.write(esu);
|
|
456
|
-
}
|
|
547
|
+
this.writeExternalOutput(data, this.options.stdout);
|
|
457
548
|
}
|
|
458
549
|
writeToStderr(data) {
|
|
459
550
|
if (this.isUnmounted) {
|
|
460
551
|
return;
|
|
461
552
|
}
|
|
553
|
+
if (this.deferUntilExplicitWidthSettlement(() => this.writeToStderr(data)))
|
|
554
|
+
return;
|
|
462
555
|
if (this.options.debug) {
|
|
463
556
|
this.options.stderr.write(data);
|
|
464
557
|
this.options.stdout.write(this.fullStaticOutput + this.lastOutput);
|
|
@@ -468,22 +561,15 @@ export default class Ink {
|
|
|
468
561
|
this.options.stderr.write(data);
|
|
469
562
|
return;
|
|
470
563
|
}
|
|
471
|
-
|
|
472
|
-
if (sync) {
|
|
473
|
-
this.options.stdout.write(bsu);
|
|
474
|
-
}
|
|
475
|
-
this.log.clear(this.getPhysicalEraseOptions());
|
|
476
|
-
this.options.stderr.write(data);
|
|
477
|
-
this.restoreLastOutput();
|
|
478
|
-
if (sync) {
|
|
479
|
-
this.options.stdout.write(esu);
|
|
480
|
-
}
|
|
564
|
+
this.writeExternalOutput(data, this.options.stderr);
|
|
481
565
|
}
|
|
482
566
|
// eslint-disable-next-line @typescript-eslint/no-restricted-types
|
|
483
567
|
unmount(error) {
|
|
484
568
|
if (this.isUnmounted || this.isUnmounting) {
|
|
485
569
|
return;
|
|
486
570
|
}
|
|
571
|
+
if (this.deferUntilExplicitWidthSettlement(() => this.unmount(error)))
|
|
572
|
+
return;
|
|
487
573
|
this.isUnmounting = true;
|
|
488
574
|
if (this.beforeExitHandler) {
|
|
489
575
|
process.off('beforeExit', this.beforeExitHandler);
|
|
@@ -504,14 +590,16 @@ export default class Ink {
|
|
|
504
590
|
this.calculateLayout();
|
|
505
591
|
this.onRender();
|
|
506
592
|
}
|
|
593
|
+
// Accept pending paints from the controlled final render before locking.
|
|
594
|
+
settleThrottle(this.throttledLog, true);
|
|
507
595
|
}
|
|
508
596
|
// Mark as unmounted after the final render but before stdout writes
|
|
509
597
|
// that could re-enter exit() via synchronous write callbacks.
|
|
510
598
|
this.isUnmounted = true;
|
|
511
599
|
this.unsubscribeExit();
|
|
512
|
-
//
|
|
513
|
-
|
|
514
|
-
|
|
600
|
+
// Cancel leftover throttled log writes so no new frame starts after unmount.
|
|
601
|
+
settleThrottle(this.throttledLog, false);
|
|
602
|
+
this.log.restoreTerminalModes();
|
|
515
603
|
if (typeof this.restoreConsole === 'function') {
|
|
516
604
|
// Once unmount starts, Ink stops trying to manage teardown-time
|
|
517
605
|
// console output. Restoring the native console before React cleanup keeps
|
|
@@ -527,7 +615,12 @@ export default class Ink {
|
|
|
527
615
|
if (this.cancelKittyDetection) {
|
|
528
616
|
this.cancelKittyDetection();
|
|
529
617
|
}
|
|
618
|
+
this.log.restoreTerminalModes();
|
|
619
|
+
const seekTeardown = this.log.getPhysicalFrame?.()?.context.strategy === 'seek-viewport';
|
|
530
620
|
if (canWriteToStdout) {
|
|
621
|
+
if (this.interactive && !this.options.debug && seekTeardown) {
|
|
622
|
+
this.log.done();
|
|
623
|
+
}
|
|
531
624
|
if (this.kittyProtocolEnabled) {
|
|
532
625
|
this.writeBestEffort(this.options.stdout, '\u001B[<u');
|
|
533
626
|
}
|
|
@@ -537,6 +630,9 @@ export default class Ink {
|
|
|
537
630
|
// diagnostics onto it. Trying to preserve teardown output across the
|
|
538
631
|
// buffer switch adds fragile lifecycle-specific behavior, so Ink keeps
|
|
539
632
|
// alternate-screen teardown intentionally simple and best-effort.
|
|
633
|
+
// Seek cursor/cache teardown must finish before this screen switch:
|
|
634
|
+
// after the switch, no seek done() path may reposition or erase the
|
|
635
|
+
// returned primary screen.
|
|
540
636
|
if (this.alternateScreen) {
|
|
541
637
|
this.writeBestEffort(this.options.stdout, ansiEscapes.exitAlternativeScreen);
|
|
542
638
|
this.writeBestEffort(this.options.stdout, showCursorEscape);
|
|
@@ -549,10 +645,16 @@ export default class Ink {
|
|
|
549
645
|
// deferred during rendering).
|
|
550
646
|
this.options.stdout.write(this.options.debug ? '\n' : this.lastOutput + '\n');
|
|
551
647
|
}
|
|
552
|
-
else if (!this.options.debug) {
|
|
648
|
+
else if (!this.options.debug && !seekTeardown) {
|
|
553
649
|
this.log.done();
|
|
554
650
|
}
|
|
555
651
|
}
|
|
652
|
+
else if (this.interactive && !this.options.debug) {
|
|
653
|
+
try {
|
|
654
|
+
this.log.done();
|
|
655
|
+
}
|
|
656
|
+
catch { }
|
|
657
|
+
}
|
|
556
658
|
this.kittyProtocolEnabled = false;
|
|
557
659
|
instances.delete(this.options.stdout);
|
|
558
660
|
// Ensure all queued writes have been processed before resolving the
|
|
@@ -565,8 +667,10 @@ export default class Ink {
|
|
|
565
667
|
// because the event loop is draining and async callbacks won't fire.
|
|
566
668
|
const { exitResult } = this;
|
|
567
669
|
const resolveOrReject = () => {
|
|
568
|
-
|
|
569
|
-
|
|
670
|
+
this.options.stdout.off('error', this.handleStdoutError);
|
|
671
|
+
const exitError = isErrorInput(error) ? error : this.outputError;
|
|
672
|
+
if (exitError) {
|
|
673
|
+
this.rejectExitPromise(exitError);
|
|
570
674
|
}
|
|
571
675
|
else {
|
|
572
676
|
this.resolveExitPromise(exitResult);
|
|
@@ -577,7 +681,17 @@ export default class Ink {
|
|
|
577
681
|
resolveOrReject();
|
|
578
682
|
}
|
|
579
683
|
else if (canWriteToStdout && hasWritableState) {
|
|
580
|
-
|
|
684
|
+
try {
|
|
685
|
+
this.options.stdout.write('', errorFromBarrier => {
|
|
686
|
+
if (errorFromBarrier && !this.outputError) {
|
|
687
|
+
this.outputError = errorFromBarrier;
|
|
688
|
+
}
|
|
689
|
+
resolveOrReject();
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
catch {
|
|
693
|
+
resolveOrReject();
|
|
694
|
+
}
|
|
581
695
|
}
|
|
582
696
|
else {
|
|
583
697
|
setImmediate(resolveOrReject);
|
|
@@ -629,6 +743,14 @@ export default class Ink {
|
|
|
629
743
|
}
|
|
630
744
|
}
|
|
631
745
|
reconciler.flushSyncWork();
|
|
746
|
+
settleThrottle(this.throttledOnRender, getWritableStreamState(this.options.stdout)
|
|
747
|
+
.canWriteToStdout);
|
|
748
|
+
if (this.widthDetection?.pending)
|
|
749
|
+
await this.widthDetection.finished;
|
|
750
|
+
if (this.isUnmounted || this.isUnmounting) {
|
|
751
|
+
await this.awaitExit();
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
632
754
|
const stdout = this.options.stdout;
|
|
633
755
|
const { canWriteToStdout, hasWritableState } = getWritableStreamState(stdout);
|
|
634
756
|
// Flush pending throttled render/log timers so their output is included in this wait.
|
|
@@ -636,35 +758,46 @@ export default class Ink {
|
|
|
636
758
|
settleThrottle(this.throttledLog, canWriteToStdout);
|
|
637
759
|
if (canWriteToStdout && hasWritableState) {
|
|
638
760
|
await new Promise(resolve => {
|
|
639
|
-
|
|
761
|
+
let settled = false;
|
|
762
|
+
const finish = (writeError) => {
|
|
763
|
+
if (settled) {
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
settled = true;
|
|
767
|
+
this.options.stdout.off('error', onError);
|
|
768
|
+
if (writeError) {
|
|
769
|
+
this.surfaceOutputError(writeError);
|
|
770
|
+
}
|
|
640
771
|
resolve();
|
|
641
|
-
}
|
|
772
|
+
};
|
|
773
|
+
const onError = (writeError) => {
|
|
774
|
+
finish(writeError);
|
|
775
|
+
};
|
|
776
|
+
this.options.stdout.once('error', onError);
|
|
777
|
+
try {
|
|
778
|
+
this.options.stdout.write('', writeError => {
|
|
779
|
+
finish(writeError);
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
catch (writeError) {
|
|
783
|
+
finish(isErrorInput(writeError)
|
|
784
|
+
? writeError
|
|
785
|
+
: new Error('Terminal output failed'));
|
|
786
|
+
}
|
|
642
787
|
});
|
|
643
788
|
return;
|
|
644
789
|
}
|
|
645
790
|
await yieldImmediate();
|
|
646
791
|
}
|
|
647
792
|
clear() {
|
|
793
|
+
if (this.deferUntilExplicitWidthSettlement(this.clear))
|
|
794
|
+
return;
|
|
648
795
|
if (this.interactive && !this.options.debug) {
|
|
649
|
-
this.
|
|
796
|
+
this.clearPhysicalFrame();
|
|
650
797
|
this.hasPhysicalFrame = false;
|
|
651
798
|
this.lastPhysicalFrameWasSoftWrapped = false;
|
|
652
799
|
}
|
|
653
800
|
}
|
|
654
|
-
getPhysicalEraseOptions() {
|
|
655
|
-
if (!this.options.stdout.isTTY ||
|
|
656
|
-
!this.hasPhysicalFrame ||
|
|
657
|
-
this.lastOutputToRender === '') {
|
|
658
|
-
return undefined;
|
|
659
|
-
}
|
|
660
|
-
const terminalWidth = getWindowSize(this.options.stdout).columns;
|
|
661
|
-
if (!isOutputSoftWrapped(this.lastOutputToRender, terminalWidth)) {
|
|
662
|
-
return undefined;
|
|
663
|
-
}
|
|
664
|
-
return {
|
|
665
|
-
eraseLineCount: getReflowedLineCount(this.lastOutputToRender, terminalWidth),
|
|
666
|
-
};
|
|
667
|
-
}
|
|
668
801
|
patchConsole() {
|
|
669
802
|
if (this.options.debug) {
|
|
670
803
|
return;
|
|
@@ -681,6 +814,23 @@ export default class Ink {
|
|
|
681
814
|
}
|
|
682
815
|
});
|
|
683
816
|
}
|
|
817
|
+
getPhysicalEraseOptions() {
|
|
818
|
+
if (isOwnedSeekViewport(this.log.getPhysicalFrame?.())) {
|
|
819
|
+
return undefined;
|
|
820
|
+
}
|
|
821
|
+
if (!this.options.stdout.isTTY ||
|
|
822
|
+
!this.hasPhysicalFrame ||
|
|
823
|
+
this.lastOutputToRender === '') {
|
|
824
|
+
return undefined;
|
|
825
|
+
}
|
|
826
|
+
const terminalWidth = getWindowSize(this.options.stdout).columns;
|
|
827
|
+
if (!isOutputSoftWrapped(this.lastOutputToRender, terminalWidth)) {
|
|
828
|
+
return undefined;
|
|
829
|
+
}
|
|
830
|
+
return {
|
|
831
|
+
eraseLineCount: getReflowedLineCount(this.lastOutputToRender, terminalWidth),
|
|
832
|
+
};
|
|
833
|
+
}
|
|
684
834
|
setAlternateScreen(enabled) {
|
|
685
835
|
this.alternateScreen = this.resolveAlternateScreenOption(enabled, this.interactive);
|
|
686
836
|
if (this.alternateScreen) {
|
|
@@ -730,9 +880,127 @@ export default class Ink {
|
|
|
730
880
|
}
|
|
731
881
|
return this.nextRenderCommit.promise;
|
|
732
882
|
}
|
|
733
|
-
renderInteractiveFrame(output, outputHeight, staticOutput) {
|
|
883
|
+
renderInteractiveFrame(output, outputHeight, staticOutput, maxVisualWidth, layoutColumns, layoutRows) {
|
|
884
|
+
const size = getWindowSize(this.options.stdout);
|
|
885
|
+
this.renderPendingFrame({
|
|
886
|
+
output,
|
|
887
|
+
outputHeight,
|
|
888
|
+
staticOutput,
|
|
889
|
+
maxVisualWidth,
|
|
890
|
+
layoutColumns: layoutColumns ?? this.lastLayoutWidth ?? size.columns,
|
|
891
|
+
layoutRows: layoutRows ?? this.lastLayoutRows ?? size.rows,
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
renderPendingFrame(frame) {
|
|
895
|
+
const { columns: terminalWidth, rows: terminalRows } = getWindowSize(this.options.stdout);
|
|
896
|
+
if (frame.layoutColumns !== terminalWidth ||
|
|
897
|
+
frame.layoutRows !== terminalRows) {
|
|
898
|
+
this.deferStatic(frame);
|
|
899
|
+
this.scheduleLayoutRender();
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
const previousPhysical = this.log.getPhysicalFrame?.();
|
|
903
|
+
const previousSeek = isOwnedSeekViewport(previousPhysical);
|
|
904
|
+
const seekContext = this.getSeekPaintContext(frame);
|
|
905
|
+
if (previousSeek &&
|
|
906
|
+
!seekContext &&
|
|
907
|
+
frame.outputHeight === previousPhysical.context.rows) {
|
|
908
|
+
this.deferStatic(frame);
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
if (previousSeek || seekContext) {
|
|
912
|
+
this.renderSeekFrame(frame, seekContext, previousSeek);
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
this.renderRawInteractiveFrame(frame);
|
|
916
|
+
}
|
|
917
|
+
renderSeekFrame(frame, seekContext, previousSeek) {
|
|
918
|
+
const isTty = this.options.stdout.isTTY;
|
|
919
|
+
const { rows: terminalRows } = getWindowSize(this.options.stdout);
|
|
920
|
+
const viewportRows = isTty ? terminalRows : 24;
|
|
921
|
+
const isFullscreen = isTty && frame.outputHeight >= viewportRows;
|
|
922
|
+
const outputToRender = isFullscreen ? frame.output : frame.output + '\n';
|
|
923
|
+
if (!seekContext) {
|
|
924
|
+
this.leaveSeekViewport();
|
|
925
|
+
this.renderRawInteractiveFrame(frame);
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
const context = seekContext;
|
|
929
|
+
const staticOutput = this.takeStaticOutput(frame);
|
|
930
|
+
const previousPhysical = this.log.getPhysicalFrame?.();
|
|
931
|
+
const dimensionsChanged = !previousPhysical?.valid ||
|
|
932
|
+
previousPhysical.context.columns !== context.columns ||
|
|
933
|
+
previousPhysical.context.rows !== context.rows ||
|
|
934
|
+
previousPhysical.context.strategy !== context.strategy;
|
|
935
|
+
const entering = !previousSeek;
|
|
936
|
+
if (staticOutput !== '') {
|
|
937
|
+
const sync = this.shouldSync();
|
|
938
|
+
if (sync) {
|
|
939
|
+
this.options.stdout.write(bsu);
|
|
940
|
+
}
|
|
941
|
+
try {
|
|
942
|
+
this.clearPhysicalFrame();
|
|
943
|
+
this.hasPhysicalFrame = false;
|
|
944
|
+
this.options.stdout.write(this.transformOutput?.(staticOutput) ?? staticOutput);
|
|
945
|
+
this.pendingHistoryAdvance = true;
|
|
946
|
+
this.consumeHistoryAdvance(context.rows);
|
|
947
|
+
this.log(outputToRender, context);
|
|
948
|
+
}
|
|
949
|
+
catch (error) {
|
|
950
|
+
this.failExternalOutput(error);
|
|
951
|
+
}
|
|
952
|
+
finally {
|
|
953
|
+
if (sync) {
|
|
954
|
+
try {
|
|
955
|
+
this.options.stdout.write(esu);
|
|
956
|
+
}
|
|
957
|
+
catch { }
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
this.publishLogicalFrame(frame, outputToRender, true);
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
if (entering || dimensionsChanged) {
|
|
964
|
+
const sync = this.shouldSync();
|
|
965
|
+
if (sync) {
|
|
966
|
+
this.options.stdout.write(bsu);
|
|
967
|
+
}
|
|
968
|
+
try {
|
|
969
|
+
if (this.pendingHistoryAdvance) {
|
|
970
|
+
this.consumeHistoryAdvance(context.rows);
|
|
971
|
+
}
|
|
972
|
+
else {
|
|
973
|
+
this.options.stdout.write(seekViewportReset);
|
|
974
|
+
this.log.reset();
|
|
975
|
+
}
|
|
976
|
+
this.log.repaint(outputToRender, undefined, context);
|
|
977
|
+
}
|
|
978
|
+
finally {
|
|
979
|
+
if (sync) {
|
|
980
|
+
try {
|
|
981
|
+
this.options.stdout.write(esu);
|
|
982
|
+
}
|
|
983
|
+
catch { }
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
this.publishLogicalFrame(frame, outputToRender, true);
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
if (outputToRender !== this.lastOutputToRender ||
|
|
990
|
+
this.log.isCursorDirty()) {
|
|
991
|
+
this.throttledLog({
|
|
992
|
+
frame,
|
|
993
|
+
outputToRender,
|
|
994
|
+
});
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
this.publishLogicalFrame(frame, outputToRender, false);
|
|
998
|
+
}
|
|
999
|
+
renderRawInteractiveFrame(frame) {
|
|
1000
|
+
const staticOutput = this.takeStaticOutput(frame);
|
|
734
1001
|
const hasStaticOutput = staticOutput !== '';
|
|
735
1002
|
const isTty = this.options.stdout.isTTY;
|
|
1003
|
+
const { output, outputHeight, maxVisualWidth } = frame;
|
|
736
1004
|
const { columns: terminalWidth, rows: terminalRows } = getWindowSize(this.options.stdout);
|
|
737
1005
|
// Detect fullscreen: output fills or exceeds terminal height.
|
|
738
1006
|
// Only apply when writing to a real TTY — piped output always gets trailing newlines.
|
|
@@ -746,7 +1014,7 @@ export default class Ink {
|
|
|
746
1014
|
const outputWillRender = hasStaticOutput || this.log.willRender(outputToRender);
|
|
747
1015
|
const shouldClassifyNextFrame = Boolean(isTty) && (columnsDecreased || outputWillRender);
|
|
748
1016
|
const nextFrameIsSoftWrapped = shouldClassifyNextFrame &&
|
|
749
|
-
isOutputSoftWrapped(outputToRender, terminalWidth);
|
|
1017
|
+
isOutputSoftWrapped(outputToRender, terminalWidth, maxVisualWidth);
|
|
750
1018
|
const shouldRepairReflow = Boolean(isTty) &&
|
|
751
1019
|
hasCachedPhysicalOutput &&
|
|
752
1020
|
(columnsDecreased ||
|
|
@@ -778,7 +1046,9 @@ export default class Ink {
|
|
|
778
1046
|
if (sync) {
|
|
779
1047
|
this.options.stdout.write(bsu);
|
|
780
1048
|
}
|
|
781
|
-
|
|
1049
|
+
const content = this.fullStaticOutput + outputToRender;
|
|
1050
|
+
this.options.stdout.write(ansiEscapes.clearTerminal +
|
|
1051
|
+
(this.transformOutput?.(content) ?? content));
|
|
782
1052
|
this.lastOutput = output;
|
|
783
1053
|
this.lastOutputToRender = outputToRender;
|
|
784
1054
|
this.lastOutputHeight = outputHeight;
|
|
@@ -788,7 +1058,8 @@ export default class Ink {
|
|
|
788
1058
|
this.hasPhysicalFrame = true;
|
|
789
1059
|
this.lastPhysicalFrameWasSoftWrapped = shouldClassifyNextFrame
|
|
790
1060
|
? nextFrameIsSoftWrapped
|
|
791
|
-
: Boolean(isTty) &&
|
|
1061
|
+
: Boolean(isTty) &&
|
|
1062
|
+
isOutputSoftWrapped(outputToRender, terminalWidth, maxVisualWidth);
|
|
792
1063
|
if (sync) {
|
|
793
1064
|
this.options.stdout.write(esu);
|
|
794
1065
|
}
|
|
@@ -802,7 +1073,7 @@ export default class Ink {
|
|
|
802
1073
|
this.options.stdout.write(bsu);
|
|
803
1074
|
}
|
|
804
1075
|
this.log.clear(reflowEraseOptions);
|
|
805
|
-
this.options.stdout.write(staticOutput);
|
|
1076
|
+
this.options.stdout.write(this.transformOutput?.(staticOutput) ?? staticOutput);
|
|
806
1077
|
this.log(outputToRender);
|
|
807
1078
|
didWriteFrame = true;
|
|
808
1079
|
if (sync) {
|
|
@@ -823,7 +1094,10 @@ export default class Ink {
|
|
|
823
1094
|
else if (outputToRender !== this.lastOutputToRender ||
|
|
824
1095
|
this.log.isCursorDirty()) {
|
|
825
1096
|
// ThrottledLog manages its own bsu/esu at actual write time
|
|
826
|
-
this.throttledLog(
|
|
1097
|
+
this.throttledLog({
|
|
1098
|
+
frame,
|
|
1099
|
+
outputToRender,
|
|
1100
|
+
});
|
|
827
1101
|
didWriteFrame = true;
|
|
828
1102
|
}
|
|
829
1103
|
this.lastOutput = output;
|
|
@@ -833,11 +1107,290 @@ export default class Ink {
|
|
|
833
1107
|
if (didWriteFrame) {
|
|
834
1108
|
this.lastPhysicalFrameWasSoftWrapped = shouldClassifyNextFrame
|
|
835
1109
|
? nextFrameIsSoftWrapped
|
|
836
|
-
: Boolean(isTty) &&
|
|
1110
|
+
: Boolean(isTty) &&
|
|
1111
|
+
isOutputSoftWrapped(outputToRender, terminalWidth, maxVisualWidth);
|
|
837
1112
|
}
|
|
838
1113
|
this.lastViewportRows = viewportRows;
|
|
839
1114
|
this.lastTerminalWidth = terminalWidth;
|
|
840
1115
|
}
|
|
1116
|
+
encodeTerminalOutput(output) {
|
|
1117
|
+
return this.transformOutput?.(output) ?? output;
|
|
1118
|
+
}
|
|
1119
|
+
commitInteractiveFrame(frame) {
|
|
1120
|
+
this.renderInteractiveFrame(frame.output, frame.outputHeight, frame.staticOutput, frame.maxVisualWidth, frame.layoutColumns, frame.layoutRows);
|
|
1121
|
+
}
|
|
1122
|
+
resolvePaintContext(frame) {
|
|
1123
|
+
const { columns, rows } = getWindowSize(this.options.stdout);
|
|
1124
|
+
return {
|
|
1125
|
+
strategy: selectPaintStrategy({
|
|
1126
|
+
eligible: this.paintEligible,
|
|
1127
|
+
capability: this.paintCapability,
|
|
1128
|
+
incremental: Boolean(this.options.incrementalRendering),
|
|
1129
|
+
outputHeight: frame.outputHeight,
|
|
1130
|
+
rows,
|
|
1131
|
+
}),
|
|
1132
|
+
columns,
|
|
1133
|
+
rows,
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
1136
|
+
getSeekPaintContext(frame) {
|
|
1137
|
+
const context = this.resolvePaintContext(frame);
|
|
1138
|
+
if (!isSeekViewport(context)) {
|
|
1139
|
+
return undefined;
|
|
1140
|
+
}
|
|
1141
|
+
const { rows } = getWindowSize(this.options.stdout);
|
|
1142
|
+
if (frame.outputHeight !== rows || context.rows !== rows) {
|
|
1143
|
+
return undefined;
|
|
1144
|
+
}
|
|
1145
|
+
return context;
|
|
1146
|
+
}
|
|
1147
|
+
clearPhysicalFrame() {
|
|
1148
|
+
const physical = this.log.getPhysicalFrame?.();
|
|
1149
|
+
if (isOwnedSeekViewport(physical)) {
|
|
1150
|
+
this.log.clear();
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1153
|
+
this.log.clear(this.getPhysicalEraseOptions());
|
|
1154
|
+
}
|
|
1155
|
+
leaveSeekViewport() {
|
|
1156
|
+
this.clearPhysicalFrame();
|
|
1157
|
+
this.log.restoreTerminalModes();
|
|
1158
|
+
this.log.reset();
|
|
1159
|
+
this.hasPhysicalFrame = false;
|
|
1160
|
+
this.lastPhysicalFrameWasSoftWrapped = false;
|
|
1161
|
+
}
|
|
1162
|
+
consumeHistoryAdvance(rows) {
|
|
1163
|
+
if (!this.pendingHistoryAdvance) {
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
this.pendingHistoryAdvance = false;
|
|
1167
|
+
if (rows > 0) {
|
|
1168
|
+
this.options.stdout.write('\r\n'.repeat(rows) + '\u001B[H');
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
failExternalOutput(error) {
|
|
1172
|
+
this.log.restoreTerminalModes();
|
|
1173
|
+
this.hasPhysicalFrame = false;
|
|
1174
|
+
this.pendingHistoryAdvance = false;
|
|
1175
|
+
throw error;
|
|
1176
|
+
}
|
|
1177
|
+
writeExternalOutput(data, stream) {
|
|
1178
|
+
const sync = this.shouldSync();
|
|
1179
|
+
if (sync) {
|
|
1180
|
+
this.options.stdout.write(bsu);
|
|
1181
|
+
}
|
|
1182
|
+
try {
|
|
1183
|
+
const ownedSeek = isOwnedSeekViewport(this.log.getPhysicalFrame?.());
|
|
1184
|
+
const size = getWindowSize(this.options.stdout);
|
|
1185
|
+
const willSeek = Boolean(this.getSeekPaintContext({
|
|
1186
|
+
output: this.lastOutput,
|
|
1187
|
+
outputHeight: this.lastOutputHeight,
|
|
1188
|
+
staticOutput: '',
|
|
1189
|
+
layoutColumns: this.lastLayoutWidth ?? size.columns,
|
|
1190
|
+
layoutRows: this.lastLayoutRows ?? size.rows,
|
|
1191
|
+
}));
|
|
1192
|
+
this.clearPhysicalFrame();
|
|
1193
|
+
this.hasPhysicalFrame = false;
|
|
1194
|
+
stream.write(data);
|
|
1195
|
+
if (ownedSeek || willSeek) {
|
|
1196
|
+
this.pendingHistoryAdvance = true;
|
|
1197
|
+
}
|
|
1198
|
+
this.restoreLastOutput();
|
|
1199
|
+
}
|
|
1200
|
+
catch (error) {
|
|
1201
|
+
this.failExternalOutput(error);
|
|
1202
|
+
}
|
|
1203
|
+
finally {
|
|
1204
|
+
if (sync) {
|
|
1205
|
+
try {
|
|
1206
|
+
this.options.stdout.write(esu);
|
|
1207
|
+
}
|
|
1208
|
+
catch { }
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
writePaintRequest(request) {
|
|
1213
|
+
if (this.isUnmounted) {
|
|
1214
|
+
return;
|
|
1215
|
+
}
|
|
1216
|
+
try {
|
|
1217
|
+
const { columns, rows } = getWindowSize(this.options.stdout);
|
|
1218
|
+
if (request.frame.layoutColumns !== columns ||
|
|
1219
|
+
request.frame.layoutRows !== rows) {
|
|
1220
|
+
this.deferStatic(request.frame);
|
|
1221
|
+
this.scheduleLayoutRender();
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
const previousPhysical = this.log.getPhysicalFrame?.();
|
|
1225
|
+
const seekContext = this.getSeekPaintContext(request.frame);
|
|
1226
|
+
if (seekContext === undefined &&
|
|
1227
|
+
isOwnedSeekViewport(previousPhysical) &&
|
|
1228
|
+
request.frame.outputHeight === previousPhysical.context.rows) {
|
|
1229
|
+
this.deferStatic(request.frame);
|
|
1230
|
+
return;
|
|
1231
|
+
}
|
|
1232
|
+
const shouldWrite = this.log.willRender(request.outputToRender, seekContext);
|
|
1233
|
+
const sync = this.shouldSync();
|
|
1234
|
+
if (sync && shouldWrite) {
|
|
1235
|
+
this.options.stdout.write(bsu);
|
|
1236
|
+
}
|
|
1237
|
+
try {
|
|
1238
|
+
this.log(request.outputToRender, seekContext);
|
|
1239
|
+
}
|
|
1240
|
+
finally {
|
|
1241
|
+
if (sync && shouldWrite) {
|
|
1242
|
+
try {
|
|
1243
|
+
this.options.stdout.write(esu);
|
|
1244
|
+
}
|
|
1245
|
+
catch { }
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
this.publishLogicalFrame(request.frame, request.outputToRender, shouldWrite);
|
|
1249
|
+
}
|
|
1250
|
+
catch (error) {
|
|
1251
|
+
this.surfaceOutputError(error);
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
publishLogicalFrame(frame, outputToRender, didWrite) {
|
|
1255
|
+
this.lastOutput = frame.output;
|
|
1256
|
+
this.lastOutputToRender = outputToRender;
|
|
1257
|
+
this.lastOutputHeight = frame.outputHeight;
|
|
1258
|
+
const size = getWindowSize(this.options.stdout);
|
|
1259
|
+
this.lastViewportRows = size.rows;
|
|
1260
|
+
this.lastTerminalWidth = size.columns;
|
|
1261
|
+
const physical = this.log.getPhysicalFrame?.();
|
|
1262
|
+
if (isOwnedSeekViewport(physical)) {
|
|
1263
|
+
this.hasPhysicalFrame = true;
|
|
1264
|
+
this.lastPhysicalFrameWasSoftWrapped = false;
|
|
1265
|
+
return;
|
|
1266
|
+
}
|
|
1267
|
+
this.hasPhysicalFrame ||= didWrite;
|
|
1268
|
+
}
|
|
1269
|
+
takeStaticOutput(frame) {
|
|
1270
|
+
const staticOutput = (this.pendingStaticOutput ?? '') + frame.staticOutput;
|
|
1271
|
+
this.pendingStaticOutput = '';
|
|
1272
|
+
this.fullStaticOutput = (this.fullStaticOutput ?? '') + staticOutput;
|
|
1273
|
+
return staticOutput;
|
|
1274
|
+
}
|
|
1275
|
+
deferStatic(frame) {
|
|
1276
|
+
this.pendingStaticOutput =
|
|
1277
|
+
(this.pendingStaticOutput ?? '') + frame.staticOutput;
|
|
1278
|
+
}
|
|
1279
|
+
scheduleLayoutRender() {
|
|
1280
|
+
if (this.isUnmounted) {
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
this.calculateLayout();
|
|
1284
|
+
this.onRender();
|
|
1285
|
+
}
|
|
1286
|
+
reserveExplicitWidthFrame() {
|
|
1287
|
+
const frame = this.pendingFrame;
|
|
1288
|
+
if (!frame || !canOverwriteFirstCell(frame.staticOutput || frame.output))
|
|
1289
|
+
return false;
|
|
1290
|
+
this.reservedFrame = frame;
|
|
1291
|
+
this.pendingFrame = undefined;
|
|
1292
|
+
return true;
|
|
1293
|
+
}
|
|
1294
|
+
settleExplicitWidth(supported) {
|
|
1295
|
+
const reserved = this.reservedFrame;
|
|
1296
|
+
this.reservedFrame = undefined;
|
|
1297
|
+
if (!this.isUnmounted &&
|
|
1298
|
+
this.paintEligible &&
|
|
1299
|
+
this.paintCapability === 'raw') {
|
|
1300
|
+
if (supported) {
|
|
1301
|
+
this.transformOutput = createExplicitWidthEncoder();
|
|
1302
|
+
this.paintCapability = 'osc66';
|
|
1303
|
+
}
|
|
1304
|
+
else {
|
|
1305
|
+
this.paintCapability = 'fallback';
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
try {
|
|
1309
|
+
if (this.isUnmounted ||
|
|
1310
|
+
!getWritableStreamState(this.options.stdout)
|
|
1311
|
+
.canWriteToStdout)
|
|
1312
|
+
return;
|
|
1313
|
+
if (reserved)
|
|
1314
|
+
this.commitInteractiveFrame(reserved);
|
|
1315
|
+
while (true) {
|
|
1316
|
+
settleThrottle(this.throttledOnRender, true);
|
|
1317
|
+
if (!this.pendingFrame)
|
|
1318
|
+
break;
|
|
1319
|
+
const pending = this.pendingFrame;
|
|
1320
|
+
this.pendingFrame = undefined;
|
|
1321
|
+
this.commitInteractiveFrame(pending);
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
finally {
|
|
1325
|
+
this.pendingFrame = undefined;
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
deferUntilExplicitWidthSettlement(action) {
|
|
1329
|
+
const detection = this.widthDetection;
|
|
1330
|
+
if (!detection || detection.settled)
|
|
1331
|
+
return false;
|
|
1332
|
+
this.deferredWidthLifecycleActions.push(action);
|
|
1333
|
+
detection.cancel();
|
|
1334
|
+
return true;
|
|
1335
|
+
}
|
|
1336
|
+
finishExplicitWidthSettlement() {
|
|
1337
|
+
this.resumePendingInput?.();
|
|
1338
|
+
for (const node of this.deferredWidthRenders.splice(0))
|
|
1339
|
+
this.render(node);
|
|
1340
|
+
const canWriteToStdout = getWritableStreamState(this.options.stdout).canWriteToStdout;
|
|
1341
|
+
settleThrottle(this.throttledOnRender, canWriteToStdout);
|
|
1342
|
+
settleThrottle(this.throttledLog, canWriteToStdout);
|
|
1343
|
+
for (const action of this.deferredWidthLifecycleActions.splice(0)) {
|
|
1344
|
+
settleThrottle(this.throttledLog, canWriteToStdout);
|
|
1345
|
+
action();
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
isTerminalResponsePending() {
|
|
1349
|
+
return Boolean(this.widthDetection?.pending || this.cancelKittyDetection);
|
|
1350
|
+
}
|
|
1351
|
+
handleInputReady(ready, resumePendingInput, afterSettlement) {
|
|
1352
|
+
this.rawInputReady = ready;
|
|
1353
|
+
if (!ready) {
|
|
1354
|
+
const finishInputRelease = () => {
|
|
1355
|
+
this.cancelKittyDetection?.();
|
|
1356
|
+
this.resumePendingInput = undefined;
|
|
1357
|
+
afterSettlement?.();
|
|
1358
|
+
};
|
|
1359
|
+
if (this.deferUntilExplicitWidthSettlement(finishInputRelease))
|
|
1360
|
+
return;
|
|
1361
|
+
finishInputRelease();
|
|
1362
|
+
return;
|
|
1363
|
+
}
|
|
1364
|
+
if (this.isUnmounted || this.isUnmounting)
|
|
1365
|
+
return;
|
|
1366
|
+
this.resumePendingInput = resumePendingInput;
|
|
1367
|
+
this.widthDetection?.setInputReady(true);
|
|
1368
|
+
if (this.kittyAutoFlags &&
|
|
1369
|
+
!this.kittyQuerySent &&
|
|
1370
|
+
this.cancelKittyDetection) {
|
|
1371
|
+
this.kittyQuerySent = true;
|
|
1372
|
+
try {
|
|
1373
|
+
this.options.stdout.write('\u001B[?u');
|
|
1374
|
+
}
|
|
1375
|
+
catch {
|
|
1376
|
+
this.cancelKittyDetection?.();
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
handleTerminalResponse(response) {
|
|
1381
|
+
if (response.type === 'cursor-position') {
|
|
1382
|
+
this.widthDetection?.accept(response);
|
|
1383
|
+
return;
|
|
1384
|
+
}
|
|
1385
|
+
if (this.kittyQuerySent &&
|
|
1386
|
+
this.kittyAutoFlags &&
|
|
1387
|
+
this.cancelKittyDetection) {
|
|
1388
|
+
const flags = this.kittyAutoFlags;
|
|
1389
|
+
this.cancelKittyDetection();
|
|
1390
|
+
if (!this.isUnmounted && !this.isUnmounting)
|
|
1391
|
+
this.enableKittyProtocol(flags);
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
841
1394
|
initKittyKeyboard() {
|
|
842
1395
|
// Protocol is opt-in: if kittyKeyboard is not specified, do nothing
|
|
843
1396
|
if (!this.options.kittyKeyboard) {
|
|
@@ -863,46 +1416,20 @@ export default class Ink {
|
|
|
863
1416
|
!this.options.stdout.isTTY) {
|
|
864
1417
|
return;
|
|
865
1418
|
}
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
//
|
|
869
|
-
|
|
870
|
-
this.confirmKittySupport(flags);
|
|
871
|
-
}
|
|
872
|
-
confirmKittySupport(flags) {
|
|
873
|
-
const { stdin, stdout } = this.options;
|
|
874
|
-
let responseBuffer = [];
|
|
1419
|
+
if (this.options.debug || this.isScreenReaderEnabled)
|
|
1420
|
+
return;
|
|
1421
|
+
// Replies flow through App's one readable parser, without input replay.
|
|
1422
|
+
this.kittyAutoFlags = flags;
|
|
875
1423
|
const cleanup = () => {
|
|
876
|
-
this.cancelKittyDetection = undefined;
|
|
877
1424
|
clearTimeout(timer);
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
// Clear responseBuffer afterwards to make cleanup idempotent.
|
|
882
|
-
const remaining = stripKittyQueryResponsesAndTrailingPartial(responseBuffer);
|
|
883
|
-
responseBuffer = [];
|
|
884
|
-
if (remaining.length > 0) {
|
|
885
|
-
stdin.unshift(Uint8Array.from(remaining));
|
|
886
|
-
}
|
|
887
|
-
};
|
|
888
|
-
const onData = (data) => {
|
|
889
|
-
const chunk = typeof data === 'string' ? textEncoder.encode(data) : data;
|
|
890
|
-
for (const byte of chunk) {
|
|
891
|
-
responseBuffer.push(byte);
|
|
892
|
-
}
|
|
893
|
-
if (hasCompleteKittyQueryResponse(responseBuffer)) {
|
|
894
|
-
cleanup();
|
|
895
|
-
if (!this.isUnmounted) {
|
|
896
|
-
this.enableKittyProtocol(flags);
|
|
897
|
-
}
|
|
898
|
-
}
|
|
1425
|
+
this.cancelKittyDetection = undefined;
|
|
1426
|
+
this.kittyAutoFlags = undefined;
|
|
1427
|
+
this.resumePendingInput?.();
|
|
899
1428
|
};
|
|
900
|
-
// Attach listener before writing the query so that synchronous
|
|
901
|
-
// or immediate responses are not missed.
|
|
902
|
-
stdin.on('data', onData);
|
|
903
1429
|
const timer = setTimeout(cleanup, 200);
|
|
904
1430
|
this.cancelKittyDetection = cleanup;
|
|
905
|
-
|
|
1431
|
+
if (this.rawInputReady)
|
|
1432
|
+
this.handleInputReady(true, this.resumePendingInput);
|
|
906
1433
|
}
|
|
907
1434
|
enableKittyProtocol(flags) {
|
|
908
1435
|
this.options.stdout.write(`\u001B[>${resolveFlags(flags)}u`);
|