@elench/testkit 0.1.114 → 0.1.115
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/lib/cli/assistant/app.mjs +4 -2
- package/lib/cli/assistant/session.mjs +5 -1
- package/lib/cli/assistant/state.mjs +1 -2
- package/lib/cli/components/blocks/run-tree.mjs +7 -2
- package/lib/cli/components/hooks/use-element-layout.mjs +63 -0
- package/lib/cli/components/hooks/use-spinner-frame.mjs +26 -0
- package/node_modules/@alcalzone/ansi-tokenize/README.md +0 -5
- package/node_modules/@alcalzone/ansi-tokenize/build/ansiCodes.d.ts +8 -0
- package/node_modules/@alcalzone/ansi-tokenize/build/ansiCodes.js +10 -8
- package/node_modules/@alcalzone/ansi-tokenize/build/ansiCodes.js.map +1 -1
- package/node_modules/@alcalzone/ansi-tokenize/build/tokenize.d.ts +1 -5
- package/node_modules/@alcalzone/ansi-tokenize/build/tokenize.js +9 -45
- package/node_modules/@alcalzone/ansi-tokenize/build/tokenize.js.map +1 -1
- package/node_modules/@alcalzone/ansi-tokenize/package.json +1 -1
- package/node_modules/@elench/next-analysis/package.json +1 -1
- package/node_modules/@elench/testkit-bridge/package.json +2 -2
- package/node_modules/@elench/testkit-protocol/package.json +1 -1
- package/node_modules/@elench/ts-analysis/package.json +1 -1
- package/node_modules/cli-boxes/index.d.ts +95 -90
- package/node_modules/cli-boxes/index.js +5 -2
- package/node_modules/cli-boxes/package.json +6 -13
- package/node_modules/cli-boxes/readme.md +15 -3
- package/node_modules/cli-truncate/index.d.ts +1 -1
- package/node_modules/cli-truncate/package.json +4 -4
- package/node_modules/cli-truncate/readme.md +1 -0
- package/node_modules/ink/build/apply-styles.js +175 -0
- package/node_modules/ink/build/build-layout.js +77 -0
- package/node_modules/ink/build/calculate-wrapped-text.js +53 -0
- package/node_modules/ink/build/components/App.d.ts +1 -4
- package/node_modules/ink/build/components/App.js +22 -142
- package/node_modules/ink/build/components/App.js.map +1 -1
- package/node_modules/ink/build/components/AppContext.d.ts +3 -23
- package/node_modules/ink/build/components/AppContext.js +4 -7
- package/node_modules/ink/build/components/AppContext.js.map +1 -1
- package/node_modules/ink/build/components/Box.d.ts +3 -16
- package/node_modules/ink/build/components/Color.js +62 -0
- package/node_modules/ink/build/components/Cursor.d.ts +83 -0
- package/node_modules/ink/build/components/Cursor.js +53 -0
- package/node_modules/ink/build/components/Cursor.js.map +1 -0
- package/node_modules/ink/build/components/ErrorBoundary.d.ts +2 -2
- package/node_modules/ink/build/components/ErrorOverview.js +6 -6
- package/node_modules/ink/build/components/ErrorOverview.js.map +1 -1
- package/node_modules/ink/build/components/Static.js.map +1 -1
- package/node_modules/ink/build/components/StdinContext.d.ts +1 -7
- package/node_modules/ink/build/components/StdinContext.js +0 -1
- package/node_modules/ink/build/components/StdinContext.js.map +1 -1
- package/node_modules/ink/build/components/Text.d.ts +1 -1
- package/node_modules/ink/build/components/Text.js +1 -1
- package/node_modules/ink/build/components/Text.js.map +1 -1
- package/node_modules/ink/build/components/Transform.d.ts +1 -1
- package/node_modules/ink/build/devtools-window-polyfill.js +4 -7
- package/node_modules/ink/build/devtools-window-polyfill.js.map +1 -1
- package/node_modules/ink/build/devtools.js +6 -31
- package/node_modules/ink/build/devtools.js.map +1 -1
- package/node_modules/ink/build/dom.d.ts +1 -5
- package/node_modules/ink/build/dom.js +1 -20
- package/node_modules/ink/build/dom.js.map +1 -1
- package/node_modules/ink/build/experimental/apply-style.js +140 -0
- package/node_modules/ink/build/experimental/dom.js +123 -0
- package/node_modules/ink/build/experimental/output.js +91 -0
- package/node_modules/ink/build/experimental/reconciler.js +141 -0
- package/node_modules/ink/build/experimental/renderer.js +81 -0
- package/node_modules/ink/build/hooks/use-app.d.ts +1 -1
- package/node_modules/ink/build/hooks/use-app.js +1 -1
- package/node_modules/ink/build/hooks/use-cursor.d.ts +1 -1
- package/node_modules/ink/build/hooks/use-cursor.js +1 -1
- package/node_modules/ink/build/hooks/use-focus-manager.d.ts +2 -17
- package/node_modules/ink/build/hooks/use-focus-manager.js +1 -2
- package/node_modules/ink/build/hooks/use-focus-manager.js.map +1 -1
- package/node_modules/ink/build/hooks/use-focus.d.ts +1 -2
- package/node_modules/ink/build/hooks/use-focus.js +4 -5
- package/node_modules/ink/build/hooks/use-focus.js.map +1 -1
- package/node_modules/ink/build/hooks/use-input.d.ts +1 -2
- package/node_modules/ink/build/hooks/use-input.js +80 -82
- package/node_modules/ink/build/hooks/use-input.js.map +1 -1
- package/node_modules/ink/build/hooks/use-is-screen-reader-enabled.d.ts +1 -2
- package/node_modules/ink/build/hooks/use-is-screen-reader-enabled.js +1 -2
- package/node_modules/ink/build/hooks/use-is-screen-reader-enabled.js.map +1 -1
- package/node_modules/ink/build/hooks/use-stderr.d.ts +1 -1
- package/node_modules/ink/build/hooks/use-stderr.js +1 -1
- package/node_modules/ink/build/hooks/use-stdin.d.ts +2 -4
- package/node_modules/ink/build/hooks/use-stdin.js +1 -2
- package/node_modules/ink/build/hooks/use-stdin.js.map +1 -1
- package/node_modules/ink/build/hooks/use-stdout.d.ts +1 -1
- package/node_modules/ink/build/hooks/use-stdout.js +1 -1
- package/node_modules/ink/build/hooks/useInput.js +38 -0
- package/node_modules/ink/build/index.d.ts +1 -8
- package/node_modules/ink/build/index.js +0 -4
- package/node_modules/ink/build/index.js.map +1 -1
- package/node_modules/ink/build/ink.d.ts +3 -48
- package/node_modules/ink/build/ink.js +155 -325
- package/node_modules/ink/build/ink.js.map +1 -1
- package/node_modules/ink/build/input-parser.d.ts +1 -4
- package/node_modules/ink/build/input-parser.js +30 -70
- package/node_modules/ink/build/input-parser.js.map +1 -1
- package/node_modules/ink/build/instance.js +205 -0
- package/node_modules/ink/build/layout.d.ts +7 -0
- package/node_modules/ink/build/layout.js +33 -0
- package/node_modules/ink/build/layout.js.map +1 -0
- package/node_modules/ink/build/log-update.d.ts +0 -1
- package/node_modules/ink/build/log-update.js +1 -13
- package/node_modules/ink/build/log-update.js.map +1 -1
- package/node_modules/ink/build/measure-element.d.ts +0 -4
- package/node_modules/ink/build/measure-element.js +0 -4
- package/node_modules/ink/build/measure-element.js.map +1 -1
- package/node_modules/ink/build/options.d.ts +52 -0
- package/node_modules/ink/build/options.js +2 -0
- package/node_modules/ink/build/options.js.map +1 -0
- package/node_modules/ink/build/output.js +0 -25
- package/node_modules/ink/build/output.js.map +1 -1
- package/node_modules/ink/build/parse-keypress.d.ts +3 -1
- package/node_modules/ink/build/parse-keypress.js +17 -19
- package/node_modules/ink/build/parse-keypress.js.map +1 -1
- package/node_modules/ink/build/reconciler.js +27 -46
- package/node_modules/ink/build/reconciler.js.map +1 -1
- package/node_modules/ink/build/render-border.js +18 -29
- package/node_modules/ink/build/render-border.js.map +1 -1
- package/node_modules/ink/build/render-to-string.js +1 -2
- package/node_modules/ink/build/render-to-string.js.map +1 -1
- package/node_modules/ink/build/render.d.ts +2 -57
- package/node_modules/ink/build/render.js +11 -18
- package/node_modules/ink/build/render.js.map +1 -1
- package/node_modules/ink/build/screen-reader-update.d.ts +13 -0
- package/node_modules/ink/build/screen-reader-update.js +38 -0
- package/node_modules/ink/build/screen-reader-update.js.map +1 -0
- package/node_modules/ink/build/styles.d.ts +16 -78
- package/node_modules/ink/build/styles.js +31 -102
- package/node_modules/ink/build/styles.js.map +1 -1
- package/node_modules/ink/build/utils.d.ts +2 -9
- package/node_modules/ink/build/utils.js +3 -18
- package/node_modules/ink/build/utils.js.map +1 -1
- package/node_modules/ink/build/wrap-text.js +0 -7
- package/node_modules/ink/build/wrap-text.js.map +1 -1
- package/node_modules/ink/build/write-synchronized.d.ts +1 -1
- package/node_modules/ink/build/write-synchronized.js +2 -4
- package/node_modules/ink/build/write-synchronized.js.map +1 -1
- package/node_modules/ink/node_modules/emoji-regex/LICENSE-MIT.txt +20 -0
- package/node_modules/ink/node_modules/emoji-regex/README.md +107 -0
- package/node_modules/ink/node_modules/emoji-regex/index.d.ts +3 -0
- package/node_modules/ink/node_modules/emoji-regex/index.js +4 -0
- package/node_modules/ink/node_modules/emoji-regex/index.mjs +4 -0
- package/node_modules/ink/node_modules/emoji-regex/package.json +45 -0
- package/node_modules/{wrap-ansi → ink/node_modules/wrap-ansi}/index.d.ts +1 -1
- package/node_modules/ink/node_modules/wrap-ansi/index.js +222 -0
- package/node_modules/ink/node_modules/wrap-ansi/node_modules/string-width/index.d.ts +39 -0
- package/node_modules/ink/node_modules/wrap-ansi/node_modules/string-width/index.js +82 -0
- package/node_modules/ink/node_modules/wrap-ansi/node_modules/string-width/license +9 -0
- package/node_modules/ink/node_modules/wrap-ansi/node_modules/string-width/package.json +64 -0
- package/node_modules/ink/node_modules/wrap-ansi/node_modules/string-width/readme.md +66 -0
- package/node_modules/{wrap-ansi → ink/node_modules/wrap-ansi}/package.json +11 -11
- package/node_modules/{wrap-ansi → ink/node_modules/wrap-ansi}/readme.md +0 -2
- package/node_modules/ink/package.json +98 -34
- package/node_modules/ink/readme.md +48 -554
- package/node_modules/slice-ansi/index.d.ts +1 -1
- package/node_modules/slice-ansi/index.js +89 -146
- package/node_modules/slice-ansi/package.json +5 -5
- package/node_modules/slice-ansi/readme.md +0 -1
- package/node_modules/slice-ansi/tokenize-ansi.js +1 -1
- package/package.json +11 -10
- package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/dist/index.d.ts +188 -0
- package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/dist/index.d.ts.map +1 -0
- package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/dist/index.js +293 -0
- package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/dist/index.js.map +1 -0
- package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/package.json +25 -0
- package/node_modules/@alcalzone/ansi-tokenize/build/consts.d.ts +0 -17
- package/node_modules/@alcalzone/ansi-tokenize/build/consts.js +0 -28
- package/node_modules/@alcalzone/ansi-tokenize/build/consts.js.map +0 -1
- package/node_modules/ink/build/components/AnimationContext.d.ts +0 -9
- package/node_modules/ink/build/components/AnimationContext.js +0 -13
- package/node_modules/ink/build/components/AnimationContext.js.map +0 -1
- package/node_modules/ink/build/hooks/use-animation.d.ts +0 -49
- package/node_modules/ink/build/hooks/use-animation.js +0 -87
- package/node_modules/ink/build/hooks/use-animation.js.map +0 -1
- package/node_modules/ink/build/hooks/use-box-metrics.d.ts +0 -59
- package/node_modules/ink/build/hooks/use-box-metrics.js +0 -88
- package/node_modules/ink/build/hooks/use-box-metrics.js.map +0 -1
- package/node_modules/ink/build/hooks/use-paste.d.ts +0 -35
- package/node_modules/ink/build/hooks/use-paste.js +0 -62
- package/node_modules/ink/build/hooks/use-paste.js.map +0 -1
- package/node_modules/ink/build/hooks/use-window-size.d.ts +0 -18
- package/node_modules/ink/build/hooks/use-window-size.js +0 -22
- package/node_modules/ink/build/hooks/use-window-size.js.map +0 -1
- package/node_modules/wrap-ansi/index.js +0 -468
- /package/node_modules/{wrap-ansi → ink/node_modules/wrap-ansi}/license +0 -0
|
@@ -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
|
|
12
|
+
import terminalSize from 'terminal-size';
|
|
13
|
+
import { isDev } from './utils.js';
|
|
13
14
|
import reconciler from './reconciler.js';
|
|
14
15
|
import render from './renderer.js';
|
|
15
16
|
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,10 +21,6 @@ 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
|
-
});
|
|
28
24
|
const kittyQueryEscapeByte = 0x1b;
|
|
29
25
|
const kittyQueryOpenBracketByte = 0x5b;
|
|
30
26
|
const kittyQueryQuestionMarkByte = 0x3f;
|
|
@@ -80,51 +76,10 @@ const stripKittyQueryResponsesAndTrailingPartial = (buffer) => {
|
|
|
80
76
|
}
|
|
81
77
|
return keptBytes;
|
|
82
78
|
};
|
|
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
79
|
const isErrorInput = (value) => {
|
|
104
80
|
return (value instanceof Error ||
|
|
105
81
|
Object.prototype.toString.call(value) === '[object Error]');
|
|
106
82
|
};
|
|
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
|
-
};
|
|
128
83
|
export default class Ink {
|
|
129
84
|
/**
|
|
130
85
|
Whether this instance is using concurrent rendering mode.
|
|
@@ -135,9 +90,6 @@ export default class Ink {
|
|
|
135
90
|
cursorPosition;
|
|
136
91
|
throttledLog;
|
|
137
92
|
isScreenReaderEnabled;
|
|
138
|
-
interactive;
|
|
139
|
-
renderThrottleMs;
|
|
140
|
-
alternateScreen;
|
|
141
93
|
// Ignore last render after unmounting a tree to prevent empty output before exit
|
|
142
94
|
isUnmounted;
|
|
143
95
|
isUnmounting;
|
|
@@ -159,7 +111,6 @@ export default class Ink {
|
|
|
159
111
|
hasPendingThrottledRender = false;
|
|
160
112
|
kittyProtocolEnabled = false;
|
|
161
113
|
cancelKittyDetection;
|
|
162
|
-
nextRenderCommit;
|
|
163
114
|
constructor(options) {
|
|
164
115
|
autoBind(this);
|
|
165
116
|
this.options = options;
|
|
@@ -168,18 +119,9 @@ export default class Ink {
|
|
|
168
119
|
this.isScreenReaderEnabled =
|
|
169
120
|
options.isScreenReaderEnabled ??
|
|
170
121
|
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;
|
|
176
122
|
const unthrottled = options.debug || this.isScreenReaderEnabled;
|
|
177
123
|
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.
|
|
181
124
|
const renderThrottleMs = maxFps > 0 ? Math.max(1, Math.ceil(1000 / maxFps)) : 0;
|
|
182
|
-
this.renderThrottleMs = unthrottled ? 0 : renderThrottleMs;
|
|
183
125
|
if (unthrottled) {
|
|
184
126
|
this.rootNode.onRender = this.onRender;
|
|
185
127
|
this.throttledOnRender = undefined;
|
|
@@ -204,7 +146,7 @@ export default class Ink {
|
|
|
204
146
|
? this.log
|
|
205
147
|
: throttle((output) => {
|
|
206
148
|
const shouldWrite = this.log.willRender(output);
|
|
207
|
-
const sync = this.
|
|
149
|
+
const sync = shouldSynchronize(this.options.stdout);
|
|
208
150
|
if (sync && shouldWrite) {
|
|
209
151
|
this.options.stdout.write(bsu);
|
|
210
152
|
}
|
|
@@ -225,7 +167,7 @@ export default class Ink {
|
|
|
225
167
|
this.lastOutput = '';
|
|
226
168
|
this.lastOutputToRender = '';
|
|
227
169
|
this.lastOutputHeight = 0;
|
|
228
|
-
this.lastTerminalWidth =
|
|
170
|
+
this.lastTerminalWidth = this.getTerminalWidth();
|
|
229
171
|
// This variable is used only in debug mode to store full static output
|
|
230
172
|
// so that it's rerendered every time, not just new static parts, like in non-debug mode
|
|
231
173
|
this.fullStaticOutput = '';
|
|
@@ -235,31 +177,32 @@ export default class Ink {
|
|
|
235
177
|
this.container = reconciler.createContainer(this.rootNode, rootTag, null, false, null, 'id', () => { }, () => { }, () => { }, () => { });
|
|
236
178
|
// Unmount when process exits
|
|
237
179
|
this.unsubscribeExit = signalExit(this.unmount, { alwaysLast: false });
|
|
238
|
-
|
|
239
|
-
if (process.env['DEV'] === 'true') {
|
|
180
|
+
if (isDev()) {
|
|
240
181
|
// @ts-expect-error outdated types
|
|
241
182
|
reconciler.injectIntoDevTools();
|
|
242
183
|
}
|
|
243
184
|
if (options.patchConsole) {
|
|
244
185
|
this.patchConsole();
|
|
245
186
|
}
|
|
246
|
-
if (
|
|
187
|
+
if (!isInCi) {
|
|
247
188
|
options.stdout.on('resize', this.resized);
|
|
248
189
|
this.unsubscribeResize = () => {
|
|
249
190
|
options.stdout.off('resize', this.resized);
|
|
250
191
|
};
|
|
251
192
|
}
|
|
252
193
|
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);
|
|
260
194
|
}
|
|
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
|
+
};
|
|
261
204
|
resized = () => {
|
|
262
|
-
const currentWidth =
|
|
205
|
+
const currentWidth = this.getTerminalWidth();
|
|
263
206
|
if (currentWidth < this.lastTerminalWidth) {
|
|
264
207
|
// We clear the screen when decreasing terminal width to prevent duplicate overlapping re-renders.
|
|
265
208
|
this.log.clear();
|
|
@@ -289,16 +232,13 @@ export default class Ink {
|
|
|
289
232
|
this.log.setCursorPosition(position);
|
|
290
233
|
};
|
|
291
234
|
restoreLastOutput = () => {
|
|
292
|
-
if (!this.interactive) {
|
|
293
|
-
return;
|
|
294
|
-
}
|
|
295
235
|
// Clear() resets log-update's cursor state, so replay the latest cursor intent
|
|
296
236
|
// before restoring output after external stdout/stderr writes.
|
|
297
237
|
this.log.setCursorPosition(this.cursorPosition);
|
|
298
238
|
this.log(this.lastOutputToRender || this.lastOutput + '\n');
|
|
299
239
|
};
|
|
300
240
|
calculateLayout = () => {
|
|
301
|
-
const terminalWidth =
|
|
241
|
+
const terminalWidth = this.getTerminalWidth();
|
|
302
242
|
this.rootNode.yogaNode.setWidth(terminalWidth);
|
|
303
243
|
this.rootNode.yogaNode.calculateLayout(undefined, undefined, Yoga.DIRECTION_LTR);
|
|
304
244
|
};
|
|
@@ -307,10 +247,6 @@ export default class Ink {
|
|
|
307
247
|
if (this.isUnmounted) {
|
|
308
248
|
return;
|
|
309
249
|
}
|
|
310
|
-
if (this.nextRenderCommit) {
|
|
311
|
-
this.nextRenderCommit.resolve();
|
|
312
|
-
this.nextRenderCommit = undefined;
|
|
313
|
-
}
|
|
314
250
|
const startTime = performance.now();
|
|
315
251
|
const { output, outputHeight, staticOutput } = render(this.rootNode, this.isScreenReaderEnabled);
|
|
316
252
|
this.options.onRender?.({ renderTime: performance.now() - startTime });
|
|
@@ -320,13 +256,10 @@ export default class Ink {
|
|
|
320
256
|
if (hasStaticOutput) {
|
|
321
257
|
this.fullStaticOutput += staticOutput;
|
|
322
258
|
}
|
|
323
|
-
this.lastOutput = output;
|
|
324
|
-
this.lastOutputToRender = output;
|
|
325
|
-
this.lastOutputHeight = outputHeight;
|
|
326
259
|
this.options.stdout.write(this.fullStaticOutput + output);
|
|
327
260
|
return;
|
|
328
261
|
}
|
|
329
|
-
if (
|
|
262
|
+
if (isInCi) {
|
|
330
263
|
if (hasStaticOutput) {
|
|
331
264
|
this.options.stdout.write(staticOutput);
|
|
332
265
|
}
|
|
@@ -336,7 +269,7 @@ export default class Ink {
|
|
|
336
269
|
return;
|
|
337
270
|
}
|
|
338
271
|
if (this.isScreenReaderEnabled) {
|
|
339
|
-
const sync = this.
|
|
272
|
+
const sync = shouldSynchronize(this.options.stdout);
|
|
340
273
|
if (sync) {
|
|
341
274
|
this.options.stdout.write(bsu);
|
|
342
275
|
}
|
|
@@ -355,7 +288,7 @@ export default class Ink {
|
|
|
355
288
|
}
|
|
356
289
|
return;
|
|
357
290
|
}
|
|
358
|
-
const terminalWidth =
|
|
291
|
+
const terminalWidth = this.getTerminalWidth();
|
|
359
292
|
const wrappedOutput = wrapAnsi(output, terminalWidth, {
|
|
360
293
|
trim: false,
|
|
361
294
|
hard: true,
|
|
@@ -382,11 +315,49 @@ export default class Ink {
|
|
|
382
315
|
if (hasStaticOutput) {
|
|
383
316
|
this.fullStaticOutput += staticOutput;
|
|
384
317
|
}
|
|
385
|
-
|
|
318
|
+
// Detect fullscreen: output fills or exceeds terminal height.
|
|
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;
|
|
386
357
|
};
|
|
387
358
|
render(node) {
|
|
388
359
|
const tree = (React.createElement(AccessibilityContext.Provider, { value: { isScreenReaderEnabled: this.isScreenReaderEnabled } },
|
|
389
|
-
React.createElement(App, { stdin: this.options.stdin, stdout: this.options.stdout, stderr: this.options.stderr, exitOnCtrlC: this.options.exitOnCtrlC,
|
|
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)));
|
|
390
361
|
if (this.options.concurrent) {
|
|
391
362
|
// Concurrent mode: use updateContainer (async scheduling)
|
|
392
363
|
reconciler.updateContainer(tree, this.container, null, noop);
|
|
@@ -405,11 +376,11 @@ export default class Ink {
|
|
|
405
376
|
this.options.stdout.write(data + this.fullStaticOutput + this.lastOutput);
|
|
406
377
|
return;
|
|
407
378
|
}
|
|
408
|
-
if (
|
|
379
|
+
if (isInCi) {
|
|
409
380
|
this.options.stdout.write(data);
|
|
410
381
|
return;
|
|
411
382
|
}
|
|
412
|
-
const sync = this.
|
|
383
|
+
const sync = shouldSynchronize(this.options.stdout);
|
|
413
384
|
if (sync) {
|
|
414
385
|
this.options.stdout.write(bsu);
|
|
415
386
|
}
|
|
@@ -429,11 +400,11 @@ export default class Ink {
|
|
|
429
400
|
this.options.stdout.write(this.fullStaticOutput + this.lastOutput);
|
|
430
401
|
return;
|
|
431
402
|
}
|
|
432
|
-
if (
|
|
403
|
+
if (isInCi) {
|
|
433
404
|
this.options.stderr.write(data);
|
|
434
405
|
return;
|
|
435
406
|
}
|
|
436
|
-
const sync = this.
|
|
407
|
+
const sync = shouldSynchronize(this.options.stdout);
|
|
437
408
|
if (sync) {
|
|
438
409
|
this.options.stdout.write(bsu);
|
|
439
410
|
}
|
|
@@ -444,7 +415,7 @@ export default class Ink {
|
|
|
444
415
|
this.options.stdout.write(esu);
|
|
445
416
|
}
|
|
446
417
|
}
|
|
447
|
-
// eslint-disable-next-line @typescript-eslint/
|
|
418
|
+
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
448
419
|
unmount(error) {
|
|
449
420
|
if (this.isUnmounted || this.isUnmounting) {
|
|
450
421
|
return;
|
|
@@ -455,10 +426,21 @@ export default class Ink {
|
|
|
455
426
|
this.beforeExitHandler = undefined;
|
|
456
427
|
}
|
|
457
428
|
const stdout = this.options.stdout;
|
|
458
|
-
const
|
|
429
|
+
const canWriteToStdout = !stdout.destroyed && !stdout.writableEnded && (stdout.writable ?? true);
|
|
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
|
+
};
|
|
459
441
|
// Clear any pending throttled render timer on unmount. When stdout is writable,
|
|
460
442
|
// flush so the final frame is emitted; otherwise cancel to avoid delayed callbacks.
|
|
461
|
-
settleThrottle(this.throttledOnRender
|
|
443
|
+
settleThrottle(this.throttledOnRender ?? {});
|
|
462
444
|
if (canWriteToStdout) {
|
|
463
445
|
// If throttling is enabled and there is already a pending render, flushing above
|
|
464
446
|
// is sufficient. Also avoid calling onRender() again when static output already
|
|
@@ -474,95 +456,84 @@ export default class Ink {
|
|
|
474
456
|
// that could re-enter exit() via synchronous write callbacks.
|
|
475
457
|
this.isUnmounted = true;
|
|
476
458
|
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);
|
|
480
459
|
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.
|
|
485
460
|
this.restoreConsole();
|
|
486
461
|
}
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
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
|
-
}
|
|
520
|
-
}
|
|
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);
|
|
462
|
+
if (typeof this.unsubscribeResize === 'function') {
|
|
463
|
+
this.unsubscribeResize();
|
|
464
|
+
}
|
|
465
|
+
// Cancel any in-progress auto-detection before checking protocol state
|
|
466
|
+
if (this.cancelKittyDetection) {
|
|
467
|
+
this.cancelKittyDetection();
|
|
468
|
+
}
|
|
469
|
+
// Flush any pending throttled log writes if possible, otherwise cancel to
|
|
470
|
+
// prevent delayed callbacks from writing to a closed stream.
|
|
471
|
+
const throttledLog = this.throttledLog;
|
|
472
|
+
settleThrottle(throttledLog);
|
|
473
|
+
if (canWriteToStdout) {
|
|
474
|
+
if (this.kittyProtocolEnabled) {
|
|
475
|
+
try {
|
|
476
|
+
this.options.stdout.write('\u001B[<u');
|
|
535
477
|
}
|
|
536
|
-
|
|
537
|
-
|
|
478
|
+
catch {
|
|
479
|
+
// Best-effort: stdout may already be destroyed during shutdown
|
|
538
480
|
}
|
|
539
|
-
};
|
|
540
|
-
const isProcessExiting = error !== undefined && !isErrorInput(error);
|
|
541
|
-
if (isProcessExiting) {
|
|
542
|
-
resolveOrReject();
|
|
543
481
|
}
|
|
544
|
-
|
|
545
|
-
|
|
482
|
+
// CIs don't handle erasing ansi escapes well, so it's better to
|
|
483
|
+
// only render last frame of non-static output
|
|
484
|
+
if (isInCi) {
|
|
485
|
+
this.options.stdout.write(this.lastOutput + '\n');
|
|
546
486
|
}
|
|
547
|
-
else {
|
|
548
|
-
|
|
487
|
+
else if (!this.options.debug) {
|
|
488
|
+
this.log.done();
|
|
549
489
|
}
|
|
550
|
-
}
|
|
551
|
-
|
|
490
|
+
}
|
|
491
|
+
this.kittyProtocolEnabled = false;
|
|
552
492
|
if (this.options.concurrent) {
|
|
553
|
-
|
|
554
|
-
reconciler.
|
|
555
|
-
concurrentReconciler.flushPassiveEffects?.();
|
|
556
|
-
finishUnmount();
|
|
493
|
+
// Concurrent mode: use updateContainer (async scheduling)
|
|
494
|
+
reconciler.updateContainer(null, this.container, null, noop);
|
|
557
495
|
}
|
|
558
496
|
else {
|
|
559
497
|
// Legacy mode: use updateContainerSync + flushSyncWork (sync)
|
|
560
498
|
reconciler.updateContainerSync(null, this.container, null, noop);
|
|
561
499
|
reconciler.flushSyncWork();
|
|
562
|
-
|
|
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);
|
|
563
530
|
}
|
|
564
531
|
}
|
|
565
532
|
async waitUntilExit() {
|
|
533
|
+
this.exitPromise ||= new Promise((resolve, reject) => {
|
|
534
|
+
this.resolveExitPromise = resolve;
|
|
535
|
+
this.rejectExitPromise = reject;
|
|
536
|
+
});
|
|
566
537
|
if (!this.beforeExitHandler) {
|
|
567
538
|
this.beforeExitHandler = () => {
|
|
568
539
|
this.unmount();
|
|
@@ -571,46 +542,8 @@ export default class Ink {
|
|
|
571
542
|
}
|
|
572
543
|
return this.exitPromise;
|
|
573
544
|
}
|
|
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
|
-
}
|
|
612
545
|
clear() {
|
|
613
|
-
if (
|
|
546
|
+
if (!isInCi && !this.options.debug) {
|
|
614
547
|
this.log.clear();
|
|
615
548
|
// Sync lastOutput so that unmount's final onRender
|
|
616
549
|
// sees it as unchanged and log-update skips it
|
|
@@ -633,106 +566,6 @@ export default class Ink {
|
|
|
633
566
|
}
|
|
634
567
|
});
|
|
635
568
|
}
|
|
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
|
-
}
|
|
736
569
|
initKittyKeyboard() {
|
|
737
570
|
// Protocol is opt-in: if kittyKeyboard is not specified, do nothing
|
|
738
571
|
if (!this.options.kittyKeyboard) {
|
|
@@ -740,29 +573,26 @@ export default class Ink {
|
|
|
740
573
|
}
|
|
741
574
|
const opts = this.options.kittyKeyboard;
|
|
742
575
|
const mode = opts.mode ?? 'auto';
|
|
743
|
-
if (mode === 'disabled'
|
|
576
|
+
if (mode === 'disabled' ||
|
|
577
|
+
!this.options.stdin.isTTY ||
|
|
578
|
+
!this.options.stdout.isTTY) {
|
|
744
579
|
return;
|
|
745
580
|
}
|
|
746
581
|
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).
|
|
749
582
|
if (mode === 'enabled') {
|
|
750
|
-
|
|
751
|
-
this.enableKittyProtocol(flags);
|
|
752
|
-
}
|
|
583
|
+
this.enableKittyProtocol(flags);
|
|
753
584
|
return;
|
|
754
585
|
}
|
|
755
|
-
// Auto mode:
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
586
|
+
// Auto mode: use heuristic precheck, then confirm with protocol query
|
|
587
|
+
const term = process.env['TERM'] ?? '';
|
|
588
|
+
const termProgram = process.env['TERM_PROGRAM'] ?? '';
|
|
589
|
+
const isKnownSupportingTerminal = 'KITTY_WINDOW_ID' in process.env ||
|
|
590
|
+
term === 'xterm-kitty' ||
|
|
591
|
+
termProgram === 'WezTerm' ||
|
|
592
|
+
termProgram === 'ghostty';
|
|
593
|
+
if (!isInCi && isKnownSupportingTerminal) {
|
|
594
|
+
this.confirmKittySupport(flags);
|
|
760
595
|
}
|
|
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);
|
|
766
596
|
}
|
|
767
597
|
confirmKittySupport(flags) {
|
|
768
598
|
const { stdin, stdout } = this.options;
|
|
@@ -777,11 +607,11 @@ export default class Ink {
|
|
|
777
607
|
const remaining = stripKittyQueryResponsesAndTrailingPartial(responseBuffer);
|
|
778
608
|
responseBuffer = [];
|
|
779
609
|
if (remaining.length > 0) {
|
|
780
|
-
stdin.unshift(
|
|
610
|
+
stdin.unshift(Buffer.from(remaining));
|
|
781
611
|
}
|
|
782
612
|
};
|
|
783
613
|
const onData = (data) => {
|
|
784
|
-
const chunk = typeof data === 'string' ?
|
|
614
|
+
const chunk = typeof data === 'string' ? Buffer.from(data) : data;
|
|
785
615
|
for (const byte of chunk) {
|
|
786
616
|
responseBuffer.push(byte);
|
|
787
617
|
}
|