@alchemy.run/sigil 0.0.0-alpha.3 → 0.0.0-alpha.5
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/README.md +23 -13
- package/THIRD_PARTY_NOTICES.md +70 -23
- package/dist/Text-D5HUf3Fj.d.ts +452 -0
- package/dist/ansi.d.ts +137 -143
- package/dist/ansi.js +87 -2
- package/dist/capabilities.d.ts +5 -0
- package/dist/capabilities.js +3 -0
- package/dist/cell-_ZVhbfl0.js +44 -0
- package/dist/color-CkbalRqK.js +2 -0
- package/dist/color-policy-BMzMwV7Q.d.ts +22 -0
- package/dist/color-policy-SVj1pYTA.js +560 -0
- package/dist/color-profile-DHhQHY55.js +36 -0
- package/dist/color-profile-u0Nhe9Nv.d.ts +97 -0
- package/dist/color.d.ts +21 -0
- package/dist/color.js +3 -0
- package/dist/cursor-position-D2LAkRG0.d.ts +7 -0
- package/dist/detect-Bh4yGP6w.d.ts +186 -0
- package/dist/detect-BuTXtY6e.js +373 -0
- package/dist/{devtools-BhYGjb7h.js → devtools-DbthxoD1.js} +22 -23
- package/dist/env-YVw64yZS.js +9 -0
- package/dist/escapes-CB_6CWOE.d.ts +72 -0
- package/dist/geometry-BxXOzJgo.d.ts +11 -0
- package/dist/index-Bmc2tRPk.d.ts +21 -0
- package/dist/index.d.ts +235 -754
- package/dist/index.js +1362 -3001
- package/dist/osc-CCH7xDoS.js +71 -0
- package/dist/osc-Cn0fw77g.d.ts +23 -0
- package/dist/paint-C19minOS.d.ts +81 -0
- package/dist/query-vaIeGOkH.d.ts +152 -0
- package/dist/router.d.ts +392 -0
- package/dist/router.js +709 -0
- package/dist/sample-Cqw1bjUL.js +445 -0
- package/dist/screen-BOh__-65.js +324 -0
- package/dist/screen-BReKIheE.d.ts +40 -0
- package/dist/screen.d.ts +5 -0
- package/dist/screen.js +5 -0
- package/dist/semantic-text-style-DIMzC7xt.js +91 -0
- package/dist/serialize-BTkAZgw1.js +79 -0
- package/dist/session-BJmzX2NJ.js +664 -0
- package/dist/sgr-BhwaWAJB.js +246 -0
- package/dist/store-CgrG9K4y.d.ts +72 -0
- package/dist/string-width-CijQwpIk.js +69 -0
- package/dist/strip-BvU4toXG.js +6 -0
- package/dist/terminal.d.ts +118 -0
- package/dist/terminal.js +2 -0
- package/dist/tokenize-AjqbvtiT.js +1242 -0
- package/dist/tokenize-Dx1y_l5H.d.ts +57 -0
- package/dist/truncate-D31fhU6i.js +562 -0
- package/dist/use-focus-C2sciZOo.js +1335 -0
- package/package.json +43 -17
- package/src/ansi/chalk.ts +12 -53
- package/src/ansi/cursor.ts +2 -4
- package/src/ansi/east-asian-width.ts +44 -0
- package/src/ansi/escapes.ts +18 -26
- package/src/ansi/graphemes.ts +8 -0
- package/src/ansi/hyperlink.ts +44 -0
- package/src/ansi/index.ts +14 -14
- package/src/ansi/osc.ts +77 -0
- package/src/ansi/sgr.ts +2 -5
- package/src/ansi/slice.ts +1 -1
- package/src/ansi/string-width.ts +91 -206
- package/src/ansi/strip.ts +13 -33
- package/src/ansi/tokenize.ts +96 -169
- package/src/ansi/truncate.ts +5 -3
- package/src/ansi/wrap.ts +13 -14
- package/src/ansi-tokenizer.ts +1 -1
- package/src/capabilities/color-policy.ts +34 -0
- package/src/capabilities/detect.ts +594 -0
- package/src/capabilities/index.ts +37 -0
- package/src/capabilities/query.ts +657 -0
- package/src/capabilities/store.ts +379 -0
- package/src/color/index.ts +3 -0
- package/src/color/paint.ts +169 -0
- package/src/color/palette.ts +48 -0
- package/src/color/sample.ts +323 -0
- package/src/color.ts +1 -0
- package/src/components/AnsiText.tsx +42 -0
- package/src/components/App.tsx +109 -22
- package/src/components/BackgroundContext.ts +2 -3
- package/src/components/Box.tsx +43 -60
- package/src/components/CursorContext.ts +1 -1
- package/src/components/ErrorBoundary.tsx +1 -1
- package/src/components/ErrorOverview.tsx +8 -7
- package/src/components/Hyperlink.tsx +56 -0
- package/src/components/Spacer.tsx +1 -1
- package/src/components/Static.tsx +1 -1
- package/src/components/StderrContext.ts +0 -2
- package/src/components/StdinContext.ts +0 -1
- package/src/components/StdoutContext.ts +1 -3
- package/src/components/TerminalOscContext.ts +25 -0
- package/src/components/Text.tsx +23 -46
- package/src/components/Transform.tsx +2 -2
- package/src/cursor-position.ts +1 -1
- package/src/devtools.ts +84 -24
- package/src/dom.ts +11 -8
- package/src/env.ts +12 -0
- package/src/get-max-width.ts +1 -1
- package/src/global.d.ts +6 -4
- package/src/{boxes.ts → glyphs.ts} +17 -18
- package/src/hooks/use-animation.ts +1 -1
- package/src/hooks/use-app.ts +1 -1
- package/src/hooks/use-box-metrics.ts +1 -1
- package/src/hooks/use-capabilities.ts +73 -0
- package/src/hooks/use-cursor.ts +3 -3
- package/src/hooks/use-focus-manager.ts +1 -1
- package/src/hooks/use-focus.ts +5 -6
- package/src/hooks/use-input.ts +4 -4
- package/src/hooks/use-is-screen-reader-enabled.ts +1 -1
- package/src/hooks/use-paste.ts +2 -2
- package/src/hooks/use-stderr.ts +1 -1
- package/src/hooks/use-stdin.ts +1 -1
- package/src/hooks/use-stdout.ts +1 -1
- package/src/hooks/use-terminal-osc.ts +59 -0
- package/src/hooks/use-window-size.ts +2 -2
- package/src/index.ts +88 -44
- package/src/ink.tsx +620 -745
- package/src/input-parser.ts +1 -1
- package/src/instances.ts +1 -1
- package/src/kitty-keyboard.ts +128 -0
- package/src/measure-element.ts +1 -1
- package/src/measure-text.ts +2 -2
- package/src/{render-node-to-output.ts → paint-tree.ts} +78 -53
- package/src/parse-keypress.ts +4 -5
- package/src/parse-stack-line.ts +0 -1
- package/src/patch-console.ts +44 -0
- package/src/reconciler.ts +36 -62
- package/src/render-background.ts +37 -16
- package/src/render-border.ts +97 -64
- package/src/render-frame.ts +83 -0
- package/src/render-to-string.ts +24 -9
- package/src/render.ts +24 -16
- package/src/router/components.tsx +343 -0
- package/src/router/context.ts +41 -0
- package/src/router/history.ts +194 -0
- package/src/router/hooks.tsx +391 -0
- package/src/router/index.ts +34 -0
- package/src/router/matcher.ts +571 -0
- package/src/sanitize-ansi.ts +1 -1
- package/src/screen/ansi.ts +184 -0
- package/src/screen/canvas.ts +137 -0
- package/src/screen/cell.ts +138 -0
- package/src/screen/color-profile.ts +47 -0
- package/src/screen/geometry.ts +9 -0
- package/src/screen/index.ts +6 -0
- package/src/screen/screen.ts +272 -0
- package/src/screen/serialize.ts +129 -0
- package/src/screen.ts +1 -0
- package/src/semantic-text-style.ts +118 -0
- package/src/signal-exit.ts +4 -5
- package/src/squash-text-nodes.ts +4 -7
- package/src/stream.ts +2 -4
- package/src/structured-text.ts +321 -0
- package/src/styles.ts +16 -17
- package/src/terminal/index.ts +2 -0
- package/src/terminal/inline-presenter.ts +120 -0
- package/src/terminal/input.ts +86 -0
- package/src/terminal/render-scheduler.ts +37 -0
- package/src/terminal/screen-presenter.ts +188 -0
- package/src/terminal/session.ts +407 -0
- package/src/terminal-size.ts +9 -8
- package/src/terminal.ts +1 -0
- package/src/testing/browser.ts +588 -0
- package/src/testing/emulators.ts +205 -0
- package/src/testing/explorer-app/index.html +12 -0
- package/src/testing/explorer-app/main.ts +381 -0
- package/src/testing/explorer-app/style.css +194 -0
- package/src/testing/explorer-app/tsconfig.json +15 -0
- package/src/testing/explorer-app/vite-env.d.ts +1 -0
- package/src/testing/index.ts +26 -0
- package/src/testing/keys.ts +56 -0
- package/src/testing/live.ts +85 -0
- package/src/testing/matchers.ts +70 -0
- package/src/testing/public.ts +94 -0
- package/src/testing/terminal.ts +349 -0
- package/src/testing/vitest.ts +157 -0
- package/src/transform-adapter.ts +14 -0
- package/src/types.ts +1 -6
- package/src/utils.ts +2 -2
- package/src/wrap-text.ts +4 -4
- package/src/yoga/config.ts +2 -2
- package/src/yoga/core/absoluteLayout.ts +15 -15
- package/src/yoga/core/baseline.ts +3 -3
- package/src/yoga/core/cache.ts +4 -4
- package/src/yoga/core/calculateLayout.ts +33 -27
- package/src/yoga/core/config.ts +1 -1
- package/src/yoga/core/flexLine.ts +3 -3
- package/src/yoga/core/helpers.ts +3 -3
- package/src/yoga/core/layoutResults.ts +4 -4
- package/src/yoga/core/node.ts +16 -16
- package/src/yoga/core/pixelGrid.ts +4 -4
- package/src/yoga/core/style.ts +11 -11
- package/src/yoga/core/types.ts +2 -2
- package/src/yoga/index.ts +6 -6
- package/src/yoga/node.ts +8 -8
- package/dist/sgr-CMfEpjSk.d.ts +0 -91
- package/dist/truncate-CBiyyZzw.js +0 -2156
- package/src/ansi/supports-color.ts +0 -213
- package/src/ansi/widest-line.ts +0 -12
- package/src/auto-bind.ts +0 -41
- package/src/colorize.ts +0 -60
- package/src/devtools-window-polyfill.ts +0 -73
- package/src/indent-string.ts +0 -16
- package/src/is-in-ci.ts +0 -7
- package/src/log-update.ts +0 -370
- package/src/output.ts +0 -308
- package/src/renderer.ts +0 -73
- package/src/write-synchronized.ts +0 -9
package/src/ink.tsx
CHANGED
|
@@ -1,122 +1,68 @@
|
|
|
1
1
|
/** @jsxImportSource react */
|
|
2
|
-
import
|
|
2
|
+
import { setImmediate as yieldImmediate } from "node:timers/promises";
|
|
3
|
+
import { isNativeError } from "node:util/types";
|
|
3
4
|
|
|
4
5
|
import { type ReactNode } from "react";
|
|
5
6
|
import { type FiberRoot } from "react-reconciler";
|
|
6
7
|
import { LegacyRoot, ConcurrentRoot } from "react-reconciler/constants.js";
|
|
7
8
|
|
|
8
|
-
import { ansiEscapes, bsu, esu } from "
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import { accessibilityContext as AccessibilityContext } from "
|
|
12
|
-
import { App } from "
|
|
13
|
-
import { type TerminalSuspension } from "
|
|
14
|
-
import {
|
|
15
|
-
import
|
|
16
|
-
import
|
|
17
|
-
import { isInCi } from "
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
9
|
+
import { ansiEscapes, bsu, esu } from "#/ansi/escapes.ts";
|
|
10
|
+
import type { ClipboardSelection, TerminalProgressState } from "#/ansi/osc.ts";
|
|
11
|
+
import { wrapAnsi } from "#/ansi/wrap.ts";
|
|
12
|
+
import { accessibilityContext as AccessibilityContext } from "#/components/AccessibilityContext.ts";
|
|
13
|
+
import { App } from "#/components/App.tsx";
|
|
14
|
+
import { type TerminalSuspension } from "#/components/AppContext.ts";
|
|
15
|
+
import { TerminalOscContext } from "#/components/TerminalOscContext.ts";
|
|
16
|
+
import type { CursorPosition } from "#/cursor-position.ts";
|
|
17
|
+
import * as dom from "#/dom.ts";
|
|
18
|
+
import { isSigilDev, isInCi, isScreenReader, isTty, isWindows } from "#/env.ts";
|
|
19
|
+
import { instances } from "#/instances.ts";
|
|
20
|
+
import {
|
|
21
|
+
type KittyKeyboardOptions,
|
|
22
|
+
type KittyFlagName,
|
|
23
|
+
resolveFlags,
|
|
24
|
+
detectKittySupport,
|
|
25
|
+
} from "#/kitty-keyboard.ts";
|
|
26
|
+
import { patchConsole, patchStreamWrite } from "#/patch-console.ts";
|
|
27
|
+
import { reconciler } from "#/reconciler.ts";
|
|
28
|
+
import { renderFrame } from "#/render-frame.ts";
|
|
29
|
+
import { Screen } from "#/screen/screen.ts";
|
|
30
|
+
import { signalExit } from "#/signal-exit.ts";
|
|
31
|
+
import { type OutputStream } from "#/stream.ts";
|
|
32
|
+
import { createInlinePresenter } from "#/terminal/inline-presenter.ts";
|
|
33
|
+
import { createRenderScheduler } from "#/terminal/render-scheduler.ts";
|
|
34
|
+
import { TerminalSession } from "#/terminal/session.ts";
|
|
35
|
+
import { type Throttled } from "#/throttle.ts";
|
|
36
|
+
import { getWindowSize } from "#/utils.ts";
|
|
37
|
+
import { Yoga } from "#/yoga/index.ts";
|
|
29
38
|
|
|
30
39
|
const noop = () => {};
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
new Promise<void>((resolve) => {
|
|
35
|
-
setImmediate(resolve);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
const kittyQueryEscapeByte = 0x1b;
|
|
39
|
-
const kittyQueryOpenBracketByte = 0x5b;
|
|
40
|
-
const kittyQueryQuestionMarkByte = 0x3f;
|
|
41
|
-
const kittyQueryLetterByte = 0x75;
|
|
42
|
-
const zeroByte = 0x30;
|
|
43
|
-
const nineByte = 0x39;
|
|
44
|
-
|
|
45
|
-
type KittyQueryResponseMatch = { state: "complete"; endIndex: number } | { state: "partial" };
|
|
46
|
-
|
|
47
|
-
const isDigitByte = (byte: number): boolean => byte >= zeroByte && byte <= nineByte;
|
|
48
|
-
|
|
49
|
-
const matchKittyQueryResponse = (
|
|
50
|
-
buffer: number[],
|
|
51
|
-
startIndex: number,
|
|
52
|
-
): KittyQueryResponseMatch | undefined => {
|
|
53
|
-
if (
|
|
54
|
-
buffer[startIndex] !== kittyQueryEscapeByte ||
|
|
55
|
-
buffer[startIndex + 1] !== kittyQueryOpenBracketByte ||
|
|
56
|
-
buffer[startIndex + 2] !== kittyQueryQuestionMarkByte
|
|
57
|
-
) {
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
let index = startIndex + 3;
|
|
62
|
-
const digitsStartIndex = index;
|
|
63
|
-
while (index < buffer.length && isDigitByte(buffer[index]!)) {
|
|
64
|
-
index++;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (index === digitsStartIndex) {
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
if (index === buffer.length) {
|
|
72
|
-
return { state: "partial" };
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (buffer[index] === kittyQueryLetterByte) {
|
|
76
|
-
return { state: "complete", endIndex: index };
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
return;
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
const hasCompleteKittyQueryResponse = (buffer: number[]): boolean => {
|
|
83
|
-
for (let index = 0; index < buffer.length; index++) {
|
|
84
|
-
const match = matchKittyQueryResponse(buffer, index);
|
|
85
|
-
if (match?.state === "complete") {
|
|
86
|
-
return true;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
return false;
|
|
40
|
+
const beforeExitCallbacks = new Set<() => void>();
|
|
41
|
+
const runBeforeExitCallbacks = (): void => {
|
|
42
|
+
for (const callback of beforeExitCallbacks) callback();
|
|
91
43
|
};
|
|
92
44
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if (
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
45
|
+
function registerBeforeExit(callback: () => void): () => void {
|
|
46
|
+
if (beforeExitCallbacks.size === 0) process.on("beforeExit", runBeforeExitCallbacks);
|
|
47
|
+
beforeExitCallbacks.add(callback);
|
|
48
|
+
return () => {
|
|
49
|
+
beforeExitCallbacks.delete(callback);
|
|
50
|
+
if (beforeExitCallbacks.size === 0) process.off("beforeExit", runBeforeExitCallbacks);
|
|
51
|
+
};
|
|
52
|
+
}
|
|
102
53
|
|
|
103
|
-
|
|
104
|
-
|
|
54
|
+
function bottomRows(screen: Screen, height: number): Screen {
|
|
55
|
+
if (screen.height <= height) return screen;
|
|
56
|
+
const cropped = new Screen(screen.width, height);
|
|
57
|
+
const offset = screen.height - height;
|
|
58
|
+
for (let y = 0; y < height; y++) {
|
|
59
|
+
for (let x = 0; x < screen.width; x++) {
|
|
60
|
+
const cell = screen.cellAt(x, y + offset);
|
|
61
|
+
if (cell && cell.width > 0) cropped.setCell(x, y, cell);
|
|
105
62
|
}
|
|
106
|
-
|
|
107
|
-
keptBytes.push(buffer[index]!);
|
|
108
|
-
index++;
|
|
109
63
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
// Windows consoles scroll the buffer when the bottom-right cell is written,
|
|
115
|
-
// unlike xterm-like terminals which defer the wrap. That extra scroll
|
|
116
|
-
// desynchronizes the incremental erase used for frames that exactly fill the
|
|
117
|
-
// viewport, leaving stale copies of previous frames behind (#969). Keep the
|
|
118
|
-
// pre-7.0 behavior of fully clearing between fullscreen frames there.
|
|
119
|
-
const isWindowsConsole = process.platform === "win32";
|
|
64
|
+
return cropped;
|
|
65
|
+
}
|
|
120
66
|
|
|
121
67
|
const shouldClearTerminalForFrame = ({
|
|
122
68
|
isTTY,
|
|
@@ -149,7 +95,12 @@ const shouldClearTerminalForFrame = ({
|
|
|
149
95
|
const isLeavingFullscreen = wasOverflowing && nextOutputHeight < viewportRows;
|
|
150
96
|
const shouldClearOnUnmount = isUnmounting && wasFullscreen;
|
|
151
97
|
|
|
152
|
-
|
|
98
|
+
// Windows consoles scroll the buffer when the bottom-right cell is written,
|
|
99
|
+
// unlike xterm-like terminals which defer the wrap. That extra scroll
|
|
100
|
+
// desynchronizes the incremental erase used for frames that exactly fill the
|
|
101
|
+
// viewport, leaving stale copies of previous frames behind (#969). Keep the
|
|
102
|
+
// pre-7.0 behavior of fully clearing between fullscreen frames there.
|
|
103
|
+
if (isWindows && (wasFullscreen || isFullscreen)) {
|
|
153
104
|
return true;
|
|
154
105
|
}
|
|
155
106
|
|
|
@@ -166,7 +117,7 @@ const shouldClearTerminalForFrame = ({
|
|
|
166
117
|
};
|
|
167
118
|
|
|
168
119
|
const isErrorInput = (value: unknown): value is Error => {
|
|
169
|
-
return value instanceof Error ||
|
|
120
|
+
return value instanceof Error || isNativeError(value);
|
|
170
121
|
};
|
|
171
122
|
|
|
172
123
|
const getWritableStreamState = (stdout: OutputStream) => {
|
|
@@ -177,23 +128,28 @@ const getWritableStreamState = (stdout: OutputStream) => {
|
|
|
177
128
|
};
|
|
178
129
|
};
|
|
179
130
|
|
|
180
|
-
const settleThrottle =
|
|
181
|
-
|
|
131
|
+
const settleThrottle = <Arguments extends unknown[]>(
|
|
132
|
+
throttled: Throttled<Arguments> | undefined,
|
|
133
|
+
canWriteToStdout: boolean,
|
|
134
|
+
): void => {
|
|
135
|
+
if (!throttled) {
|
|
182
136
|
return;
|
|
183
137
|
}
|
|
184
138
|
|
|
185
|
-
const throttledValue = throttled as {
|
|
186
|
-
flush: () => void;
|
|
187
|
-
cancel?: () => void;
|
|
188
|
-
};
|
|
189
|
-
|
|
190
139
|
if (canWriteToStdout) {
|
|
191
|
-
|
|
192
|
-
} else
|
|
193
|
-
|
|
140
|
+
throttled.flush();
|
|
141
|
+
} else {
|
|
142
|
+
throttled.cancel();
|
|
194
143
|
}
|
|
195
144
|
};
|
|
196
145
|
|
|
146
|
+
// Best-effort write: streams may already be destroyed during shutdown.
|
|
147
|
+
const writeBestEffort = (stream: OutputStream, data: string): void => {
|
|
148
|
+
try {
|
|
149
|
+
stream.write(data);
|
|
150
|
+
} catch {}
|
|
151
|
+
};
|
|
152
|
+
|
|
197
153
|
/**
|
|
198
154
|
The origin of a chunk captured by `patchConsole`: a patched `console.*`
|
|
199
155
|
method, or a direct `stdout.write` / `stderr.write` call.
|
|
@@ -261,9 +217,8 @@ export type Options = {
|
|
|
261
217
|
) => boolean | undefined | void;
|
|
262
218
|
onRender?: (metrics: RenderMetrics) => void;
|
|
263
219
|
isScreenReaderEnabled?: boolean;
|
|
264
|
-
waitUntilExit?: () => Promise<unknown>;
|
|
265
220
|
maxFps?: number;
|
|
266
|
-
|
|
221
|
+
colorProfile?: import("#/screen/color-profile.ts").ColorProfile;
|
|
267
222
|
|
|
268
223
|
/**
|
|
269
224
|
Enable React Concurrent Rendering mode.
|
|
@@ -284,7 +239,7 @@ export type Options = {
|
|
|
284
239
|
/**
|
|
285
240
|
Override automatic interactive mode detection.
|
|
286
241
|
|
|
287
|
-
By default, Ink detects whether the environment is interactive based on CI detection (
|
|
242
|
+
By default, Ink detects whether the environment is interactive based on CI detection (the `CI` environment variable) and `stdout.isTTY`. Most users should not need to set this.
|
|
288
243
|
|
|
289
244
|
When non-interactive, Ink disables ANSI erase sequences, cursor manipulation, synchronized output, resize handling, and kitty keyboard auto-detection, writing only the final frame at unmount.
|
|
290
245
|
|
|
@@ -316,359 +271,378 @@ export type Options = {
|
|
|
316
271
|
alternateScreen?: boolean;
|
|
317
272
|
};
|
|
318
273
|
|
|
319
|
-
|
|
274
|
+
/**
|
|
275
|
+
A live React terminal runtime for one stdout stream, created by `createInk`.
|
|
276
|
+
*/
|
|
277
|
+
export type Ink = {
|
|
320
278
|
/**
|
|
321
|
-
|
|
279
|
+
Replace the previous root node with a new one or update props of the current root node.
|
|
322
280
|
*/
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
private readonly options: Options;
|
|
326
|
-
private readonly log: LogUpdate;
|
|
327
|
-
private cursorPosition: CursorPosition | undefined;
|
|
328
|
-
private readonly throttledLog: LogUpdate | Throttled<[output: string]>;
|
|
281
|
+
render: (node: ReactNode) => void;
|
|
329
282
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
283
|
+
/**
|
|
284
|
+
Unmount the app and release the terminal.
|
|
285
|
+
*/
|
|
286
|
+
// eslint-disable-next-line @typescript-eslint/no-restricted-types
|
|
287
|
+
unmount: (error?: Error | number | null) => void;
|
|
334
288
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
private lastOutputToRender: string;
|
|
340
|
-
private lastOutputHeight: number;
|
|
341
|
-
private lastTerminalWidth: number;
|
|
342
|
-
private lastTerminalHeight: number;
|
|
343
|
-
private readonly container: FiberRoot;
|
|
344
|
-
private readonly rootNode: dom.DOMElement;
|
|
345
|
-
// This variable is used only in debug mode to store full static output
|
|
346
|
-
// so that it's rerendered every time, not just new static parts, like in non-debug mode
|
|
347
|
-
private fullStaticOutput: string;
|
|
348
|
-
private readonly exitPromise!: Promise<unknown>;
|
|
349
|
-
private exitResult: unknown;
|
|
350
|
-
private beforeExitHandler?: () => void;
|
|
351
|
-
private restoreConsole?: () => void;
|
|
352
|
-
// Set when patchConsole is "stdio": the real streams whose write is patched.
|
|
353
|
-
private readonly captureTargets?: { stdout: OutputStream; stderr: OutputStream };
|
|
354
|
-
// Partial trailing lines from captured direct writes, held until a newline.
|
|
355
|
-
private readonly capturedStdioTails = { stdout: "", stderr: "" };
|
|
356
|
-
private readonly unsubscribeResize?: () => void;
|
|
357
|
-
private readonly throttledOnRender?: Throttled<never[]>;
|
|
358
|
-
private hasPendingThrottledRender = false;
|
|
359
|
-
private kittyProtocolEnabled = false;
|
|
360
|
-
private kittyFlags: KittyFlagName[] | undefined;
|
|
361
|
-
private cancelKittyDetection?: () => void;
|
|
362
|
-
private nextRenderCommit?: { promise: Promise<void>; resolve: () => void };
|
|
363
|
-
// Set while suspendTerminal() has handed the terminal to a child process.
|
|
364
|
-
private isSuspended = false;
|
|
365
|
-
// Input pause/resume hooks registered by the App component, which owns raw
|
|
366
|
-
// mode and bracketed paste state.
|
|
367
|
-
private pauseInput?: () => void;
|
|
368
|
-
private resumeInput?: () => void;
|
|
369
|
-
|
|
370
|
-
constructor(options: Options) {
|
|
371
|
-
autoBind(this);
|
|
372
|
-
|
|
373
|
-
if (options.patchConsole === "stdio") {
|
|
374
|
-
// Keep the real streams for patching (and for the instance registry,
|
|
375
|
-
// which is keyed by the stream passed to render()), and render
|
|
376
|
-
// through passthrough facades that bypass the capture.
|
|
377
|
-
this.captureTargets = { stdout: options.stdout, stderr: options.stderr };
|
|
378
|
-
options = {
|
|
379
|
-
...options,
|
|
380
|
-
stdout: createRenderPassthrough(options.stdout),
|
|
381
|
-
stderr: createRenderPassthrough(options.stderr),
|
|
382
|
-
};
|
|
383
|
-
}
|
|
289
|
+
/**
|
|
290
|
+
Returns a promise that settles when the app is unmounted.
|
|
291
|
+
*/
|
|
292
|
+
waitUntilExit: () => Promise<unknown>;
|
|
384
293
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
294
|
+
/**
|
|
295
|
+
Returns a promise that settles after pending render output is flushed to stdout.
|
|
296
|
+
*/
|
|
297
|
+
waitUntilRenderFlush: () => Promise<void>;
|
|
388
298
|
|
|
389
|
-
|
|
390
|
-
|
|
299
|
+
/**
|
|
300
|
+
Clear output.
|
|
301
|
+
*/
|
|
302
|
+
clear: () => void;
|
|
391
303
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
// where the property is absent (e.g. `node app.js | cat`).
|
|
395
|
-
this.interactive = this.resolveInteractiveOption(options.interactive);
|
|
304
|
+
/** Copy text through the renderer-owned terminal session. */
|
|
305
|
+
copyToClipboard: (text: string, selection?: ClipboardSelection) => boolean;
|
|
396
306
|
|
|
397
|
-
|
|
307
|
+
/** Update terminal-native progress through the renderer-owned session. */
|
|
308
|
+
setProgress: (state: TerminalProgressState, value?: number) => boolean;
|
|
309
|
+
};
|
|
398
310
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
311
|
+
export const createInk = (options: Options): Ink => {
|
|
312
|
+
// Set when patchConsole is "stdio": the real streams whose write is patched.
|
|
313
|
+
let captureTargets: { stdout: OutputStream; stderr: OutputStream } | undefined;
|
|
314
|
+
|
|
315
|
+
if (options.patchConsole === "stdio") {
|
|
316
|
+
// Keep the real streams for patching (and for the instance registry,
|
|
317
|
+
// which is keyed by the stream passed to render()), and render
|
|
318
|
+
// through passthrough facades that bypass the capture.
|
|
319
|
+
captureTargets = { stdout: options.stdout, stderr: options.stderr };
|
|
320
|
+
options = {
|
|
321
|
+
...options,
|
|
322
|
+
stdout: createRenderPassthrough(options.stdout),
|
|
323
|
+
stderr: createRenderPassthrough(options.stderr),
|
|
324
|
+
};
|
|
325
|
+
}
|
|
406
326
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
this.throttledOnRender = undefined;
|
|
410
|
-
} else {
|
|
411
|
-
const throttled = throttle(this.onRender, renderThrottleMs);
|
|
412
|
-
this.rootNode.onRender = () => {
|
|
413
|
-
this.hasPendingThrottledRender = true;
|
|
414
|
-
throttled();
|
|
415
|
-
};
|
|
327
|
+
const rootNode = dom.createNode("ink-root");
|
|
328
|
+
rootNode.onComputeLayout = calculateLayout;
|
|
416
329
|
|
|
417
|
-
|
|
418
|
-
}
|
|
330
|
+
const isScreenReaderEnabled = options.isScreenReaderEnabled ?? isScreenReader;
|
|
419
331
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
});
|
|
425
|
-
this.cursorPosition = undefined;
|
|
426
|
-
this.throttledLog = unthrottled
|
|
427
|
-
? this.log
|
|
428
|
-
: throttle((output: string) => {
|
|
429
|
-
const shouldWrite = this.log.willRender(output);
|
|
430
|
-
const sync = this.shouldSync();
|
|
431
|
-
if (sync && shouldWrite) {
|
|
432
|
-
this.options.stdout.write(bsu);
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
this.log(output);
|
|
436
|
-
|
|
437
|
-
if (sync && shouldWrite) {
|
|
438
|
-
this.options.stdout.write(esu);
|
|
439
|
-
}
|
|
440
|
-
});
|
|
332
|
+
// CI detection takes precedence: even a TTY stdout in CI defaults to non-interactive.
|
|
333
|
+
// Using Boolean(isTTY) (rather than an 'in' guard) correctly handles piped streams
|
|
334
|
+
// where the property is absent (e.g. `node app.js | cat`).
|
|
335
|
+
const interactive = options.interactive ?? (!isInCi && Boolean(options.stdout.isTTY));
|
|
441
336
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
337
|
+
const terminal = new TerminalSession({
|
|
338
|
+
stdin: options.stdin,
|
|
339
|
+
stdout: options.stdout,
|
|
340
|
+
stderr: options.stderr,
|
|
341
|
+
colorPolicy: options.colorProfile ?? "auto",
|
|
342
|
+
onCapabilitiesChange: () => rootNode.onRender?.(),
|
|
343
|
+
});
|
|
344
|
+
const terminalOsc = {
|
|
345
|
+
publishProgress: (
|
|
346
|
+
owner: symbol,
|
|
347
|
+
state: import("#/ansi/osc.ts").TerminalProgressState,
|
|
348
|
+
value?: number,
|
|
349
|
+
) => {
|
|
350
|
+
terminal.publishProgress(owner, state, value);
|
|
351
|
+
},
|
|
352
|
+
copyToClipboard: (text: string, selection?: import("#/ansi/osc.ts").ClipboardSelection) => {
|
|
353
|
+
terminal.copyToClipboard(text, selection);
|
|
354
|
+
},
|
|
355
|
+
publishTitle: (owner: symbol, title?: string) => terminal.publishTitle(owner, title),
|
|
356
|
+
setWorkingDirectory: (directory: URL | string) => terminal.setWorkingDirectory(directory),
|
|
357
|
+
notify: (title: string) => terminal.notify(title),
|
|
358
|
+
setPointerShape: (shape: string) => terminal.setPointerShape(shape),
|
|
359
|
+
};
|
|
360
|
+
const capabilitiesStore = terminal.capabilities;
|
|
445
361
|
|
|
446
|
-
|
|
447
|
-
|
|
362
|
+
const unthrottled = options.debug || isScreenReaderEnabled;
|
|
363
|
+
const renderScheduler = createRenderScheduler(onRender, {
|
|
364
|
+
unthrottled,
|
|
365
|
+
maxFps: options.maxFps ?? 30,
|
|
366
|
+
});
|
|
367
|
+
rootNode.onRender = renderScheduler.schedule;
|
|
368
|
+
rootNode.onImmediateRender = renderScheduler.immediate;
|
|
369
|
+
rootNode.onStaticChange = handleStaticChange;
|
|
370
|
+
const accessiblePresenter = isScreenReaderEnabled
|
|
371
|
+
? createInlinePresenter(options.stdout, { showCursor: true })
|
|
372
|
+
: undefined;
|
|
373
|
+
// Ignore last render after unmounting a tree to prevent empty output before exit
|
|
374
|
+
let isUnmounted = false;
|
|
375
|
+
let isUnmounting = false;
|
|
448
376
|
|
|
449
|
-
|
|
450
|
-
this.lastOutput = "";
|
|
451
|
-
this.lastOutputToRender = "";
|
|
452
|
-
this.lastOutputHeight = 0;
|
|
453
|
-
this.lastTerminalWidth = getWindowSize(this.options.stdout).columns;
|
|
454
|
-
this.lastTerminalHeight = getWindowSize(this.options.stdout).rows;
|
|
377
|
+
const isConcurrent = options.concurrent ?? false;
|
|
455
378
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
379
|
+
// Store last output to only rerender when needed
|
|
380
|
+
let lastOutput = "";
|
|
381
|
+
let lastOutputToRender = "";
|
|
382
|
+
let lastOutputHeight = 0;
|
|
383
|
+
let lastScreen: Screen | undefined;
|
|
384
|
+
let lastTerminalWidth = getWindowSize(options.stdout).columns;
|
|
385
|
+
let lastTerminalHeight = getWindowSize(options.stdout).rows;
|
|
459
386
|
|
|
460
|
-
|
|
461
|
-
|
|
387
|
+
// This variable is used only in debug mode to store full static output
|
|
388
|
+
// so that it's rerendered every time, not just new static parts, like in non-debug mode
|
|
389
|
+
let fullStaticOutput = "";
|
|
462
390
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
391
|
+
let exitResult: unknown;
|
|
392
|
+
let unsubscribeBeforeExit: (() => void) | undefined;
|
|
393
|
+
let restoreConsole: (() => void) | undefined;
|
|
394
|
+
// Partial trailing lines from captured direct writes, held until a newline.
|
|
395
|
+
const capturedStdioTails = { stdout: "", stderr: "" };
|
|
396
|
+
let unsubscribeResize: (() => void) | undefined;
|
|
397
|
+
let kittyProtocolEnabled = false;
|
|
398
|
+
let kittyFlags: KittyFlagName[] | undefined;
|
|
399
|
+
let cancelKittyDetection: (() => void) | undefined;
|
|
400
|
+
let nextRenderCommit: { promise: Promise<void>; resolve: () => void } | undefined;
|
|
401
|
+
// Input pause/resume hooks registered by the App component, which owns raw
|
|
402
|
+
// mode and bracketed paste state.
|
|
403
|
+
let pauseInput: (() => void) | undefined;
|
|
404
|
+
let resumeInput: (() => void) | undefined;
|
|
405
|
+
|
|
406
|
+
// Use ConcurrentRoot for concurrent mode, LegacyRoot for legacy mode
|
|
407
|
+
const rootTag = isConcurrent ? ConcurrentRoot : LegacyRoot;
|
|
408
|
+
|
|
409
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
410
|
+
const container: FiberRoot = reconciler.createContainer(
|
|
411
|
+
rootNode,
|
|
412
|
+
rootTag,
|
|
413
|
+
null,
|
|
414
|
+
false,
|
|
415
|
+
null,
|
|
416
|
+
"id",
|
|
417
|
+
() => {},
|
|
418
|
+
() => {},
|
|
419
|
+
() => {},
|
|
420
|
+
() => {},
|
|
421
|
+
);
|
|
476
422
|
|
|
477
|
-
|
|
478
|
-
|
|
423
|
+
// Unmount when process exits
|
|
424
|
+
const unsubscribeExit = signalExit(unmount, { alwaysLast: false });
|
|
479
425
|
|
|
480
|
-
|
|
426
|
+
setAlternateScreen(Boolean(options.alternateScreen));
|
|
481
427
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
reconciler.injectIntoDevTools();
|
|
485
|
-
}
|
|
428
|
+
// @ts-expect-error outdated types
|
|
429
|
+
if (isSigilDev) reconciler.injectIntoDevTools();
|
|
486
430
|
|
|
487
|
-
|
|
488
|
-
this.patchConsole();
|
|
489
|
-
}
|
|
431
|
+
if (options.patchConsole) installConsolePatch();
|
|
490
432
|
|
|
491
|
-
|
|
492
|
-
|
|
433
|
+
if (interactive) {
|
|
434
|
+
options.stdout.on("resize", resized);
|
|
493
435
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
436
|
+
unsubscribeResize = () => {
|
|
437
|
+
options.stdout.off("resize", resized);
|
|
438
|
+
};
|
|
439
|
+
}
|
|
498
440
|
|
|
499
|
-
|
|
441
|
+
initKittyKeyboard();
|
|
500
442
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
443
|
+
const {
|
|
444
|
+
promise: exitPromise,
|
|
445
|
+
resolve: resolveExitPromise,
|
|
446
|
+
reject: rejectExitPromise,
|
|
447
|
+
} = Promise.withResolvers<unknown>();
|
|
448
|
+
// Prevent global unhandled-rejection crashes when app code exits with an
|
|
449
|
+
// error but consumers never call waitUntilExit().
|
|
507
450
|
|
|
508
|
-
|
|
509
|
-
}
|
|
451
|
+
void exitPromise.catch(noop);
|
|
510
452
|
|
|
511
|
-
resized
|
|
512
|
-
const currentWidth = getWindowSize(
|
|
513
|
-
const currentHeight = getWindowSize(
|
|
453
|
+
function resized(): void {
|
|
454
|
+
const currentWidth = getWindowSize(options.stdout).columns;
|
|
455
|
+
const currentHeight = getWindowSize(options.stdout).rows;
|
|
514
456
|
|
|
515
457
|
// A width decrease rewraps lines and any height change moves content
|
|
516
458
|
// through scrollback, so the incremental render state no longer
|
|
517
459
|
// matches the screen. Erase what is still visible and force the next
|
|
518
460
|
// render to be a full rewrite instead of an incremental diff that
|
|
519
461
|
// would skip "unchanged" lines over stale screen content.
|
|
520
|
-
if (currentWidth <
|
|
521
|
-
//
|
|
462
|
+
if (currentWidth < lastTerminalWidth || currentHeight !== lastTerminalHeight) {
|
|
463
|
+
// Clearing erases the full previous frame line count from
|
|
522
464
|
// the cursor upward — after a height grow that also covers frame
|
|
523
465
|
// lines the emulator pulled back from scrollback, so no extra
|
|
524
466
|
// erase is needed for them.
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
467
|
+
clearLiveOutput();
|
|
468
|
+
resetLiveOutput();
|
|
469
|
+
lastOutput = "";
|
|
470
|
+
lastOutputToRender = "";
|
|
528
471
|
// Also forget the previous frame height: it described a frame
|
|
529
472
|
// that no longer exists on screen, and letting it flow into
|
|
530
473
|
// shouldClearTerminalForFrame would trigger a scrollback-erasing
|
|
531
474
|
// clearTerminal on a height shrink.
|
|
532
|
-
|
|
475
|
+
lastOutputHeight = 0;
|
|
533
476
|
}
|
|
534
477
|
|
|
535
|
-
|
|
536
|
-
dom.emitLayoutListeners(
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
this.lastTerminalWidth = currentWidth;
|
|
540
|
-
this.lastTerminalHeight = currentHeight;
|
|
541
|
-
};
|
|
478
|
+
calculateLayout();
|
|
479
|
+
dom.emitLayoutListeners(rootNode);
|
|
480
|
+
onRender();
|
|
542
481
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
482
|
+
lastTerminalWidth = currentWidth;
|
|
483
|
+
lastTerminalHeight = currentHeight;
|
|
484
|
+
}
|
|
546
485
|
|
|
547
|
-
handleAppExit
|
|
548
|
-
if (
|
|
486
|
+
function handleAppExit(errorOrResult?: unknown): void {
|
|
487
|
+
if (isUnmounted || isUnmounting) {
|
|
549
488
|
return;
|
|
550
489
|
}
|
|
551
490
|
|
|
552
491
|
if (isErrorInput(errorOrResult)) {
|
|
553
|
-
|
|
492
|
+
unmount(errorOrResult);
|
|
554
493
|
return;
|
|
555
494
|
}
|
|
556
495
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
}
|
|
496
|
+
exitResult = errorOrResult;
|
|
497
|
+
unmount();
|
|
498
|
+
}
|
|
560
499
|
|
|
561
|
-
setCursorPosition
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
}
|
|
500
|
+
function setCursorPosition(position: CursorPosition | undefined): void {
|
|
501
|
+
terminal.setCursor(position);
|
|
502
|
+
accessiblePresenter?.setCursorPosition(position);
|
|
503
|
+
}
|
|
565
504
|
|
|
566
|
-
restoreLastOutput
|
|
567
|
-
if (!
|
|
505
|
+
function restoreLastOutput(): void {
|
|
506
|
+
if (!interactive) {
|
|
568
507
|
return;
|
|
569
508
|
}
|
|
570
509
|
|
|
571
|
-
//
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
510
|
+
// Replay the latest cursor intent when restoring after external output.
|
|
511
|
+
if (isScreenReaderEnabled) {
|
|
512
|
+
accessiblePresenter!.setCursorPosition(terminal.cursor.position);
|
|
513
|
+
accessiblePresenter!(lastOutputToRender || lastOutput + "\n");
|
|
514
|
+
} else if (lastScreen) {
|
|
515
|
+
terminal.present(lastScreen, {
|
|
516
|
+
fullscreen: lastOutputToRender === lastOutput,
|
|
517
|
+
forceRewrite: true,
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
}
|
|
576
521
|
|
|
577
|
-
|
|
578
|
-
|
|
522
|
+
function clearLiveOutput(): void {
|
|
523
|
+
if (isScreenReaderEnabled) accessiblePresenter!.clear();
|
|
524
|
+
else terminal.clearFrame();
|
|
525
|
+
}
|
|
579
526
|
|
|
580
|
-
|
|
527
|
+
function finishLiveOutput(): void {
|
|
528
|
+
if (isScreenReaderEnabled) accessiblePresenter!.done();
|
|
529
|
+
else terminal.finishFrame();
|
|
530
|
+
}
|
|
581
531
|
|
|
582
|
-
|
|
583
|
-
|
|
532
|
+
function resetLiveOutput(): void {
|
|
533
|
+
if (isScreenReaderEnabled) accessiblePresenter!.reset();
|
|
534
|
+
else terminal.resetFrame();
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function calculateLayout(): void {
|
|
538
|
+
const terminalWidth = getWindowSize(options.stdout).columns;
|
|
539
|
+
|
|
540
|
+
rootNode.yogaNode!.setWidth(terminalWidth);
|
|
541
|
+
|
|
542
|
+
rootNode.yogaNode!.calculateLayout(undefined, undefined, Yoga.DIRECTION_LTR);
|
|
543
|
+
}
|
|
584
544
|
|
|
585
545
|
// Resets `fullStaticOutput` when the <Static> identity changes so stale items from a previous instance are not replayed on future rewrites.
|
|
586
|
-
handleStaticChange
|
|
587
|
-
|
|
588
|
-
}
|
|
546
|
+
function handleStaticChange(): void {
|
|
547
|
+
fullStaticOutput = "";
|
|
548
|
+
}
|
|
589
549
|
|
|
590
|
-
onRender
|
|
591
|
-
|
|
550
|
+
function onRender(): void {
|
|
551
|
+
renderScheduler.markRendered();
|
|
592
552
|
|
|
593
|
-
if (
|
|
553
|
+
if (isUnmounted) {
|
|
594
554
|
return;
|
|
595
555
|
}
|
|
596
556
|
|
|
597
557
|
// While suspended, the terminal belongs to a child process. Discard queued
|
|
598
558
|
// renders; resume() forces a full redraw once Ink reclaims the terminal.
|
|
599
559
|
// Resolve any awaited render commit so callers don't hang during suspension.
|
|
600
|
-
if (
|
|
601
|
-
if (
|
|
602
|
-
|
|
603
|
-
|
|
560
|
+
if (terminal.suspended) {
|
|
561
|
+
if (nextRenderCommit) {
|
|
562
|
+
nextRenderCommit.resolve();
|
|
563
|
+
nextRenderCommit = undefined;
|
|
604
564
|
}
|
|
605
565
|
|
|
606
566
|
return;
|
|
607
567
|
}
|
|
608
568
|
|
|
609
|
-
if (
|
|
610
|
-
|
|
611
|
-
|
|
569
|
+
if (nextRenderCommit) {
|
|
570
|
+
nextRenderCommit.resolve();
|
|
571
|
+
nextRenderCommit = undefined;
|
|
612
572
|
}
|
|
613
573
|
|
|
614
574
|
const startTime = performance.now();
|
|
615
|
-
const
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
575
|
+
const rendered = renderFrame(rootNode, isScreenReaderEnabled, {
|
|
576
|
+
colorProfile: terminal.colorProfile,
|
|
577
|
+
paintContext: {
|
|
578
|
+
appearance: capabilitiesStore.current.theme.appearance,
|
|
579
|
+
palette: capabilitiesStore.current.theme.palette,
|
|
580
|
+
},
|
|
581
|
+
});
|
|
582
|
+
const screen = rendered.screen;
|
|
583
|
+
const output = rendered.accessibleText ?? (screen ? terminal.encode(screen) : "");
|
|
584
|
+
const outputHeight =
|
|
585
|
+
rendered.accessibleText === undefined
|
|
586
|
+
? (screen?.height ?? 0)
|
|
587
|
+
: rendered.accessibleText === ""
|
|
588
|
+
? 0
|
|
589
|
+
: rendered.accessibleText.split("\n").length;
|
|
590
|
+
const staticBody =
|
|
591
|
+
rendered.staticAccessibleText ??
|
|
592
|
+
(rendered.staticScreen ? terminal.encode(rendered.staticScreen) : "");
|
|
593
|
+
const staticOutput = staticBody ? `${staticBody}\n` : "";
|
|
594
|
+
|
|
595
|
+
options.onRender?.({ renderTime: performance.now() - startTime });
|
|
621
596
|
|
|
622
597
|
// If <Static> output isn't empty, it means new children have been added to it
|
|
623
598
|
const hasStaticOutput = staticOutput && staticOutput !== "\n";
|
|
624
599
|
|
|
625
|
-
if (
|
|
600
|
+
if (options.debug) {
|
|
626
601
|
if (hasStaticOutput) {
|
|
627
|
-
|
|
602
|
+
fullStaticOutput += staticOutput;
|
|
628
603
|
}
|
|
629
604
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
605
|
+
lastOutput = output;
|
|
606
|
+
lastOutputToRender = output;
|
|
607
|
+
lastOutputHeight = outputHeight;
|
|
608
|
+
options.stdout.write(fullStaticOutput + output);
|
|
634
609
|
return;
|
|
635
610
|
}
|
|
636
611
|
|
|
637
|
-
if (!
|
|
612
|
+
if (!interactive) {
|
|
638
613
|
if (hasStaticOutput) {
|
|
639
|
-
|
|
614
|
+
options.stdout.write(staticOutput);
|
|
640
615
|
}
|
|
641
616
|
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
617
|
+
lastOutput = output;
|
|
618
|
+
lastOutputToRender = output + "\n";
|
|
619
|
+
lastOutputHeight = outputHeight;
|
|
645
620
|
return;
|
|
646
621
|
}
|
|
647
622
|
|
|
648
|
-
if (
|
|
649
|
-
const sync =
|
|
623
|
+
if (isScreenReaderEnabled) {
|
|
624
|
+
const sync = shouldSync();
|
|
650
625
|
if (sync) {
|
|
651
|
-
|
|
626
|
+
options.stdout.write(bsu);
|
|
652
627
|
}
|
|
653
628
|
|
|
654
629
|
if (hasStaticOutput) {
|
|
655
630
|
// We need to erase the main output before writing new static output
|
|
656
|
-
const erase =
|
|
657
|
-
|
|
658
|
-
this.options.stdout.write(erase + staticOutput);
|
|
631
|
+
const erase = lastOutputHeight > 0 ? ansiEscapes.eraseLines(lastOutputHeight) : "";
|
|
632
|
+
options.stdout.write(erase + staticOutput);
|
|
659
633
|
// After erasing, the last output is gone, so we should reset its height
|
|
660
|
-
|
|
634
|
+
lastOutputHeight = 0;
|
|
661
635
|
}
|
|
662
636
|
|
|
663
|
-
if (output ===
|
|
637
|
+
if (output === lastOutput && !hasStaticOutput) {
|
|
664
638
|
if (sync) {
|
|
665
|
-
|
|
639
|
+
options.stdout.write(esu);
|
|
666
640
|
}
|
|
667
641
|
|
|
668
642
|
return;
|
|
669
643
|
}
|
|
670
644
|
|
|
671
|
-
const terminalWidth = getWindowSize(
|
|
645
|
+
const terminalWidth = getWindowSize(options.stdout).columns;
|
|
672
646
|
|
|
673
647
|
const wrappedOutput = wrapAnsi(output, terminalWidth, {
|
|
674
648
|
trim: false,
|
|
@@ -677,204 +651,206 @@ export class Ink {
|
|
|
677
651
|
|
|
678
652
|
// If we haven't erased yet, do it now.
|
|
679
653
|
if (hasStaticOutput) {
|
|
680
|
-
|
|
654
|
+
options.stdout.write(wrappedOutput);
|
|
681
655
|
} else {
|
|
682
|
-
const erase =
|
|
683
|
-
|
|
684
|
-
this.options.stdout.write(erase + wrappedOutput);
|
|
656
|
+
const erase = lastOutputHeight > 0 ? ansiEscapes.eraseLines(lastOutputHeight) : "";
|
|
657
|
+
options.stdout.write(erase + wrappedOutput);
|
|
685
658
|
}
|
|
686
659
|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
660
|
+
lastOutput = output;
|
|
661
|
+
lastOutputToRender = wrappedOutput;
|
|
662
|
+
lastOutputHeight = wrappedOutput === "" ? 0 : wrappedOutput.split("\n").length;
|
|
690
663
|
|
|
691
664
|
if (sync) {
|
|
692
|
-
|
|
665
|
+
options.stdout.write(esu);
|
|
693
666
|
}
|
|
694
667
|
|
|
695
668
|
return;
|
|
696
669
|
}
|
|
697
670
|
|
|
698
671
|
if (hasStaticOutput) {
|
|
699
|
-
|
|
672
|
+
fullStaticOutput += staticOutput;
|
|
700
673
|
}
|
|
701
674
|
|
|
702
|
-
|
|
703
|
-
|
|
675
|
+
renderInteractiveFrame(
|
|
676
|
+
output,
|
|
677
|
+
outputHeight,
|
|
678
|
+
hasStaticOutput ? staticOutput : "",
|
|
679
|
+
screen,
|
|
680
|
+
false,
|
|
681
|
+
);
|
|
682
|
+
}
|
|
704
683
|
|
|
705
|
-
render(node: ReactNode): void {
|
|
684
|
+
function render(node: ReactNode): void {
|
|
706
685
|
const tree = (
|
|
707
|
-
<AccessibilityContext.Provider value={{ isScreenReaderEnabled
|
|
708
|
-
<
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
686
|
+
<AccessibilityContext.Provider value={{ isScreenReaderEnabled }}>
|
|
687
|
+
<TerminalOscContext.Provider value={terminalOsc}>
|
|
688
|
+
<App
|
|
689
|
+
stdin={options.stdin}
|
|
690
|
+
stdout={options.stdout}
|
|
691
|
+
stderr={options.stderr}
|
|
692
|
+
exitOnCtrlC={options.exitOnCtrlC}
|
|
693
|
+
interactive={interactive}
|
|
694
|
+
renderThrottleMs={renderScheduler.intervalMs}
|
|
695
|
+
terminalInput={terminal.input}
|
|
696
|
+
writeToStdout={writeToStdout}
|
|
697
|
+
writeToStderr={writeToStderr}
|
|
698
|
+
setCursorPosition={setCursorPosition}
|
|
699
|
+
onExit={handleAppExit}
|
|
700
|
+
onWaitUntilRenderFlush={waitUntilRenderFlush}
|
|
701
|
+
onSuspendTerminal={suspendTerminal}
|
|
702
|
+
onRegisterInputControl={registerInputControl}
|
|
703
|
+
>
|
|
704
|
+
{node}
|
|
705
|
+
</App>
|
|
706
|
+
</TerminalOscContext.Provider>
|
|
725
707
|
</AccessibilityContext.Provider>
|
|
726
708
|
);
|
|
727
709
|
|
|
728
|
-
if (
|
|
710
|
+
if (isConcurrent) {
|
|
729
711
|
// Concurrent mode: use updateContainer (async scheduling)
|
|
730
|
-
reconciler.updateContainer(tree,
|
|
712
|
+
reconciler.updateContainer(tree, container, null, noop);
|
|
731
713
|
} else {
|
|
732
714
|
// Legacy mode: use updateContainerSync + flushSyncWork (sync)
|
|
733
|
-
reconciler.updateContainerSync(tree,
|
|
715
|
+
reconciler.updateContainerSync(tree, container, null, noop);
|
|
734
716
|
reconciler.flushSyncWork();
|
|
735
717
|
}
|
|
736
718
|
}
|
|
737
719
|
|
|
738
|
-
writeToStdout(data: string): void {
|
|
739
|
-
if (
|
|
720
|
+
function writeToStdout(data: string): void {
|
|
721
|
+
if (isUnmounted) {
|
|
740
722
|
return;
|
|
741
723
|
}
|
|
742
724
|
|
|
743
725
|
// While suspended, the terminal belongs to a child process. Don't erase or
|
|
744
726
|
// repaint Ink's frame around console output; the forced redraw on resume
|
|
745
727
|
// restores the screen.
|
|
746
|
-
if (
|
|
728
|
+
if (terminal.suspended) {
|
|
747
729
|
return;
|
|
748
730
|
}
|
|
749
731
|
|
|
750
|
-
if (
|
|
751
|
-
|
|
732
|
+
if (options.debug) {
|
|
733
|
+
options.stdout.write(data + fullStaticOutput + lastOutput);
|
|
752
734
|
return;
|
|
753
735
|
}
|
|
754
736
|
|
|
755
|
-
if (!
|
|
756
|
-
|
|
737
|
+
if (!interactive) {
|
|
738
|
+
options.stdout.write(data);
|
|
757
739
|
return;
|
|
758
740
|
}
|
|
759
741
|
|
|
760
|
-
const sync =
|
|
742
|
+
const sync = shouldSync();
|
|
761
743
|
if (sync) {
|
|
762
|
-
|
|
744
|
+
options.stdout.write(bsu);
|
|
763
745
|
}
|
|
764
746
|
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
747
|
+
clearLiveOutput();
|
|
748
|
+
options.stdout.write(data);
|
|
749
|
+
restoreLastOutput();
|
|
768
750
|
|
|
769
751
|
if (sync) {
|
|
770
|
-
|
|
752
|
+
options.stdout.write(esu);
|
|
771
753
|
}
|
|
772
754
|
}
|
|
773
755
|
|
|
774
|
-
writeToStderr(data: string): void {
|
|
775
|
-
if (
|
|
756
|
+
function writeToStderr(data: string): void {
|
|
757
|
+
if (isUnmounted) {
|
|
776
758
|
return;
|
|
777
759
|
}
|
|
778
760
|
|
|
779
761
|
// See writeToStdout: stay off the terminal while suspended.
|
|
780
|
-
if (
|
|
762
|
+
if (terminal.suspended) {
|
|
781
763
|
return;
|
|
782
764
|
}
|
|
783
765
|
|
|
784
|
-
if (
|
|
785
|
-
|
|
786
|
-
|
|
766
|
+
if (options.debug) {
|
|
767
|
+
options.stderr.write(data);
|
|
768
|
+
options.stdout.write(fullStaticOutput + lastOutput);
|
|
787
769
|
return;
|
|
788
770
|
}
|
|
789
771
|
|
|
790
|
-
if (!
|
|
791
|
-
|
|
772
|
+
if (!interactive) {
|
|
773
|
+
options.stderr.write(data);
|
|
792
774
|
return;
|
|
793
775
|
}
|
|
794
776
|
|
|
795
|
-
const sync =
|
|
777
|
+
const sync = shouldSync();
|
|
796
778
|
if (sync) {
|
|
797
|
-
|
|
779
|
+
options.stdout.write(bsu);
|
|
798
780
|
}
|
|
799
781
|
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
782
|
+
clearLiveOutput();
|
|
783
|
+
options.stderr.write(data);
|
|
784
|
+
restoreLastOutput();
|
|
803
785
|
|
|
804
786
|
if (sync) {
|
|
805
|
-
|
|
787
|
+
options.stdout.write(esu);
|
|
806
788
|
}
|
|
807
789
|
}
|
|
808
790
|
|
|
809
791
|
// eslint-disable-next-line @typescript-eslint/no-restricted-types
|
|
810
|
-
unmount(error?: Error | number | null): void {
|
|
811
|
-
if (
|
|
792
|
+
function unmount(error?: Error | number | null): void {
|
|
793
|
+
if (isUnmounted || isUnmounting) {
|
|
812
794
|
return;
|
|
813
795
|
}
|
|
814
796
|
|
|
815
|
-
|
|
797
|
+
isUnmounting = true;
|
|
816
798
|
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
this.beforeExitHandler = undefined;
|
|
820
|
-
}
|
|
799
|
+
unsubscribeBeforeExit?.();
|
|
800
|
+
unsubscribeBeforeExit = undefined;
|
|
821
801
|
|
|
822
|
-
const {
|
|
823
|
-
const { canWriteToStdout } = getWritableStreamState(stdout);
|
|
802
|
+
const { canWriteToStdout } = getWritableStreamState(options.stdout);
|
|
824
803
|
|
|
825
804
|
// Display any partial captured stdio lines while writes still go through.
|
|
826
805
|
if (canWriteToStdout) {
|
|
827
|
-
|
|
806
|
+
flushCapturedStdio();
|
|
828
807
|
}
|
|
829
808
|
|
|
830
809
|
// Clear any pending throttled render timer on unmount. When stdout is writable,
|
|
831
810
|
// flush so the final frame is emitted; otherwise cancel to avoid delayed callbacks.
|
|
832
|
-
settleThrottle(
|
|
811
|
+
settleThrottle(renderScheduler.throttled, canWriteToStdout);
|
|
833
812
|
|
|
834
813
|
if (canWriteToStdout) {
|
|
835
814
|
// If throttling is enabled and there is already a pending render, flushing above
|
|
836
815
|
// is sufficient. Also avoid calling onRender() again when static output already
|
|
837
816
|
// exists, as that can duplicate <Static> children output on exit (see issue #397).
|
|
838
817
|
const shouldRenderFinalFrame =
|
|
839
|
-
!
|
|
840
|
-
(!this.hasPendingThrottledRender && this.fullStaticOutput === "");
|
|
818
|
+
!renderScheduler.throttled || (!renderScheduler.pending && fullStaticOutput === "");
|
|
841
819
|
|
|
842
820
|
if (shouldRenderFinalFrame) {
|
|
843
|
-
|
|
844
|
-
|
|
821
|
+
calculateLayout();
|
|
822
|
+
onRender();
|
|
845
823
|
}
|
|
846
824
|
}
|
|
847
825
|
|
|
848
826
|
// Mark as unmounted after the final render but before stdout writes
|
|
849
827
|
// that could re-enter exit() via synchronous write callbacks.
|
|
850
|
-
|
|
828
|
+
isUnmounted = true;
|
|
851
829
|
|
|
852
|
-
|
|
830
|
+
unsubscribeExit();
|
|
831
|
+
terminal.cleanup();
|
|
853
832
|
|
|
854
|
-
|
|
855
|
-
// prevent delayed callbacks from writing to a closed stream.
|
|
856
|
-
settleThrottle(this.throttledLog, canWriteToStdout);
|
|
857
|
-
if (typeof this.restoreConsole === "function") {
|
|
833
|
+
if (typeof restoreConsole === "function") {
|
|
858
834
|
// Once unmount starts, Ink stops trying to manage teardown-time
|
|
859
835
|
// console output. Restoring the native console before React cleanup keeps
|
|
860
836
|
// unmount behavior simple and avoids special-case handling for custom
|
|
861
837
|
// streams, fullscreen frames, and alternate-screen teardown.
|
|
862
|
-
|
|
838
|
+
restoreConsole();
|
|
863
839
|
}
|
|
864
840
|
|
|
865
841
|
const finishUnmount = (): void => {
|
|
866
|
-
if (typeof
|
|
867
|
-
|
|
842
|
+
if (typeof unsubscribeResize === "function") {
|
|
843
|
+
unsubscribeResize();
|
|
868
844
|
}
|
|
869
845
|
|
|
870
846
|
// Cancel any in-progress auto-detection before checking protocol state
|
|
871
|
-
if (
|
|
872
|
-
|
|
847
|
+
if (cancelKittyDetection) {
|
|
848
|
+
cancelKittyDetection();
|
|
873
849
|
}
|
|
874
850
|
|
|
875
851
|
if (canWriteToStdout) {
|
|
876
|
-
if (
|
|
877
|
-
|
|
852
|
+
if (kittyProtocolEnabled) {
|
|
853
|
+
writeBestEffort(options.stdout, ansiEscapes.popKittyKeyboard);
|
|
878
854
|
}
|
|
879
855
|
|
|
880
856
|
// Alternate-screen content is disposable by design. We intentionally
|
|
@@ -883,26 +859,25 @@ export class Ink {
|
|
|
883
859
|
// diagnostics onto it. Trying to preserve teardown output across the
|
|
884
860
|
// buffer switch adds fragile lifecycle-specific behavior, so Ink keeps
|
|
885
861
|
// alternate-screen teardown intentionally simple and best-effort.
|
|
886
|
-
if (
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
this.alternateScreen = false;
|
|
862
|
+
if (terminal.alternateScreen) {
|
|
863
|
+
terminal.setAlternateScreen(false);
|
|
864
|
+
terminal.setCursorAppearance({ visible: true });
|
|
890
865
|
}
|
|
891
866
|
|
|
892
|
-
if (!
|
|
867
|
+
if (!interactive) {
|
|
893
868
|
// Non-interactive environments don't handle erasing ansi escapes well.
|
|
894
869
|
// In debug mode, each render already writes to stdout, so only a trailing
|
|
895
870
|
// newline is needed. In non-debug mode, write the last frame now (it was
|
|
896
871
|
// deferred during rendering).
|
|
897
|
-
|
|
898
|
-
} else if (!
|
|
899
|
-
|
|
872
|
+
options.stdout.write(options.debug ? "\n" : lastOutput + "\n");
|
|
873
|
+
} else if (!options.debug) {
|
|
874
|
+
finishLiveOutput();
|
|
900
875
|
}
|
|
901
876
|
}
|
|
902
877
|
|
|
903
|
-
|
|
878
|
+
kittyProtocolEnabled = false;
|
|
904
879
|
|
|
905
|
-
instances.delete(
|
|
880
|
+
instances.delete(captureTargets?.stdout ?? options.stdout);
|
|
906
881
|
|
|
907
882
|
// Ensure all queued writes have been processed before resolving the
|
|
908
883
|
// exit promise. Queue an empty write as a barrier — its callback fires
|
|
@@ -911,13 +886,13 @@ export class Ink {
|
|
|
911
886
|
// When called from signal-exit during process shutdown (error is a
|
|
912
887
|
// number or null rather than undefined/Error), resolve synchronously
|
|
913
888
|
// because the event loop is draining and async callbacks won't fire.
|
|
914
|
-
const
|
|
889
|
+
const finalExitResult = exitResult;
|
|
915
890
|
|
|
916
891
|
const resolveOrReject = () => {
|
|
917
892
|
if (isErrorInput(error)) {
|
|
918
|
-
|
|
893
|
+
rejectExitPromise(error);
|
|
919
894
|
} else {
|
|
920
|
-
|
|
895
|
+
resolveExitPromise(finalExitResult);
|
|
921
896
|
}
|
|
922
897
|
};
|
|
923
898
|
|
|
@@ -926,7 +901,7 @@ export class Ink {
|
|
|
926
901
|
if (isProcessExiting) {
|
|
927
902
|
resolveOrReject();
|
|
928
903
|
} else if (canWriteToStdout) {
|
|
929
|
-
|
|
904
|
+
options.stdout.write("", resolveOrReject);
|
|
930
905
|
} else {
|
|
931
906
|
setImmediate(resolveOrReject);
|
|
932
907
|
}
|
|
@@ -936,34 +911,32 @@ export class Ink {
|
|
|
936
911
|
flushPassiveEffects?: () => boolean;
|
|
937
912
|
};
|
|
938
913
|
|
|
939
|
-
if (
|
|
940
|
-
reconciler.updateContainerSync(null,
|
|
914
|
+
if (isConcurrent) {
|
|
915
|
+
reconciler.updateContainerSync(null, container, null, noop);
|
|
941
916
|
reconciler.flushSyncWork();
|
|
942
917
|
concurrentReconciler.flushPassiveEffects?.();
|
|
943
918
|
finishUnmount();
|
|
944
919
|
} else {
|
|
945
920
|
// Legacy mode: use updateContainerSync + flushSyncWork (sync)
|
|
946
|
-
reconciler.updateContainerSync(null,
|
|
921
|
+
reconciler.updateContainerSync(null, container, null, noop);
|
|
947
922
|
reconciler.flushSyncWork();
|
|
948
923
|
finishUnmount();
|
|
949
924
|
}
|
|
950
925
|
}
|
|
951
926
|
|
|
952
|
-
async waitUntilExit(): Promise<unknown> {
|
|
953
|
-
if (!
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
};
|
|
957
|
-
|
|
958
|
-
process.once("beforeExit", this.beforeExitHandler);
|
|
927
|
+
async function waitUntilExit(): Promise<unknown> {
|
|
928
|
+
if (!unsubscribeBeforeExit) {
|
|
929
|
+
unsubscribeBeforeExit = registerBeforeExit(() => {
|
|
930
|
+
unmount();
|
|
931
|
+
});
|
|
959
932
|
}
|
|
960
933
|
|
|
961
|
-
return
|
|
934
|
+
return exitPromise;
|
|
962
935
|
}
|
|
963
936
|
|
|
964
|
-
async waitUntilRenderFlush(): Promise<void> {
|
|
965
|
-
if (
|
|
966
|
-
await
|
|
937
|
+
async function waitUntilRenderFlush(): Promise<void> {
|
|
938
|
+
if (isUnmounted || isUnmounting) {
|
|
939
|
+
await awaitExit();
|
|
967
940
|
return;
|
|
968
941
|
}
|
|
969
942
|
|
|
@@ -971,35 +944,33 @@ export class Ink {
|
|
|
971
944
|
// fire passive effects and process any work they enqueued.
|
|
972
945
|
await yieldImmediate();
|
|
973
946
|
|
|
974
|
-
if (
|
|
975
|
-
await
|
|
947
|
+
if (isUnmounted || isUnmounting) {
|
|
948
|
+
await awaitExit();
|
|
976
949
|
return;
|
|
977
950
|
}
|
|
978
951
|
|
|
979
952
|
// In concurrent mode, React's scheduler may still be mid-render after
|
|
980
953
|
// the yield. Wait for the next render commit instead of polling.
|
|
981
|
-
if (
|
|
982
|
-
await Promise.race([
|
|
954
|
+
if (isConcurrent && hasPendingConcurrentWork()) {
|
|
955
|
+
await Promise.race([awaitNextRender(), awaitExit()]);
|
|
983
956
|
|
|
984
|
-
if (
|
|
985
|
-
|
|
986
|
-
await
|
|
957
|
+
if (isUnmounted || isUnmounting) {
|
|
958
|
+
nextRenderCommit = undefined;
|
|
959
|
+
await awaitExit();
|
|
987
960
|
return;
|
|
988
961
|
}
|
|
989
962
|
}
|
|
990
963
|
|
|
991
964
|
reconciler.flushSyncWork();
|
|
992
965
|
|
|
993
|
-
const {
|
|
994
|
-
const { canWriteToStdout } = getWritableStreamState(stdout);
|
|
966
|
+
const { canWriteToStdout } = getWritableStreamState(options.stdout);
|
|
995
967
|
|
|
996
|
-
// Flush pending
|
|
997
|
-
settleThrottle(
|
|
998
|
-
settleThrottle(this.throttledLog, canWriteToStdout);
|
|
968
|
+
// Flush pending scheduled rendering so its output is included in this wait.
|
|
969
|
+
settleThrottle(renderScheduler.throttled, canWriteToStdout);
|
|
999
970
|
|
|
1000
971
|
if (canWriteToStdout) {
|
|
1001
972
|
await new Promise<void>((resolve) => {
|
|
1002
|
-
|
|
973
|
+
options.stdout.write("", () => {
|
|
1003
974
|
resolve();
|
|
1004
975
|
});
|
|
1005
976
|
});
|
|
@@ -1009,41 +980,41 @@ export class Ink {
|
|
|
1009
980
|
await yieldImmediate();
|
|
1010
981
|
}
|
|
1011
982
|
|
|
1012
|
-
clear(): void {
|
|
1013
|
-
if (
|
|
1014
|
-
|
|
983
|
+
function clear(): void {
|
|
984
|
+
if (interactive && !options.debug) {
|
|
985
|
+
clearLiveOutput();
|
|
1015
986
|
// Sync lastOutput so that unmount's final onRender
|
|
1016
|
-
// sees it as unchanged and
|
|
1017
|
-
|
|
987
|
+
// sees it as unchanged and the presenter skips it
|
|
988
|
+
if (isScreenReaderEnabled) accessiblePresenter!.sync(lastOutputToRender || lastOutput + "\n");
|
|
1018
989
|
}
|
|
1019
990
|
}
|
|
1020
991
|
|
|
1021
|
-
|
|
1022
|
-
if (
|
|
992
|
+
function installConsolePatch(): void {
|
|
993
|
+
if (options.debug) {
|
|
1023
994
|
return;
|
|
1024
995
|
}
|
|
1025
996
|
|
|
1026
997
|
const restoreConsoleMethods = patchConsole((stream, data) => {
|
|
1027
|
-
if (
|
|
998
|
+
if (options.onCapturedOutput?.(stream, data, "console") === true) {
|
|
1028
999
|
return;
|
|
1029
1000
|
}
|
|
1030
1001
|
|
|
1031
1002
|
if (stream === "stdout") {
|
|
1032
|
-
|
|
1003
|
+
writeToStdout(data);
|
|
1033
1004
|
}
|
|
1034
1005
|
|
|
1035
1006
|
if (stream === "stderr") {
|
|
1036
1007
|
const isReactMessage = data.startsWith("The above error occurred");
|
|
1037
1008
|
|
|
1038
1009
|
if (!isReactMessage) {
|
|
1039
|
-
|
|
1010
|
+
writeToStderr(data);
|
|
1040
1011
|
}
|
|
1041
1012
|
}
|
|
1042
1013
|
});
|
|
1043
1014
|
|
|
1044
|
-
const restoreDirectStdio =
|
|
1015
|
+
const restoreDirectStdio = patchDirectStdio();
|
|
1045
1016
|
|
|
1046
|
-
|
|
1017
|
+
restoreConsole = () => {
|
|
1047
1018
|
restoreConsoleMethods();
|
|
1048
1019
|
restoreDirectStdio?.();
|
|
1049
1020
|
};
|
|
@@ -1051,53 +1022,17 @@ export class Ink {
|
|
|
1051
1022
|
|
|
1052
1023
|
// Intercept direct `write` calls on the real streams. Ink renders through
|
|
1053
1024
|
// passthrough facades, so everything arriving here is external output.
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
if (!targets) {
|
|
1025
|
+
function patchDirectStdio(): (() => void) | undefined {
|
|
1026
|
+
if (!captureTargets) {
|
|
1058
1027
|
return;
|
|
1059
1028
|
}
|
|
1060
1029
|
|
|
1061
|
-
const
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
const patchedWrite = (
|
|
1068
|
-
chunk: unknown,
|
|
1069
|
-
encodingOrCallback?: unknown,
|
|
1070
|
-
callback?: unknown,
|
|
1071
|
-
): boolean => {
|
|
1072
|
-
const data =
|
|
1073
|
-
typeof chunk === "string"
|
|
1074
|
-
? chunk
|
|
1075
|
-
: chunk instanceof Uint8Array
|
|
1076
|
-
? Buffer.from(chunk).toString()
|
|
1077
|
-
: String(chunk);
|
|
1078
|
-
|
|
1079
|
-
this.handleCapturedStdio(name, data);
|
|
1080
|
-
|
|
1081
|
-
const done =
|
|
1082
|
-
typeof encodingOrCallback === "function"
|
|
1083
|
-
? encodingOrCallback
|
|
1084
|
-
: typeof callback === "function"
|
|
1085
|
-
? callback
|
|
1086
|
-
: undefined;
|
|
1087
|
-
done?.();
|
|
1088
|
-
|
|
1089
|
-
return true;
|
|
1090
|
-
};
|
|
1091
|
-
|
|
1092
|
-
stream.write = patchedWrite;
|
|
1093
|
-
|
|
1094
|
-
return () => {
|
|
1095
|
-
stream.write = originalWrite;
|
|
1096
|
-
};
|
|
1097
|
-
};
|
|
1098
|
-
|
|
1099
|
-
const restoreStdout = patch("stdout", targets.stdout);
|
|
1100
|
-
const restoreStderr = patch("stderr", targets.stderr);
|
|
1030
|
+
const restoreStdout = patchStreamWrite(captureTargets.stdout, (data) => {
|
|
1031
|
+
handleCapturedStdio("stdout", data);
|
|
1032
|
+
});
|
|
1033
|
+
const restoreStderr = patchStreamWrite(captureTargets.stderr, (data) => {
|
|
1034
|
+
handleCapturedStdio("stderr", data);
|
|
1035
|
+
});
|
|
1101
1036
|
|
|
1102
1037
|
return () => {
|
|
1103
1038
|
restoreStdout();
|
|
@@ -1105,8 +1040,8 @@ export class Ink {
|
|
|
1105
1040
|
};
|
|
1106
1041
|
}
|
|
1107
1042
|
|
|
1108
|
-
|
|
1109
|
-
if (
|
|
1043
|
+
function handleCapturedStdio(stream: "stdout" | "stderr", data: string): void {
|
|
1044
|
+
if (options.onCapturedOutput?.(stream, data, "stdio") === true) {
|
|
1110
1045
|
return;
|
|
1111
1046
|
}
|
|
1112
1047
|
|
|
@@ -1114,8 +1049,8 @@ export class Ink {
|
|
|
1114
1049
|
// spinners), and only complete lines can be spliced above the live
|
|
1115
1050
|
// frame without corrupting it. The trailing partial line is held until
|
|
1116
1051
|
// its newline arrives, or flushed at unmount/suspend.
|
|
1117
|
-
const parts = (
|
|
1118
|
-
|
|
1052
|
+
const parts = (capturedStdioTails[stream] + data).split(/\r?\n/);
|
|
1053
|
+
capturedStdioTails[stream] = parts.pop() ?? "";
|
|
1119
1054
|
|
|
1120
1055
|
if (parts.length === 0) {
|
|
1121
1056
|
return;
|
|
@@ -1124,100 +1059,82 @@ export class Ink {
|
|
|
1124
1059
|
const payload = parts.join("\n") + "\n";
|
|
1125
1060
|
|
|
1126
1061
|
if (stream === "stdout") {
|
|
1127
|
-
|
|
1062
|
+
writeToStdout(payload);
|
|
1128
1063
|
} else {
|
|
1129
|
-
|
|
1064
|
+
writeToStderr(payload);
|
|
1130
1065
|
}
|
|
1131
1066
|
}
|
|
1132
1067
|
|
|
1133
1068
|
// Display any partial captured lines that never received a newline.
|
|
1134
|
-
|
|
1069
|
+
function flushCapturedStdio(): void {
|
|
1135
1070
|
for (const stream of ["stdout", "stderr"] as const) {
|
|
1136
|
-
const tail =
|
|
1071
|
+
const tail = capturedStdioTails[stream];
|
|
1137
1072
|
|
|
1138
1073
|
if (tail === "") {
|
|
1139
1074
|
continue;
|
|
1140
1075
|
}
|
|
1141
1076
|
|
|
1142
|
-
|
|
1077
|
+
capturedStdioTails[stream] = "";
|
|
1143
1078
|
|
|
1144
1079
|
if (stream === "stdout") {
|
|
1145
|
-
|
|
1080
|
+
writeToStdout(tail + "\n");
|
|
1146
1081
|
} else {
|
|
1147
|
-
|
|
1082
|
+
writeToStderr(tail + "\n");
|
|
1148
1083
|
}
|
|
1149
1084
|
}
|
|
1150
1085
|
}
|
|
1151
1086
|
|
|
1152
|
-
registerInputControl(
|
|
1153
|
-
|
|
1154
|
-
|
|
1087
|
+
function registerInputControl(pause: () => void, resume: () => void): void {
|
|
1088
|
+
pauseInput = pause;
|
|
1089
|
+
resumeInput = resume;
|
|
1155
1090
|
}
|
|
1156
1091
|
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
async suspendTerminal(
|
|
1160
|
-
|
|
1092
|
+
function suspendTerminal(callback: () => void | Promise<void>): Promise<void>;
|
|
1093
|
+
function suspendTerminal(): Promise<TerminalSuspension>;
|
|
1094
|
+
async function suspendTerminal(
|
|
1095
|
+
callback?: () => void | Promise<void>,
|
|
1096
|
+
): Promise<void | TerminalSuspension> {
|
|
1097
|
+
beginSuspend();
|
|
1161
1098
|
|
|
1162
1099
|
if (callback) {
|
|
1163
1100
|
try {
|
|
1164
1101
|
await callback();
|
|
1165
1102
|
} finally {
|
|
1166
|
-
await
|
|
1103
|
+
await endSuspend();
|
|
1167
1104
|
}
|
|
1168
1105
|
|
|
1169
1106
|
return;
|
|
1170
1107
|
}
|
|
1171
1108
|
|
|
1172
1109
|
const resume = async (): Promise<void> => {
|
|
1173
|
-
await
|
|
1110
|
+
await endSuspend();
|
|
1174
1111
|
};
|
|
1175
1112
|
|
|
1176
1113
|
return { resume, [Symbol.asyncDispose]: resume };
|
|
1177
1114
|
}
|
|
1178
1115
|
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
this.writeBestEffort(this.options.stdout, ansiEscapes.enterAlternativeScreen);
|
|
1184
|
-
this.writeBestEffort(this.options.stdout, hideCursorEscape);
|
|
1185
|
-
}
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
|
-
private resolveInteractiveOption(interactive: boolean | undefined): boolean {
|
|
1189
|
-
return interactive ?? (!isInCi && Boolean(this.options.stdout.isTTY));
|
|
1190
|
-
}
|
|
1191
|
-
|
|
1192
|
-
private resolveAlternateScreenOption(
|
|
1193
|
-
alternateScreen: boolean | undefined,
|
|
1194
|
-
interactive: boolean,
|
|
1195
|
-
): boolean {
|
|
1196
|
-
return Boolean(alternateScreen) && interactive && Boolean(this.options.stdout.isTTY);
|
|
1116
|
+
function setAlternateScreen(enabled: boolean): void {
|
|
1117
|
+
terminal.setAlternateScreen(enabled && interactive && Boolean(options.stdout.isTTY), {
|
|
1118
|
+
hideCursor: true,
|
|
1119
|
+
});
|
|
1197
1120
|
}
|
|
1198
1121
|
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
// Best-effort write: streams may already be destroyed during shutdown.
|
|
1204
|
-
private writeBestEffort(stream: OutputStream, data: string): void {
|
|
1205
|
-
try {
|
|
1206
|
-
stream.write(data);
|
|
1207
|
-
} catch {}
|
|
1122
|
+
function shouldSync(): boolean {
|
|
1123
|
+
// `interactive` already folds in CI detection and the caller's override.
|
|
1124
|
+
return Boolean(options.stdout.isTTY) && interactive;
|
|
1208
1125
|
}
|
|
1209
1126
|
|
|
1210
1127
|
// Waits for the exit promise to settle, suppressing any rejection.
|
|
1211
1128
|
// Errors are surfaced via waitUntilExit() instead.
|
|
1212
|
-
|
|
1129
|
+
async function awaitExit(): Promise<void> {
|
|
1213
1130
|
try {
|
|
1214
|
-
await
|
|
1131
|
+
await exitPromise;
|
|
1215
1132
|
} catch {}
|
|
1216
1133
|
}
|
|
1217
1134
|
|
|
1218
|
-
|
|
1135
|
+
function hasPendingConcurrentWork(): boolean {
|
|
1219
1136
|
// oxlint-disable-next-line typescript/no-unsafe-type-assertion
|
|
1220
|
-
const concurrentContainer =
|
|
1137
|
+
const concurrentContainer = container as {
|
|
1221
1138
|
pendingLanes?: number;
|
|
1222
1139
|
callbackNode?: unknown;
|
|
1223
1140
|
};
|
|
@@ -1228,25 +1145,25 @@ export class Ink {
|
|
|
1228
1145
|
);
|
|
1229
1146
|
}
|
|
1230
1147
|
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
const promise = new Promise<void>((resolve) => {
|
|
1235
|
-
resolveRender = resolve;
|
|
1236
|
-
});
|
|
1237
|
-
this.nextRenderCommit = { promise, resolve: resolveRender };
|
|
1238
|
-
}
|
|
1239
|
-
|
|
1240
|
-
return this.nextRenderCommit.promise;
|
|
1148
|
+
async function awaitNextRender(): Promise<void> {
|
|
1149
|
+
nextRenderCommit ??= Promise.withResolvers<void>();
|
|
1150
|
+
return nextRenderCommit.promise;
|
|
1241
1151
|
}
|
|
1242
1152
|
|
|
1243
|
-
|
|
1153
|
+
function renderInteractiveFrame(
|
|
1154
|
+
output: string,
|
|
1155
|
+
outputHeight: number,
|
|
1156
|
+
staticOutput: string,
|
|
1157
|
+
screen: Screen | undefined,
|
|
1158
|
+
hasOverflow: boolean,
|
|
1159
|
+
): void {
|
|
1160
|
+
if (!screen) return;
|
|
1244
1161
|
const hasStaticOutput = staticOutput !== "";
|
|
1245
|
-
const isTTY = Boolean(
|
|
1162
|
+
const isTTY = Boolean(options.stdout.isTTY);
|
|
1246
1163
|
|
|
1247
1164
|
// Detect fullscreen: output fills or exceeds terminal height.
|
|
1248
1165
|
// Only apply when writing to a real TTY — piped output always gets trailing newlines.
|
|
1249
|
-
const viewportRows = isTTY ? getWindowSize(
|
|
1166
|
+
const viewportRows = isTTY ? getWindowSize(options.stdout).rows : 24;
|
|
1250
1167
|
|
|
1251
1168
|
// Clamp the frame to the viewport, keeping its bottom rows. Rows above
|
|
1252
1169
|
// the top margin cannot be updated or erased in place, and the
|
|
@@ -1258,6 +1175,7 @@ export class Ink {
|
|
|
1258
1175
|
const lines = output.split("\n");
|
|
1259
1176
|
output = lines.slice(lines.length - viewportRows).join("\n");
|
|
1260
1177
|
outputHeight = viewportRows;
|
|
1178
|
+
screen = bottomRows(screen, viewportRows);
|
|
1261
1179
|
}
|
|
1262
1180
|
|
|
1263
1181
|
const isFullscreen = isTTY && outputHeight >= viewportRows;
|
|
@@ -1266,61 +1184,60 @@ export class Ink {
|
|
|
1266
1184
|
const shouldClearTerminal = shouldClearTerminalForFrame({
|
|
1267
1185
|
isTTY,
|
|
1268
1186
|
viewportRows,
|
|
1269
|
-
previousOutputHeight:
|
|
1187
|
+
previousOutputHeight: lastOutputHeight,
|
|
1270
1188
|
nextOutputHeight: outputHeight,
|
|
1271
|
-
isUnmounting
|
|
1189
|
+
isUnmounting,
|
|
1272
1190
|
});
|
|
1273
1191
|
|
|
1274
1192
|
if (shouldClearTerminal) {
|
|
1275
|
-
const sync =
|
|
1193
|
+
const sync = shouldSync();
|
|
1276
1194
|
if (sync) {
|
|
1277
|
-
|
|
1195
|
+
options.stdout.write(bsu);
|
|
1278
1196
|
}
|
|
1279
1197
|
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1198
|
+
options.stdout.write(ansiEscapes.clearTerminal + fullStaticOutput + outputToRender);
|
|
1199
|
+
lastOutput = output;
|
|
1200
|
+
lastOutputToRender = outputToRender;
|
|
1201
|
+
lastOutputHeight = outputHeight;
|
|
1202
|
+
lastScreen = screen;
|
|
1203
|
+
terminal.resetFrame();
|
|
1285
1204
|
|
|
1286
1205
|
if (sync) {
|
|
1287
|
-
|
|
1206
|
+
options.stdout.write(esu);
|
|
1288
1207
|
}
|
|
1289
1208
|
|
|
1290
1209
|
return;
|
|
1291
1210
|
}
|
|
1292
1211
|
|
|
1293
|
-
|
|
1212
|
+
const willPresent = terminal.willPresent(screen, {
|
|
1213
|
+
fullscreen: isFullscreen,
|
|
1214
|
+
forceRewrite: hasStaticOutput || hasOverflow,
|
|
1215
|
+
});
|
|
1216
|
+
const sync = shouldSync() && willPresent;
|
|
1217
|
+
if (sync) terminal.write(bsu);
|
|
1294
1218
|
if (hasStaticOutput) {
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
this.options.stdout.write(bsu);
|
|
1298
|
-
}
|
|
1299
|
-
|
|
1300
|
-
this.log.clear();
|
|
1301
|
-
this.options.stdout.write(staticOutput);
|
|
1302
|
-
this.log(outputToRender);
|
|
1303
|
-
|
|
1304
|
-
if (sync) {
|
|
1305
|
-
this.options.stdout.write(esu);
|
|
1306
|
-
}
|
|
1307
|
-
} else if (output !== this.lastOutput || this.log.isCursorDirty()) {
|
|
1308
|
-
// ThrottledLog manages its own bsu/esu at actual write time
|
|
1309
|
-
this.throttledLog(outputToRender);
|
|
1219
|
+
terminal.clearFrame();
|
|
1220
|
+
terminal.write(staticOutput);
|
|
1310
1221
|
}
|
|
1222
|
+
terminal.present(screen, {
|
|
1223
|
+
fullscreen: isFullscreen,
|
|
1224
|
+
forceRewrite: hasStaticOutput || hasOverflow,
|
|
1225
|
+
});
|
|
1226
|
+
if (sync) terminal.write(esu);
|
|
1311
1227
|
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1228
|
+
lastOutput = output;
|
|
1229
|
+
lastOutputToRender = outputToRender;
|
|
1230
|
+
lastOutputHeight = outputHeight;
|
|
1231
|
+
lastScreen = screen;
|
|
1315
1232
|
}
|
|
1316
1233
|
|
|
1317
|
-
|
|
1234
|
+
function initKittyKeyboard(): void {
|
|
1318
1235
|
// Protocol is opt-in: if kittyKeyboard is not specified, do nothing
|
|
1319
|
-
if (!
|
|
1236
|
+
if (!options.kittyKeyboard) {
|
|
1320
1237
|
return;
|
|
1321
1238
|
}
|
|
1322
1239
|
|
|
1323
|
-
const opts =
|
|
1240
|
+
const opts = options.kittyKeyboard;
|
|
1324
1241
|
const mode = opts.mode ?? "auto";
|
|
1325
1242
|
|
|
1326
1243
|
if (mode === "disabled") {
|
|
@@ -1332,160 +1249,108 @@ export class Ink {
|
|
|
1332
1249
|
// 'enabled' force-enables the protocol as long as both streams are TTYs,
|
|
1333
1250
|
// regardless of the interactive setting (e.g. even in CI).
|
|
1334
1251
|
if (mode === "enabled") {
|
|
1335
|
-
if (isTty(
|
|
1336
|
-
|
|
1252
|
+
if (isTty(options.stdin) && options.stdout.isTTY) {
|
|
1253
|
+
enableKittyProtocol(flags);
|
|
1337
1254
|
}
|
|
1338
1255
|
|
|
1339
1256
|
return;
|
|
1340
1257
|
}
|
|
1341
1258
|
|
|
1342
1259
|
// Auto mode: require interactive + TTY
|
|
1343
|
-
if (!
|
|
1260
|
+
if (!interactive || !isTty(options.stdin) || !options.stdout.isTTY) {
|
|
1344
1261
|
return;
|
|
1345
1262
|
}
|
|
1346
1263
|
|
|
1347
1264
|
// Auto mode: query the terminal for kitty keyboard protocol support.
|
|
1348
|
-
// The CSI ? u query is safe to send to any terminal — unsupporting
|
|
1349
|
-
// terminals simply won't respond, and the 200ms timeout handles that.
|
|
1350
1265
|
// This avoids maintaining a hardcoded whitelist of terminal names.
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
const { stdin, stdout } = this.options;
|
|
1356
|
-
|
|
1357
|
-
let responseBuffer: number[] = [];
|
|
1358
|
-
|
|
1359
|
-
const cleanup = (): void => {
|
|
1360
|
-
this.cancelKittyDetection = undefined;
|
|
1361
|
-
clearTimeout(timer);
|
|
1362
|
-
stdin.removeListener("data", onData);
|
|
1363
|
-
|
|
1364
|
-
// Re-emit any buffered data that wasn't the protocol response,
|
|
1365
|
-
// so it isn't lost from Ink's normal input pipeline.
|
|
1366
|
-
// Clear responseBuffer afterwards to make cleanup idempotent.
|
|
1367
|
-
const remaining = stripKittyQueryResponsesAndTrailingPartial(responseBuffer);
|
|
1368
|
-
responseBuffer = [];
|
|
1369
|
-
if (remaining.length > 0) {
|
|
1370
|
-
stdin.unshift(Uint8Array.from(remaining));
|
|
1371
|
-
}
|
|
1372
|
-
};
|
|
1373
|
-
|
|
1374
|
-
const onData = (data: Uint8Array | string): void => {
|
|
1375
|
-
const chunk = typeof data === "string" ? textEncoder.encode(data) : data;
|
|
1376
|
-
for (const byte of chunk) {
|
|
1377
|
-
responseBuffer.push(byte);
|
|
1266
|
+
cancelKittyDetection = detectKittySupport(options.stdin, options.stdout, () => {
|
|
1267
|
+
cancelKittyDetection = undefined;
|
|
1268
|
+
if (!isUnmounted) {
|
|
1269
|
+
enableKittyProtocol(flags);
|
|
1378
1270
|
}
|
|
1379
|
-
|
|
1380
|
-
if (hasCompleteKittyQueryResponse(responseBuffer)) {
|
|
1381
|
-
cleanup();
|
|
1382
|
-
if (!this.isUnmounted) {
|
|
1383
|
-
this.enableKittyProtocol(flags);
|
|
1384
|
-
}
|
|
1385
|
-
}
|
|
1386
|
-
};
|
|
1387
|
-
|
|
1388
|
-
// Attach listener before writing the query so that synchronous
|
|
1389
|
-
// or immediate responses are not missed.
|
|
1390
|
-
stdin.on("data", onData);
|
|
1391
|
-
const timer = setTimeout(cleanup, 200);
|
|
1392
|
-
this.cancelKittyDetection = cleanup;
|
|
1393
|
-
|
|
1394
|
-
stdout.write(ansiEscapes.kittyQuery);
|
|
1271
|
+
});
|
|
1395
1272
|
}
|
|
1396
1273
|
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1274
|
+
function enableKittyProtocol(flags: KittyFlagName[]): void {
|
|
1275
|
+
options.stdout.write(ansiEscapes.pushKittyKeyboard(resolveFlags(flags)));
|
|
1276
|
+
kittyProtocolEnabled = true;
|
|
1400
1277
|
// Remember the flags so suspendTerminal() can re-enable the same protocol
|
|
1401
1278
|
// after a child process has had the terminal.
|
|
1402
|
-
|
|
1279
|
+
kittyFlags = flags;
|
|
1403
1280
|
}
|
|
1404
1281
|
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
throw new Error(
|
|
1408
|
-
"The terminal is already suspended. Resume the current suspension before suspending again.",
|
|
1409
|
-
);
|
|
1410
|
-
}
|
|
1411
|
-
|
|
1412
|
-
this.isSuspended = true;
|
|
1282
|
+
function beginSuspend(): void {
|
|
1283
|
+
terminal.beginSuspension();
|
|
1413
1284
|
|
|
1414
|
-
if (!
|
|
1285
|
+
if (!interactive || isUnmounted || isUnmounting) {
|
|
1415
1286
|
return;
|
|
1416
1287
|
}
|
|
1417
1288
|
|
|
1418
1289
|
try {
|
|
1419
|
-
const {
|
|
1420
|
-
const { canWriteToStdout } = getWritableStreamState(stdout);
|
|
1290
|
+
const { canWriteToStdout } = getWritableStreamState(options.stdout);
|
|
1421
1291
|
|
|
1422
|
-
// Flush any pending render
|
|
1423
|
-
settleThrottle(
|
|
1424
|
-
settleThrottle(this.throttledLog, canWriteToStdout);
|
|
1292
|
+
// Flush any pending render so the child starts from a settled screen.
|
|
1293
|
+
settleThrottle(renderScheduler.throttled, canWriteToStdout);
|
|
1425
1294
|
|
|
1426
1295
|
if (canWriteToStdout) {
|
|
1427
|
-
|
|
1296
|
+
flushCapturedStdio();
|
|
1428
1297
|
}
|
|
1429
1298
|
|
|
1430
1299
|
if (canWriteToStdout) {
|
|
1431
1300
|
// Erase Ink's current frame, then show the cursor and re-arm the hide.
|
|
1432
1301
|
// The forced redraw on resume hides the cursor again.
|
|
1433
|
-
|
|
1434
|
-
|
|
1302
|
+
clearLiveOutput();
|
|
1303
|
+
finishLiveOutput();
|
|
1435
1304
|
|
|
1436
|
-
if (
|
|
1437
|
-
|
|
1305
|
+
if (kittyProtocolEnabled) {
|
|
1306
|
+
writeBestEffort(options.stdout, ansiEscapes.popKittyKeyboard);
|
|
1438
1307
|
}
|
|
1439
1308
|
|
|
1440
|
-
if (
|
|
1441
|
-
|
|
1309
|
+
if (terminal.alternateScreen) {
|
|
1310
|
+
writeBestEffort(options.stdout, ansiEscapes.exitAlternativeScreen);
|
|
1442
1311
|
}
|
|
1443
1312
|
}
|
|
1444
1313
|
|
|
1445
1314
|
// Hand input back to the terminal (raw mode off, bracketed paste off).
|
|
1446
|
-
|
|
1315
|
+
pauseInput?.();
|
|
1447
1316
|
} catch (error) {
|
|
1448
1317
|
// If handing over the terminal fails partway, don't strand the app in a
|
|
1449
1318
|
// suspended state with no way back. Best-effort reclaim input, clear the
|
|
1450
1319
|
// flag, and rethrow so the caller sees the failure.
|
|
1451
|
-
|
|
1320
|
+
terminal.resume();
|
|
1452
1321
|
|
|
1453
1322
|
try {
|
|
1454
|
-
|
|
1323
|
+
resumeInput?.();
|
|
1455
1324
|
} catch {}
|
|
1456
1325
|
|
|
1457
1326
|
throw error;
|
|
1458
1327
|
}
|
|
1459
1328
|
}
|
|
1460
1329
|
|
|
1461
|
-
|
|
1462
|
-
if (!
|
|
1330
|
+
async function endSuspend(): Promise<void> {
|
|
1331
|
+
if (!terminal.suspended) {
|
|
1463
1332
|
return;
|
|
1464
1333
|
}
|
|
1465
1334
|
|
|
1466
|
-
|
|
1335
|
+
terminal.resume();
|
|
1467
1336
|
|
|
1468
1337
|
// Reclaim input even mid-unmount: pauseInput already ran in beginSuspend, so
|
|
1469
1338
|
// restoring it is symmetric regardless of any state change during suspension.
|
|
1470
|
-
|
|
1339
|
+
resumeInput?.();
|
|
1471
1340
|
|
|
1472
|
-
if (!
|
|
1341
|
+
if (!interactive || isUnmounted || isUnmounting) {
|
|
1473
1342
|
return;
|
|
1474
1343
|
}
|
|
1475
1344
|
|
|
1476
|
-
const {
|
|
1477
|
-
const { canWriteToStdout } = getWritableStreamState(stdout);
|
|
1345
|
+
const { canWriteToStdout } = getWritableStreamState(options.stdout);
|
|
1478
1346
|
|
|
1479
1347
|
if (canWriteToStdout) {
|
|
1480
|
-
if (
|
|
1481
|
-
|
|
1348
|
+
if (terminal.alternateScreen) {
|
|
1349
|
+
writeBestEffort(options.stdout, ansiEscapes.enterAlternativeScreen);
|
|
1482
1350
|
}
|
|
1483
1351
|
|
|
1484
|
-
if (
|
|
1485
|
-
|
|
1486
|
-
this.options.stdout,
|
|
1487
|
-
ansiEscapes.pushKittyKeyboard(resolveFlags(this.kittyFlags)),
|
|
1488
|
-
);
|
|
1352
|
+
if (kittyProtocolEnabled && kittyFlags) {
|
|
1353
|
+
writeBestEffort(options.stdout, ansiEscapes.pushKittyKeyboard(resolveFlags(kittyFlags)));
|
|
1489
1354
|
}
|
|
1490
1355
|
}
|
|
1491
1356
|
|
|
@@ -1493,15 +1358,25 @@ export class Ink {
|
|
|
1493
1358
|
// frame, which the child process may have overwritten. A redraw failure here
|
|
1494
1359
|
// is best-effort: it must not mask a callback error propagating through the
|
|
1495
1360
|
// caller's finally block.
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1361
|
+
lastOutput = "";
|
|
1362
|
+
lastOutputToRender = "";
|
|
1363
|
+
lastOutputHeight = 0;
|
|
1364
|
+
resetLiveOutput();
|
|
1500
1365
|
|
|
1501
1366
|
try {
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
await
|
|
1367
|
+
calculateLayout();
|
|
1368
|
+
onRender();
|
|
1369
|
+
await waitUntilRenderFlush();
|
|
1505
1370
|
} catch {}
|
|
1506
1371
|
}
|
|
1507
|
-
|
|
1372
|
+
|
|
1373
|
+
return {
|
|
1374
|
+
render,
|
|
1375
|
+
unmount,
|
|
1376
|
+
waitUntilExit,
|
|
1377
|
+
waitUntilRenderFlush,
|
|
1378
|
+
clear,
|
|
1379
|
+
copyToClipboard: (text, selection) => terminal.copyToClipboard(text, selection),
|
|
1380
|
+
setProgress: (state, value) => terminal.setProgress(state, value),
|
|
1381
|
+
};
|
|
1382
|
+
};
|