@nuvin/ink 7.6.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/components/App.d.ts +5 -1
- package/build/components/App.js +48 -10
- package/build/components/App.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/ink.d.ts +43 -1
- package/build/ink.js +651 -143
- 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/log-update.d.ts +16 -8
- package/build/log-update.js +313 -33
- package/build/log-update.js.map +1 -1
- package/build/output.js +66 -34
- package/build/output.js.map +1 -1
- package/build/render.d.ts +11 -0
- package/build/render.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/package.json +2 -1
- package/readme.md +50 -0
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;
|
|
@@ -166,6 +113,7 @@ export default class Ink {
|
|
|
166
113
|
// Ignore last render after unmounting a tree to prevent empty output before exit
|
|
167
114
|
isUnmounted;
|
|
168
115
|
isUnmounting;
|
|
116
|
+
outputError;
|
|
169
117
|
lastOutput;
|
|
170
118
|
lastOutputToRender;
|
|
171
119
|
lastOutputHeight;
|
|
@@ -190,6 +138,21 @@ export default class Ink {
|
|
|
190
138
|
nextRenderCommit;
|
|
191
139
|
textSelection;
|
|
192
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;
|
|
193
156
|
constructor(options) {
|
|
194
157
|
autoBind(this);
|
|
195
158
|
this.options = options;
|
|
@@ -208,6 +171,41 @@ export default class Ink {
|
|
|
208
171
|
// Using Boolean(isTTY) (rather than an 'in' guard) correctly handles piped streams
|
|
209
172
|
// where the property is absent (e.g. `node app.js | cat`).
|
|
210
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
|
+
}
|
|
211
209
|
this.alternateScreen = false;
|
|
212
210
|
const unthrottled = options.debug || this.isScreenReaderEnabled;
|
|
213
211
|
const maxFps = options.maxFps ?? 30;
|
|
@@ -234,27 +232,24 @@ export default class Ink {
|
|
|
234
232
|
this.rootNode.onImmediateRender = this.onRender;
|
|
235
233
|
this.log = logUpdate.create(options.stdout, {
|
|
236
234
|
incremental: options.incrementalRendering,
|
|
235
|
+
transformOutput: this.widthDetection
|
|
236
|
+
? this.encodeTerminalOutput
|
|
237
|
+
: this.transformOutput,
|
|
237
238
|
});
|
|
238
239
|
this.cursorPosition = undefined;
|
|
240
|
+
const writePaintRequest = (request) => {
|
|
241
|
+
this.writePaintRequest(request);
|
|
242
|
+
};
|
|
239
243
|
this.throttledLog = unthrottled
|
|
240
|
-
?
|
|
241
|
-
: throttle(
|
|
242
|
-
const shouldWrite = this.log.willRender(output);
|
|
243
|
-
const sync = this.shouldSync();
|
|
244
|
-
if (sync && shouldWrite) {
|
|
245
|
-
this.options.stdout.write(bsu);
|
|
246
|
-
}
|
|
247
|
-
this.log(output);
|
|
248
|
-
if (sync && shouldWrite) {
|
|
249
|
-
this.options.stdout.write(esu);
|
|
250
|
-
}
|
|
251
|
-
}, undefined, {
|
|
244
|
+
? writePaintRequest
|
|
245
|
+
: throttle(writePaintRequest, undefined, {
|
|
252
246
|
leading: true,
|
|
253
247
|
trailing: true,
|
|
254
248
|
});
|
|
255
249
|
// Ignore last render after unmounting a tree to prevent empty output before exit
|
|
256
250
|
this.isUnmounted = false;
|
|
257
251
|
this.isUnmounting = false;
|
|
252
|
+
this.outputError = undefined;
|
|
258
253
|
// Store concurrent mode setting
|
|
259
254
|
this.isConcurrent = options.concurrent ?? false;
|
|
260
255
|
// Store last output to only rerender when needed
|
|
@@ -265,6 +260,7 @@ export default class Ink {
|
|
|
265
260
|
this.lastPhysicalFrameWasSoftWrapped = false;
|
|
266
261
|
this.lastViewportRows = getWindowSize(this.options.stdout).rows;
|
|
267
262
|
this.lastTerminalWidth = getWindowSize(this.options.stdout).columns;
|
|
263
|
+
this.lastLayoutRows = this.lastViewportRows;
|
|
268
264
|
// This variable is used only in debug mode to store full static output
|
|
269
265
|
// so that it's rerendered every time, not just new static parts, like in non-debug mode
|
|
270
266
|
this.fullStaticOutput = '';
|
|
@@ -288,6 +284,7 @@ export default class Ink {
|
|
|
288
284
|
options.stdout.off('resize', this.resized);
|
|
289
285
|
};
|
|
290
286
|
}
|
|
287
|
+
options.stdout.on('error', this.handleStdoutError);
|
|
291
288
|
this.initKittyKeyboard();
|
|
292
289
|
this.exitPromise = new Promise((resolve, reject) => {
|
|
293
290
|
this.resolveExitPromise = resolve;
|
|
@@ -298,6 +295,9 @@ export default class Ink {
|
|
|
298
295
|
void this.exitPromise.catch(noop);
|
|
299
296
|
}
|
|
300
297
|
resized = () => {
|
|
298
|
+
if (this.deferUntilExplicitWidthSettlement(this.resized))
|
|
299
|
+
return;
|
|
300
|
+
settleThrottle(this.throttledLog, false);
|
|
301
301
|
const currentWidth = getWindowSize(this.options.stdout).columns;
|
|
302
302
|
this.calculateLayout();
|
|
303
303
|
this.onRender();
|
|
@@ -317,6 +317,24 @@ export default class Ink {
|
|
|
317
317
|
this.exitResult = errorOrResult;
|
|
318
318
|
this.unmount();
|
|
319
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
|
+
}
|
|
320
338
|
setCursorPosition = (position) => {
|
|
321
339
|
this.cursorPosition = position;
|
|
322
340
|
this.log.setCursorPosition(position);
|
|
@@ -329,20 +347,45 @@ export default class Ink {
|
|
|
329
347
|
// before restoring output after external stdout/stderr writes.
|
|
330
348
|
this.log.setCursorPosition(this.cursorPosition);
|
|
331
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;
|
|
332
374
|
this.log(outputToRestore);
|
|
333
375
|
this.hasPhysicalFrame = true;
|
|
334
376
|
this.lastPhysicalFrameWasSoftWrapped =
|
|
335
377
|
Boolean(this.options.stdout.isTTY) &&
|
|
336
|
-
isOutputSoftWrapped(outputToRestore,
|
|
378
|
+
isOutputSoftWrapped(outputToRestore, size.columns);
|
|
337
379
|
};
|
|
338
380
|
calculateLayout = () => {
|
|
339
|
-
const terminalWidth = getWindowSize(this.options.stdout)
|
|
381
|
+
const { columns: terminalWidth, rows: terminalRows } = getWindowSize(this.options.stdout);
|
|
340
382
|
const yoga = this.rootNode.yogaNode;
|
|
341
383
|
const widthChanged = this.lastLayoutWidth !== terminalWidth;
|
|
342
384
|
if (widthChanged) {
|
|
343
385
|
yoga.setWidth(terminalWidth);
|
|
344
386
|
this.lastLayoutWidth = terminalWidth;
|
|
345
387
|
}
|
|
388
|
+
this.lastLayoutRows = terminalRows;
|
|
346
389
|
if (!widthChanged && !yoga.isDirty())
|
|
347
390
|
return;
|
|
348
391
|
yoga.calculateLayout(undefined, undefined, Yoga.DIRECTION_LTR);
|
|
@@ -431,14 +474,52 @@ export default class Ink {
|
|
|
431
474
|
}
|
|
432
475
|
return;
|
|
433
476
|
}
|
|
434
|
-
|
|
435
|
-
|
|
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);
|
|
436
507
|
}
|
|
437
|
-
this.renderInteractiveFrame(output, outputHeight, hasStaticOutput ? staticOutput : '', maxVisualWidth);
|
|
438
508
|
};
|
|
439
509
|
render(node) {
|
|
510
|
+
if (this.widthDetection?.publicationBlocked) {
|
|
511
|
+
this.deferredWidthRenders.push(node);
|
|
512
|
+
this.widthDetection.cancel();
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
440
515
|
const tree = (React.createElement(AccessibilityContext.Provider, { value: { isScreenReaderEnabled: this.isScreenReaderEnabled } },
|
|
441
|
-
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)));
|
|
442
523
|
if (this.options.concurrent) {
|
|
443
524
|
// Concurrent mode: use updateContainer (async scheduling)
|
|
444
525
|
reconciler.updateContainer(tree, this.container, null, noop);
|
|
@@ -453,6 +534,8 @@ export default class Ink {
|
|
|
453
534
|
if (this.isUnmounted) {
|
|
454
535
|
return;
|
|
455
536
|
}
|
|
537
|
+
if (this.deferUntilExplicitWidthSettlement(() => this.writeToStdout(data)))
|
|
538
|
+
return;
|
|
456
539
|
if (this.options.debug) {
|
|
457
540
|
this.options.stdout.write(data + this.fullStaticOutput + this.lastOutput);
|
|
458
541
|
return;
|
|
@@ -461,21 +544,14 @@ export default class Ink {
|
|
|
461
544
|
this.options.stdout.write(data);
|
|
462
545
|
return;
|
|
463
546
|
}
|
|
464
|
-
|
|
465
|
-
if (sync) {
|
|
466
|
-
this.options.stdout.write(bsu);
|
|
467
|
-
}
|
|
468
|
-
this.log.clear(this.getPhysicalEraseOptions());
|
|
469
|
-
this.options.stdout.write(data);
|
|
470
|
-
this.restoreLastOutput();
|
|
471
|
-
if (sync) {
|
|
472
|
-
this.options.stdout.write(esu);
|
|
473
|
-
}
|
|
547
|
+
this.writeExternalOutput(data, this.options.stdout);
|
|
474
548
|
}
|
|
475
549
|
writeToStderr(data) {
|
|
476
550
|
if (this.isUnmounted) {
|
|
477
551
|
return;
|
|
478
552
|
}
|
|
553
|
+
if (this.deferUntilExplicitWidthSettlement(() => this.writeToStderr(data)))
|
|
554
|
+
return;
|
|
479
555
|
if (this.options.debug) {
|
|
480
556
|
this.options.stderr.write(data);
|
|
481
557
|
this.options.stdout.write(this.fullStaticOutput + this.lastOutput);
|
|
@@ -485,22 +561,15 @@ export default class Ink {
|
|
|
485
561
|
this.options.stderr.write(data);
|
|
486
562
|
return;
|
|
487
563
|
}
|
|
488
|
-
|
|
489
|
-
if (sync) {
|
|
490
|
-
this.options.stdout.write(bsu);
|
|
491
|
-
}
|
|
492
|
-
this.log.clear(this.getPhysicalEraseOptions());
|
|
493
|
-
this.options.stderr.write(data);
|
|
494
|
-
this.restoreLastOutput();
|
|
495
|
-
if (sync) {
|
|
496
|
-
this.options.stdout.write(esu);
|
|
497
|
-
}
|
|
564
|
+
this.writeExternalOutput(data, this.options.stderr);
|
|
498
565
|
}
|
|
499
566
|
// eslint-disable-next-line @typescript-eslint/no-restricted-types
|
|
500
567
|
unmount(error) {
|
|
501
568
|
if (this.isUnmounted || this.isUnmounting) {
|
|
502
569
|
return;
|
|
503
570
|
}
|
|
571
|
+
if (this.deferUntilExplicitWidthSettlement(() => this.unmount(error)))
|
|
572
|
+
return;
|
|
504
573
|
this.isUnmounting = true;
|
|
505
574
|
if (this.beforeExitHandler) {
|
|
506
575
|
process.off('beforeExit', this.beforeExitHandler);
|
|
@@ -521,14 +590,16 @@ export default class Ink {
|
|
|
521
590
|
this.calculateLayout();
|
|
522
591
|
this.onRender();
|
|
523
592
|
}
|
|
593
|
+
// Accept pending paints from the controlled final render before locking.
|
|
594
|
+
settleThrottle(this.throttledLog, true);
|
|
524
595
|
}
|
|
525
596
|
// Mark as unmounted after the final render but before stdout writes
|
|
526
597
|
// that could re-enter exit() via synchronous write callbacks.
|
|
527
598
|
this.isUnmounted = true;
|
|
528
599
|
this.unsubscribeExit();
|
|
529
|
-
//
|
|
530
|
-
|
|
531
|
-
|
|
600
|
+
// Cancel leftover throttled log writes so no new frame starts after unmount.
|
|
601
|
+
settleThrottle(this.throttledLog, false);
|
|
602
|
+
this.log.restoreTerminalModes();
|
|
532
603
|
if (typeof this.restoreConsole === 'function') {
|
|
533
604
|
// Once unmount starts, Ink stops trying to manage teardown-time
|
|
534
605
|
// console output. Restoring the native console before React cleanup keeps
|
|
@@ -544,7 +615,12 @@ export default class Ink {
|
|
|
544
615
|
if (this.cancelKittyDetection) {
|
|
545
616
|
this.cancelKittyDetection();
|
|
546
617
|
}
|
|
618
|
+
this.log.restoreTerminalModes();
|
|
619
|
+
const seekTeardown = this.log.getPhysicalFrame?.()?.context.strategy === 'seek-viewport';
|
|
547
620
|
if (canWriteToStdout) {
|
|
621
|
+
if (this.interactive && !this.options.debug && seekTeardown) {
|
|
622
|
+
this.log.done();
|
|
623
|
+
}
|
|
548
624
|
if (this.kittyProtocolEnabled) {
|
|
549
625
|
this.writeBestEffort(this.options.stdout, '\u001B[<u');
|
|
550
626
|
}
|
|
@@ -554,6 +630,9 @@ export default class Ink {
|
|
|
554
630
|
// diagnostics onto it. Trying to preserve teardown output across the
|
|
555
631
|
// buffer switch adds fragile lifecycle-specific behavior, so Ink keeps
|
|
556
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.
|
|
557
636
|
if (this.alternateScreen) {
|
|
558
637
|
this.writeBestEffort(this.options.stdout, ansiEscapes.exitAlternativeScreen);
|
|
559
638
|
this.writeBestEffort(this.options.stdout, showCursorEscape);
|
|
@@ -566,10 +645,16 @@ export default class Ink {
|
|
|
566
645
|
// deferred during rendering).
|
|
567
646
|
this.options.stdout.write(this.options.debug ? '\n' : this.lastOutput + '\n');
|
|
568
647
|
}
|
|
569
|
-
else if (!this.options.debug) {
|
|
648
|
+
else if (!this.options.debug && !seekTeardown) {
|
|
570
649
|
this.log.done();
|
|
571
650
|
}
|
|
572
651
|
}
|
|
652
|
+
else if (this.interactive && !this.options.debug) {
|
|
653
|
+
try {
|
|
654
|
+
this.log.done();
|
|
655
|
+
}
|
|
656
|
+
catch { }
|
|
657
|
+
}
|
|
573
658
|
this.kittyProtocolEnabled = false;
|
|
574
659
|
instances.delete(this.options.stdout);
|
|
575
660
|
// Ensure all queued writes have been processed before resolving the
|
|
@@ -582,8 +667,10 @@ export default class Ink {
|
|
|
582
667
|
// because the event loop is draining and async callbacks won't fire.
|
|
583
668
|
const { exitResult } = this;
|
|
584
669
|
const resolveOrReject = () => {
|
|
585
|
-
|
|
586
|
-
|
|
670
|
+
this.options.stdout.off('error', this.handleStdoutError);
|
|
671
|
+
const exitError = isErrorInput(error) ? error : this.outputError;
|
|
672
|
+
if (exitError) {
|
|
673
|
+
this.rejectExitPromise(exitError);
|
|
587
674
|
}
|
|
588
675
|
else {
|
|
589
676
|
this.resolveExitPromise(exitResult);
|
|
@@ -594,7 +681,17 @@ export default class Ink {
|
|
|
594
681
|
resolveOrReject();
|
|
595
682
|
}
|
|
596
683
|
else if (canWriteToStdout && hasWritableState) {
|
|
597
|
-
|
|
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
|
+
}
|
|
598
695
|
}
|
|
599
696
|
else {
|
|
600
697
|
setImmediate(resolveOrReject);
|
|
@@ -646,6 +743,14 @@ export default class Ink {
|
|
|
646
743
|
}
|
|
647
744
|
}
|
|
648
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
|
+
}
|
|
649
754
|
const stdout = this.options.stdout;
|
|
650
755
|
const { canWriteToStdout, hasWritableState } = getWritableStreamState(stdout);
|
|
651
756
|
// Flush pending throttled render/log timers so their output is included in this wait.
|
|
@@ -653,17 +758,42 @@ export default class Ink {
|
|
|
653
758
|
settleThrottle(this.throttledLog, canWriteToStdout);
|
|
654
759
|
if (canWriteToStdout && hasWritableState) {
|
|
655
760
|
await new Promise(resolve => {
|
|
656
|
-
|
|
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
|
+
}
|
|
657
771
|
resolve();
|
|
658
|
-
}
|
|
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
|
+
}
|
|
659
787
|
});
|
|
660
788
|
return;
|
|
661
789
|
}
|
|
662
790
|
await yieldImmediate();
|
|
663
791
|
}
|
|
664
792
|
clear() {
|
|
793
|
+
if (this.deferUntilExplicitWidthSettlement(this.clear))
|
|
794
|
+
return;
|
|
665
795
|
if (this.interactive && !this.options.debug) {
|
|
666
|
-
this.
|
|
796
|
+
this.clearPhysicalFrame();
|
|
667
797
|
this.hasPhysicalFrame = false;
|
|
668
798
|
this.lastPhysicalFrameWasSoftWrapped = false;
|
|
669
799
|
}
|
|
@@ -685,6 +815,9 @@ export default class Ink {
|
|
|
685
815
|
});
|
|
686
816
|
}
|
|
687
817
|
getPhysicalEraseOptions() {
|
|
818
|
+
if (isOwnedSeekViewport(this.log.getPhysicalFrame?.())) {
|
|
819
|
+
return undefined;
|
|
820
|
+
}
|
|
688
821
|
if (!this.options.stdout.isTTY ||
|
|
689
822
|
!this.hasPhysicalFrame ||
|
|
690
823
|
this.lastOutputToRender === '') {
|
|
@@ -747,9 +880,127 @@ export default class Ink {
|
|
|
747
880
|
}
|
|
748
881
|
return this.nextRenderCommit.promise;
|
|
749
882
|
}
|
|
750
|
-
renderInteractiveFrame(output, outputHeight, staticOutput, maxVisualWidth) {
|
|
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);
|
|
751
1001
|
const hasStaticOutput = staticOutput !== '';
|
|
752
1002
|
const isTty = this.options.stdout.isTTY;
|
|
1003
|
+
const { output, outputHeight, maxVisualWidth } = frame;
|
|
753
1004
|
const { columns: terminalWidth, rows: terminalRows } = getWindowSize(this.options.stdout);
|
|
754
1005
|
// Detect fullscreen: output fills or exceeds terminal height.
|
|
755
1006
|
// Only apply when writing to a real TTY — piped output always gets trailing newlines.
|
|
@@ -795,7 +1046,9 @@ export default class Ink {
|
|
|
795
1046
|
if (sync) {
|
|
796
1047
|
this.options.stdout.write(bsu);
|
|
797
1048
|
}
|
|
798
|
-
|
|
1049
|
+
const content = this.fullStaticOutput + outputToRender;
|
|
1050
|
+
this.options.stdout.write(ansiEscapes.clearTerminal +
|
|
1051
|
+
(this.transformOutput?.(content) ?? content));
|
|
799
1052
|
this.lastOutput = output;
|
|
800
1053
|
this.lastOutputToRender = outputToRender;
|
|
801
1054
|
this.lastOutputHeight = outputHeight;
|
|
@@ -820,7 +1073,7 @@ export default class Ink {
|
|
|
820
1073
|
this.options.stdout.write(bsu);
|
|
821
1074
|
}
|
|
822
1075
|
this.log.clear(reflowEraseOptions);
|
|
823
|
-
this.options.stdout.write(staticOutput);
|
|
1076
|
+
this.options.stdout.write(this.transformOutput?.(staticOutput) ?? staticOutput);
|
|
824
1077
|
this.log(outputToRender);
|
|
825
1078
|
didWriteFrame = true;
|
|
826
1079
|
if (sync) {
|
|
@@ -841,7 +1094,10 @@ export default class Ink {
|
|
|
841
1094
|
else if (outputToRender !== this.lastOutputToRender ||
|
|
842
1095
|
this.log.isCursorDirty()) {
|
|
843
1096
|
// ThrottledLog manages its own bsu/esu at actual write time
|
|
844
|
-
this.throttledLog(
|
|
1097
|
+
this.throttledLog({
|
|
1098
|
+
frame,
|
|
1099
|
+
outputToRender,
|
|
1100
|
+
});
|
|
845
1101
|
didWriteFrame = true;
|
|
846
1102
|
}
|
|
847
1103
|
this.lastOutput = output;
|
|
@@ -857,6 +1113,284 @@ export default class Ink {
|
|
|
857
1113
|
this.lastViewportRows = viewportRows;
|
|
858
1114
|
this.lastTerminalWidth = terminalWidth;
|
|
859
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
|
+
}
|
|
860
1394
|
initKittyKeyboard() {
|
|
861
1395
|
// Protocol is opt-in: if kittyKeyboard is not specified, do nothing
|
|
862
1396
|
if (!this.options.kittyKeyboard) {
|
|
@@ -882,46 +1416,20 @@ export default class Ink {
|
|
|
882
1416
|
!this.options.stdout.isTTY) {
|
|
883
1417
|
return;
|
|
884
1418
|
}
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
//
|
|
888
|
-
|
|
889
|
-
this.confirmKittySupport(flags);
|
|
890
|
-
}
|
|
891
|
-
confirmKittySupport(flags) {
|
|
892
|
-
const { stdin, stdout } = this.options;
|
|
893
|
-
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;
|
|
894
1423
|
const cleanup = () => {
|
|
895
|
-
this.cancelKittyDetection = undefined;
|
|
896
1424
|
clearTimeout(timer);
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
// Clear responseBuffer afterwards to make cleanup idempotent.
|
|
901
|
-
const remaining = stripKittyQueryResponsesAndTrailingPartial(responseBuffer);
|
|
902
|
-
responseBuffer = [];
|
|
903
|
-
if (remaining.length > 0) {
|
|
904
|
-
stdin.unshift(Uint8Array.from(remaining));
|
|
905
|
-
}
|
|
906
|
-
};
|
|
907
|
-
const onData = (data) => {
|
|
908
|
-
const chunk = typeof data === 'string' ? textEncoder.encode(data) : data;
|
|
909
|
-
for (const byte of chunk) {
|
|
910
|
-
responseBuffer.push(byte);
|
|
911
|
-
}
|
|
912
|
-
if (hasCompleteKittyQueryResponse(responseBuffer)) {
|
|
913
|
-
cleanup();
|
|
914
|
-
if (!this.isUnmounted) {
|
|
915
|
-
this.enableKittyProtocol(flags);
|
|
916
|
-
}
|
|
917
|
-
}
|
|
1425
|
+
this.cancelKittyDetection = undefined;
|
|
1426
|
+
this.kittyAutoFlags = undefined;
|
|
1427
|
+
this.resumePendingInput?.();
|
|
918
1428
|
};
|
|
919
|
-
// Attach listener before writing the query so that synchronous
|
|
920
|
-
// or immediate responses are not missed.
|
|
921
|
-
stdin.on('data', onData);
|
|
922
1429
|
const timer = setTimeout(cleanup, 200);
|
|
923
1430
|
this.cancelKittyDetection = cleanup;
|
|
924
|
-
|
|
1431
|
+
if (this.rawInputReady)
|
|
1432
|
+
this.handleInputReady(true, this.resumePendingInput);
|
|
925
1433
|
}
|
|
926
1434
|
enableKittyProtocol(flags) {
|
|
927
1435
|
this.options.stdout.write(`\u001B[>${resolveFlags(flags)}u`);
|