ink 6.7.0 → 7.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/ansi-tokenizer.d.ts +38 -0
- package/build/ansi-tokenizer.js +316 -0
- package/build/ansi-tokenizer.js.map +1 -0
- 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 +5 -2
- package/build/components/App.js +192 -41
- package/build/components/App.js.map +1 -1
- package/build/components/AppContext.d.ts +33 -3
- package/build/components/AppContext.js +2 -1
- 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 +25 -5
- 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 +5 -2
- 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 +10 -1
- package/build/index.js +5 -0
- package/build/index.js.map +1 -1
- package/build/ink.d.ts +55 -6
- package/build/ink.js +433 -162
- package/build/ink.js.map +1 -1
- package/build/input-parser.d.ts +10 -0
- package/build/input-parser.js +194 -0
- package/build/input-parser.js.map +1 -0
- 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.d.ts +1 -0
- package/build/output.js +63 -5
- 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 +48 -19
- 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.d.ts +38 -0
- package/build/render-to-string.js +116 -0
- package/build/render-to-string.js.map +1 -0
- package/build/render.d.ts +69 -3
- package/build/render.js +18 -11
- package/build/render.js.map +1 -1
- package/build/sanitize-ansi.d.ts +2 -0
- package/build/sanitize-ansi.js +27 -0
- package/build/sanitize-ansi.js.map +1 -0
- package/build/squash-text-nodes.js +2 -1
- package/build/squash-text-nodes.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 -0
- package/build/utils.js +19 -0
- package/build/utils.js.map +1 -0
- 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 +40 -101
- package/readme.md +674 -56
- 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/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/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,10 +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
|
|
12
|
+
import { getWindowSize } from './utils.js';
|
|
13
13
|
import reconciler from './reconciler.js';
|
|
14
14
|
import render from './renderer.js';
|
|
15
15
|
import * as dom from './dom.js';
|
|
16
|
+
import { hideCursorEscape, showCursorEscape } from './cursor-helpers.js';
|
|
16
17
|
import logUpdate from './log-update.js';
|
|
17
18
|
import { bsu, esu, shouldSynchronize } from './write-synchronized.js';
|
|
18
19
|
import instances from './instances.js';
|
|
@@ -20,6 +21,110 @@ import App from './components/App.js';
|
|
|
20
21
|
import { accessibilityContext as AccessibilityContext } from './components/AccessibilityContext.js';
|
|
21
22
|
import { resolveFlags, } from './kitty-keyboard.js';
|
|
22
23
|
const noop = () => { };
|
|
24
|
+
const textEncoder = new TextEncoder();
|
|
25
|
+
const yieldImmediate = async () => new Promise(resolve => {
|
|
26
|
+
setImmediate(resolve);
|
|
27
|
+
});
|
|
28
|
+
const kittyQueryEscapeByte = 0x1b;
|
|
29
|
+
const kittyQueryOpenBracketByte = 0x5b;
|
|
30
|
+
const kittyQueryQuestionMarkByte = 0x3f;
|
|
31
|
+
const kittyQueryLetterByte = 0x75;
|
|
32
|
+
const zeroByte = 0x30;
|
|
33
|
+
const nineByte = 0x39;
|
|
34
|
+
const isDigitByte = (byte) => byte >= zeroByte && byte <= nineByte;
|
|
35
|
+
const matchKittyQueryResponse = (buffer, startIndex) => {
|
|
36
|
+
if (buffer[startIndex] !== kittyQueryEscapeByte ||
|
|
37
|
+
buffer[startIndex + 1] !== kittyQueryOpenBracketByte ||
|
|
38
|
+
buffer[startIndex + 2] !== kittyQueryQuestionMarkByte) {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
let index = startIndex + 3;
|
|
42
|
+
const digitsStartIndex = index;
|
|
43
|
+
while (index < buffer.length && isDigitByte(buffer[index])) {
|
|
44
|
+
index++;
|
|
45
|
+
}
|
|
46
|
+
if (index === digitsStartIndex) {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
if (index === buffer.length) {
|
|
50
|
+
return { state: 'partial' };
|
|
51
|
+
}
|
|
52
|
+
if (buffer[index] === kittyQueryLetterByte) {
|
|
53
|
+
return { state: 'complete', endIndex: index };
|
|
54
|
+
}
|
|
55
|
+
return undefined;
|
|
56
|
+
};
|
|
57
|
+
const hasCompleteKittyQueryResponse = (buffer) => {
|
|
58
|
+
for (let index = 0; index < buffer.length; index++) {
|
|
59
|
+
const match = matchKittyQueryResponse(buffer, index);
|
|
60
|
+
if (match?.state === 'complete') {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return false;
|
|
65
|
+
};
|
|
66
|
+
const stripKittyQueryResponsesAndTrailingPartial = (buffer) => {
|
|
67
|
+
const keptBytes = [];
|
|
68
|
+
let index = 0;
|
|
69
|
+
while (index < buffer.length) {
|
|
70
|
+
const match = matchKittyQueryResponse(buffer, index);
|
|
71
|
+
if (match?.state === 'complete') {
|
|
72
|
+
index = match.endIndex + 1;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (match?.state === 'partial') {
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
keptBytes.push(buffer[index]);
|
|
79
|
+
index++;
|
|
80
|
+
}
|
|
81
|
+
return keptBytes;
|
|
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
|
+
};
|
|
103
|
+
const isErrorInput = (value) => {
|
|
104
|
+
return (value instanceof Error ||
|
|
105
|
+
Object.prototype.toString.call(value) === '[object Error]');
|
|
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
|
+
};
|
|
23
128
|
export default class Ink {
|
|
24
129
|
/**
|
|
25
130
|
Whether this instance is using concurrent rendering mode.
|
|
@@ -30,8 +135,12 @@ export default class Ink {
|
|
|
30
135
|
cursorPosition;
|
|
31
136
|
throttledLog;
|
|
32
137
|
isScreenReaderEnabled;
|
|
138
|
+
interactive;
|
|
139
|
+
renderThrottleMs;
|
|
140
|
+
alternateScreen;
|
|
33
141
|
// Ignore last render after unmounting a tree to prevent empty output before exit
|
|
34
142
|
isUnmounted;
|
|
143
|
+
isUnmounting;
|
|
35
144
|
lastOutput;
|
|
36
145
|
lastOutputToRender;
|
|
37
146
|
lastOutputHeight;
|
|
@@ -42,12 +151,15 @@ export default class Ink {
|
|
|
42
151
|
// so that it's rerendered every time, not just new static parts, like in non-debug mode
|
|
43
152
|
fullStaticOutput;
|
|
44
153
|
exitPromise;
|
|
154
|
+
exitResult;
|
|
45
155
|
beforeExitHandler;
|
|
46
156
|
restoreConsole;
|
|
47
157
|
unsubscribeResize;
|
|
48
158
|
throttledOnRender;
|
|
159
|
+
hasPendingThrottledRender = false;
|
|
49
160
|
kittyProtocolEnabled = false;
|
|
50
161
|
cancelKittyDetection;
|
|
162
|
+
nextRenderCommit;
|
|
51
163
|
constructor(options) {
|
|
52
164
|
autoBind(this);
|
|
53
165
|
this.options = options;
|
|
@@ -56,9 +168,18 @@ export default class Ink {
|
|
|
56
168
|
this.isScreenReaderEnabled =
|
|
57
169
|
options.isScreenReaderEnabled ??
|
|
58
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;
|
|
59
176
|
const unthrottled = options.debug || this.isScreenReaderEnabled;
|
|
60
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.
|
|
61
181
|
const renderThrottleMs = maxFps > 0 ? Math.max(1, Math.ceil(1000 / maxFps)) : 0;
|
|
182
|
+
this.renderThrottleMs = unthrottled ? 0 : renderThrottleMs;
|
|
62
183
|
if (unthrottled) {
|
|
63
184
|
this.rootNode.onRender = this.onRender;
|
|
64
185
|
this.throttledOnRender = undefined;
|
|
@@ -68,7 +189,10 @@ export default class Ink {
|
|
|
68
189
|
leading: true,
|
|
69
190
|
trailing: true,
|
|
70
191
|
});
|
|
71
|
-
this.rootNode.onRender =
|
|
192
|
+
this.rootNode.onRender = () => {
|
|
193
|
+
this.hasPendingThrottledRender = true;
|
|
194
|
+
throttled();
|
|
195
|
+
};
|
|
72
196
|
this.throttledOnRender = throttled;
|
|
73
197
|
}
|
|
74
198
|
this.rootNode.onImmediateRender = this.onRender;
|
|
@@ -80,7 +204,7 @@ export default class Ink {
|
|
|
80
204
|
? this.log
|
|
81
205
|
: throttle((output) => {
|
|
82
206
|
const shouldWrite = this.log.willRender(output);
|
|
83
|
-
const sync =
|
|
207
|
+
const sync = this.shouldSync();
|
|
84
208
|
if (sync && shouldWrite) {
|
|
85
209
|
this.options.stdout.write(bsu);
|
|
86
210
|
}
|
|
@@ -94,13 +218,14 @@ export default class Ink {
|
|
|
94
218
|
});
|
|
95
219
|
// Ignore last render after unmounting a tree to prevent empty output before exit
|
|
96
220
|
this.isUnmounted = false;
|
|
221
|
+
this.isUnmounting = false;
|
|
97
222
|
// Store concurrent mode setting
|
|
98
223
|
this.isConcurrent = options.concurrent ?? false;
|
|
99
224
|
// Store last output to only rerender when needed
|
|
100
225
|
this.lastOutput = '';
|
|
101
226
|
this.lastOutputToRender = '';
|
|
102
227
|
this.lastOutputHeight = 0;
|
|
103
|
-
this.lastTerminalWidth = this.
|
|
228
|
+
this.lastTerminalWidth = getWindowSize(this.options.stdout).columns;
|
|
104
229
|
// This variable is used only in debug mode to store full static output
|
|
105
230
|
// so that it's rerendered every time, not just new static parts, like in non-debug mode
|
|
106
231
|
this.fullStaticOutput = '';
|
|
@@ -110,37 +235,31 @@ export default class Ink {
|
|
|
110
235
|
this.container = reconciler.createContainer(this.rootNode, rootTag, null, false, null, 'id', () => { }, () => { }, () => { }, () => { });
|
|
111
236
|
// Unmount when process exits
|
|
112
237
|
this.unsubscribeExit = signalExit(this.unmount, { alwaysLast: false });
|
|
238
|
+
this.setAlternateScreen(Boolean(options.alternateScreen));
|
|
113
239
|
if (process.env['DEV'] === 'true') {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
// Reporting React DOM's version, not Ink's
|
|
117
|
-
// See https://github.com/facebook/react/issues/16666#issuecomment-532639905
|
|
118
|
-
version: '16.13.1',
|
|
119
|
-
rendererPackageName: 'ink',
|
|
120
|
-
});
|
|
240
|
+
// @ts-expect-error outdated types
|
|
241
|
+
reconciler.injectIntoDevTools();
|
|
121
242
|
}
|
|
122
243
|
if (options.patchConsole) {
|
|
123
244
|
this.patchConsole();
|
|
124
245
|
}
|
|
125
|
-
if (
|
|
246
|
+
if (this.interactive) {
|
|
126
247
|
options.stdout.on('resize', this.resized);
|
|
127
248
|
this.unsubscribeResize = () => {
|
|
128
249
|
options.stdout.off('resize', this.resized);
|
|
129
250
|
};
|
|
130
251
|
}
|
|
131
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);
|
|
132
260
|
}
|
|
133
|
-
getTerminalWidth = () => {
|
|
134
|
-
// The 'columns' property can be undefined or 0 when not using a TTY.
|
|
135
|
-
// Use terminal-size as a fallback for piped processes, then default to 80.
|
|
136
|
-
if (this.options.stdout.columns) {
|
|
137
|
-
return this.options.stdout.columns;
|
|
138
|
-
}
|
|
139
|
-
const size = terminalSize();
|
|
140
|
-
return size?.columns ?? 80;
|
|
141
|
-
};
|
|
142
261
|
resized = () => {
|
|
143
|
-
const currentWidth = this.
|
|
262
|
+
const currentWidth = getWindowSize(this.options.stdout).columns;
|
|
144
263
|
if (currentWidth < this.lastTerminalWidth) {
|
|
145
264
|
// We clear the screen when decreasing terminal width to prevent duplicate overlapping re-renders.
|
|
146
265
|
this.log.clear();
|
|
@@ -154,25 +273,44 @@ export default class Ink {
|
|
|
154
273
|
resolveExitPromise = () => { };
|
|
155
274
|
rejectExitPromise = () => { };
|
|
156
275
|
unsubscribeExit = () => { };
|
|
276
|
+
handleAppExit = (errorOrResult) => {
|
|
277
|
+
if (this.isUnmounted || this.isUnmounting) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
if (isErrorInput(errorOrResult)) {
|
|
281
|
+
this.unmount(errorOrResult);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
this.exitResult = errorOrResult;
|
|
285
|
+
this.unmount();
|
|
286
|
+
};
|
|
157
287
|
setCursorPosition = (position) => {
|
|
158
288
|
this.cursorPosition = position;
|
|
159
289
|
this.log.setCursorPosition(position);
|
|
160
290
|
};
|
|
161
291
|
restoreLastOutput = () => {
|
|
292
|
+
if (!this.interactive) {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
162
295
|
// Clear() resets log-update's cursor state, so replay the latest cursor intent
|
|
163
296
|
// before restoring output after external stdout/stderr writes.
|
|
164
297
|
this.log.setCursorPosition(this.cursorPosition);
|
|
165
298
|
this.log(this.lastOutputToRender || this.lastOutput + '\n');
|
|
166
299
|
};
|
|
167
300
|
calculateLayout = () => {
|
|
168
|
-
const terminalWidth = this.
|
|
301
|
+
const terminalWidth = getWindowSize(this.options.stdout).columns;
|
|
169
302
|
this.rootNode.yogaNode.setWidth(terminalWidth);
|
|
170
303
|
this.rootNode.yogaNode.calculateLayout(undefined, undefined, Yoga.DIRECTION_LTR);
|
|
171
304
|
};
|
|
172
305
|
onRender = () => {
|
|
306
|
+
this.hasPendingThrottledRender = false;
|
|
173
307
|
if (this.isUnmounted) {
|
|
174
308
|
return;
|
|
175
309
|
}
|
|
310
|
+
if (this.nextRenderCommit) {
|
|
311
|
+
this.nextRenderCommit.resolve();
|
|
312
|
+
this.nextRenderCommit = undefined;
|
|
313
|
+
}
|
|
176
314
|
const startTime = performance.now();
|
|
177
315
|
const { output, outputHeight, staticOutput } = render(this.rootNode, this.isScreenReaderEnabled);
|
|
178
316
|
this.options.onRender?.({ renderTime: performance.now() - startTime });
|
|
@@ -182,10 +320,13 @@ export default class Ink {
|
|
|
182
320
|
if (hasStaticOutput) {
|
|
183
321
|
this.fullStaticOutput += staticOutput;
|
|
184
322
|
}
|
|
323
|
+
this.lastOutput = output;
|
|
324
|
+
this.lastOutputToRender = output;
|
|
325
|
+
this.lastOutputHeight = outputHeight;
|
|
185
326
|
this.options.stdout.write(this.fullStaticOutput + output);
|
|
186
327
|
return;
|
|
187
328
|
}
|
|
188
|
-
if (
|
|
329
|
+
if (!this.interactive) {
|
|
189
330
|
if (hasStaticOutput) {
|
|
190
331
|
this.options.stdout.write(staticOutput);
|
|
191
332
|
}
|
|
@@ -195,7 +336,7 @@ export default class Ink {
|
|
|
195
336
|
return;
|
|
196
337
|
}
|
|
197
338
|
if (this.isScreenReaderEnabled) {
|
|
198
|
-
const sync =
|
|
339
|
+
const sync = this.shouldSync();
|
|
199
340
|
if (sync) {
|
|
200
341
|
this.options.stdout.write(bsu);
|
|
201
342
|
}
|
|
@@ -214,7 +355,7 @@ export default class Ink {
|
|
|
214
355
|
}
|
|
215
356
|
return;
|
|
216
357
|
}
|
|
217
|
-
const terminalWidth = this.
|
|
358
|
+
const terminalWidth = getWindowSize(this.options.stdout).columns;
|
|
218
359
|
const wrappedOutput = wrapAnsi(output, terminalWidth, {
|
|
219
360
|
trim: false,
|
|
220
361
|
hard: true,
|
|
@@ -241,49 +382,11 @@ export default class Ink {
|
|
|
241
382
|
if (hasStaticOutput) {
|
|
242
383
|
this.fullStaticOutput += staticOutput;
|
|
243
384
|
}
|
|
244
|
-
|
|
245
|
-
// Only apply when writing to a real TTY — piped output always gets trailing newlines.
|
|
246
|
-
const isFullscreen = this.options.stdout.isTTY && outputHeight >= this.options.stdout.rows;
|
|
247
|
-
const outputToRender = isFullscreen ? output : output + '\n';
|
|
248
|
-
if (this.lastOutputHeight >= this.options.stdout.rows) {
|
|
249
|
-
const sync = shouldSynchronize(this.options.stdout);
|
|
250
|
-
if (sync) {
|
|
251
|
-
this.options.stdout.write(bsu);
|
|
252
|
-
}
|
|
253
|
-
this.options.stdout.write(ansiEscapes.clearTerminal + this.fullStaticOutput + output);
|
|
254
|
-
this.lastOutput = output;
|
|
255
|
-
this.lastOutputToRender = outputToRender;
|
|
256
|
-
this.lastOutputHeight = outputHeight;
|
|
257
|
-
this.log.sync(outputToRender);
|
|
258
|
-
if (sync) {
|
|
259
|
-
this.options.stdout.write(esu);
|
|
260
|
-
}
|
|
261
|
-
return;
|
|
262
|
-
}
|
|
263
|
-
// To ensure static output is cleanly rendered before main output, clear main output first
|
|
264
|
-
if (hasStaticOutput) {
|
|
265
|
-
const sync = shouldSynchronize(this.options.stdout);
|
|
266
|
-
if (sync) {
|
|
267
|
-
this.options.stdout.write(bsu);
|
|
268
|
-
}
|
|
269
|
-
this.log.clear();
|
|
270
|
-
this.options.stdout.write(staticOutput);
|
|
271
|
-
this.log(outputToRender);
|
|
272
|
-
if (sync) {
|
|
273
|
-
this.options.stdout.write(esu);
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
else if (output !== this.lastOutput || this.log.isCursorDirty()) {
|
|
277
|
-
// ThrottledLog manages its own bsu/esu at actual write time
|
|
278
|
-
this.throttledLog(outputToRender);
|
|
279
|
-
}
|
|
280
|
-
this.lastOutput = output;
|
|
281
|
-
this.lastOutputToRender = outputToRender;
|
|
282
|
-
this.lastOutputHeight = outputHeight;
|
|
385
|
+
this.renderInteractiveFrame(output, outputHeight, hasStaticOutput ? staticOutput : '');
|
|
283
386
|
};
|
|
284
387
|
render(node) {
|
|
285
388
|
const tree = (React.createElement(AccessibilityContext.Provider, { value: { isScreenReaderEnabled: this.isScreenReaderEnabled } },
|
|
286
|
-
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.
|
|
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)));
|
|
287
390
|
if (this.options.concurrent) {
|
|
288
391
|
// Concurrent mode: use updateContainer (async scheduling)
|
|
289
392
|
reconciler.updateContainer(tree, this.container, null, noop);
|
|
@@ -302,11 +405,11 @@ export default class Ink {
|
|
|
302
405
|
this.options.stdout.write(data + this.fullStaticOutput + this.lastOutput);
|
|
303
406
|
return;
|
|
304
407
|
}
|
|
305
|
-
if (
|
|
408
|
+
if (!this.interactive) {
|
|
306
409
|
this.options.stdout.write(data);
|
|
307
410
|
return;
|
|
308
411
|
}
|
|
309
|
-
const sync =
|
|
412
|
+
const sync = this.shouldSync();
|
|
310
413
|
if (sync) {
|
|
311
414
|
this.options.stdout.write(bsu);
|
|
312
415
|
}
|
|
@@ -326,11 +429,11 @@ export default class Ink {
|
|
|
326
429
|
this.options.stdout.write(this.fullStaticOutput + this.lastOutput);
|
|
327
430
|
return;
|
|
328
431
|
}
|
|
329
|
-
if (
|
|
432
|
+
if (!this.interactive) {
|
|
330
433
|
this.options.stderr.write(data);
|
|
331
434
|
return;
|
|
332
435
|
}
|
|
333
|
-
const sync =
|
|
436
|
+
const sync = this.shouldSync();
|
|
334
437
|
if (sync) {
|
|
335
438
|
this.options.stdout.write(bsu);
|
|
336
439
|
}
|
|
@@ -341,98 +444,125 @@ export default class Ink {
|
|
|
341
444
|
this.options.stdout.write(esu);
|
|
342
445
|
}
|
|
343
446
|
}
|
|
344
|
-
// eslint-disable-next-line @typescript-eslint/
|
|
447
|
+
// eslint-disable-next-line @typescript-eslint/no-restricted-types
|
|
345
448
|
unmount(error) {
|
|
346
|
-
if (this.isUnmounted) {
|
|
449
|
+
if (this.isUnmounted || this.isUnmounting) {
|
|
347
450
|
return;
|
|
348
451
|
}
|
|
452
|
+
this.isUnmounting = true;
|
|
349
453
|
if (this.beforeExitHandler) {
|
|
350
454
|
process.off('beforeExit', this.beforeExitHandler);
|
|
351
455
|
this.beforeExitHandler = undefined;
|
|
352
456
|
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
457
|
+
const stdout = this.options.stdout;
|
|
458
|
+
const { canWriteToStdout, hasWritableState } = getWritableStreamState(stdout);
|
|
459
|
+
// Clear any pending throttled render timer on unmount. When stdout is writable,
|
|
460
|
+
// flush so the final frame is emitted; otherwise cancel to avoid delayed callbacks.
|
|
461
|
+
settleThrottle(this.throttledOnRender, canWriteToStdout);
|
|
462
|
+
if (canWriteToStdout) {
|
|
463
|
+
// If throttling is enabled and there is already a pending render, flushing above
|
|
464
|
+
// is sufficient. Also avoid calling onRender() again when static output already
|
|
465
|
+
// exists, as that can duplicate <Static> children output on exit (see issue #397).
|
|
466
|
+
const shouldRenderFinalFrame = !this.throttledOnRender ||
|
|
467
|
+
(!this.hasPendingThrottledRender && this.fullStaticOutput === '');
|
|
468
|
+
if (shouldRenderFinalFrame) {
|
|
469
|
+
this.calculateLayout();
|
|
470
|
+
this.onRender();
|
|
471
|
+
}
|
|
356
472
|
}
|
|
357
|
-
|
|
358
|
-
|
|
473
|
+
// Mark as unmounted after the final render but before stdout writes
|
|
474
|
+
// that could re-enter exit() via synchronous write callbacks.
|
|
475
|
+
this.isUnmounted = true;
|
|
359
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);
|
|
360
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.
|
|
361
485
|
this.restoreConsole();
|
|
362
486
|
}
|
|
363
|
-
|
|
364
|
-
this.unsubscribeResize
|
|
365
|
-
|
|
366
|
-
// Flush any pending throttled log writes
|
|
367
|
-
const throttledLog = this.throttledLog;
|
|
368
|
-
if (typeof throttledLog.flush === 'function') {
|
|
369
|
-
throttledLog.flush();
|
|
370
|
-
}
|
|
371
|
-
// Cancel any in-progress auto-detection before checking protocol state
|
|
372
|
-
if (this.cancelKittyDetection) {
|
|
373
|
-
this.cancelKittyDetection();
|
|
374
|
-
}
|
|
375
|
-
if (this.kittyProtocolEnabled) {
|
|
376
|
-
try {
|
|
377
|
-
this.options.stdout.write('\u001B[<u');
|
|
487
|
+
const finishUnmount = () => {
|
|
488
|
+
if (typeof this.unsubscribeResize === 'function') {
|
|
489
|
+
this.unsubscribeResize();
|
|
378
490
|
}
|
|
379
|
-
|
|
380
|
-
|
|
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');
|
|
516
|
+
}
|
|
517
|
+
else if (!this.options.debug) {
|
|
518
|
+
this.log.done();
|
|
519
|
+
}
|
|
381
520
|
}
|
|
382
521
|
this.kittyProtocolEnabled = false;
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
//
|
|
408
|
-
// When called from signal-exit during process shutdown (error is a
|
|
409
|
-
// number or null rather than undefined/Error), resolve synchronously
|
|
410
|
-
// because the event loop is draining and async callbacks won't fire.
|
|
411
|
-
const resolveOrReject = () => {
|
|
412
|
-
if (error instanceof Error) {
|
|
413
|
-
this.rejectExitPromise(error);
|
|
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();
|
|
543
|
+
}
|
|
544
|
+
else if (canWriteToStdout && hasWritableState) {
|
|
545
|
+
this.options.stdout.write('', resolveOrReject);
|
|
414
546
|
}
|
|
415
547
|
else {
|
|
416
|
-
|
|
548
|
+
setImmediate(resolveOrReject);
|
|
417
549
|
}
|
|
418
550
|
};
|
|
419
|
-
const
|
|
420
|
-
if (
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
this.options.stdout.write('', resolveOrReject);
|
|
551
|
+
const concurrentReconciler = reconciler;
|
|
552
|
+
if (this.options.concurrent) {
|
|
553
|
+
reconciler.updateContainerSync(null, this.container, null, noop);
|
|
554
|
+
reconciler.flushSyncWork();
|
|
555
|
+
concurrentReconciler.flushPassiveEffects?.();
|
|
556
|
+
finishUnmount();
|
|
426
557
|
}
|
|
427
558
|
else {
|
|
428
|
-
|
|
559
|
+
// Legacy mode: use updateContainerSync + flushSyncWork (sync)
|
|
560
|
+
reconciler.updateContainerSync(null, this.container, null, noop);
|
|
561
|
+
reconciler.flushSyncWork();
|
|
562
|
+
finishUnmount();
|
|
429
563
|
}
|
|
430
564
|
}
|
|
431
565
|
async waitUntilExit() {
|
|
432
|
-
this.exitPromise ||= new Promise((resolve, reject) => {
|
|
433
|
-
this.resolveExitPromise = resolve;
|
|
434
|
-
this.rejectExitPromise = reject;
|
|
435
|
-
});
|
|
436
566
|
if (!this.beforeExitHandler) {
|
|
437
567
|
this.beforeExitHandler = () => {
|
|
438
568
|
this.unmount();
|
|
@@ -441,8 +571,46 @@ export default class Ink {
|
|
|
441
571
|
}
|
|
442
572
|
return this.exitPromise;
|
|
443
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
|
+
}
|
|
444
612
|
clear() {
|
|
445
|
-
if (
|
|
613
|
+
if (this.interactive && !this.options.debug) {
|
|
446
614
|
this.log.clear();
|
|
447
615
|
// Sync lastOutput so that unmount's final onRender
|
|
448
616
|
// sees it as unchanged and log-update skips it
|
|
@@ -465,6 +633,106 @@ export default class Ink {
|
|
|
465
633
|
}
|
|
466
634
|
});
|
|
467
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
|
+
}
|
|
468
736
|
initKittyKeyboard() {
|
|
469
737
|
// Protocol is opt-in: if kittyKeyboard is not specified, do nothing
|
|
470
738
|
if (!this.options.kittyKeyboard) {
|
|
@@ -472,30 +740,33 @@ export default class Ink {
|
|
|
472
740
|
}
|
|
473
741
|
const opts = this.options.kittyKeyboard;
|
|
474
742
|
const mode = opts.mode ?? 'auto';
|
|
475
|
-
if (mode === 'disabled'
|
|
476
|
-
!this.options.stdin.isTTY ||
|
|
477
|
-
!this.options.stdout.isTTY) {
|
|
743
|
+
if (mode === 'disabled') {
|
|
478
744
|
return;
|
|
479
745
|
}
|
|
480
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).
|
|
481
749
|
if (mode === 'enabled') {
|
|
482
|
-
this.
|
|
750
|
+
if (this.options.stdin.isTTY && this.options.stdout.isTTY) {
|
|
751
|
+
this.enableKittyProtocol(flags);
|
|
752
|
+
}
|
|
483
753
|
return;
|
|
484
754
|
}
|
|
485
|
-
// Auto mode:
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
termProgram === 'WezTerm' ||
|
|
491
|
-
termProgram === 'ghostty';
|
|
492
|
-
if (!isInCi && isKnownSupportingTerminal) {
|
|
493
|
-
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;
|
|
494
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);
|
|
495
766
|
}
|
|
496
767
|
confirmKittySupport(flags) {
|
|
497
768
|
const { stdin, stdout } = this.options;
|
|
498
|
-
let responseBuffer =
|
|
769
|
+
let responseBuffer = [];
|
|
499
770
|
const cleanup = () => {
|
|
500
771
|
this.cancelKittyDetection = undefined;
|
|
501
772
|
clearTimeout(timer);
|
|
@@ -503,18 +774,18 @@ export default class Ink {
|
|
|
503
774
|
// Re-emit any buffered data that wasn't the protocol response,
|
|
504
775
|
// so it isn't lost from Ink's normal input pipeline.
|
|
505
776
|
// Clear responseBuffer afterwards to make cleanup idempotent.
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
stdin.unshift(Buffer.from(remaining));
|
|
777
|
+
const remaining = stripKittyQueryResponsesAndTrailingPartial(responseBuffer);
|
|
778
|
+
responseBuffer = [];
|
|
779
|
+
if (remaining.length > 0) {
|
|
780
|
+
stdin.unshift(Uint8Array.from(remaining));
|
|
511
781
|
}
|
|
512
782
|
};
|
|
513
783
|
const onData = (data) => {
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
784
|
+
const chunk = typeof data === 'string' ? textEncoder.encode(data) : data;
|
|
785
|
+
for (const byte of chunk) {
|
|
786
|
+
responseBuffer.push(byte);
|
|
787
|
+
}
|
|
788
|
+
if (hasCompleteKittyQueryResponse(responseBuffer)) {
|
|
518
789
|
cleanup();
|
|
519
790
|
if (!this.isUnmounted) {
|
|
520
791
|
this.enableKittyProtocol(flags);
|