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