ink 6.8.0 → 7.0.1
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/components/AnimationContext.d.ts +9 -0
- package/build/components/AnimationContext.js +13 -0
- package/build/components/AnimationContext.js.map +1 -0
- package/build/components/App.d.ts +4 -1
- package/build/components/App.js +142 -22
- package/build/components/App.js.map +1 -1
- package/build/components/AppContext.d.ts +23 -3
- package/build/components/AppContext.js +7 -4
- package/build/components/AppContext.js.map +1 -1
- package/build/components/Box.d.ts +16 -3
- package/build/components/ErrorBoundary.d.ts +2 -2
- package/build/components/ErrorOverview.js +6 -6
- package/build/components/ErrorOverview.js.map +1 -1
- package/build/components/Static.js.map +1 -1
- package/build/components/StdinContext.d.ts +7 -1
- package/build/components/StdinContext.js +1 -0
- package/build/components/StdinContext.js.map +1 -1
- package/build/components/Text.d.ts +1 -1
- package/build/components/Text.js +1 -1
- package/build/components/Text.js.map +1 -1
- package/build/components/Transform.d.ts +1 -1
- package/build/devtools-window-polyfill.js +7 -4
- package/build/devtools-window-polyfill.js.map +1 -1
- package/build/devtools.js +31 -6
- package/build/devtools.js.map +1 -1
- package/build/dom.d.ts +5 -1
- package/build/dom.js +20 -1
- package/build/dom.js.map +1 -1
- package/build/hooks/use-animation.d.ts +49 -0
- package/build/hooks/use-animation.js +87 -0
- package/build/hooks/use-animation.js.map +1 -0
- package/build/hooks/use-app.d.ts +1 -1
- package/build/hooks/use-app.js +1 -1
- package/build/hooks/use-box-metrics.d.ts +59 -0
- package/build/hooks/use-box-metrics.js +88 -0
- package/build/hooks/use-box-metrics.js.map +1 -0
- package/build/hooks/use-cursor.d.ts +1 -1
- package/build/hooks/use-cursor.js +1 -1
- package/build/hooks/use-focus-manager.d.ts +17 -2
- package/build/hooks/use-focus-manager.js +2 -1
- package/build/hooks/use-focus-manager.js.map +1 -1
- package/build/hooks/use-focus.d.ts +2 -1
- package/build/hooks/use-focus.js +5 -4
- package/build/hooks/use-focus.js.map +1 -1
- package/build/hooks/use-input.d.ts +2 -1
- package/build/hooks/use-input.js +82 -80
- package/build/hooks/use-input.js.map +1 -1
- package/build/hooks/use-is-screen-reader-enabled.d.ts +2 -1
- package/build/hooks/use-is-screen-reader-enabled.js +2 -1
- package/build/hooks/use-is-screen-reader-enabled.js.map +1 -1
- package/build/hooks/use-paste.d.ts +35 -0
- package/build/hooks/use-paste.js +62 -0
- package/build/hooks/use-paste.js.map +1 -0
- package/build/hooks/use-stderr.d.ts +1 -1
- package/build/hooks/use-stderr.js +1 -1
- package/build/hooks/use-stdin.d.ts +4 -2
- package/build/hooks/use-stdin.js +2 -1
- package/build/hooks/use-stdin.js.map +1 -1
- package/build/hooks/use-stdout.d.ts +1 -1
- package/build/hooks/use-stdout.js +1 -1
- package/build/hooks/use-window-size.d.ts +18 -0
- package/build/hooks/use-window-size.js +22 -0
- package/build/hooks/use-window-size.js.map +1 -0
- package/build/index.d.ts +8 -1
- package/build/index.js +4 -0
- package/build/index.js.map +1 -1
- package/build/ink.d.ts +48 -3
- package/build/ink.js +325 -155
- package/build/ink.js.map +1 -1
- package/build/input-parser.d.ts +4 -1
- package/build/input-parser.js +70 -30
- package/build/input-parser.js.map +1 -1
- package/build/log-update.d.ts +1 -0
- package/build/log-update.js +13 -1
- package/build/log-update.js.map +1 -1
- package/build/measure-element.d.ts +4 -0
- package/build/measure-element.js +4 -0
- package/build/measure-element.js.map +1 -1
- package/build/output.js +25 -0
- package/build/output.js.map +1 -1
- package/build/parse-keypress.d.ts +1 -3
- package/build/parse-keypress.js +19 -17
- package/build/parse-keypress.js.map +1 -1
- package/build/reconciler.js +46 -27
- package/build/reconciler.js.map +1 -1
- package/build/render-border.js +29 -18
- package/build/render-border.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 +57 -2
- package/build/render.js +18 -11
- package/build/render.js.map +1 -1
- package/build/styles.d.ts +78 -16
- package/build/styles.js +102 -31
- package/build/styles.js.map +1 -1
- package/build/utils.d.ts +9 -2
- package/build/utils.js +18 -3
- package/build/utils.js.map +1 -1
- package/build/wrap-text.js +7 -0
- package/build/wrap-text.js.map +1 -1
- package/build/write-synchronized.d.ts +1 -1
- package/build/write-synchronized.js +4 -2
- package/build/write-synchronized.js.map +1 -1
- package/package.json +34 -98
- package/readme.md +554 -48
- package/build/apply-styles.js +0 -175
- package/build/build-layout.js +0 -77
- package/build/calculate-wrapped-text.js +0 -53
- package/build/components/Color.js +0 -62
- package/build/components/Cursor.d.ts +0 -83
- package/build/components/Cursor.js +0 -53
- package/build/components/Cursor.js.map +0 -1
- package/build/experimental/apply-style.js +0 -140
- package/build/experimental/dom.js +0 -123
- package/build/experimental/output.js +0 -91
- package/build/experimental/reconciler.js +0 -141
- package/build/experimental/renderer.js +0 -81
- package/build/hooks/useInput.js +0 -38
- package/build/instance.js +0 -205
- package/build/layout.d.ts +0 -7
- package/build/layout.js +0 -33
- package/build/layout.js.map +0 -1
- package/build/options.d.ts +0 -52
- package/build/options.js +0 -2
- package/build/options.js.map +0 -1
- package/build/screen-reader-update.d.ts +0 -13
- package/build/screen-reader-update.js +0 -38
- package/build/screen-reader-update.js.map +0 -1
package/build/ink.js
CHANGED
|
@@ -9,11 +9,11 @@ import patchConsole from 'patch-console';
|
|
|
9
9
|
import { LegacyRoot, ConcurrentRoot } from 'react-reconciler/constants.js';
|
|
10
10
|
import Yoga from 'yoga-layout';
|
|
11
11
|
import wrapAnsi from 'wrap-ansi';
|
|
12
|
-
import
|
|
13
|
-
import { isDev } from './utils.js';
|
|
12
|
+
import { getWindowSize } from './utils.js';
|
|
14
13
|
import reconciler from './reconciler.js';
|
|
15
14
|
import render from './renderer.js';
|
|
16
15
|
import * as dom from './dom.js';
|
|
16
|
+
import { hideCursorEscape, showCursorEscape } from './cursor-helpers.js';
|
|
17
17
|
import logUpdate from './log-update.js';
|
|
18
18
|
import { bsu, esu, shouldSynchronize } from './write-synchronized.js';
|
|
19
19
|
import instances from './instances.js';
|
|
@@ -21,6 +21,10 @@ import App from './components/App.js';
|
|
|
21
21
|
import { accessibilityContext as AccessibilityContext } from './components/AccessibilityContext.js';
|
|
22
22
|
import { resolveFlags, } from './kitty-keyboard.js';
|
|
23
23
|
const noop = () => { };
|
|
24
|
+
const textEncoder = new TextEncoder();
|
|
25
|
+
const yieldImmediate = async () => new Promise(resolve => {
|
|
26
|
+
setImmediate(resolve);
|
|
27
|
+
});
|
|
24
28
|
const kittyQueryEscapeByte = 0x1b;
|
|
25
29
|
const kittyQueryOpenBracketByte = 0x5b;
|
|
26
30
|
const kittyQueryQuestionMarkByte = 0x3f;
|
|
@@ -76,10 +80,51 @@ const stripKittyQueryResponsesAndTrailingPartial = (buffer) => {
|
|
|
76
80
|
}
|
|
77
81
|
return keptBytes;
|
|
78
82
|
};
|
|
83
|
+
const shouldClearTerminalForFrame = ({ isTty, viewportRows, previousOutputHeight, nextOutputHeight, isUnmounting, }) => {
|
|
84
|
+
if (!isTty) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
const hadPreviousFrame = previousOutputHeight > 0;
|
|
88
|
+
const wasFullscreen = previousOutputHeight >= viewportRows;
|
|
89
|
+
const wasOverflowing = previousOutputHeight > viewportRows;
|
|
90
|
+
const isOverflowing = nextOutputHeight > viewportRows;
|
|
91
|
+
const isLeavingFullscreen = wasFullscreen && nextOutputHeight < viewportRows;
|
|
92
|
+
const shouldClearOnUnmount = isUnmounting && wasFullscreen;
|
|
93
|
+
return (
|
|
94
|
+
// Overflowing frames still need full clear fallback.
|
|
95
|
+
wasOverflowing ||
|
|
96
|
+
(isOverflowing && hadPreviousFrame) ||
|
|
97
|
+
// Clear when shrinking from fullscreen to non-fullscreen output.
|
|
98
|
+
isLeavingFullscreen ||
|
|
99
|
+
// Preserve legacy unmount behavior for fullscreen frames: final teardown
|
|
100
|
+
// render should clear once to avoid leaving a scrolled viewport state.
|
|
101
|
+
shouldClearOnUnmount);
|
|
102
|
+
};
|
|
79
103
|
const isErrorInput = (value) => {
|
|
80
104
|
return (value instanceof Error ||
|
|
81
105
|
Object.prototype.toString.call(value) === '[object Error]');
|
|
82
106
|
};
|
|
107
|
+
const getWritableStreamState = (stdout) => {
|
|
108
|
+
const canWriteToStdout = !stdout.destroyed && !stdout.writableEnded && (stdout.writable ?? true);
|
|
109
|
+
const hasWritableState = stdout._writableState !== undefined || stdout.writableLength !== undefined;
|
|
110
|
+
return {
|
|
111
|
+
canWriteToStdout,
|
|
112
|
+
hasWritableState,
|
|
113
|
+
};
|
|
114
|
+
};
|
|
115
|
+
const settleThrottle = (throttled, canWriteToStdout) => {
|
|
116
|
+
if (!throttled ||
|
|
117
|
+
typeof throttled.flush !== 'function') {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const throttledValue = throttled;
|
|
121
|
+
if (canWriteToStdout) {
|
|
122
|
+
throttledValue.flush();
|
|
123
|
+
}
|
|
124
|
+
else if (typeof throttledValue.cancel === 'function') {
|
|
125
|
+
throttledValue.cancel();
|
|
126
|
+
}
|
|
127
|
+
};
|
|
83
128
|
export default class Ink {
|
|
84
129
|
/**
|
|
85
130
|
Whether this instance is using concurrent rendering mode.
|
|
@@ -90,6 +135,9 @@ export default class Ink {
|
|
|
90
135
|
cursorPosition;
|
|
91
136
|
throttledLog;
|
|
92
137
|
isScreenReaderEnabled;
|
|
138
|
+
interactive;
|
|
139
|
+
renderThrottleMs;
|
|
140
|
+
alternateScreen;
|
|
93
141
|
// Ignore last render after unmounting a tree to prevent empty output before exit
|
|
94
142
|
isUnmounted;
|
|
95
143
|
isUnmounting;
|
|
@@ -111,6 +159,7 @@ export default class Ink {
|
|
|
111
159
|
hasPendingThrottledRender = false;
|
|
112
160
|
kittyProtocolEnabled = false;
|
|
113
161
|
cancelKittyDetection;
|
|
162
|
+
nextRenderCommit;
|
|
114
163
|
constructor(options) {
|
|
115
164
|
autoBind(this);
|
|
116
165
|
this.options = options;
|
|
@@ -119,9 +168,18 @@ export default class Ink {
|
|
|
119
168
|
this.isScreenReaderEnabled =
|
|
120
169
|
options.isScreenReaderEnabled ??
|
|
121
170
|
process.env['INK_SCREEN_READER'] === 'true';
|
|
171
|
+
// CI detection takes precedence: even a TTY stdout in CI defaults to non-interactive.
|
|
172
|
+
// Using Boolean(isTTY) (rather than an 'in' guard) correctly handles piped streams
|
|
173
|
+
// where the property is absent (e.g. `node app.js | cat`).
|
|
174
|
+
this.interactive = this.resolveInteractiveOption(options.interactive);
|
|
175
|
+
this.alternateScreen = false;
|
|
122
176
|
const unthrottled = options.debug || this.isScreenReaderEnabled;
|
|
123
177
|
const maxFps = options.maxFps ?? 30;
|
|
178
|
+
// Treat non-positive maxFps as an internal fallback case, not a supported
|
|
179
|
+
// "disable throttling" mode. Keep animation scheduling on a normal cadence
|
|
180
|
+
// so future changes don't accidentally reintroduce zero-delay loops.
|
|
124
181
|
const renderThrottleMs = maxFps > 0 ? Math.max(1, Math.ceil(1000 / maxFps)) : 0;
|
|
182
|
+
this.renderThrottleMs = unthrottled ? 0 : renderThrottleMs;
|
|
125
183
|
if (unthrottled) {
|
|
126
184
|
this.rootNode.onRender = this.onRender;
|
|
127
185
|
this.throttledOnRender = undefined;
|
|
@@ -146,7 +204,7 @@ export default class Ink {
|
|
|
146
204
|
? this.log
|
|
147
205
|
: throttle((output) => {
|
|
148
206
|
const shouldWrite = this.log.willRender(output);
|
|
149
|
-
const sync =
|
|
207
|
+
const sync = this.shouldSync();
|
|
150
208
|
if (sync && shouldWrite) {
|
|
151
209
|
this.options.stdout.write(bsu);
|
|
152
210
|
}
|
|
@@ -167,7 +225,7 @@ export default class Ink {
|
|
|
167
225
|
this.lastOutput = '';
|
|
168
226
|
this.lastOutputToRender = '';
|
|
169
227
|
this.lastOutputHeight = 0;
|
|
170
|
-
this.lastTerminalWidth = this.
|
|
228
|
+
this.lastTerminalWidth = getWindowSize(this.options.stdout).columns;
|
|
171
229
|
// This variable is used only in debug mode to store full static output
|
|
172
230
|
// so that it's rerendered every time, not just new static parts, like in non-debug mode
|
|
173
231
|
this.fullStaticOutput = '';
|
|
@@ -177,32 +235,31 @@ export default class Ink {
|
|
|
177
235
|
this.container = reconciler.createContainer(this.rootNode, rootTag, null, false, null, 'id', () => { }, () => { }, () => { }, () => { });
|
|
178
236
|
// Unmount when process exits
|
|
179
237
|
this.unsubscribeExit = signalExit(this.unmount, { alwaysLast: false });
|
|
180
|
-
|
|
238
|
+
this.setAlternateScreen(Boolean(options.alternateScreen));
|
|
239
|
+
if (process.env['DEV'] === 'true') {
|
|
181
240
|
// @ts-expect-error outdated types
|
|
182
241
|
reconciler.injectIntoDevTools();
|
|
183
242
|
}
|
|
184
243
|
if (options.patchConsole) {
|
|
185
244
|
this.patchConsole();
|
|
186
245
|
}
|
|
187
|
-
if (
|
|
246
|
+
if (this.interactive) {
|
|
188
247
|
options.stdout.on('resize', this.resized);
|
|
189
248
|
this.unsubscribeResize = () => {
|
|
190
249
|
options.stdout.off('resize', this.resized);
|
|
191
250
|
};
|
|
192
251
|
}
|
|
193
252
|
this.initKittyKeyboard();
|
|
253
|
+
this.exitPromise = new Promise((resolve, reject) => {
|
|
254
|
+
this.resolveExitPromise = resolve;
|
|
255
|
+
this.rejectExitPromise = reject;
|
|
256
|
+
});
|
|
257
|
+
// Prevent global unhandled-rejection crashes when app code exits with an
|
|
258
|
+
// error but consumers never call waitUntilExit().
|
|
259
|
+
void this.exitPromise.catch(noop);
|
|
194
260
|
}
|
|
195
|
-
getTerminalWidth = () => {
|
|
196
|
-
// The 'columns' property can be undefined or 0 when not using a TTY.
|
|
197
|
-
// Use terminal-size as a fallback for piped processes, then default to 80.
|
|
198
|
-
if (this.options.stdout.columns) {
|
|
199
|
-
return this.options.stdout.columns;
|
|
200
|
-
}
|
|
201
|
-
const size = terminalSize();
|
|
202
|
-
return size?.columns ?? 80;
|
|
203
|
-
};
|
|
204
261
|
resized = () => {
|
|
205
|
-
const currentWidth = this.
|
|
262
|
+
const currentWidth = getWindowSize(this.options.stdout).columns;
|
|
206
263
|
if (currentWidth < this.lastTerminalWidth) {
|
|
207
264
|
// We clear the screen when decreasing terminal width to prevent duplicate overlapping re-renders.
|
|
208
265
|
this.log.clear();
|
|
@@ -232,13 +289,16 @@ export default class Ink {
|
|
|
232
289
|
this.log.setCursorPosition(position);
|
|
233
290
|
};
|
|
234
291
|
restoreLastOutput = () => {
|
|
292
|
+
if (!this.interactive) {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
235
295
|
// Clear() resets log-update's cursor state, so replay the latest cursor intent
|
|
236
296
|
// before restoring output after external stdout/stderr writes.
|
|
237
297
|
this.log.setCursorPosition(this.cursorPosition);
|
|
238
298
|
this.log(this.lastOutputToRender || this.lastOutput + '\n');
|
|
239
299
|
};
|
|
240
300
|
calculateLayout = () => {
|
|
241
|
-
const terminalWidth = this.
|
|
301
|
+
const terminalWidth = getWindowSize(this.options.stdout).columns;
|
|
242
302
|
this.rootNode.yogaNode.setWidth(terminalWidth);
|
|
243
303
|
this.rootNode.yogaNode.calculateLayout(undefined, undefined, Yoga.DIRECTION_LTR);
|
|
244
304
|
};
|
|
@@ -247,6 +307,10 @@ export default class Ink {
|
|
|
247
307
|
if (this.isUnmounted) {
|
|
248
308
|
return;
|
|
249
309
|
}
|
|
310
|
+
if (this.nextRenderCommit) {
|
|
311
|
+
this.nextRenderCommit.resolve();
|
|
312
|
+
this.nextRenderCommit = undefined;
|
|
313
|
+
}
|
|
250
314
|
const startTime = performance.now();
|
|
251
315
|
const { output, outputHeight, staticOutput } = render(this.rootNode, this.isScreenReaderEnabled);
|
|
252
316
|
this.options.onRender?.({ renderTime: performance.now() - startTime });
|
|
@@ -256,10 +320,13 @@ export default class Ink {
|
|
|
256
320
|
if (hasStaticOutput) {
|
|
257
321
|
this.fullStaticOutput += staticOutput;
|
|
258
322
|
}
|
|
323
|
+
this.lastOutput = output;
|
|
324
|
+
this.lastOutputToRender = output;
|
|
325
|
+
this.lastOutputHeight = outputHeight;
|
|
259
326
|
this.options.stdout.write(this.fullStaticOutput + output);
|
|
260
327
|
return;
|
|
261
328
|
}
|
|
262
|
-
if (
|
|
329
|
+
if (!this.interactive) {
|
|
263
330
|
if (hasStaticOutput) {
|
|
264
331
|
this.options.stdout.write(staticOutput);
|
|
265
332
|
}
|
|
@@ -269,7 +336,7 @@ export default class Ink {
|
|
|
269
336
|
return;
|
|
270
337
|
}
|
|
271
338
|
if (this.isScreenReaderEnabled) {
|
|
272
|
-
const sync =
|
|
339
|
+
const sync = this.shouldSync();
|
|
273
340
|
if (sync) {
|
|
274
341
|
this.options.stdout.write(bsu);
|
|
275
342
|
}
|
|
@@ -288,7 +355,7 @@ export default class Ink {
|
|
|
288
355
|
}
|
|
289
356
|
return;
|
|
290
357
|
}
|
|
291
|
-
const terminalWidth = this.
|
|
358
|
+
const terminalWidth = getWindowSize(this.options.stdout).columns;
|
|
292
359
|
const wrappedOutput = wrapAnsi(output, terminalWidth, {
|
|
293
360
|
trim: false,
|
|
294
361
|
hard: true,
|
|
@@ -315,49 +382,11 @@ export default class Ink {
|
|
|
315
382
|
if (hasStaticOutput) {
|
|
316
383
|
this.fullStaticOutput += staticOutput;
|
|
317
384
|
}
|
|
318
|
-
|
|
319
|
-
// Only apply when writing to a real TTY — piped output always gets trailing newlines.
|
|
320
|
-
const isFullscreen = this.options.stdout.isTTY && outputHeight >= this.options.stdout.rows;
|
|
321
|
-
const outputToRender = isFullscreen ? output : output + '\n';
|
|
322
|
-
if (this.lastOutputHeight >= this.options.stdout.rows) {
|
|
323
|
-
const sync = shouldSynchronize(this.options.stdout);
|
|
324
|
-
if (sync) {
|
|
325
|
-
this.options.stdout.write(bsu);
|
|
326
|
-
}
|
|
327
|
-
this.options.stdout.write(ansiEscapes.clearTerminal + this.fullStaticOutput + output);
|
|
328
|
-
this.lastOutput = output;
|
|
329
|
-
this.lastOutputToRender = outputToRender;
|
|
330
|
-
this.lastOutputHeight = outputHeight;
|
|
331
|
-
this.log.sync(outputToRender);
|
|
332
|
-
if (sync) {
|
|
333
|
-
this.options.stdout.write(esu);
|
|
334
|
-
}
|
|
335
|
-
return;
|
|
336
|
-
}
|
|
337
|
-
// To ensure static output is cleanly rendered before main output, clear main output first
|
|
338
|
-
if (hasStaticOutput) {
|
|
339
|
-
const sync = shouldSynchronize(this.options.stdout);
|
|
340
|
-
if (sync) {
|
|
341
|
-
this.options.stdout.write(bsu);
|
|
342
|
-
}
|
|
343
|
-
this.log.clear();
|
|
344
|
-
this.options.stdout.write(staticOutput);
|
|
345
|
-
this.log(outputToRender);
|
|
346
|
-
if (sync) {
|
|
347
|
-
this.options.stdout.write(esu);
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
else if (output !== this.lastOutput || this.log.isCursorDirty()) {
|
|
351
|
-
// ThrottledLog manages its own bsu/esu at actual write time
|
|
352
|
-
this.throttledLog(outputToRender);
|
|
353
|
-
}
|
|
354
|
-
this.lastOutput = output;
|
|
355
|
-
this.lastOutputToRender = outputToRender;
|
|
356
|
-
this.lastOutputHeight = outputHeight;
|
|
385
|
+
this.renderInteractiveFrame(output, outputHeight, hasStaticOutput ? staticOutput : '');
|
|
357
386
|
};
|
|
358
387
|
render(node) {
|
|
359
388
|
const tree = (React.createElement(AccessibilityContext.Provider, { value: { isScreenReaderEnabled: this.isScreenReaderEnabled } },
|
|
360
|
-
React.createElement(App, { stdin: this.options.stdin, stdout: this.options.stdout, stderr: this.options.stderr, exitOnCtrlC: this.options.exitOnCtrlC, writeToStdout: this.writeToStdout, writeToStderr: this.writeToStderr, setCursorPosition: this.setCursorPosition, onExit: this.handleAppExit }, node)));
|
|
389
|
+
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, onExit: this.handleAppExit, onWaitUntilRenderFlush: this.waitUntilRenderFlush }, node)));
|
|
361
390
|
if (this.options.concurrent) {
|
|
362
391
|
// Concurrent mode: use updateContainer (async scheduling)
|
|
363
392
|
reconciler.updateContainer(tree, this.container, null, noop);
|
|
@@ -376,11 +405,11 @@ export default class Ink {
|
|
|
376
405
|
this.options.stdout.write(data + this.fullStaticOutput + this.lastOutput);
|
|
377
406
|
return;
|
|
378
407
|
}
|
|
379
|
-
if (
|
|
408
|
+
if (!this.interactive) {
|
|
380
409
|
this.options.stdout.write(data);
|
|
381
410
|
return;
|
|
382
411
|
}
|
|
383
|
-
const sync =
|
|
412
|
+
const sync = this.shouldSync();
|
|
384
413
|
if (sync) {
|
|
385
414
|
this.options.stdout.write(bsu);
|
|
386
415
|
}
|
|
@@ -400,11 +429,11 @@ export default class Ink {
|
|
|
400
429
|
this.options.stdout.write(this.fullStaticOutput + this.lastOutput);
|
|
401
430
|
return;
|
|
402
431
|
}
|
|
403
|
-
if (
|
|
432
|
+
if (!this.interactive) {
|
|
404
433
|
this.options.stderr.write(data);
|
|
405
434
|
return;
|
|
406
435
|
}
|
|
407
|
-
const sync =
|
|
436
|
+
const sync = this.shouldSync();
|
|
408
437
|
if (sync) {
|
|
409
438
|
this.options.stdout.write(bsu);
|
|
410
439
|
}
|
|
@@ -415,7 +444,7 @@ export default class Ink {
|
|
|
415
444
|
this.options.stdout.write(esu);
|
|
416
445
|
}
|
|
417
446
|
}
|
|
418
|
-
// eslint-disable-next-line @typescript-eslint/
|
|
447
|
+
// eslint-disable-next-line @typescript-eslint/no-restricted-types
|
|
419
448
|
unmount(error) {
|
|
420
449
|
if (this.isUnmounted || this.isUnmounting) {
|
|
421
450
|
return;
|
|
@@ -426,21 +455,10 @@ export default class Ink {
|
|
|
426
455
|
this.beforeExitHandler = undefined;
|
|
427
456
|
}
|
|
428
457
|
const stdout = this.options.stdout;
|
|
429
|
-
const canWriteToStdout
|
|
430
|
-
const settleThrottle = (throttled) => {
|
|
431
|
-
if (typeof throttled.flush !== 'function') {
|
|
432
|
-
return;
|
|
433
|
-
}
|
|
434
|
-
if (canWriteToStdout) {
|
|
435
|
-
throttled.flush();
|
|
436
|
-
}
|
|
437
|
-
else if (typeof throttled.cancel === 'function') {
|
|
438
|
-
throttled.cancel();
|
|
439
|
-
}
|
|
440
|
-
};
|
|
458
|
+
const { canWriteToStdout, hasWritableState } = getWritableStreamState(stdout);
|
|
441
459
|
// Clear any pending throttled render timer on unmount. When stdout is writable,
|
|
442
460
|
// flush so the final frame is emitted; otherwise cancel to avoid delayed callbacks.
|
|
443
|
-
settleThrottle(this.throttledOnRender
|
|
461
|
+
settleThrottle(this.throttledOnRender, canWriteToStdout);
|
|
444
462
|
if (canWriteToStdout) {
|
|
445
463
|
// If throttling is enabled and there is already a pending render, flushing above
|
|
446
464
|
// is sufficient. Also avoid calling onRender() again when static output already
|
|
@@ -456,84 +474,95 @@ export default class Ink {
|
|
|
456
474
|
// that could re-enter exit() via synchronous write callbacks.
|
|
457
475
|
this.isUnmounted = true;
|
|
458
476
|
this.unsubscribeExit();
|
|
477
|
+
// Flush any pending throttled log writes if possible, otherwise cancel to
|
|
478
|
+
// prevent delayed callbacks from writing to a closed stream.
|
|
479
|
+
settleThrottle(this.throttledLog, canWriteToStdout);
|
|
459
480
|
if (typeof this.restoreConsole === 'function') {
|
|
481
|
+
// Once unmount starts, Ink stops trying to manage teardown-time
|
|
482
|
+
// console output. Restoring the native console before React cleanup keeps
|
|
483
|
+
// unmount behavior simple and avoids special-case handling for custom
|
|
484
|
+
// streams, fullscreen frames, and alternate-screen teardown.
|
|
460
485
|
this.restoreConsole();
|
|
461
486
|
}
|
|
462
|
-
|
|
463
|
-
this.unsubscribeResize
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
this.cancelKittyDetection
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
487
|
+
const finishUnmount = () => {
|
|
488
|
+
if (typeof this.unsubscribeResize === 'function') {
|
|
489
|
+
this.unsubscribeResize();
|
|
490
|
+
}
|
|
491
|
+
// Cancel any in-progress auto-detection before checking protocol state
|
|
492
|
+
if (this.cancelKittyDetection) {
|
|
493
|
+
this.cancelKittyDetection();
|
|
494
|
+
}
|
|
495
|
+
if (canWriteToStdout) {
|
|
496
|
+
if (this.kittyProtocolEnabled) {
|
|
497
|
+
this.writeBestEffort(this.options.stdout, '\u001B[<u');
|
|
498
|
+
}
|
|
499
|
+
// Alternate-screen content is disposable by design. We intentionally
|
|
500
|
+
// leave it active until React cleanup finishes, then restore the
|
|
501
|
+
// primary buffer without replaying prior frames, hook writes, or
|
|
502
|
+
// diagnostics onto it. Trying to preserve teardown output across the
|
|
503
|
+
// buffer switch adds fragile lifecycle-specific behavior, so Ink keeps
|
|
504
|
+
// alternate-screen teardown intentionally simple and best-effort.
|
|
505
|
+
if (this.alternateScreen) {
|
|
506
|
+
this.writeBestEffort(this.options.stdout, ansiEscapes.exitAlternativeScreen);
|
|
507
|
+
this.writeBestEffort(this.options.stdout, showCursorEscape);
|
|
508
|
+
this.alternateScreen = false;
|
|
509
|
+
}
|
|
510
|
+
if (!this.interactive) {
|
|
511
|
+
// Non-interactive environments don't handle erasing ansi escapes well.
|
|
512
|
+
// In debug mode, each render already writes to stdout, so only a trailing
|
|
513
|
+
// newline is needed. In non-debug mode, write the last frame now (it was
|
|
514
|
+
// deferred during rendering).
|
|
515
|
+
this.options.stdout.write(this.options.debug ? '\n' : this.lastOutput + '\n');
|
|
477
516
|
}
|
|
478
|
-
|
|
479
|
-
|
|
517
|
+
else if (!this.options.debug) {
|
|
518
|
+
this.log.done();
|
|
480
519
|
}
|
|
481
520
|
}
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
521
|
+
this.kittyProtocolEnabled = false;
|
|
522
|
+
instances.delete(this.options.stdout);
|
|
523
|
+
// Ensure all queued writes have been processed before resolving the
|
|
524
|
+
// exit promise. For real writable streams, queue an empty write as a
|
|
525
|
+
// barrier — its callback fires only after all prior writes complete.
|
|
526
|
+
// For non-stream objects (e.g. test spies), resolve on next tick.
|
|
527
|
+
//
|
|
528
|
+
// When called from signal-exit during process shutdown (error is a
|
|
529
|
+
// number or null rather than undefined/Error), resolve synchronously
|
|
530
|
+
// because the event loop is draining and async callbacks won't fire.
|
|
531
|
+
const { exitResult } = this;
|
|
532
|
+
const resolveOrReject = () => {
|
|
533
|
+
if (isErrorInput(error)) {
|
|
534
|
+
this.rejectExitPromise(error);
|
|
535
|
+
}
|
|
536
|
+
else {
|
|
537
|
+
this.resolveExitPromise(exitResult);
|
|
538
|
+
}
|
|
539
|
+
};
|
|
540
|
+
const isProcessExiting = error !== undefined && !isErrorInput(error);
|
|
541
|
+
if (isProcessExiting) {
|
|
542
|
+
resolveOrReject();
|
|
486
543
|
}
|
|
487
|
-
else if (
|
|
488
|
-
this.
|
|
544
|
+
else if (canWriteToStdout && hasWritableState) {
|
|
545
|
+
this.options.stdout.write('', resolveOrReject);
|
|
489
546
|
}
|
|
490
|
-
|
|
491
|
-
|
|
547
|
+
else {
|
|
548
|
+
setImmediate(resolveOrReject);
|
|
549
|
+
}
|
|
550
|
+
};
|
|
551
|
+
const concurrentReconciler = reconciler;
|
|
492
552
|
if (this.options.concurrent) {
|
|
493
|
-
|
|
494
|
-
reconciler.
|
|
553
|
+
reconciler.updateContainerSync(null, this.container, null, noop);
|
|
554
|
+
reconciler.flushSyncWork();
|
|
555
|
+
concurrentReconciler.flushPassiveEffects?.();
|
|
556
|
+
finishUnmount();
|
|
495
557
|
}
|
|
496
558
|
else {
|
|
497
559
|
// Legacy mode: use updateContainerSync + flushSyncWork (sync)
|
|
498
560
|
reconciler.updateContainerSync(null, this.container, null, noop);
|
|
499
561
|
reconciler.flushSyncWork();
|
|
500
|
-
|
|
501
|
-
instances.delete(this.options.stdout);
|
|
502
|
-
// Ensure all queued writes have been processed before resolving the
|
|
503
|
-
// exit promise. For real writable streams, queue an empty write as a
|
|
504
|
-
// barrier — its callback fires only after all prior writes complete.
|
|
505
|
-
// For non-stream objects (e.g. test spies), resolve on next tick.
|
|
506
|
-
//
|
|
507
|
-
// When called from signal-exit during process shutdown (error is a
|
|
508
|
-
// number or null rather than undefined/Error), resolve synchronously
|
|
509
|
-
// because the event loop is draining and async callbacks won't fire.
|
|
510
|
-
const { exitResult } = this;
|
|
511
|
-
const resolveOrReject = () => {
|
|
512
|
-
if (isErrorInput(error)) {
|
|
513
|
-
this.rejectExitPromise(error);
|
|
514
|
-
}
|
|
515
|
-
else {
|
|
516
|
-
this.resolveExitPromise(exitResult);
|
|
517
|
-
}
|
|
518
|
-
};
|
|
519
|
-
const isProcessExiting = error !== undefined && !isErrorInput(error);
|
|
520
|
-
const hasWritableState = stdout._writableState !== undefined ||
|
|
521
|
-
stdout.writableLength !== undefined;
|
|
522
|
-
if (isProcessExiting) {
|
|
523
|
-
resolveOrReject();
|
|
524
|
-
}
|
|
525
|
-
else if (canWriteToStdout && hasWritableState) {
|
|
526
|
-
this.options.stdout.write('', resolveOrReject);
|
|
527
|
-
}
|
|
528
|
-
else {
|
|
529
|
-
setImmediate(resolveOrReject);
|
|
562
|
+
finishUnmount();
|
|
530
563
|
}
|
|
531
564
|
}
|
|
532
565
|
async waitUntilExit() {
|
|
533
|
-
this.exitPromise ||= new Promise((resolve, reject) => {
|
|
534
|
-
this.resolveExitPromise = resolve;
|
|
535
|
-
this.rejectExitPromise = reject;
|
|
536
|
-
});
|
|
537
566
|
if (!this.beforeExitHandler) {
|
|
538
567
|
this.beforeExitHandler = () => {
|
|
539
568
|
this.unmount();
|
|
@@ -542,8 +571,46 @@ export default class Ink {
|
|
|
542
571
|
}
|
|
543
572
|
return this.exitPromise;
|
|
544
573
|
}
|
|
574
|
+
async waitUntilRenderFlush() {
|
|
575
|
+
if (this.isUnmounted || this.isUnmounting) {
|
|
576
|
+
await this.awaitExit();
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
// Yield to the macrotask queue so that React's scheduler has a chance to
|
|
580
|
+
// fire passive effects and process any work they enqueued.
|
|
581
|
+
await yieldImmediate();
|
|
582
|
+
if (this.isUnmounted || this.isUnmounting) {
|
|
583
|
+
await this.awaitExit();
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
// In concurrent mode, React's scheduler may still be mid-render after
|
|
587
|
+
// the yield. Wait for the next render commit instead of polling.
|
|
588
|
+
if (this.isConcurrent && this.hasPendingConcurrentWork()) {
|
|
589
|
+
await Promise.race([this.awaitNextRender(), this.awaitExit()]);
|
|
590
|
+
if (this.isUnmounted || this.isUnmounting) {
|
|
591
|
+
this.nextRenderCommit = undefined;
|
|
592
|
+
await this.awaitExit();
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
reconciler.flushSyncWork();
|
|
597
|
+
const stdout = this.options.stdout;
|
|
598
|
+
const { canWriteToStdout, hasWritableState } = getWritableStreamState(stdout);
|
|
599
|
+
// Flush pending throttled render/log timers so their output is included in this wait.
|
|
600
|
+
settleThrottle(this.throttledOnRender, canWriteToStdout);
|
|
601
|
+
settleThrottle(this.throttledLog, canWriteToStdout);
|
|
602
|
+
if (canWriteToStdout && hasWritableState) {
|
|
603
|
+
await new Promise(resolve => {
|
|
604
|
+
this.options.stdout.write('', () => {
|
|
605
|
+
resolve();
|
|
606
|
+
});
|
|
607
|
+
});
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
await yieldImmediate();
|
|
611
|
+
}
|
|
545
612
|
clear() {
|
|
546
|
-
if (
|
|
613
|
+
if (this.interactive && !this.options.debug) {
|
|
547
614
|
this.log.clear();
|
|
548
615
|
// Sync lastOutput so that unmount's final onRender
|
|
549
616
|
// sees it as unchanged and log-update skips it
|
|
@@ -566,6 +633,106 @@ export default class Ink {
|
|
|
566
633
|
}
|
|
567
634
|
});
|
|
568
635
|
}
|
|
636
|
+
setAlternateScreen(enabled) {
|
|
637
|
+
this.alternateScreen = this.resolveAlternateScreenOption(enabled, this.interactive);
|
|
638
|
+
if (this.alternateScreen) {
|
|
639
|
+
this.writeBestEffort(this.options.stdout, ansiEscapes.enterAlternativeScreen);
|
|
640
|
+
this.writeBestEffort(this.options.stdout, hideCursorEscape);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
resolveInteractiveOption(interactive) {
|
|
644
|
+
return interactive ?? (!isInCi && Boolean(this.options.stdout.isTTY));
|
|
645
|
+
}
|
|
646
|
+
resolveAlternateScreenOption(alternateScreen, interactive) {
|
|
647
|
+
return (Boolean(alternateScreen) &&
|
|
648
|
+
interactive &&
|
|
649
|
+
Boolean(this.options.stdout.isTTY));
|
|
650
|
+
}
|
|
651
|
+
shouldSync() {
|
|
652
|
+
return shouldSynchronize(this.options.stdout, this.interactive);
|
|
653
|
+
}
|
|
654
|
+
// Best-effort write: streams may already be destroyed during shutdown.
|
|
655
|
+
writeBestEffort(stream, data) {
|
|
656
|
+
try {
|
|
657
|
+
stream.write(data);
|
|
658
|
+
}
|
|
659
|
+
catch { }
|
|
660
|
+
}
|
|
661
|
+
// Waits for the exit promise to settle, suppressing any rejection.
|
|
662
|
+
// Errors are surfaced via waitUntilExit() instead.
|
|
663
|
+
async awaitExit() {
|
|
664
|
+
try {
|
|
665
|
+
await this.exitPromise;
|
|
666
|
+
}
|
|
667
|
+
catch { }
|
|
668
|
+
}
|
|
669
|
+
hasPendingConcurrentWork() {
|
|
670
|
+
const concurrentContainer = this.container;
|
|
671
|
+
return ((concurrentContainer.pendingLanes ?? 0) !== 0 &&
|
|
672
|
+
concurrentContainer.callbackNode !== undefined &&
|
|
673
|
+
concurrentContainer.callbackNode !== null);
|
|
674
|
+
}
|
|
675
|
+
async awaitNextRender() {
|
|
676
|
+
if (!this.nextRenderCommit) {
|
|
677
|
+
let resolveRender;
|
|
678
|
+
const promise = new Promise(resolve => {
|
|
679
|
+
resolveRender = resolve;
|
|
680
|
+
});
|
|
681
|
+
this.nextRenderCommit = { promise, resolve: resolveRender };
|
|
682
|
+
}
|
|
683
|
+
return this.nextRenderCommit.promise;
|
|
684
|
+
}
|
|
685
|
+
renderInteractiveFrame(output, outputHeight, staticOutput) {
|
|
686
|
+
const hasStaticOutput = staticOutput !== '';
|
|
687
|
+
const isTty = this.options.stdout.isTTY;
|
|
688
|
+
// Detect fullscreen: output fills or exceeds terminal height.
|
|
689
|
+
// Only apply when writing to a real TTY — piped output always gets trailing newlines.
|
|
690
|
+
const viewportRows = isTty ? getWindowSize(this.options.stdout).rows : 24;
|
|
691
|
+
const isFullscreen = isTty && outputHeight >= viewportRows;
|
|
692
|
+
const outputToRender = isFullscreen ? output : output + '\n';
|
|
693
|
+
const shouldClearTerminal = shouldClearTerminalForFrame({
|
|
694
|
+
isTty,
|
|
695
|
+
viewportRows,
|
|
696
|
+
previousOutputHeight: this.lastOutputHeight,
|
|
697
|
+
nextOutputHeight: outputHeight,
|
|
698
|
+
isUnmounting: this.isUnmounting,
|
|
699
|
+
});
|
|
700
|
+
if (shouldClearTerminal) {
|
|
701
|
+
const sync = this.shouldSync();
|
|
702
|
+
if (sync) {
|
|
703
|
+
this.options.stdout.write(bsu);
|
|
704
|
+
}
|
|
705
|
+
this.options.stdout.write(ansiEscapes.clearTerminal + this.fullStaticOutput + output);
|
|
706
|
+
this.lastOutput = output;
|
|
707
|
+
this.lastOutputToRender = outputToRender;
|
|
708
|
+
this.lastOutputHeight = outputHeight;
|
|
709
|
+
this.log.sync(outputToRender);
|
|
710
|
+
if (sync) {
|
|
711
|
+
this.options.stdout.write(esu);
|
|
712
|
+
}
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
// To ensure static output is cleanly rendered before main output, clear main output first
|
|
716
|
+
if (hasStaticOutput) {
|
|
717
|
+
const sync = this.shouldSync();
|
|
718
|
+
if (sync) {
|
|
719
|
+
this.options.stdout.write(bsu);
|
|
720
|
+
}
|
|
721
|
+
this.log.clear();
|
|
722
|
+
this.options.stdout.write(staticOutput);
|
|
723
|
+
this.log(outputToRender);
|
|
724
|
+
if (sync) {
|
|
725
|
+
this.options.stdout.write(esu);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
else if (output !== this.lastOutput || this.log.isCursorDirty()) {
|
|
729
|
+
// ThrottledLog manages its own bsu/esu at actual write time
|
|
730
|
+
this.throttledLog(outputToRender);
|
|
731
|
+
}
|
|
732
|
+
this.lastOutput = output;
|
|
733
|
+
this.lastOutputToRender = outputToRender;
|
|
734
|
+
this.lastOutputHeight = outputHeight;
|
|
735
|
+
}
|
|
569
736
|
initKittyKeyboard() {
|
|
570
737
|
// Protocol is opt-in: if kittyKeyboard is not specified, do nothing
|
|
571
738
|
if (!this.options.kittyKeyboard) {
|
|
@@ -573,26 +740,29 @@ export default class Ink {
|
|
|
573
740
|
}
|
|
574
741
|
const opts = this.options.kittyKeyboard;
|
|
575
742
|
const mode = opts.mode ?? 'auto';
|
|
576
|
-
if (mode === 'disabled'
|
|
577
|
-
!this.options.stdin.isTTY ||
|
|
578
|
-
!this.options.stdout.isTTY) {
|
|
743
|
+
if (mode === 'disabled') {
|
|
579
744
|
return;
|
|
580
745
|
}
|
|
581
746
|
const flags = opts.flags ?? ['disambiguateEscapeCodes'];
|
|
747
|
+
// 'enabled' force-enables the protocol as long as both streams are TTYs,
|
|
748
|
+
// regardless of the interactive setting (e.g. even in CI).
|
|
582
749
|
if (mode === 'enabled') {
|
|
583
|
-
this.
|
|
750
|
+
if (this.options.stdin.isTTY && this.options.stdout.isTTY) {
|
|
751
|
+
this.enableKittyProtocol(flags);
|
|
752
|
+
}
|
|
584
753
|
return;
|
|
585
754
|
}
|
|
586
|
-
// Auto mode:
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
termProgram === 'WezTerm' ||
|
|
592
|
-
termProgram === 'ghostty';
|
|
593
|
-
if (!isInCi && isKnownSupportingTerminal) {
|
|
594
|
-
this.confirmKittySupport(flags);
|
|
755
|
+
// Auto mode: require interactive + TTY
|
|
756
|
+
if (!this.interactive ||
|
|
757
|
+
!this.options.stdin.isTTY ||
|
|
758
|
+
!this.options.stdout.isTTY) {
|
|
759
|
+
return;
|
|
595
760
|
}
|
|
761
|
+
// Auto mode: query the terminal for kitty keyboard protocol support.
|
|
762
|
+
// The CSI ? u query is safe to send to any terminal — unsupporting
|
|
763
|
+
// terminals simply won't respond, and the 200ms timeout handles that.
|
|
764
|
+
// This avoids maintaining a hardcoded whitelist of terminal names.
|
|
765
|
+
this.confirmKittySupport(flags);
|
|
596
766
|
}
|
|
597
767
|
confirmKittySupport(flags) {
|
|
598
768
|
const { stdin, stdout } = this.options;
|
|
@@ -607,11 +777,11 @@ export default class Ink {
|
|
|
607
777
|
const remaining = stripKittyQueryResponsesAndTrailingPartial(responseBuffer);
|
|
608
778
|
responseBuffer = [];
|
|
609
779
|
if (remaining.length > 0) {
|
|
610
|
-
stdin.unshift(
|
|
780
|
+
stdin.unshift(Uint8Array.from(remaining));
|
|
611
781
|
}
|
|
612
782
|
};
|
|
613
783
|
const onData = (data) => {
|
|
614
|
-
const chunk = typeof data === 'string' ?
|
|
784
|
+
const chunk = typeof data === 'string' ? textEncoder.encode(data) : data;
|
|
615
785
|
for (const byte of chunk) {
|
|
616
786
|
responseBuffer.push(byte);
|
|
617
787
|
}
|