@alchemy.run/sigil 0.0.0-alpha.1 → 0.0.0-alpha.11
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 +499 -313
- package/THIRD_PARTY_NOTICES.md +98 -23
- package/dist/Text-DV9CuzAT.d.ts +452 -0
- package/dist/ansi.d.ts +217 -0
- package/dist/ansi.js +87 -0
- 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-BAC9-TZX.js +592 -0
- package/dist/color-policy-CCxuHIdD.d.ts +22 -0
- package/dist/color-profile-CyeHnG1T.d.ts +97 -0
- package/dist/color-profile-DHhQHY55.js +36 -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-B3dL4Q11.js +374 -0
- package/dist/detect-Db6GbKOm.d.ts +195 -0
- package/dist/{devtools-QpCMm9JH.mjs → devtools-DbthxoD1.js} +23 -24
- 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-D48vQhhe.d.ts +21 -0
- package/dist/index-DDVME65c.d.ts +919 -0
- package/dist/index.d.ts +1310 -0
- package/dist/index.js +3211 -0
- package/dist/jsx-dev-runtime.d.ts +2 -0
- package/dist/jsx-dev-runtime.js +227 -0
- package/dist/jsx-runtime-XtwIVFsg.js +255 -0
- package/dist/jsx-runtime.d.ts +2 -0
- package/dist/jsx-runtime.js +8 -0
- package/dist/osc-BFKKSqpg.js +71 -0
- package/dist/osc-Cn0fw77g.d.ts +23 -0
- package/dist/paint-Cx-zC_sX.d.ts +81 -0
- package/dist/query-BNc2B8GD.d.ts +152 -0
- package/dist/react-CTZ_8dwh.js +1197 -0
- package/dist/react.d.ts +2 -0
- package/dist/react.js +49 -0
- package/dist/router.d.ts +392 -0
- package/dist/router.js +711 -0
- package/dist/sample-Cqw1bjUL.js +445 -0
- package/dist/screen-CgC2WlVM.d.ts +49 -0
- package/dist/screen-CiPytswf.js +342 -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-DDQ5V300.js +723 -0
- package/dist/sgr-BhwaWAJB.js +246 -0
- package/dist/store-C1P5fOUi.d.ts +72 -0
- package/dist/string-width-CijQwpIk.js +69 -0
- package/dist/strip-BvU4toXG.js +6 -0
- package/dist/terminal.d.ts +121 -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-B5npZrJU.js +18736 -0
- package/dist/yoga-5jKhYCJC.js +3465 -0
- package/dist/yoga.d.ts +2 -0
- package/dist/yoga.js +2 -0
- package/package.json +89 -29
- package/src/ansi/chalk.ts +138 -0
- package/src/ansi/cursor.ts +46 -0
- package/src/ansi/east-asian-width.ts +259 -0
- package/src/ansi/escapes.ts +120 -0
- package/src/ansi/graphemes.ts +8 -0
- package/src/ansi/hyperlink.ts +44 -0
- package/src/ansi/index.ts +27 -0
- package/src/ansi/osc.ts +77 -0
- package/src/ansi/sgr.ts +234 -0
- package/src/ansi/slice.ts +43 -0
- package/src/ansi/string-width.ts +121 -0
- package/src/ansi/strip.ts +13 -0
- package/src/ansi/tokenize.ts +380 -0
- package/src/ansi/truncate.ts +196 -0
- package/src/ansi/wrap.ts +765 -0
- package/src/ansi-tokenizer.ts +510 -0
- package/src/capabilities/color-policy.ts +34 -0
- package/src/capabilities/detect.ts +608 -0
- package/src/capabilities/index.ts +37 -0
- package/src/capabilities/query.ts +679 -0
- package/src/capabilities/store.ts +394 -0
- package/src/code-excerpt.ts +39 -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/AccessibilityContext.ts +5 -0
- package/src/components/AnimationContext.ts +24 -0
- package/src/components/AnsiText.tsx +42 -0
- package/src/components/App.tsx +878 -0
- package/src/components/AppContext.ts +111 -0
- package/src/components/BackgroundContext.ts +7 -0
- package/src/components/Box.tsx +100 -0
- package/src/components/CursorContext.ts +19 -0
- package/src/components/ErrorBoundary.tsx +39 -0
- package/src/components/ErrorOverview.tsx +135 -0
- package/src/components/FocusContext.ts +30 -0
- package/src/components/Hyperlink.tsx +56 -0
- package/src/components/Newline.tsx +16 -0
- package/src/components/Spacer.tsx +11 -0
- package/src/components/Static.tsx +60 -0
- package/src/components/StderrContext.ts +24 -0
- package/src/components/StdinContext.ts +48 -0
- package/src/components/StdoutContext.ts +26 -0
- package/src/components/TerminalOscContext.ts +25 -0
- package/src/components/Text.tsx +122 -0
- package/src/components/Transform.tsx +38 -0
- package/src/components/VirtualList.tsx +128 -0
- package/src/cursor-position.ts +103 -0
- package/src/devtools.ts +103 -0
- package/src/dom.ts +301 -0
- package/src/env.ts +12 -0
- package/src/get-max-width.ts +11 -0
- package/src/global.d.ts +38 -0
- package/src/glyphs.ts +99 -0
- package/src/hooks/use-animation.ts +142 -0
- package/src/hooks/use-app.ts +8 -0
- package/src/hooks/use-box-metrics.ts +134 -0
- package/src/hooks/use-capabilities.ts +73 -0
- package/src/hooks/use-cursor.ts +33 -0
- package/src/hooks/use-focus-manager.ts +62 -0
- package/src/hooks/use-focus.ts +82 -0
- package/src/hooks/use-input.ts +267 -0
- package/src/hooks/use-is-screen-reader-enabled.ts +12 -0
- package/src/hooks/use-paste.ts +78 -0
- package/src/hooks/use-stderr.ts +8 -0
- package/src/hooks/use-stdin.ts +10 -0
- package/src/hooks/use-stdout.ts +8 -0
- package/src/hooks/use-terminal-osc.ts +59 -0
- package/src/hooks/use-virtual-scroll.ts +84 -0
- package/src/hooks/use-window-size.ts +37 -0
- package/src/index.ts +96 -0
- package/src/ink.tsx +1452 -0
- package/src/input-parser.ts +303 -0
- package/src/instances.ts +9 -0
- package/src/jsx-dev-runtime.ts +3 -0
- package/src/jsx-runtime.ts +6 -0
- package/src/kitty-keyboard.ts +185 -0
- package/src/measure-element.ts +62 -0
- package/src/measure-text.ts +31 -0
- package/src/paint-tree.ts +220 -0
- package/src/parse-keypress.ts +515 -0
- package/src/parse-stack-line.ts +138 -0
- package/src/patch-console.ts +106 -0
- package/src/quick-lru.ts +85 -0
- package/src/react.ts +53 -0
- package/src/reconciler.ts +476 -0
- package/src/render-background.ts +59 -0
- package/src/render-border.ts +167 -0
- package/src/render-frame.ts +83 -0
- package/src/render-to-string.ts +146 -0
- package/src/render.ts +284 -0
- 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 +33 -0
- package/src/screen/ansi.ts +184 -0
- package/src/screen/canvas.ts +160 -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 +305 -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 +106 -0
- package/src/squash-text-nodes.ts +37 -0
- package/src/stream.ts +28 -0
- package/src/structured-text.ts +325 -0
- package/src/styles.ts +753 -0
- package/src/terminal/index.ts +2 -0
- package/src/terminal/inline-presenter.ts +120 -0
- package/src/terminal/input.ts +92 -0
- package/src/terminal/render-scheduler.ts +37 -0
- package/src/terminal/screen-presenter.ts +242 -0
- package/src/terminal/session.ts +408 -0
- package/src/terminal-size.ts +58 -0
- 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 +361 -0
- package/src/testing/vitest.ts +157 -0
- package/src/throttle.ts +73 -0
- package/src/transform-adapter.ts +14 -0
- package/src/types.ts +10 -0
- package/src/virtual-scroll.ts +133 -0
- package/src/wrap-text.ts +54 -0
- package/src/yoga/config.ts +57 -0
- package/src/yoga/core/absoluteLayout.ts +626 -0
- package/src/yoga/core/baseline.ts +66 -0
- package/src/yoga/core/cache.ts +136 -0
- package/src/yoga/core/calculateLayout.ts +2926 -0
- package/src/yoga/core/config.ts +104 -0
- package/src/yoga/core/flexLine.ts +177 -0
- package/src/yoga/core/helpers.ts +293 -0
- package/src/yoga/core/layoutResults.ts +167 -0
- package/src/yoga/core/node.ts +611 -0
- package/src/yoga/core/numeric.ts +44 -0
- package/src/yoga/core/pixelGrid.ts +151 -0
- package/src/yoga/core/style.ts +887 -0
- package/src/yoga/core/types.ts +224 -0
- package/src/yoga/generated/YGEnums.ts +263 -0
- package/src/yoga/index.ts +19 -0
- package/src/yoga/node.ts +1140 -0
- package/dist/index.d.mts +0 -2379
- package/dist/index.mjs +0 -10072
package/dist/index.js
ADDED
|
@@ -0,0 +1,3211 @@
|
|
|
1
|
+
import { r as __toESM, t as require_react } from "./react-CTZ_8dwh.js";
|
|
2
|
+
import { _ as squashTextNodes, a as reconciler, b as require_react_reconciler, c as kittyModifiers, d as FocusContext, f as Text, g as transformAnsiLine, h as emitLayoutListeners, i as useStdinContext, l as resolveFlags, m as createNode, n as useInput, o as detectKittySupport, p as addLayoutListener, r as useStdin, s as kittyFlags, t as useFocus, u as StdinContext, v as accessibilityContext, y as require_constants } from "./use-focus-B5npZrJU.js";
|
|
3
|
+
import { C as bsu, S as ansiEscapes, V as esu, W as link, _ as CSI } from "./sgr-BhwaWAJB.js";
|
|
4
|
+
import { h as tokenizeAnsi, p as graphemes } from "./tokenize-AjqbvtiT.js";
|
|
5
|
+
import { t as stringWidth } from "./string-width-CijQwpIk.js";
|
|
6
|
+
import { n as sliceAnsi, r as wrapAnsi } from "./truncate-D31fhU6i.js";
|
|
7
|
+
import { a as isTty, i as isSigilDev, o as isWindows, r as isScreenReader, t as isInCi } from "./env-YVw64yZS.js";
|
|
8
|
+
import { a as detectTerminal, i as detectHyperlinkSupport, n as detectCapabilities, o as detectUnicodeSupport, r as detectColorLevel, s as signalExit, t as createSupportsColor } from "./detect-B3dL4Q11.js";
|
|
9
|
+
import { u as cliCursor } from "./osc-BFKKSqpg.js";
|
|
10
|
+
import { a as registerTerminalIntegration, c as ensureTerminalQuery, f as queryTerminal, i as getCapabilities, l as getTerminalQuery, o as getRawModeStream, p as refreshTerminalQuery, r as capabilities, s as applyTerminalQuery, u as getTerminalQueryPromise } from "./color-policy-BAC9-TZX.js";
|
|
11
|
+
import { a as buildCursorSuffix, c as hideCursorEscape, i as buildCursorOnlySequence, o as buildReturnToBottomPrefix, s as cursorPositionChanged, t as TerminalSession } from "./session-DDQ5V300.js";
|
|
12
|
+
import { t as Yoga } from "./yoga-5jKhYCJC.js";
|
|
13
|
+
import { t as require_jsx_runtime } from "./jsx-runtime-XtwIVFsg.js";
|
|
14
|
+
import { r as createCell, t as cellAttributes } from "./cell-_ZVhbfl0.js";
|
|
15
|
+
import { i as semanticTextStyleToCellStyle, n as mergeSemanticTextStyles, t as emptySemanticTextStyle } from "./semantic-text-style-DIMzC7xt.js";
|
|
16
|
+
import { a as samplePaint } from "./sample-Cqw1bjUL.js";
|
|
17
|
+
import { n as cellFromStyledChar, r as cellsFromAnsi, t as Screen } from "./screen-CiPytswf.js";
|
|
18
|
+
import { t as colorProfileFromLevel } from "./color-profile-DHhQHY55.js";
|
|
19
|
+
import { n as serializeScreen, t as serializeLine } from "./serialize-BTkAZgw1.js";
|
|
20
|
+
import { t as stripAnsi } from "./strip-BvU4toXG.js";
|
|
21
|
+
import "./color-CkbalRqK.js";
|
|
22
|
+
import { PassThrough, Stream } from "node:stream";
|
|
23
|
+
import { setImmediate as setImmediate$1 } from "node:timers/promises";
|
|
24
|
+
import { isNativeError } from "node:util/types";
|
|
25
|
+
import { EventEmitter } from "node:events";
|
|
26
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
27
|
+
import { cwd } from "node:process";
|
|
28
|
+
import { Console } from "node:console";
|
|
29
|
+
require_react_reconciler();
|
|
30
|
+
var import_constants = require_constants();
|
|
31
|
+
var import_react = /* @__PURE__ */ __toESM(require_react(), 1);
|
|
32
|
+
const animationContext = (0, import_react.createContext)({
|
|
33
|
+
renderThrottleMs: 0,
|
|
34
|
+
subscribe() {
|
|
35
|
+
return {
|
|
36
|
+
startTime: 0,
|
|
37
|
+
unsubscribe() {}
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
animationContext.displayName = "InternalAnimationContext";
|
|
42
|
+
//#endregion
|
|
43
|
+
//#region src/components/AppContext.ts
|
|
44
|
+
/**
|
|
45
|
+
`AppContext` is a React context that exposes lifecycle methods for the app.
|
|
46
|
+
*/
|
|
47
|
+
const noopSuspension = {
|
|
48
|
+
async resume() {},
|
|
49
|
+
async [Symbol.asyncDispose]() {}
|
|
50
|
+
};
|
|
51
|
+
const AppContext = (0, import_react.createContext)({
|
|
52
|
+
exit(_errorOrResult) {},
|
|
53
|
+
async waitUntilRenderFlush() {},
|
|
54
|
+
suspendTerminal: (async (callback) => {
|
|
55
|
+
if (callback) {
|
|
56
|
+
await callback();
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
return noopSuspension;
|
|
60
|
+
})
|
|
61
|
+
});
|
|
62
|
+
AppContext.displayName = "InternalAppContext";
|
|
63
|
+
//#endregion
|
|
64
|
+
//#region src/components/CursorContext.ts
|
|
65
|
+
const CursorContext = (0, import_react.createContext)({ setCursorPosition() {} });
|
|
66
|
+
CursorContext.displayName = "InternalCursorContext";
|
|
67
|
+
//#endregion
|
|
68
|
+
//#region src/code-excerpt.ts
|
|
69
|
+
const tabsToSpaces = (input, spaces = 2) => input.replaceAll(/^\t+/gm, (tabs) => " ".repeat(tabs.length * spaces));
|
|
70
|
+
const generateLineNumbers = (line, around) => {
|
|
71
|
+
const lineNumbers = [];
|
|
72
|
+
for (let lineNumber = line - around; lineNumber <= line + around; lineNumber++) lineNumbers.push(lineNumber);
|
|
73
|
+
return lineNumbers;
|
|
74
|
+
};
|
|
75
|
+
const codeExcerpt = (source, line, options = {}) => {
|
|
76
|
+
if (!line || line < 1) throw new TypeError("Line number must start from `1`.");
|
|
77
|
+
const lines = tabsToSpaces(source).split(/\r?\n/);
|
|
78
|
+
if (line > lines.length) return;
|
|
79
|
+
return generateLineNumbers(line, options.around ?? 3).filter((lineNumber) => lines[lineNumber - 1] !== void 0).map((lineNumber) => ({
|
|
80
|
+
line: lineNumber,
|
|
81
|
+
value: lines[lineNumber - 1]
|
|
82
|
+
}));
|
|
83
|
+
};
|
|
84
|
+
//#endregion
|
|
85
|
+
//#region src/glyphs.ts
|
|
86
|
+
const BOXES = {
|
|
87
|
+
single: {
|
|
88
|
+
topLeft: "┌",
|
|
89
|
+
top: "─",
|
|
90
|
+
topRight: "┐",
|
|
91
|
+
right: "│",
|
|
92
|
+
bottomRight: "┘",
|
|
93
|
+
bottom: "─",
|
|
94
|
+
bottomLeft: "└",
|
|
95
|
+
left: "│"
|
|
96
|
+
},
|
|
97
|
+
double: {
|
|
98
|
+
topLeft: "╔",
|
|
99
|
+
top: "═",
|
|
100
|
+
topRight: "╗",
|
|
101
|
+
right: "║",
|
|
102
|
+
bottomRight: "╝",
|
|
103
|
+
bottom: "═",
|
|
104
|
+
bottomLeft: "╚",
|
|
105
|
+
left: "║"
|
|
106
|
+
},
|
|
107
|
+
round: {
|
|
108
|
+
topLeft: "╭",
|
|
109
|
+
top: "─",
|
|
110
|
+
topRight: "╮",
|
|
111
|
+
right: "│",
|
|
112
|
+
bottomRight: "╯",
|
|
113
|
+
bottom: "─",
|
|
114
|
+
bottomLeft: "╰",
|
|
115
|
+
left: "│"
|
|
116
|
+
},
|
|
117
|
+
bold: {
|
|
118
|
+
topLeft: "┏",
|
|
119
|
+
top: "━",
|
|
120
|
+
topRight: "┓",
|
|
121
|
+
right: "┃",
|
|
122
|
+
bottomRight: "┛",
|
|
123
|
+
bottom: "━",
|
|
124
|
+
bottomLeft: "┗",
|
|
125
|
+
left: "┃"
|
|
126
|
+
},
|
|
127
|
+
singleDouble: {
|
|
128
|
+
topLeft: "╓",
|
|
129
|
+
top: "─",
|
|
130
|
+
topRight: "╖",
|
|
131
|
+
right: "║",
|
|
132
|
+
bottomRight: "╜",
|
|
133
|
+
bottom: "─",
|
|
134
|
+
bottomLeft: "╙",
|
|
135
|
+
left: "║"
|
|
136
|
+
},
|
|
137
|
+
doubleSingle: {
|
|
138
|
+
topLeft: "╒",
|
|
139
|
+
top: "═",
|
|
140
|
+
topRight: "╕",
|
|
141
|
+
right: "│",
|
|
142
|
+
bottomRight: "╛",
|
|
143
|
+
bottom: "═",
|
|
144
|
+
bottomLeft: "╘",
|
|
145
|
+
left: "│"
|
|
146
|
+
},
|
|
147
|
+
classic: {
|
|
148
|
+
topLeft: "+",
|
|
149
|
+
top: "-",
|
|
150
|
+
topRight: "+",
|
|
151
|
+
right: "|",
|
|
152
|
+
bottomRight: "+",
|
|
153
|
+
bottom: "-",
|
|
154
|
+
bottomLeft: "+",
|
|
155
|
+
left: "|"
|
|
156
|
+
},
|
|
157
|
+
arrow: {
|
|
158
|
+
topLeft: "↘",
|
|
159
|
+
top: "↓",
|
|
160
|
+
topRight: "↙",
|
|
161
|
+
right: "←",
|
|
162
|
+
bottomRight: "↖",
|
|
163
|
+
bottom: "↑",
|
|
164
|
+
bottomLeft: "↗",
|
|
165
|
+
left: "→"
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
//#endregion
|
|
169
|
+
//#region src/components/Box.tsx
|
|
170
|
+
/** @jsxImportSource react */
|
|
171
|
+
var import_jsx_runtime = require_jsx_runtime();
|
|
172
|
+
/**
|
|
173
|
+
`<Box>` is an essential Ink component to build your layout. It's like `<div style="display: flex">` in the browser.
|
|
174
|
+
*/
|
|
175
|
+
function Box({ children, ref, backgroundColor, "aria-label": ariaLabel, "aria-hidden": ariaHidden, "aria-role": role, "aria-state": ariaState, ...style }) {
|
|
176
|
+
const { isScreenReaderEnabled } = (0, import_react.useContext)(accessibilityContext);
|
|
177
|
+
const label = ariaLabel ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ink-text", { children: ariaLabel }) : void 0;
|
|
178
|
+
if (isScreenReaderEnabled && ariaHidden) return null;
|
|
179
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ink-box", {
|
|
180
|
+
ref,
|
|
181
|
+
style: {
|
|
182
|
+
flexWrap: "nowrap",
|
|
183
|
+
flexDirection: "row",
|
|
184
|
+
flexGrow: 0,
|
|
185
|
+
flexShrink: 1,
|
|
186
|
+
...style,
|
|
187
|
+
backgroundColor,
|
|
188
|
+
overflowX: style.overflowX ?? style.overflow ?? "visible",
|
|
189
|
+
overflowY: style.overflowY ?? style.overflow ?? "visible"
|
|
190
|
+
},
|
|
191
|
+
internal_accessibility: {
|
|
192
|
+
role,
|
|
193
|
+
state: ariaState
|
|
194
|
+
},
|
|
195
|
+
children: isScreenReaderEnabled && label ? label : children
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
//#endregion
|
|
199
|
+
//#region src/parse-stack-line.ts
|
|
200
|
+
const lineRegExp = new RegExp("^" + String.raw`(?:\s*at )?` + "(?:(new) )?" + String.raw`(?:(.*?) \()?` + String.raw`(?:eval at ([^ ]+) \((.+?):(\d+):(\d+)\), )?` + String.raw`(?:(.+?):(\d+):(\d+)|(native))` + String.raw`(\)?)$`);
|
|
201
|
+
const methodRegExp = /^(.*?) \[as (.*?)]$/;
|
|
202
|
+
const cwd$1 = process.cwd().replaceAll("\\", "/");
|
|
203
|
+
const setFile = (result, filename) => {
|
|
204
|
+
if (filename) {
|
|
205
|
+
filename = filename.replaceAll("\\", "/");
|
|
206
|
+
if (filename.startsWith(`${cwd$1}/`)) filename = filename.slice(cwd$1.length + 1);
|
|
207
|
+
result.file = filename;
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
const parseStackLine = (line) => {
|
|
211
|
+
const match = lineRegExp.exec(line);
|
|
212
|
+
if (!match) return;
|
|
213
|
+
let functionName = match[2];
|
|
214
|
+
const evalOrigin = match[3];
|
|
215
|
+
const evalFile = match[4];
|
|
216
|
+
const evalLine = Number(match[5]);
|
|
217
|
+
const evalColumn = Number(match[6]);
|
|
218
|
+
let file = match[7];
|
|
219
|
+
const lineNumber = match[8];
|
|
220
|
+
const columnNumber = match[9];
|
|
221
|
+
const native = match[10] === "native";
|
|
222
|
+
const closeParen = match[11] === ")";
|
|
223
|
+
let method;
|
|
224
|
+
const result = {};
|
|
225
|
+
if (lineNumber) result.line = Number(lineNumber);
|
|
226
|
+
if (columnNumber) result.column = Number(columnNumber);
|
|
227
|
+
if (closeParen && file) {
|
|
228
|
+
let closes = 0;
|
|
229
|
+
for (let index = file.length - 1; index > 0; index--) if (file.charAt(index) === ")") closes++;
|
|
230
|
+
else if (file.charAt(index) === "(" && file.charAt(index - 1) === " ") {
|
|
231
|
+
closes--;
|
|
232
|
+
if (closes === -1 && file.charAt(index - 1) === " ") {
|
|
233
|
+
const before = file.slice(0, index - 1);
|
|
234
|
+
file = file.slice(index + 1);
|
|
235
|
+
functionName += ` (${before}`;
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (functionName) {
|
|
241
|
+
const methodMatch = methodRegExp.exec(functionName);
|
|
242
|
+
if (methodMatch) {
|
|
243
|
+
functionName = methodMatch[1];
|
|
244
|
+
method = methodMatch[2];
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (file) setFile(result, file);
|
|
248
|
+
if (evalOrigin) {
|
|
249
|
+
result.evalOrigin = evalOrigin;
|
|
250
|
+
result.evalLine = evalLine;
|
|
251
|
+
result.evalColumn = evalColumn;
|
|
252
|
+
result.evalFile = evalFile?.replaceAll("\\", "/");
|
|
253
|
+
}
|
|
254
|
+
if (native) result.native = true;
|
|
255
|
+
if (functionName) result.function = functionName;
|
|
256
|
+
if (method && functionName !== method) result.method = method;
|
|
257
|
+
return result;
|
|
258
|
+
};
|
|
259
|
+
//#endregion
|
|
260
|
+
//#region src/components/ErrorOverview.tsx
|
|
261
|
+
/** @jsxImportSource react */
|
|
262
|
+
const cleanupPath = (path) => {
|
|
263
|
+
return path?.replace(`file://${cwd()}/`, "");
|
|
264
|
+
};
|
|
265
|
+
function ErrorOverview({ error }) {
|
|
266
|
+
const stack = error.stack ? error.stack.split("\n").slice(1) : void 0;
|
|
267
|
+
const origin = stack ? parseStackLine(stack[0]) : void 0;
|
|
268
|
+
const filePath = cleanupPath(origin?.file);
|
|
269
|
+
let excerpt;
|
|
270
|
+
let lineWidth = 0;
|
|
271
|
+
const stackLineCounts = /* @__PURE__ */ new Map();
|
|
272
|
+
if (filePath && origin?.line && existsSync(filePath)) {
|
|
273
|
+
const sourceCode = readFileSync(filePath, "utf8");
|
|
274
|
+
excerpt = codeExcerpt(sourceCode, origin.line);
|
|
275
|
+
if (excerpt) for (const { line } of excerpt) lineWidth = Math.max(lineWidth, String(line).length);
|
|
276
|
+
}
|
|
277
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
|
|
278
|
+
flexDirection: "column",
|
|
279
|
+
padding: 1,
|
|
280
|
+
children: [
|
|
281
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
|
|
282
|
+
backgroundColor: "red",
|
|
283
|
+
color: "white",
|
|
284
|
+
children: [
|
|
285
|
+
" ",
|
|
286
|
+
"ERROR",
|
|
287
|
+
" "
|
|
288
|
+
]
|
|
289
|
+
}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, { children: [" ", error.message] })] }),
|
|
290
|
+
origin && filePath ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
|
|
291
|
+
marginTop: 1,
|
|
292
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
|
|
293
|
+
dimColor: true,
|
|
294
|
+
children: [
|
|
295
|
+
filePath,
|
|
296
|
+
":",
|
|
297
|
+
origin.line,
|
|
298
|
+
":",
|
|
299
|
+
origin.column
|
|
300
|
+
]
|
|
301
|
+
})
|
|
302
|
+
}) : null,
|
|
303
|
+
origin && excerpt ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
|
|
304
|
+
marginTop: 1,
|
|
305
|
+
flexDirection: "column",
|
|
306
|
+
children: excerpt.map(({ line, value }) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
|
|
307
|
+
width: lineWidth + 1,
|
|
308
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
|
|
309
|
+
dimColor: line !== origin.line,
|
|
310
|
+
backgroundColor: line === origin.line ? "red" : void 0,
|
|
311
|
+
color: line === origin.line ? "white" : void 0,
|
|
312
|
+
"aria-label": line === origin.line ? `Line ${line}, error` : `Line ${line}`,
|
|
313
|
+
children: [String(line).padStart(lineWidth, " "), ":"]
|
|
314
|
+
})
|
|
315
|
+
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
|
|
316
|
+
backgroundColor: line === origin.line ? "red" : void 0,
|
|
317
|
+
color: line === origin.line ? "white" : void 0,
|
|
318
|
+
children: " " + value
|
|
319
|
+
}, line)] }, line))
|
|
320
|
+
}) : null,
|
|
321
|
+
error.stack ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
|
|
322
|
+
marginTop: 1,
|
|
323
|
+
flexDirection: "column",
|
|
324
|
+
children: error.stack.split("\n").slice(1).map((line) => {
|
|
325
|
+
const parsedLine = parseStackLine(line);
|
|
326
|
+
const lineCount = stackLineCounts.get(line) ?? 0;
|
|
327
|
+
stackLineCounts.set(line, lineCount + 1);
|
|
328
|
+
const key = `${line}-${lineCount}`;
|
|
329
|
+
if (!parsedLine?.file || !parsedLine.line || !parsedLine.column) return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
|
|
330
|
+
dimColor: true,
|
|
331
|
+
children: "- "
|
|
332
|
+
}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
|
|
333
|
+
dimColor: true,
|
|
334
|
+
bold: true,
|
|
335
|
+
children: [
|
|
336
|
+
line,
|
|
337
|
+
"\\t",
|
|
338
|
+
" "
|
|
339
|
+
]
|
|
340
|
+
})] }, key);
|
|
341
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, { children: [
|
|
342
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
|
|
343
|
+
dimColor: true,
|
|
344
|
+
children: "- "
|
|
345
|
+
}),
|
|
346
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
|
|
347
|
+
dimColor: true,
|
|
348
|
+
bold: true,
|
|
349
|
+
children: parsedLine.function
|
|
350
|
+
}),
|
|
351
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
|
|
352
|
+
dimColor: true,
|
|
353
|
+
color: "gray",
|
|
354
|
+
"aria-label": `at ${cleanupPath(parsedLine.file) ?? ""} line ${parsedLine.line} column ${parsedLine.column}`,
|
|
355
|
+
children: [
|
|
356
|
+
" ",
|
|
357
|
+
"(",
|
|
358
|
+
cleanupPath(parsedLine.file) ?? "",
|
|
359
|
+
":",
|
|
360
|
+
parsedLine.line,
|
|
361
|
+
":",
|
|
362
|
+
parsedLine.column,
|
|
363
|
+
")"
|
|
364
|
+
]
|
|
365
|
+
})
|
|
366
|
+
] }, key);
|
|
367
|
+
})
|
|
368
|
+
}) : null
|
|
369
|
+
]
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
//#endregion
|
|
373
|
+
//#region src/components/ErrorBoundary.tsx
|
|
374
|
+
/** @jsxImportSource react */
|
|
375
|
+
var ErrorBoundary = class extends import_react.PureComponent {
|
|
376
|
+
static displayName = "InternalErrorBoundary";
|
|
377
|
+
static getDerivedStateFromError(error) {
|
|
378
|
+
return { error };
|
|
379
|
+
}
|
|
380
|
+
state = { error: void 0 };
|
|
381
|
+
componentDidCatch(error) {
|
|
382
|
+
this.props.onError(error);
|
|
383
|
+
}
|
|
384
|
+
render() {
|
|
385
|
+
if (this.state.error) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ErrorOverview, { error: this.state.error });
|
|
386
|
+
return this.props.children;
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
//#endregion
|
|
390
|
+
//#region src/components/StderrContext.ts
|
|
391
|
+
/**
|
|
392
|
+
`StderrContext` is a React context that exposes the stderr stream.
|
|
393
|
+
*/
|
|
394
|
+
const StderrContext = (0, import_react.createContext)({
|
|
395
|
+
stderr: process.stderr,
|
|
396
|
+
write() {}
|
|
397
|
+
});
|
|
398
|
+
StderrContext.displayName = "InternalStderrContext";
|
|
399
|
+
//#endregion
|
|
400
|
+
//#region src/components/StdoutContext.ts
|
|
401
|
+
/**
|
|
402
|
+
`StdoutContext` is a React context that exposes the stdout stream where Ink renders your app.
|
|
403
|
+
*/
|
|
404
|
+
const StdoutContext = (0, import_react.createContext)({
|
|
405
|
+
stdout: process.stdout,
|
|
406
|
+
write() {}
|
|
407
|
+
});
|
|
408
|
+
StdoutContext.displayName = "InternalStdoutContext";
|
|
409
|
+
//#endregion
|
|
410
|
+
//#region src/components/App.tsx
|
|
411
|
+
/** @jsxImportSource react */
|
|
412
|
+
const tab = " ";
|
|
413
|
+
const shiftTab = `${CSI}Z`;
|
|
414
|
+
const escape = "\x1B";
|
|
415
|
+
function App({ children, stdin, stdout, stderr, writeToStdout, writeToStderr, exitOnCtrlC, onExit, onWaitUntilRenderFlush, onSuspendTerminal, onRegisterInputControl, setCursorPosition, interactive, renderThrottleMs, terminalInput }) {
|
|
416
|
+
const [isFocusEnabled, setIsFocusEnabled] = (0, import_react.useState)(true);
|
|
417
|
+
const [activeFocusId, setActiveFocusId] = (0, import_react.useState)(void 0);
|
|
418
|
+
const [, setFocusables] = (0, import_react.useState)([]);
|
|
419
|
+
const focusablesCountRef = (0, import_react.useRef)(0);
|
|
420
|
+
const animationSubscribersRef = (0, import_react.useRef)(/* @__PURE__ */ new Map());
|
|
421
|
+
const animationTimerRef = (0, import_react.useRef)(void 0);
|
|
422
|
+
const rawModeEnabledCount = (0, import_react.useRef)(0);
|
|
423
|
+
const pendingDisableRawModeRef = (0, import_react.useRef)(false);
|
|
424
|
+
const bracketedPasteModeEnabledCount = (0, import_react.useRef)(0);
|
|
425
|
+
const internal_eventEmitter = (0, import_react.useRef)(new EventEmitter());
|
|
426
|
+
internal_eventEmitter.current.setMaxListeners(Infinity);
|
|
427
|
+
const readableListenerRef = (0, import_react.useRef)(void 0);
|
|
428
|
+
const isMountedRef = (0, import_react.useRef)(true);
|
|
429
|
+
const pendingInputFlushRef = (0, import_react.useRef)(void 0);
|
|
430
|
+
const pendingInputFlushDelayMilliseconds = 20;
|
|
431
|
+
const clearPendingInputFlush = (0, import_react.useCallback)(() => {
|
|
432
|
+
if (!pendingInputFlushRef.current) return;
|
|
433
|
+
clearTimeout(pendingInputFlushRef.current);
|
|
434
|
+
pendingInputFlushRef.current = void 0;
|
|
435
|
+
}, []);
|
|
436
|
+
const clearAnimationTimer = (0, import_react.useCallback)(() => {
|
|
437
|
+
if (!animationTimerRef.current) return;
|
|
438
|
+
clearTimeout(animationTimerRef.current);
|
|
439
|
+
animationTimerRef.current = void 0;
|
|
440
|
+
}, []);
|
|
441
|
+
const scheduleAnimationTick = (0, import_react.useCallback)(() => {
|
|
442
|
+
clearAnimationTimer();
|
|
443
|
+
if (animationSubscribersRef.current.size === 0) return;
|
|
444
|
+
let nextDueTime = Number.POSITIVE_INFINITY;
|
|
445
|
+
for (const subscriber of animationSubscribersRef.current.values()) nextDueTime = Math.min(nextDueTime, subscriber.nextDueTime);
|
|
446
|
+
const delay = Math.max(0, nextDueTime - performance.now());
|
|
447
|
+
animationTimerRef.current = setTimeout(() => {
|
|
448
|
+
animationTimerRef.current = void 0;
|
|
449
|
+
const currentTime = performance.now();
|
|
450
|
+
for (const subscriber of animationSubscribersRef.current.values()) {
|
|
451
|
+
if (currentTime < subscriber.nextDueTime) continue;
|
|
452
|
+
subscriber.callback(currentTime);
|
|
453
|
+
const elapsedTime = currentTime - subscriber.startTime;
|
|
454
|
+
const elapsedFrames = Math.floor(elapsedTime / subscriber.interval) + 1;
|
|
455
|
+
subscriber.nextDueTime = subscriber.startTime + elapsedFrames * subscriber.interval;
|
|
456
|
+
}
|
|
457
|
+
scheduleAnimationTick();
|
|
458
|
+
}, delay);
|
|
459
|
+
}, [clearAnimationTimer]);
|
|
460
|
+
const animationSubscribe = (0, import_react.useCallback)((callback, interval) => {
|
|
461
|
+
const startTime = performance.now();
|
|
462
|
+
animationSubscribersRef.current.set(callback, {
|
|
463
|
+
callback,
|
|
464
|
+
interval,
|
|
465
|
+
startTime,
|
|
466
|
+
nextDueTime: startTime + interval
|
|
467
|
+
});
|
|
468
|
+
scheduleAnimationTick();
|
|
469
|
+
return {
|
|
470
|
+
startTime,
|
|
471
|
+
unsubscribe() {
|
|
472
|
+
animationSubscribersRef.current.delete(callback);
|
|
473
|
+
if (animationSubscribersRef.current.size === 0) {
|
|
474
|
+
clearAnimationTimer();
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
scheduleAnimationTick();
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
}, [clearAnimationTimer, scheduleAnimationTick]);
|
|
481
|
+
(0, import_react.useEffect)(() => {
|
|
482
|
+
return () => {
|
|
483
|
+
clearAnimationTimer();
|
|
484
|
+
};
|
|
485
|
+
}, [clearAnimationTimer]);
|
|
486
|
+
const rawModeStdin = getRawModeStream(stdin);
|
|
487
|
+
const isRawModeSupported = rawModeStdin !== void 0;
|
|
488
|
+
const detachReadableListener = (0, import_react.useCallback)(() => {
|
|
489
|
+
if (!readableListenerRef.current) return;
|
|
490
|
+
stdin.removeListener("readable", readableListenerRef.current);
|
|
491
|
+
readableListenerRef.current = void 0;
|
|
492
|
+
}, [stdin]);
|
|
493
|
+
const clearInputState = (0, import_react.useCallback)(() => {
|
|
494
|
+
terminalInput.reset();
|
|
495
|
+
clearPendingInputFlush();
|
|
496
|
+
detachReadableListener();
|
|
497
|
+
}, [
|
|
498
|
+
clearPendingInputFlush,
|
|
499
|
+
detachReadableListener,
|
|
500
|
+
terminalInput
|
|
501
|
+
]);
|
|
502
|
+
const disableRawMode = (0, import_react.useCallback)(() => {
|
|
503
|
+
if (!rawModeStdin) return;
|
|
504
|
+
pendingDisableRawModeRef.current = false;
|
|
505
|
+
rawModeStdin.setRawMode(false);
|
|
506
|
+
rawModeStdin.unref?.();
|
|
507
|
+
rawModeEnabledCount.current = 0;
|
|
508
|
+
clearInputState();
|
|
509
|
+
}, [rawModeStdin, clearInputState]);
|
|
510
|
+
const handleExit = (0, import_react.useCallback)((errorOrResult) => {
|
|
511
|
+
if (isRawModeSupported && (rawModeEnabledCount.current > 0 || pendingDisableRawModeRef.current)) disableRawMode();
|
|
512
|
+
onExit(errorOrResult);
|
|
513
|
+
}, [
|
|
514
|
+
isRawModeSupported,
|
|
515
|
+
disableRawMode,
|
|
516
|
+
onExit
|
|
517
|
+
]);
|
|
518
|
+
const handleInput = (0, import_react.useCallback)((input) => {
|
|
519
|
+
if (input === "" && exitOnCtrlC) {
|
|
520
|
+
handleExit();
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
if (input === escape && isFocusEnabled) setActiveFocusId(void 0);
|
|
524
|
+
}, [
|
|
525
|
+
exitOnCtrlC,
|
|
526
|
+
handleExit,
|
|
527
|
+
isFocusEnabled
|
|
528
|
+
]);
|
|
529
|
+
const emitInput = (0, import_react.useCallback)((input) => {
|
|
530
|
+
handleInput(input);
|
|
531
|
+
internal_eventEmitter.current.emit("input", input);
|
|
532
|
+
}, [handleInput]);
|
|
533
|
+
const schedulePendingInputFlush = (0, import_react.useCallback)(() => {
|
|
534
|
+
clearPendingInputFlush();
|
|
535
|
+
pendingInputFlushRef.current = setTimeout(() => {
|
|
536
|
+
pendingInputFlushRef.current = void 0;
|
|
537
|
+
const pendingEscape = terminalInput.flushPendingEscape();
|
|
538
|
+
if (!pendingEscape) return;
|
|
539
|
+
emitInput(pendingEscape);
|
|
540
|
+
}, pendingInputFlushDelayMilliseconds);
|
|
541
|
+
}, [
|
|
542
|
+
clearPendingInputFlush,
|
|
543
|
+
emitInput,
|
|
544
|
+
terminalInput
|
|
545
|
+
]);
|
|
546
|
+
const handleReadable = (0, import_react.useCallback)(() => {
|
|
547
|
+
clearPendingInputFlush();
|
|
548
|
+
let chunk;
|
|
549
|
+
while ((chunk = stdin.read()) !== null) {
|
|
550
|
+
const inputEvents = terminalInput.push(chunk);
|
|
551
|
+
for (const event of inputEvents) if (typeof event === "string") emitInput(event);
|
|
552
|
+
else {
|
|
553
|
+
if (internal_eventEmitter.current.listenerCount("paste") === 0) {
|
|
554
|
+
emitInput(event.paste);
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
internal_eventEmitter.current.emit("paste", event.paste);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
if (terminalInput.hasPendingEscape()) schedulePendingInputFlush();
|
|
561
|
+
}, [
|
|
562
|
+
stdin,
|
|
563
|
+
emitInput,
|
|
564
|
+
clearPendingInputFlush,
|
|
565
|
+
schedulePendingInputFlush,
|
|
566
|
+
terminalInput
|
|
567
|
+
]);
|
|
568
|
+
const attachReadableListener = (0, import_react.useCallback)(() => {
|
|
569
|
+
if (readableListenerRef.current) return;
|
|
570
|
+
readableListenerRef.current = handleReadable;
|
|
571
|
+
stdin.addListener("readable", handleReadable);
|
|
572
|
+
}, [stdin, handleReadable]);
|
|
573
|
+
const handleSetRawMode = (0, import_react.useCallback)((isEnabled) => {
|
|
574
|
+
if (!rawModeStdin) {
|
|
575
|
+
if (stdin === process.stdin) throw new Error("Raw mode is not supported on the current process.stdin, which Ink uses as input stream by default.\nRead about how to prevent this error on https://github.com/vadimdemedes/ink/#israwmodesupported");
|
|
576
|
+
else throw new Error("Raw mode is not supported on the stdin provided to Ink.\nRead about how to prevent this error on https://github.com/vadimdemedes/ink/#israwmodesupported");
|
|
577
|
+
}
|
|
578
|
+
rawModeStdin.setEncoding("utf8");
|
|
579
|
+
if (isEnabled) {
|
|
580
|
+
if (rawModeEnabledCount.current === 0) {
|
|
581
|
+
const isRawModeAlreadyEnabled = pendingDisableRawModeRef.current;
|
|
582
|
+
pendingDisableRawModeRef.current = false;
|
|
583
|
+
if (!isRawModeAlreadyEnabled) {
|
|
584
|
+
rawModeStdin.ref?.();
|
|
585
|
+
rawModeStdin.setRawMode(true);
|
|
586
|
+
}
|
|
587
|
+
attachReadableListener();
|
|
588
|
+
}
|
|
589
|
+
rawModeEnabledCount.current++;
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
if (rawModeEnabledCount.current === 0) return;
|
|
593
|
+
if (--rawModeEnabledCount.current === 0) {
|
|
594
|
+
clearInputState();
|
|
595
|
+
pendingDisableRawModeRef.current = true;
|
|
596
|
+
queueMicrotask(() => {
|
|
597
|
+
if (!pendingDisableRawModeRef.current) return;
|
|
598
|
+
disableRawMode();
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
}, [
|
|
602
|
+
rawModeStdin,
|
|
603
|
+
stdin,
|
|
604
|
+
attachReadableListener,
|
|
605
|
+
clearInputState,
|
|
606
|
+
disableRawMode
|
|
607
|
+
]);
|
|
608
|
+
const handleQueryTerminal = (0, import_react.useCallback)(async ({ refresh = false } = {}) => {
|
|
609
|
+
if (!interactive || !isRawModeSupported || !stdout.isTTY) return;
|
|
610
|
+
if (!refresh) {
|
|
611
|
+
const existing = getTerminalQueryPromise(stdout);
|
|
612
|
+
if (existing) return existing;
|
|
613
|
+
}
|
|
614
|
+
handleSetRawMode(true);
|
|
615
|
+
detachReadableListener();
|
|
616
|
+
try {
|
|
617
|
+
return await (refresh ? refreshTerminalQuery(stdin, stdout) : ensureTerminalQuery(stdin, stdout));
|
|
618
|
+
} finally {
|
|
619
|
+
if (isMountedRef.current) attachReadableListener();
|
|
620
|
+
handleSetRawMode(false);
|
|
621
|
+
}
|
|
622
|
+
}, [
|
|
623
|
+
interactive,
|
|
624
|
+
isRawModeSupported,
|
|
625
|
+
stdin,
|
|
626
|
+
stdout,
|
|
627
|
+
handleSetRawMode,
|
|
628
|
+
detachReadableListener,
|
|
629
|
+
attachReadableListener
|
|
630
|
+
]);
|
|
631
|
+
const reportFeedActiveRef = (0, import_react.useRef)(false);
|
|
632
|
+
const handleSetReportFeed = (0, import_react.useCallback)((active) => {
|
|
633
|
+
if (active === reportFeedActiveRef.current || !isRawModeSupported) return;
|
|
634
|
+
reportFeedActiveRef.current = active;
|
|
635
|
+
handleSetRawMode(active);
|
|
636
|
+
}, [isRawModeSupported, handleSetRawMode]);
|
|
637
|
+
(0, import_react.useInsertionEffect)(() => {
|
|
638
|
+
const unregister = registerTerminalIntegration(stdout, {
|
|
639
|
+
runQuery: handleQueryTerminal,
|
|
640
|
+
setReportFeed: handleSetReportFeed
|
|
641
|
+
});
|
|
642
|
+
return () => {
|
|
643
|
+
unregister();
|
|
644
|
+
handleSetReportFeed(false);
|
|
645
|
+
};
|
|
646
|
+
}, [
|
|
647
|
+
stdout,
|
|
648
|
+
handleQueryTerminal,
|
|
649
|
+
handleSetReportFeed
|
|
650
|
+
]);
|
|
651
|
+
const handleSetBracketedPasteMode = (0, import_react.useCallback)((isEnabled) => {
|
|
652
|
+
if (!stdout.isTTY) return;
|
|
653
|
+
if (isEnabled) {
|
|
654
|
+
if (bracketedPasteModeEnabledCount.current === 0) stdout.write(ansiEscapes.enableBracketedPaste);
|
|
655
|
+
bracketedPasteModeEnabledCount.current++;
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
if (bracketedPasteModeEnabledCount.current === 0) return;
|
|
659
|
+
if (--bracketedPasteModeEnabledCount.current === 0) stdout.write(ansiEscapes.disableBracketedPaste);
|
|
660
|
+
}, [stdout]);
|
|
661
|
+
const suspendedInputStateRef = (0, import_react.useRef)({
|
|
662
|
+
rawMode: false,
|
|
663
|
+
bracketedPaste: false
|
|
664
|
+
});
|
|
665
|
+
const pauseInput = (0, import_react.useCallback)(() => {
|
|
666
|
+
const wasRawMode = isRawModeSupported && rawModeEnabledCount.current > 0;
|
|
667
|
+
const wasBracketedPaste = bracketedPasteModeEnabledCount.current > 0;
|
|
668
|
+
suspendedInputStateRef.current = {
|
|
669
|
+
rawMode: wasRawMode,
|
|
670
|
+
bracketedPaste: wasBracketedPaste
|
|
671
|
+
};
|
|
672
|
+
if (wasBracketedPaste && stdout.isTTY) try {
|
|
673
|
+
stdout.write(ansiEscapes.disableBracketedPaste);
|
|
674
|
+
} catch {}
|
|
675
|
+
if (wasRawMode) {
|
|
676
|
+
rawModeStdin?.setRawMode(false);
|
|
677
|
+
rawModeStdin?.unref?.();
|
|
678
|
+
clearInputState();
|
|
679
|
+
}
|
|
680
|
+
}, [
|
|
681
|
+
isRawModeSupported,
|
|
682
|
+
rawModeStdin,
|
|
683
|
+
stdout,
|
|
684
|
+
clearInputState
|
|
685
|
+
]);
|
|
686
|
+
const resumeInput = (0, import_react.useCallback)(() => {
|
|
687
|
+
const { rawMode, bracketedPaste } = suspendedInputStateRef.current;
|
|
688
|
+
if (rawMode) {
|
|
689
|
+
rawModeStdin?.setEncoding("utf8");
|
|
690
|
+
rawModeStdin?.ref?.();
|
|
691
|
+
rawModeStdin?.setRawMode(true);
|
|
692
|
+
attachReadableListener();
|
|
693
|
+
}
|
|
694
|
+
if (bracketedPaste && stdout.isTTY) try {
|
|
695
|
+
stdout.write(ansiEscapes.enableBracketedPaste);
|
|
696
|
+
} catch {}
|
|
697
|
+
}, [
|
|
698
|
+
rawModeStdin,
|
|
699
|
+
stdout,
|
|
700
|
+
attachReadableListener
|
|
701
|
+
]);
|
|
702
|
+
(0, import_react.useInsertionEffect)(() => {
|
|
703
|
+
onRegisterInputControl(pauseInput, resumeInput);
|
|
704
|
+
}, [
|
|
705
|
+
onRegisterInputControl,
|
|
706
|
+
pauseInput,
|
|
707
|
+
resumeInput
|
|
708
|
+
]);
|
|
709
|
+
const findNextFocusable = (0, import_react.useCallback)((currentFocusables, currentActiveFocusId) => {
|
|
710
|
+
const activeIndex = currentFocusables.findIndex((focusable) => {
|
|
711
|
+
return focusable.id === currentActiveFocusId;
|
|
712
|
+
});
|
|
713
|
+
for (let index = activeIndex + 1; index < currentFocusables.length; index++) {
|
|
714
|
+
const focusable = currentFocusables[index];
|
|
715
|
+
if (focusable?.isActive) return focusable.id;
|
|
716
|
+
}
|
|
717
|
+
}, []);
|
|
718
|
+
const findPreviousFocusable = (0, import_react.useCallback)((currentFocusables, currentActiveFocusId) => {
|
|
719
|
+
const activeIndex = currentFocusables.findIndex((focusable) => {
|
|
720
|
+
return focusable.id === currentActiveFocusId;
|
|
721
|
+
});
|
|
722
|
+
for (let index = activeIndex - 1; index >= 0; index--) {
|
|
723
|
+
const focusable = currentFocusables[index];
|
|
724
|
+
if (focusable?.isActive) return focusable.id;
|
|
725
|
+
}
|
|
726
|
+
}, []);
|
|
727
|
+
const focusNext = (0, import_react.useCallback)(() => {
|
|
728
|
+
setFocusables((currentFocusables) => {
|
|
729
|
+
setActiveFocusId((currentActiveFocusId) => {
|
|
730
|
+
const firstFocusableId = currentFocusables.find((focusable) => focusable.isActive)?.id;
|
|
731
|
+
return findNextFocusable(currentFocusables, currentActiveFocusId) ?? firstFocusableId;
|
|
732
|
+
});
|
|
733
|
+
return currentFocusables;
|
|
734
|
+
});
|
|
735
|
+
}, [findNextFocusable]);
|
|
736
|
+
const focusPrevious = (0, import_react.useCallback)(() => {
|
|
737
|
+
setFocusables((currentFocusables) => {
|
|
738
|
+
setActiveFocusId((currentActiveFocusId) => {
|
|
739
|
+
const lastFocusableId = currentFocusables.findLast((focusable) => focusable.isActive)?.id;
|
|
740
|
+
return findPreviousFocusable(currentFocusables, currentActiveFocusId) ?? lastFocusableId;
|
|
741
|
+
});
|
|
742
|
+
return currentFocusables;
|
|
743
|
+
});
|
|
744
|
+
}, [findPreviousFocusable]);
|
|
745
|
+
(0, import_react.useEffect)(() => {
|
|
746
|
+
const handleTabNavigation = (input) => {
|
|
747
|
+
if (!isFocusEnabled || focusablesCountRef.current === 0) return;
|
|
748
|
+
if (input === tab) focusNext();
|
|
749
|
+
if (input === shiftTab) focusPrevious();
|
|
750
|
+
};
|
|
751
|
+
internal_eventEmitter.current.on("input", handleTabNavigation);
|
|
752
|
+
const emitter = internal_eventEmitter.current;
|
|
753
|
+
return () => {
|
|
754
|
+
emitter.off("input", handleTabNavigation);
|
|
755
|
+
};
|
|
756
|
+
}, [
|
|
757
|
+
isFocusEnabled,
|
|
758
|
+
focusNext,
|
|
759
|
+
focusPrevious
|
|
760
|
+
]);
|
|
761
|
+
const enableFocus = (0, import_react.useCallback)(() => {
|
|
762
|
+
setIsFocusEnabled(true);
|
|
763
|
+
}, []);
|
|
764
|
+
const disableFocus = (0, import_react.useCallback)(() => {
|
|
765
|
+
setIsFocusEnabled(false);
|
|
766
|
+
}, []);
|
|
767
|
+
const focus = (0, import_react.useCallback)((id) => {
|
|
768
|
+
setFocusables((currentFocusables) => {
|
|
769
|
+
if (currentFocusables.some((focusable) => focusable?.id === id)) setActiveFocusId(id);
|
|
770
|
+
return currentFocusables;
|
|
771
|
+
});
|
|
772
|
+
}, []);
|
|
773
|
+
const addFocusable = (0, import_react.useCallback)((id, { autoFocus }) => {
|
|
774
|
+
setFocusables((currentFocusables) => {
|
|
775
|
+
focusablesCountRef.current = currentFocusables.length + 1;
|
|
776
|
+
return [...currentFocusables, {
|
|
777
|
+
id,
|
|
778
|
+
isActive: true
|
|
779
|
+
}];
|
|
780
|
+
});
|
|
781
|
+
if (autoFocus) setActiveFocusId((currentActiveFocusId) => {
|
|
782
|
+
if (!currentActiveFocusId) return id;
|
|
783
|
+
return currentActiveFocusId;
|
|
784
|
+
});
|
|
785
|
+
}, []);
|
|
786
|
+
const removeFocusable = (0, import_react.useCallback)((id) => {
|
|
787
|
+
setActiveFocusId((currentActiveFocusId) => {
|
|
788
|
+
if (currentActiveFocusId === id) return;
|
|
789
|
+
return currentActiveFocusId;
|
|
790
|
+
});
|
|
791
|
+
setFocusables((currentFocusables) => {
|
|
792
|
+
const filtered = currentFocusables.filter((focusable) => {
|
|
793
|
+
return focusable.id !== id;
|
|
794
|
+
});
|
|
795
|
+
focusablesCountRef.current = filtered.length;
|
|
796
|
+
return filtered;
|
|
797
|
+
});
|
|
798
|
+
}, []);
|
|
799
|
+
const activateFocusable = (0, import_react.useCallback)((id) => {
|
|
800
|
+
setFocusables((currentFocusables) => currentFocusables.map((focusable) => {
|
|
801
|
+
if (focusable.id !== id) return focusable;
|
|
802
|
+
return {
|
|
803
|
+
id,
|
|
804
|
+
isActive: true
|
|
805
|
+
};
|
|
806
|
+
}));
|
|
807
|
+
}, []);
|
|
808
|
+
const deactivateFocusable = (0, import_react.useCallback)((id) => {
|
|
809
|
+
setActiveFocusId((currentActiveFocusId) => {
|
|
810
|
+
if (currentActiveFocusId === id) return;
|
|
811
|
+
return currentActiveFocusId;
|
|
812
|
+
});
|
|
813
|
+
setFocusables((currentFocusables) => currentFocusables.map((focusable) => {
|
|
814
|
+
if (focusable.id !== id) return focusable;
|
|
815
|
+
return {
|
|
816
|
+
id,
|
|
817
|
+
isActive: false
|
|
818
|
+
};
|
|
819
|
+
}));
|
|
820
|
+
}, []);
|
|
821
|
+
(0, import_react.useEffect)(() => {
|
|
822
|
+
isMountedRef.current = true;
|
|
823
|
+
return () => {
|
|
824
|
+
isMountedRef.current = false;
|
|
825
|
+
const canWriteToStdout = !stdout.destroyed && !stdout.writableEnded;
|
|
826
|
+
if (interactive && canWriteToStdout) cliCursor.show(stdout);
|
|
827
|
+
if (isRawModeSupported && (rawModeEnabledCount.current > 0 || pendingDisableRawModeRef.current)) disableRawMode();
|
|
828
|
+
if (bracketedPasteModeEnabledCount.current > 0) {
|
|
829
|
+
if (stdout.isTTY && canWriteToStdout) stdout.write(ansiEscapes.disableBracketedPaste);
|
|
830
|
+
bracketedPasteModeEnabledCount.current = 0;
|
|
831
|
+
}
|
|
832
|
+
};
|
|
833
|
+
}, [
|
|
834
|
+
stdout,
|
|
835
|
+
isRawModeSupported,
|
|
836
|
+
disableRawMode,
|
|
837
|
+
interactive
|
|
838
|
+
]);
|
|
839
|
+
const appContextValue = (0, import_react.useMemo)(() => ({
|
|
840
|
+
exit: handleExit,
|
|
841
|
+
waitUntilRenderFlush: onWaitUntilRenderFlush,
|
|
842
|
+
suspendTerminal: onSuspendTerminal
|
|
843
|
+
}), [
|
|
844
|
+
handleExit,
|
|
845
|
+
onWaitUntilRenderFlush,
|
|
846
|
+
onSuspendTerminal
|
|
847
|
+
]);
|
|
848
|
+
const stdinContextValue = (0, import_react.useMemo)(() => ({
|
|
849
|
+
stdin,
|
|
850
|
+
setRawMode: handleSetRawMode,
|
|
851
|
+
setBracketedPasteMode: handleSetBracketedPasteMode,
|
|
852
|
+
isRawModeSupported,
|
|
853
|
+
internal_exitOnCtrlC: exitOnCtrlC,
|
|
854
|
+
internal_eventEmitter: internal_eventEmitter.current
|
|
855
|
+
}), [
|
|
856
|
+
stdin,
|
|
857
|
+
handleSetRawMode,
|
|
858
|
+
handleSetBracketedPasteMode,
|
|
859
|
+
isRawModeSupported,
|
|
860
|
+
exitOnCtrlC
|
|
861
|
+
]);
|
|
862
|
+
const stdoutContextValue = (0, import_react.useMemo)(() => ({
|
|
863
|
+
stdout,
|
|
864
|
+
write: writeToStdout
|
|
865
|
+
}), [stdout, writeToStdout]);
|
|
866
|
+
const stderrContextValue = (0, import_react.useMemo)(() => ({
|
|
867
|
+
stderr,
|
|
868
|
+
write: writeToStderr
|
|
869
|
+
}), [stderr, writeToStderr]);
|
|
870
|
+
const cursorContextValue = (0, import_react.useMemo)(() => ({ setCursorPosition }), [setCursorPosition]);
|
|
871
|
+
const focusContextValue = (0, import_react.useMemo)(() => ({
|
|
872
|
+
activeId: activeFocusId,
|
|
873
|
+
add: addFocusable,
|
|
874
|
+
remove: removeFocusable,
|
|
875
|
+
activate: activateFocusable,
|
|
876
|
+
deactivate: deactivateFocusable,
|
|
877
|
+
enableFocus,
|
|
878
|
+
disableFocus,
|
|
879
|
+
focusNext,
|
|
880
|
+
focusPrevious,
|
|
881
|
+
focus
|
|
882
|
+
}), [
|
|
883
|
+
activeFocusId,
|
|
884
|
+
addFocusable,
|
|
885
|
+
removeFocusable,
|
|
886
|
+
activateFocusable,
|
|
887
|
+
deactivateFocusable,
|
|
888
|
+
enableFocus,
|
|
889
|
+
disableFocus,
|
|
890
|
+
focusNext,
|
|
891
|
+
focusPrevious,
|
|
892
|
+
focus
|
|
893
|
+
]);
|
|
894
|
+
const animationContextValue = (0, import_react.useMemo)(() => ({
|
|
895
|
+
renderThrottleMs,
|
|
896
|
+
subscribe: animationSubscribe
|
|
897
|
+
}), [animationSubscribe, renderThrottleMs]);
|
|
898
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AppContext.Provider, {
|
|
899
|
+
value: appContextValue,
|
|
900
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(StdinContext.Provider, {
|
|
901
|
+
value: stdinContextValue,
|
|
902
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(StdoutContext.Provider, {
|
|
903
|
+
value: stdoutContextValue,
|
|
904
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(StderrContext.Provider, {
|
|
905
|
+
value: stderrContextValue,
|
|
906
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FocusContext.Provider, {
|
|
907
|
+
value: focusContextValue,
|
|
908
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(animationContext.Provider, {
|
|
909
|
+
value: animationContextValue,
|
|
910
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CursorContext.Provider, {
|
|
911
|
+
value: cursorContextValue,
|
|
912
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ErrorBoundary, {
|
|
913
|
+
onError: handleExit,
|
|
914
|
+
children
|
|
915
|
+
})
|
|
916
|
+
})
|
|
917
|
+
})
|
|
918
|
+
})
|
|
919
|
+
})
|
|
920
|
+
})
|
|
921
|
+
})
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
App.displayName = "InternalApp";
|
|
925
|
+
//#endregion
|
|
926
|
+
//#region src/components/TerminalOscContext.ts
|
|
927
|
+
const noop$1 = () => {};
|
|
928
|
+
const TerminalOscContext = (0, import_react.createContext)({
|
|
929
|
+
publishProgress: noop$1,
|
|
930
|
+
copyToClipboard: noop$1,
|
|
931
|
+
publishTitle: noop$1,
|
|
932
|
+
setWorkingDirectory: noop$1,
|
|
933
|
+
notify: noop$1,
|
|
934
|
+
setPointerShape: noop$1
|
|
935
|
+
});
|
|
936
|
+
TerminalOscContext.displayName = "InternalTerminalOscContext";
|
|
937
|
+
//#endregion
|
|
938
|
+
//#region src/instances.ts
|
|
939
|
+
const instances = /* @__PURE__ */ new WeakMap();
|
|
940
|
+
//#endregion
|
|
941
|
+
//#region src/patch-console.ts
|
|
942
|
+
const consoleMethods = [
|
|
943
|
+
"assert",
|
|
944
|
+
"count",
|
|
945
|
+
"countReset",
|
|
946
|
+
"debug",
|
|
947
|
+
"dir",
|
|
948
|
+
"dirxml",
|
|
949
|
+
"error",
|
|
950
|
+
"group",
|
|
951
|
+
"groupCollapsed",
|
|
952
|
+
"groupEnd",
|
|
953
|
+
"info",
|
|
954
|
+
"log",
|
|
955
|
+
"table",
|
|
956
|
+
"time",
|
|
957
|
+
"timeEnd",
|
|
958
|
+
"timeLog",
|
|
959
|
+
"trace",
|
|
960
|
+
"warn"
|
|
961
|
+
];
|
|
962
|
+
const patchConsole = (callback) => {
|
|
963
|
+
const stdout = new PassThrough();
|
|
964
|
+
const stderr = new PassThrough();
|
|
965
|
+
stdout.write = (data) => {
|
|
966
|
+
callback("stdout", String(data));
|
|
967
|
+
return true;
|
|
968
|
+
};
|
|
969
|
+
stderr.write = (data) => {
|
|
970
|
+
callback("stderr", String(data));
|
|
971
|
+
return true;
|
|
972
|
+
};
|
|
973
|
+
const internalConsole = new Console(stdout, stderr);
|
|
974
|
+
const originalMethods = /* @__PURE__ */ new Map();
|
|
975
|
+
for (const method of consoleMethods) {
|
|
976
|
+
originalMethods.set(method, console[method]);
|
|
977
|
+
console[method] = internalConsole[method];
|
|
978
|
+
}
|
|
979
|
+
return () => {
|
|
980
|
+
for (const method of consoleMethods) console[method] = originalMethods.get(method);
|
|
981
|
+
originalMethods.clear();
|
|
982
|
+
};
|
|
983
|
+
};
|
|
984
|
+
const patchStreamWrite = (stream, onData) => {
|
|
985
|
+
const originalWrite = stream.write;
|
|
986
|
+
const patchedWrite = (chunk, encodingOrCallback, callback) => {
|
|
987
|
+
onData(typeof chunk === "string" ? chunk : chunk instanceof Uint8Array ? Buffer.from(chunk).toString() : String(chunk));
|
|
988
|
+
(typeof encodingOrCallback === "function" ? encodingOrCallback : typeof callback === "function" ? callback : void 0)?.();
|
|
989
|
+
return true;
|
|
990
|
+
};
|
|
991
|
+
stream.write = patchedWrite;
|
|
992
|
+
return () => {
|
|
993
|
+
stream.write = originalWrite;
|
|
994
|
+
};
|
|
995
|
+
};
|
|
996
|
+
//#endregion
|
|
997
|
+
//#region src/get-max-width.ts
|
|
998
|
+
const getMaxWidth = (yogaNode) => {
|
|
999
|
+
return yogaNode.getComputedWidth() - yogaNode.getComputedPadding(Yoga.EDGE_LEFT) - yogaNode.getComputedPadding(Yoga.EDGE_RIGHT) - yogaNode.getComputedBorder(Yoga.EDGE_LEFT) - yogaNode.getComputedBorder(Yoga.EDGE_RIGHT);
|
|
1000
|
+
};
|
|
1001
|
+
//#endregion
|
|
1002
|
+
//#region src/render-background.ts
|
|
1003
|
+
const renderBackground = (x, y, node, output) => {
|
|
1004
|
+
if (node.style.backgroundColor === void 0) return;
|
|
1005
|
+
const width = node.yogaNode.getComputedWidth();
|
|
1006
|
+
const height = node.yogaNode.getComputedHeight();
|
|
1007
|
+
const leftBorderWidth = node.style.borderStyle && node.style.borderLeft !== false ? 1 : 0;
|
|
1008
|
+
const rightBorderWidth = node.style.borderStyle && node.style.borderRight !== false ? 1 : 0;
|
|
1009
|
+
const topBorderHeight = node.style.borderStyle && node.style.borderTop !== false ? 1 : 0;
|
|
1010
|
+
const bottomBorderHeight = node.style.borderStyle && node.style.borderBottom !== false ? 1 : 0;
|
|
1011
|
+
const contentWidth = width - leftBorderWidth - rightBorderWidth;
|
|
1012
|
+
const contentHeight = height - topBorderHeight - bottomBorderHeight;
|
|
1013
|
+
if (!(contentWidth > 0 && contentHeight > 0)) return;
|
|
1014
|
+
const bounds = {
|
|
1015
|
+
x: x + leftBorderWidth,
|
|
1016
|
+
y: y + topBorderHeight,
|
|
1017
|
+
width: contentWidth,
|
|
1018
|
+
height: contentHeight
|
|
1019
|
+
};
|
|
1020
|
+
const lines = Array.from({ length: contentHeight }, (_unused, row) => Array.from({ length: contentWidth }, (_empty, column) => {
|
|
1021
|
+
const resetBackground = node.style.backgroundColor === "";
|
|
1022
|
+
const background = resetBackground ? void 0 : samplePaint(node.style.backgroundColor, bounds.x + column, bounds.y + row, bounds, output.paintContext);
|
|
1023
|
+
return createCell(" ", 1, {
|
|
1024
|
+
...background ? { background } : {},
|
|
1025
|
+
underline: "none",
|
|
1026
|
+
attributes: cellAttributes.none
|
|
1027
|
+
}, void 0, resetBackground ? { background: true } : void 0);
|
|
1028
|
+
}));
|
|
1029
|
+
output.writeCells(bounds.x, bounds.y, lines);
|
|
1030
|
+
};
|
|
1031
|
+
//#endregion
|
|
1032
|
+
//#region src/render-border.ts
|
|
1033
|
+
const stylePiece = (segment, x, y, bounds, context, fg, bg, dim) => {
|
|
1034
|
+
let column = x;
|
|
1035
|
+
return [...graphemes(segment)].map((grapheme) => {
|
|
1036
|
+
const foreground = fg ? samplePaint(fg, column, y, bounds, context) : void 0;
|
|
1037
|
+
const background = bg ? samplePaint(bg, column, y, bounds, context) : void 0;
|
|
1038
|
+
const width = Math.max(1, stringWidth(grapheme));
|
|
1039
|
+
column += width;
|
|
1040
|
+
return createCell(grapheme, width, {
|
|
1041
|
+
...foreground ? { foreground } : {},
|
|
1042
|
+
...background ? { background } : {},
|
|
1043
|
+
underline: "none",
|
|
1044
|
+
attributes: dim ? cellAttributes.faint : cellAttributes.none
|
|
1045
|
+
});
|
|
1046
|
+
});
|
|
1047
|
+
};
|
|
1048
|
+
const renderBorder = (x, y, node, output) => {
|
|
1049
|
+
if (node.style.borderStyle) {
|
|
1050
|
+
const width = node.yogaNode.getComputedWidth();
|
|
1051
|
+
const height = node.yogaNode.getComputedHeight();
|
|
1052
|
+
const bounds = {
|
|
1053
|
+
x,
|
|
1054
|
+
y,
|
|
1055
|
+
width,
|
|
1056
|
+
height
|
|
1057
|
+
};
|
|
1058
|
+
const box = typeof node.style.borderStyle === "string" ? BOXES[node.style.borderStyle] : node.style.borderStyle;
|
|
1059
|
+
const topBorderColor = node.style.borderTopColor ?? node.style.borderColor;
|
|
1060
|
+
const bottomBorderColor = node.style.borderBottomColor ?? node.style.borderColor;
|
|
1061
|
+
const leftBorderColor = node.style.borderLeftColor ?? node.style.borderColor;
|
|
1062
|
+
const rightBorderColor = node.style.borderRightColor ?? node.style.borderColor;
|
|
1063
|
+
const topBorderBackgroundColor = node.style.borderTopBackgroundColor ?? node.style.borderBackgroundColor;
|
|
1064
|
+
const bottomBorderBackgroundColor = node.style.borderBottomBackgroundColor ?? node.style.borderBackgroundColor;
|
|
1065
|
+
const leftBorderBackgroundColor = node.style.borderLeftBackgroundColor ?? node.style.borderBackgroundColor;
|
|
1066
|
+
const rightBorderBackgroundColor = node.style.borderRightBackgroundColor ?? node.style.borderBackgroundColor;
|
|
1067
|
+
const dimTopBorderColor = node.style.borderTopDimColor ?? node.style.borderDimColor;
|
|
1068
|
+
const dimBottomBorderColor = node.style.borderBottomDimColor ?? node.style.borderDimColor;
|
|
1069
|
+
const dimLeftBorderColor = node.style.borderLeftDimColor ?? node.style.borderDimColor;
|
|
1070
|
+
const dimRightBorderColor = node.style.borderRightDimColor ?? node.style.borderDimColor;
|
|
1071
|
+
const showTopBorder = node.style.borderTop !== false;
|
|
1072
|
+
const showBottomBorder = node.style.borderBottom !== false;
|
|
1073
|
+
const showLeftBorder = node.style.borderLeft !== false;
|
|
1074
|
+
const showRightBorder = node.style.borderRight !== false;
|
|
1075
|
+
const contentWidth = width - (showLeftBorder ? 1 : 0) - (showRightBorder ? 1 : 0);
|
|
1076
|
+
const topBorder = showTopBorder ? (showLeftBorder ? box.topLeft : "") + box.top.repeat(contentWidth) + (showRightBorder ? box.topRight : "") : void 0;
|
|
1077
|
+
let verticalBorderHeight = height;
|
|
1078
|
+
if (showTopBorder) verticalBorderHeight -= 1;
|
|
1079
|
+
if (showBottomBorder) verticalBorderHeight -= 1;
|
|
1080
|
+
const offsetY = showTopBorder ? 1 : 0;
|
|
1081
|
+
const bottomBorder = showBottomBorder ? (showLeftBorder ? box.bottomLeft : "") + box.bottom.repeat(contentWidth) + (showRightBorder ? box.bottomRight : "") : void 0;
|
|
1082
|
+
if (topBorder) output.writeCells(x, y, [stylePiece(topBorder, x, y, bounds, output.paintContext, topBorderColor, topBorderBackgroundColor, dimTopBorderColor)]);
|
|
1083
|
+
if (showLeftBorder) output.writeCells(x, y + offsetY, Array.from({ length: verticalBorderHeight }, (_, row) => stylePiece(box.left, x, y + offsetY + row, bounds, output.paintContext, leftBorderColor, leftBorderBackgroundColor, dimLeftBorderColor)));
|
|
1084
|
+
if (showRightBorder) output.writeCells(x + width - 1, y + offsetY, Array.from({ length: verticalBorderHeight }, (_, row) => stylePiece(box.right, x + width - 1, y + offsetY + row, bounds, output.paintContext, rightBorderColor, rightBorderBackgroundColor, dimRightBorderColor)));
|
|
1085
|
+
if (bottomBorder) output.writeCells(x, y + height - 1, [stylePiece(bottomBorder, x, y + height - 1, bounds, output.paintContext, bottomBorderColor, bottomBorderBackgroundColor, dimBottomBorderColor)]);
|
|
1086
|
+
}
|
|
1087
|
+
};
|
|
1088
|
+
//#endregion
|
|
1089
|
+
//#region src/structured-text.ts
|
|
1090
|
+
/** Whether a text subtree must retain the ANSI compatibility pipeline. */
|
|
1091
|
+
function hasCompatibilityText(node) {
|
|
1092
|
+
if (node.internal_transform && !node.internal_textStyle) return true;
|
|
1093
|
+
return node.childNodes.some((child) => child.nodeName !== "#text" && hasCompatibilityText(child));
|
|
1094
|
+
}
|
|
1095
|
+
/** Rasterizes an unwrapped native Text subtree into semantic cell lines. */
|
|
1096
|
+
function structuredTextLines(node) {
|
|
1097
|
+
const lines = [[]];
|
|
1098
|
+
appendNode(node, emptySemanticTextStyle, lines, false);
|
|
1099
|
+
return lines;
|
|
1100
|
+
}
|
|
1101
|
+
/** Samples semantic paints only after wrapping and final layout are known. */
|
|
1102
|
+
function sampleStructuredText(lines, bounds, context) {
|
|
1103
|
+
return lines.map((line, row) => {
|
|
1104
|
+
let column = 0;
|
|
1105
|
+
return line.map((cell) => {
|
|
1106
|
+
const foreground = cell.foregroundPaint ? samplePaint(cell.foregroundPaint, bounds.x + column, bounds.y + row, bounds, context) : cell.style.foreground;
|
|
1107
|
+
const background = cell.backgroundPaint ? samplePaint(cell.backgroundPaint, bounds.x + column, bounds.y + row, bounds, context) : cell.style.background;
|
|
1108
|
+
column += cell.width;
|
|
1109
|
+
return createCell(cell.grapheme, cell.width, {
|
|
1110
|
+
...cell.style,
|
|
1111
|
+
...foreground ? { foreground } : {},
|
|
1112
|
+
...background ? { background } : {}
|
|
1113
|
+
}, cell.hyperlink, cell.reset);
|
|
1114
|
+
});
|
|
1115
|
+
});
|
|
1116
|
+
}
|
|
1117
|
+
function structuredTextBaseStyle(node) {
|
|
1118
|
+
return semanticTextStyleToCellStyle(mergeSemanticTextStyles(emptySemanticTextStyle, node.internal_textStyle));
|
|
1119
|
+
}
|
|
1120
|
+
/** Wraps or truncates semantic cells without converting them through ANSI. */
|
|
1121
|
+
function wrapStructuredText(input, maxWidth, wrap, baseStyle) {
|
|
1122
|
+
if (wrap === "none") return input.map((line) => [...line]);
|
|
1123
|
+
if (maxWidth < 1) return [[]];
|
|
1124
|
+
return input.flatMap((line) => {
|
|
1125
|
+
if (lineWidth(line) <= maxWidth) return [[...line]];
|
|
1126
|
+
if (wrap === "hard") return hardWrap(line, maxWidth);
|
|
1127
|
+
if (wrap === "wrap") return wordWrap(line, maxWidth);
|
|
1128
|
+
return [truncate(line, maxWidth, wrap, baseStyle)];
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1131
|
+
function wordWrap(line, maxWidth) {
|
|
1132
|
+
const words = [[]];
|
|
1133
|
+
const spaces = [];
|
|
1134
|
+
for (const cell of line) if (cell.grapheme === " ") {
|
|
1135
|
+
spaces.push(cell);
|
|
1136
|
+
words.push([]);
|
|
1137
|
+
} else words.at(-1).push(cell);
|
|
1138
|
+
const output = [[]];
|
|
1139
|
+
appendHard(output, words[0], maxWidth);
|
|
1140
|
+
for (let index = 1; index < words.length; index++) {
|
|
1141
|
+
appendHard(output, [spaces[index - 1]], maxWidth);
|
|
1142
|
+
const word = words[index];
|
|
1143
|
+
const current = output.at(-1);
|
|
1144
|
+
const wordWidth = lineWidth(word);
|
|
1145
|
+
const currentWidth = lineWidth(current);
|
|
1146
|
+
if (wordWidth <= maxWidth && currentWidth + wordWidth > maxWidth) output.push([]);
|
|
1147
|
+
else if (wordWidth > maxWidth && currentWidth > 0) {
|
|
1148
|
+
const remaining = maxWidth - currentWidth;
|
|
1149
|
+
const breaksHere = 1 + Math.floor((wordWidth - remaining - 1) / maxWidth);
|
|
1150
|
+
if (Math.floor((wordWidth - 1) / maxWidth) < breaksHere) output.push([]);
|
|
1151
|
+
}
|
|
1152
|
+
appendHard(output, word, maxWidth);
|
|
1153
|
+
}
|
|
1154
|
+
return output;
|
|
1155
|
+
}
|
|
1156
|
+
function hardWrap(line, maxWidth) {
|
|
1157
|
+
const output = [[]];
|
|
1158
|
+
appendHard(output, line, maxWidth);
|
|
1159
|
+
return output;
|
|
1160
|
+
}
|
|
1161
|
+
function appendHard(output, cells, maxWidth) {
|
|
1162
|
+
for (const cell of cells) {
|
|
1163
|
+
const current = output.at(-1);
|
|
1164
|
+
if (current.length > 0 && lineWidth(current) + cell.width > maxWidth) output.push([]);
|
|
1165
|
+
output.at(-1).push(cell);
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
function truncate(line, maxWidth, wrap, baseStyle) {
|
|
1169
|
+
if (maxWidth === 1) return [ellipsis(baseStyle ?? cellStyleIntersection(line.at(0), line.at(-1)))];
|
|
1170
|
+
if (wrap === "truncate-start") {
|
|
1171
|
+
const right = sliceCells(line, lineWidth(line) - maxWidth + 1, lineWidth(line));
|
|
1172
|
+
return [ellipsis(right[0]?.style), ...right];
|
|
1173
|
+
}
|
|
1174
|
+
if (wrap === "truncate-middle") {
|
|
1175
|
+
const half = Math.min(Math.floor(maxWidth / 2), maxWidth - 1);
|
|
1176
|
+
const left = sliceCells(line, 0, half);
|
|
1177
|
+
const rightWidth = maxWidth - half - 1;
|
|
1178
|
+
const right = sliceCells(line, lineWidth(line) - rightWidth, lineWidth(line));
|
|
1179
|
+
return [
|
|
1180
|
+
...left,
|
|
1181
|
+
ellipsis(baseStyle ?? cellStyleIntersection(left.at(-1), right.at(0))),
|
|
1182
|
+
...right
|
|
1183
|
+
];
|
|
1184
|
+
}
|
|
1185
|
+
const left = sliceCells(line, 0, maxWidth - 1);
|
|
1186
|
+
return [...left, ellipsis(left.at(-1)?.style)];
|
|
1187
|
+
}
|
|
1188
|
+
function sliceCells(line, start, end) {
|
|
1189
|
+
const output = [];
|
|
1190
|
+
let column = 0;
|
|
1191
|
+
for (const cell of line) {
|
|
1192
|
+
const nextColumn = column + cell.width;
|
|
1193
|
+
if (column >= start && nextColumn <= end) output.push(cell);
|
|
1194
|
+
column = nextColumn;
|
|
1195
|
+
}
|
|
1196
|
+
return output;
|
|
1197
|
+
}
|
|
1198
|
+
function ellipsis(style = semanticTextStyleToCellStyle(emptySemanticTextStyle)) {
|
|
1199
|
+
return createCell("…", 1, style);
|
|
1200
|
+
}
|
|
1201
|
+
function cellStyleIntersection(left, right) {
|
|
1202
|
+
if (!left) return right?.style ?? semanticTextStyleToCellStyle(emptySemanticTextStyle);
|
|
1203
|
+
if (!right) return left.style;
|
|
1204
|
+
return {
|
|
1205
|
+
...sameColor(left.style.foreground, right.style.foreground) ? { foreground: left.style.foreground } : {},
|
|
1206
|
+
...sameColor(left.style.background, right.style.background) ? { background: left.style.background } : {},
|
|
1207
|
+
...sameColor(left.style.underlineColor, right.style.underlineColor) ? { underlineColor: left.style.underlineColor } : {},
|
|
1208
|
+
underline: left.style.underline === right.style.underline ? left.style.underline : "none",
|
|
1209
|
+
attributes: left.style.attributes & right.style.attributes
|
|
1210
|
+
};
|
|
1211
|
+
}
|
|
1212
|
+
function sameColor(left, right) {
|
|
1213
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
1214
|
+
}
|
|
1215
|
+
function lineWidth(line) {
|
|
1216
|
+
return line.reduce((width, cell) => width + cell.width, 0);
|
|
1217
|
+
}
|
|
1218
|
+
function appendNode(node, inherited, lines, ansi) {
|
|
1219
|
+
if (node.nodeName === "#text") {
|
|
1220
|
+
if (ansi) appendAnsiText(node.nodeValue, inherited, lines);
|
|
1221
|
+
else appendText(node.nodeValue, inherited, lines);
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
const style = mergeSemanticTextStyles(inherited, node.internal_textStyle);
|
|
1225
|
+
for (const child of node.childNodes) appendNode(child, style, lines, ansi || node.internal_ansi === true);
|
|
1226
|
+
}
|
|
1227
|
+
function appendAnsiText(text, semantic, lines) {
|
|
1228
|
+
const base = semanticTextStyleToCellStyle(semantic);
|
|
1229
|
+
for (const cell of cellsFromAnsi(text)) {
|
|
1230
|
+
if (cell.grapheme === "\n" || cell.grapheme === "\r\n") {
|
|
1231
|
+
lines.push([]);
|
|
1232
|
+
continue;
|
|
1233
|
+
}
|
|
1234
|
+
const hasForeground = cell.style.foreground !== void 0;
|
|
1235
|
+
const hasBackground = cell.style.background !== void 0;
|
|
1236
|
+
lines.at(-1).push({
|
|
1237
|
+
...cell,
|
|
1238
|
+
style: {
|
|
1239
|
+
...cell.style.foreground ?? base.foreground ? { foreground: cell.style.foreground ?? base.foreground } : {},
|
|
1240
|
+
...cell.style.background ?? base.background ? { background: cell.style.background ?? base.background } : {},
|
|
1241
|
+
...cell.style.underlineColor ? { underlineColor: cell.style.underlineColor } : {},
|
|
1242
|
+
underline: cell.style.underline === "none" ? base.underline : cell.style.underline,
|
|
1243
|
+
attributes: base.attributes | cell.style.attributes
|
|
1244
|
+
},
|
|
1245
|
+
...!hasForeground && semantic.foreground ? { foregroundPaint: semantic.foreground } : {},
|
|
1246
|
+
...!hasBackground && semantic.background ? { backgroundPaint: semantic.background } : {}
|
|
1247
|
+
});
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
function appendText(text, style, lines) {
|
|
1251
|
+
const cellStyle = semanticTextStyleToCellStyle(style);
|
|
1252
|
+
const plainText = tokenizeAnsi(text).filter((token) => token.type === "text").map((token) => token.value).join("");
|
|
1253
|
+
for (const segment of graphemes(stripAnsi(plainText))) {
|
|
1254
|
+
if (segment === "\n" || segment === "\r\n") {
|
|
1255
|
+
lines.push([]);
|
|
1256
|
+
continue;
|
|
1257
|
+
}
|
|
1258
|
+
lines.at(-1).push({
|
|
1259
|
+
...createCell(segment, Math.max(1, stringWidth(segment)), cellStyle, void 0, style.resetForeground || style.resetBackground ? {
|
|
1260
|
+
foreground: style.resetForeground,
|
|
1261
|
+
background: style.resetBackground
|
|
1262
|
+
} : void 0),
|
|
1263
|
+
...style.foreground ? { foregroundPaint: style.foreground } : {},
|
|
1264
|
+
...style.background ? { backgroundPaint: style.background } : {}
|
|
1265
|
+
});
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
//#endregion
|
|
1269
|
+
//#region src/paint-tree.ts
|
|
1270
|
+
const renderAccessibleText = (node, options = {}) => {
|
|
1271
|
+
if (options.skipStaticElements && node.internal_static) return "";
|
|
1272
|
+
if (node.yogaNode?.getDisplay() === Yoga.DISPLAY_NONE) return "";
|
|
1273
|
+
let output = "";
|
|
1274
|
+
if (node.nodeName === "ink-text") output = squashTextNodes(node);
|
|
1275
|
+
else if (node.nodeName === "ink-box" || node.nodeName === "ink-root") {
|
|
1276
|
+
const separator = node.style.flexDirection === "row" || node.style.flexDirection === "row-reverse" ? " " : "\n";
|
|
1277
|
+
output = (node.style.flexDirection === "row-reverse" || node.style.flexDirection === "column-reverse" ? [...node.childNodes].reverse() : [...node.childNodes]).map((childNode) => {
|
|
1278
|
+
return renderAccessibleText(childNode, {
|
|
1279
|
+
parentRole: node.internal_accessibility?.role,
|
|
1280
|
+
skipStaticElements: options.skipStaticElements
|
|
1281
|
+
});
|
|
1282
|
+
}).filter(Boolean).join(separator);
|
|
1283
|
+
}
|
|
1284
|
+
if (node.internal_accessibility) {
|
|
1285
|
+
const { role, state } = node.internal_accessibility;
|
|
1286
|
+
if (state) {
|
|
1287
|
+
const stateDescription = Object.keys(state).filter((key) => state[key]).join(", ");
|
|
1288
|
+
if (stateDescription) output = `(${stateDescription}) ${output}`;
|
|
1289
|
+
}
|
|
1290
|
+
if (role && role !== options.parentRole) output = `${role}: ${output}`;
|
|
1291
|
+
}
|
|
1292
|
+
return output;
|
|
1293
|
+
};
|
|
1294
|
+
const paintTree = (node, output, options) => {
|
|
1295
|
+
const { offsetX = 0, offsetY = 0, transformers = [], skipStaticElements } = options;
|
|
1296
|
+
if (skipStaticElements && node.internal_static) return;
|
|
1297
|
+
const { yogaNode } = node;
|
|
1298
|
+
if (yogaNode) {
|
|
1299
|
+
if (yogaNode.getDisplay() === Yoga.DISPLAY_NONE) return;
|
|
1300
|
+
const x = offsetX + yogaNode.getComputedLeft();
|
|
1301
|
+
const y = offsetY + yogaNode.getComputedTop();
|
|
1302
|
+
const newTransformers = node.internal_transform ? [node.internal_transform, ...transformers] : transformers;
|
|
1303
|
+
if (node.nodeName === "ink-text") {
|
|
1304
|
+
if (squashTextNodes(node).length > 0) {
|
|
1305
|
+
const maxWidth = getMaxWidth(yogaNode);
|
|
1306
|
+
const firstChildYoga = node.childNodes[0]?.yogaNode;
|
|
1307
|
+
const paddingX = firstChildYoga?.getComputedLeft() ?? 0;
|
|
1308
|
+
const paddingY = firstChildYoga?.getComputedTop() ?? 0;
|
|
1309
|
+
const textWrap = node.style.textWrap ?? "wrap";
|
|
1310
|
+
const overflow = textWrap === "none";
|
|
1311
|
+
const lines = wrapStructuredText(structuredTextLines(node), maxWidth, textWrap, structuredTextBaseStyle(node));
|
|
1312
|
+
const paintBounds = {
|
|
1313
|
+
x: x + paddingX,
|
|
1314
|
+
y: y + paddingY,
|
|
1315
|
+
width: Math.max(1, ...lines.map((line) => line.reduce((width, cell) => width + cell.width, 0))),
|
|
1316
|
+
height: lines.length
|
|
1317
|
+
};
|
|
1318
|
+
const sampled = sampleStructuredText(lines, paintBounds, output.paintContext);
|
|
1319
|
+
if (hasCompatibilityText(node)) {
|
|
1320
|
+
const textTransformers = collectTransformers(node, transformers);
|
|
1321
|
+
output.writeAnsi(paintBounds.x, paintBounds.y, sampled.map((line) => serializeLine(line, {
|
|
1322
|
+
colorProfile: output.paintContext.profile ?? "truecolor",
|
|
1323
|
+
trimEnd: false
|
|
1324
|
+
})).join("\n"), {
|
|
1325
|
+
transformers: textTransformers,
|
|
1326
|
+
overflow
|
|
1327
|
+
});
|
|
1328
|
+
} else output.writeCells(paintBounds.x, paintBounds.y, sampled, { overflow });
|
|
1329
|
+
}
|
|
1330
|
+
return;
|
|
1331
|
+
}
|
|
1332
|
+
let clipped = false;
|
|
1333
|
+
if (node.nodeName === "ink-box") {
|
|
1334
|
+
renderBackground(x, y, node, output);
|
|
1335
|
+
renderBorder(x, y, node, output);
|
|
1336
|
+
const clipHorizontally = node.style.overflowX === "hidden" || node.style.overflow === "hidden";
|
|
1337
|
+
const clipVertically = node.style.overflowY === "hidden" || node.style.overflow === "hidden";
|
|
1338
|
+
if (clipHorizontally || clipVertically) {
|
|
1339
|
+
const x1 = clipHorizontally ? x + yogaNode.getComputedBorder(Yoga.EDGE_LEFT) : void 0;
|
|
1340
|
+
const x2 = clipHorizontally ? x + yogaNode.getComputedWidth() - yogaNode.getComputedBorder(Yoga.EDGE_RIGHT) : void 0;
|
|
1341
|
+
const y1 = clipVertically ? y + yogaNode.getComputedBorder(Yoga.EDGE_TOP) : void 0;
|
|
1342
|
+
const y2 = clipVertically ? y + yogaNode.getComputedHeight() - yogaNode.getComputedBorder(Yoga.EDGE_BOTTOM) : void 0;
|
|
1343
|
+
output.clip({
|
|
1344
|
+
x1,
|
|
1345
|
+
x2,
|
|
1346
|
+
y1,
|
|
1347
|
+
y2
|
|
1348
|
+
});
|
|
1349
|
+
clipped = true;
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
if (node.nodeName === "ink-root" || node.nodeName === "ink-box") {
|
|
1353
|
+
for (const childNode of node.childNodes) paintTree(childNode, output, {
|
|
1354
|
+
offsetX: x,
|
|
1355
|
+
offsetY: y,
|
|
1356
|
+
transformers: newTransformers,
|
|
1357
|
+
skipStaticElements
|
|
1358
|
+
});
|
|
1359
|
+
if (clipped) output.unclip();
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
};
|
|
1363
|
+
function collectTransformers(node, inherited) {
|
|
1364
|
+
return [
|
|
1365
|
+
...node.childNodes.flatMap((child) => child.nodeName === "#text" ? [] : collectTransformers(child, [])),
|
|
1366
|
+
...node.internal_transform ? [node.internal_transform] : [],
|
|
1367
|
+
...inherited
|
|
1368
|
+
];
|
|
1369
|
+
}
|
|
1370
|
+
//#endregion
|
|
1371
|
+
//#region src/screen/canvas.ts
|
|
1372
|
+
/** Immediate structured drawing canvas with one explicit ANSI compatibility adapter. */
|
|
1373
|
+
var Canvas = class {
|
|
1374
|
+
width;
|
|
1375
|
+
height;
|
|
1376
|
+
paintContext;
|
|
1377
|
+
#screen;
|
|
1378
|
+
#clips = [];
|
|
1379
|
+
constructor(options) {
|
|
1380
|
+
this.width = options.width;
|
|
1381
|
+
this.height = options.height;
|
|
1382
|
+
this.paintContext = {
|
|
1383
|
+
profile: options.colorProfile ?? "truecolor",
|
|
1384
|
+
...options.paintContext
|
|
1385
|
+
};
|
|
1386
|
+
this.#screen = new Screen(this.width, this.height);
|
|
1387
|
+
}
|
|
1388
|
+
writeCells(x, y, lines, options) {
|
|
1389
|
+
const clip = this.#clips.at(-1);
|
|
1390
|
+
const overflow = options?.overflow === true;
|
|
1391
|
+
for (const [rowOffset, line] of lines.entries()) {
|
|
1392
|
+
const currentY = y + rowOffset;
|
|
1393
|
+
if (!this.#insideY(currentY, clip)) continue;
|
|
1394
|
+
let currentX = x;
|
|
1395
|
+
for (const cell of line) {
|
|
1396
|
+
const endX = currentX + cell.width;
|
|
1397
|
+
if (!(clip?.x1 !== void 0 && currentX < clip.x1 || clip?.x2 !== void 0 && endX > clip.x2)) this.#composeNativeCell(currentX, currentY, cell, overflow);
|
|
1398
|
+
currentX = endX;
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
writeAnsi(x, y, text, options) {
|
|
1403
|
+
if (!text) return;
|
|
1404
|
+
let lines = text.split("\n");
|
|
1405
|
+
const clip = this.#clips.at(-1);
|
|
1406
|
+
const overflow = options.overflow === true;
|
|
1407
|
+
if (clip?.y1 !== void 0 && y < clip.y1) {
|
|
1408
|
+
lines = lines.slice(clip.y1 - y);
|
|
1409
|
+
y = clip.y1;
|
|
1410
|
+
}
|
|
1411
|
+
if (clip?.y2 !== void 0) lines = lines.slice(0, Math.max(0, clip.y2 - y));
|
|
1412
|
+
for (const [lineIndex, original] of lines.entries()) {
|
|
1413
|
+
const currentY = y + lineIndex;
|
|
1414
|
+
if (!this.#insideY(currentY, clip)) continue;
|
|
1415
|
+
let line = original;
|
|
1416
|
+
let currentX = x;
|
|
1417
|
+
if (clip?.x1 !== void 0 && currentX < clip.x1) {
|
|
1418
|
+
line = sliceAnsi(line, clip.x1 - currentX);
|
|
1419
|
+
currentX = clip.x1;
|
|
1420
|
+
}
|
|
1421
|
+
if (clip?.x2 !== void 0) line = sliceAnsi(line, 0, Math.max(0, clip.x2 - currentX));
|
|
1422
|
+
for (const character of transformAnsiLine(line, lineIndex, options.transformers)) {
|
|
1423
|
+
const width = Math.max(1, stringWidth(character.value));
|
|
1424
|
+
if (clip?.x2 !== void 0 && currentX + width > clip.x2) break;
|
|
1425
|
+
this.#writeCompatibilityCell(currentX, currentY, character, width, overflow);
|
|
1426
|
+
currentX += width;
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
clip(clip) {
|
|
1431
|
+
this.#clips.push(clip);
|
|
1432
|
+
}
|
|
1433
|
+
unclip() {
|
|
1434
|
+
this.#clips.pop();
|
|
1435
|
+
}
|
|
1436
|
+
finish() {
|
|
1437
|
+
return this.#screen;
|
|
1438
|
+
}
|
|
1439
|
+
#insideY(y, clip) {
|
|
1440
|
+
return y >= 0 && y < this.#screen.height && (clip?.y1 === void 0 || y >= clip.y1) && (clip?.y2 === void 0 || y < clip.y2);
|
|
1441
|
+
}
|
|
1442
|
+
#composeNativeCell(x, y, cell, overflow = false) {
|
|
1443
|
+
if (!overflow && x >= this.#screen.width) return;
|
|
1444
|
+
this.#screen.composeCell(x, y, {
|
|
1445
|
+
content: {
|
|
1446
|
+
grapheme: cell.grapheme,
|
|
1447
|
+
width: cell.width
|
|
1448
|
+
},
|
|
1449
|
+
foreground: cell.reset?.foreground ? null : cell.style.foreground ?? null,
|
|
1450
|
+
background: cell.reset?.background ? null : cell.style.background,
|
|
1451
|
+
underlineColor: cell.style.underlineColor ?? null,
|
|
1452
|
+
underline: cell.style.underline,
|
|
1453
|
+
attributes: cell.style.attributes,
|
|
1454
|
+
hyperlink: cell.hyperlink ?? null
|
|
1455
|
+
}, { overflow });
|
|
1456
|
+
}
|
|
1457
|
+
#writeCompatibilityCell(x, y, character, width, overflow = false) {
|
|
1458
|
+
if (!overflow && x >= this.#screen.width) return;
|
|
1459
|
+
const cell = cellFromStyledChar(character, width);
|
|
1460
|
+
this.#screen.composeCell(x, y, {
|
|
1461
|
+
content: {
|
|
1462
|
+
grapheme: cell.grapheme,
|
|
1463
|
+
width: cell.width
|
|
1464
|
+
},
|
|
1465
|
+
foreground: cell.style.foreground ?? null,
|
|
1466
|
+
background: cell.style.background,
|
|
1467
|
+
underlineColor: cell.style.underlineColor ?? null,
|
|
1468
|
+
underline: cell.style.underline,
|
|
1469
|
+
attributes: cell.style.attributes,
|
|
1470
|
+
hyperlink: cell.hyperlink ?? null
|
|
1471
|
+
}, { overflow });
|
|
1472
|
+
}
|
|
1473
|
+
};
|
|
1474
|
+
//#endregion
|
|
1475
|
+
//#region src/render-frame.ts
|
|
1476
|
+
const renderDimension = (value) => Number.isFinite(value) ? Math.max(0, Math.ceil(value)) : 0;
|
|
1477
|
+
const renderFrame = (node, isScreenReaderEnabled, options = {}) => {
|
|
1478
|
+
if (node.yogaNode) {
|
|
1479
|
+
if (isScreenReaderEnabled) {
|
|
1480
|
+
const output = renderAccessibleText(node, { skipStaticElements: true });
|
|
1481
|
+
let staticOutput = "";
|
|
1482
|
+
if (node.staticNode) staticOutput = renderAccessibleText(node.staticNode, { skipStaticElements: false });
|
|
1483
|
+
return {
|
|
1484
|
+
accessibleText: output,
|
|
1485
|
+
staticAccessibleText: staticOutput
|
|
1486
|
+
};
|
|
1487
|
+
}
|
|
1488
|
+
const output = new Canvas({
|
|
1489
|
+
width: renderDimension(node.yogaNode.getComputedWidth()),
|
|
1490
|
+
height: renderDimension(node.yogaNode.getComputedHeight()),
|
|
1491
|
+
colorProfile: options.colorProfile,
|
|
1492
|
+
paintContext: options.paintContext
|
|
1493
|
+
});
|
|
1494
|
+
paintTree(node, output, { skipStaticElements: true });
|
|
1495
|
+
let staticOutput;
|
|
1496
|
+
if (node.staticNode?.yogaNode) {
|
|
1497
|
+
staticOutput = new Canvas({
|
|
1498
|
+
width: renderDimension(node.staticNode.yogaNode.getComputedWidth()),
|
|
1499
|
+
height: renderDimension(node.staticNode.yogaNode.getComputedHeight()),
|
|
1500
|
+
colorProfile: options.colorProfile,
|
|
1501
|
+
paintContext: options.paintContext
|
|
1502
|
+
});
|
|
1503
|
+
paintTree(node.staticNode, staticOutput, { skipStaticElements: false });
|
|
1504
|
+
}
|
|
1505
|
+
return {
|
|
1506
|
+
screen: output.finish(),
|
|
1507
|
+
...staticOutput ? { staticScreen: staticOutput.finish() } : {}
|
|
1508
|
+
};
|
|
1509
|
+
}
|
|
1510
|
+
return { accessibleText: "" };
|
|
1511
|
+
};
|
|
1512
|
+
//#endregion
|
|
1513
|
+
//#region src/terminal/inline-presenter.ts
|
|
1514
|
+
/** Full-rewrite fallback and cursor state for the structured inline renderer. */
|
|
1515
|
+
function createInlinePresenter(stream, options = {}) {
|
|
1516
|
+
let previousLineCount = 0;
|
|
1517
|
+
let previousOutput = "";
|
|
1518
|
+
let hasHiddenCursor = false;
|
|
1519
|
+
let cursorPosition;
|
|
1520
|
+
let cursorDirty = false;
|
|
1521
|
+
let previousCursorPosition;
|
|
1522
|
+
let cursorWasShown = false;
|
|
1523
|
+
const showCursor = options.showCursor ?? false;
|
|
1524
|
+
const activeCursor = () => cursorDirty ? cursorPosition : void 0;
|
|
1525
|
+
const hasChanges = (output, cursor) => output !== previousOutput || cursorPositionChanged(cursor, previousCursorPosition);
|
|
1526
|
+
const present = (output) => {
|
|
1527
|
+
if (!showCursor && !hasHiddenCursor) {
|
|
1528
|
+
cliCursor.hide(stream);
|
|
1529
|
+
hasHiddenCursor = true;
|
|
1530
|
+
}
|
|
1531
|
+
const cursor = activeCursor();
|
|
1532
|
+
cursorDirty = false;
|
|
1533
|
+
const cursorChanged = cursorPositionChanged(cursor, previousCursorPosition);
|
|
1534
|
+
if (!hasChanges(output, cursor)) return false;
|
|
1535
|
+
const lines = output.split("\n");
|
|
1536
|
+
const suffix = buildCursorSuffix(lines.length - 1, cursor);
|
|
1537
|
+
if (output === previousOutput && cursorChanged) stream.write(buildCursorOnlySequence({
|
|
1538
|
+
cursorWasShown,
|
|
1539
|
+
previousLineCount,
|
|
1540
|
+
previousCursorPosition,
|
|
1541
|
+
cursorPosition: cursor
|
|
1542
|
+
}));
|
|
1543
|
+
else {
|
|
1544
|
+
previousOutput = output;
|
|
1545
|
+
stream.write(buildReturnToBottomPrefix(cursorWasShown, previousLineCount, previousCursorPosition) + ansiEscapes.eraseLines(previousLineCount) + output + suffix);
|
|
1546
|
+
previousLineCount = lines.length;
|
|
1547
|
+
}
|
|
1548
|
+
previousCursorPosition = cursor ? { ...cursor } : void 0;
|
|
1549
|
+
cursorWasShown = cursor !== void 0;
|
|
1550
|
+
return true;
|
|
1551
|
+
};
|
|
1552
|
+
present.clear = () => {
|
|
1553
|
+
stream.write(buildReturnToBottomPrefix(cursorWasShown, previousLineCount, previousCursorPosition) + ansiEscapes.eraseLines(previousLineCount));
|
|
1554
|
+
previousOutput = "";
|
|
1555
|
+
previousLineCount = 0;
|
|
1556
|
+
previousCursorPosition = void 0;
|
|
1557
|
+
cursorWasShown = false;
|
|
1558
|
+
};
|
|
1559
|
+
present.done = () => {
|
|
1560
|
+
previousOutput = "";
|
|
1561
|
+
previousLineCount = 0;
|
|
1562
|
+
previousCursorPosition = void 0;
|
|
1563
|
+
cursorWasShown = false;
|
|
1564
|
+
if (!showCursor) {
|
|
1565
|
+
cliCursor.show(stream);
|
|
1566
|
+
hasHiddenCursor = false;
|
|
1567
|
+
}
|
|
1568
|
+
};
|
|
1569
|
+
present.reset = () => {
|
|
1570
|
+
previousOutput = "";
|
|
1571
|
+
previousLineCount = 0;
|
|
1572
|
+
previousCursorPosition = void 0;
|
|
1573
|
+
cursorWasShown = false;
|
|
1574
|
+
};
|
|
1575
|
+
present.sync = (output) => {
|
|
1576
|
+
const cursor = activeCursor();
|
|
1577
|
+
cursorDirty = false;
|
|
1578
|
+
const lines = output.split("\n");
|
|
1579
|
+
previousOutput = output;
|
|
1580
|
+
previousLineCount = lines.length;
|
|
1581
|
+
if (!cursor && cursorWasShown) stream.write(hideCursorEscape);
|
|
1582
|
+
if (cursor) stream.write(buildCursorSuffix(lines.length - 1, cursor));
|
|
1583
|
+
previousCursorPosition = cursor ? { ...cursor } : void 0;
|
|
1584
|
+
cursorWasShown = cursor !== void 0;
|
|
1585
|
+
};
|
|
1586
|
+
present.setCursorPosition = (position) => {
|
|
1587
|
+
cursorPosition = position;
|
|
1588
|
+
cursorDirty = true;
|
|
1589
|
+
};
|
|
1590
|
+
present.isCursorDirty = () => cursorDirty;
|
|
1591
|
+
present.willRender = (output) => hasChanges(output, activeCursor());
|
|
1592
|
+
return present;
|
|
1593
|
+
}
|
|
1594
|
+
//#endregion
|
|
1595
|
+
//#region src/throttle.ts
|
|
1596
|
+
/**
|
|
1597
|
+
Invokes `fn` at most once per `wait` milliseconds.
|
|
1598
|
+
|
|
1599
|
+
The first call in a window fires immediately (leading edge). Calls made while
|
|
1600
|
+
the window is open are coalesced into a single trailing invocation with the
|
|
1601
|
+
latest arguments. Matches the lodash semantics Ink relied on: a single call
|
|
1602
|
+
produces only a leading invocation, no trailing one.
|
|
1603
|
+
*/
|
|
1604
|
+
const throttle = (fn, wait = 0) => {
|
|
1605
|
+
let timer;
|
|
1606
|
+
let pendingArgs;
|
|
1607
|
+
const invokePending = () => {
|
|
1608
|
+
const args = pendingArgs;
|
|
1609
|
+
pendingArgs = void 0;
|
|
1610
|
+
fn(...args);
|
|
1611
|
+
};
|
|
1612
|
+
const onTimer = () => {
|
|
1613
|
+
if (pendingArgs) {
|
|
1614
|
+
invokePending();
|
|
1615
|
+
timer = setTimeout(onTimer, wait);
|
|
1616
|
+
} else timer = void 0;
|
|
1617
|
+
};
|
|
1618
|
+
const throttled = (...args) => {
|
|
1619
|
+
if (timer) {
|
|
1620
|
+
pendingArgs = args;
|
|
1621
|
+
return;
|
|
1622
|
+
}
|
|
1623
|
+
fn(...args);
|
|
1624
|
+
timer = setTimeout(onTimer, wait);
|
|
1625
|
+
};
|
|
1626
|
+
throttled.cancel = () => {
|
|
1627
|
+
if (timer) {
|
|
1628
|
+
clearTimeout(timer);
|
|
1629
|
+
timer = void 0;
|
|
1630
|
+
}
|
|
1631
|
+
pendingArgs = void 0;
|
|
1632
|
+
};
|
|
1633
|
+
throttled.flush = () => {
|
|
1634
|
+
if (!pendingArgs) return;
|
|
1635
|
+
if (timer) {
|
|
1636
|
+
clearTimeout(timer);
|
|
1637
|
+
timer = void 0;
|
|
1638
|
+
}
|
|
1639
|
+
invokePending();
|
|
1640
|
+
};
|
|
1641
|
+
return throttled;
|
|
1642
|
+
};
|
|
1643
|
+
//#endregion
|
|
1644
|
+
//#region src/terminal/render-scheduler.ts
|
|
1645
|
+
/** Owns frame cadence independently of React reconciliation and terminal presentation. */
|
|
1646
|
+
function createRenderScheduler(render, options) {
|
|
1647
|
+
const frameInterval = options.maxFps > 0 ? Math.max(1, Math.ceil(1e3 / options.maxFps)) : 0;
|
|
1648
|
+
let pending = false;
|
|
1649
|
+
const throttled = options.unthrottled ? void 0 : throttle(render, frameInterval);
|
|
1650
|
+
return {
|
|
1651
|
+
intervalMs: options.unthrottled ? 0 : frameInterval,
|
|
1652
|
+
throttled,
|
|
1653
|
+
get pending() {
|
|
1654
|
+
return pending;
|
|
1655
|
+
},
|
|
1656
|
+
schedule: options.unthrottled ? render : () => {
|
|
1657
|
+
pending = true;
|
|
1658
|
+
throttled();
|
|
1659
|
+
},
|
|
1660
|
+
immediate: render,
|
|
1661
|
+
markRendered() {
|
|
1662
|
+
pending = false;
|
|
1663
|
+
}
|
|
1664
|
+
};
|
|
1665
|
+
}
|
|
1666
|
+
//#endregion
|
|
1667
|
+
//#region src/ink.tsx
|
|
1668
|
+
/** @jsxImportSource react */
|
|
1669
|
+
const noop = () => {};
|
|
1670
|
+
const beforeExitCallbacks = /* @__PURE__ */ new Set();
|
|
1671
|
+
const runBeforeExitCallbacks = () => {
|
|
1672
|
+
for (const callback of beforeExitCallbacks) callback();
|
|
1673
|
+
};
|
|
1674
|
+
function registerBeforeExit(callback) {
|
|
1675
|
+
if (beforeExitCallbacks.size === 0) process.on("beforeExit", runBeforeExitCallbacks);
|
|
1676
|
+
beforeExitCallbacks.add(callback);
|
|
1677
|
+
return () => {
|
|
1678
|
+
beforeExitCallbacks.delete(callback);
|
|
1679
|
+
if (beforeExitCallbacks.size === 0) process.off("beforeExit", runBeforeExitCallbacks);
|
|
1680
|
+
};
|
|
1681
|
+
}
|
|
1682
|
+
function bottomRows(screen, height) {
|
|
1683
|
+
if (screen.height <= height) return screen;
|
|
1684
|
+
const cropped = new Screen(screen.width, height);
|
|
1685
|
+
const offset = screen.height - height;
|
|
1686
|
+
for (let y = 0; y < height; y++) for (let x = 0; x < screen.width; x++) {
|
|
1687
|
+
const cell = screen.cellAt(x, y + offset);
|
|
1688
|
+
if (cell && cell.width > 0) cropped.setCell(x, y, cell);
|
|
1689
|
+
}
|
|
1690
|
+
return cropped;
|
|
1691
|
+
}
|
|
1692
|
+
const RESIZE_SETTLE_MS = 50;
|
|
1693
|
+
const shouldClearTerminalForFrame = ({ isTTY, viewportRows, previousOutputHeight, nextOutputHeight, isUnmounting }) => {
|
|
1694
|
+
if (!isTTY) return false;
|
|
1695
|
+
const hadPreviousFrame = previousOutputHeight > 0;
|
|
1696
|
+
const wasFullscreen = previousOutputHeight >= viewportRows;
|
|
1697
|
+
const wasOverflowing = previousOutputHeight > viewportRows;
|
|
1698
|
+
const isOverflowing = nextOutputHeight > viewportRows;
|
|
1699
|
+
const isFullscreen = nextOutputHeight >= viewportRows;
|
|
1700
|
+
const isLeavingFullscreen = wasOverflowing && nextOutputHeight < viewportRows;
|
|
1701
|
+
const shouldClearOnUnmount = isUnmounting && wasFullscreen;
|
|
1702
|
+
if (isWindows && (wasFullscreen || isFullscreen)) return true;
|
|
1703
|
+
return wasOverflowing || isOverflowing && hadPreviousFrame || isLeavingFullscreen || shouldClearOnUnmount;
|
|
1704
|
+
};
|
|
1705
|
+
const isErrorInput = (value) => {
|
|
1706
|
+
return value instanceof Error || isNativeError(value);
|
|
1707
|
+
};
|
|
1708
|
+
const getWritableStreamState = (stdout) => {
|
|
1709
|
+
return { canWriteToStdout: !stdout.destroyed && !stdout.writableEnded && (stdout.writable ?? true) };
|
|
1710
|
+
};
|
|
1711
|
+
const settleThrottle = (throttled, canWriteToStdout) => {
|
|
1712
|
+
if (!throttled) return;
|
|
1713
|
+
if (canWriteToStdout) throttled.flush();
|
|
1714
|
+
else throttled.cancel();
|
|
1715
|
+
};
|
|
1716
|
+
const writeBestEffort = (stream, data) => {
|
|
1717
|
+
try {
|
|
1718
|
+
stream.write(data);
|
|
1719
|
+
} catch {}
|
|
1720
|
+
};
|
|
1721
|
+
const createRenderPassthrough = (stream) => {
|
|
1722
|
+
const passthrough = Object.create(stream);
|
|
1723
|
+
passthrough.write = stream.write.bind(stream);
|
|
1724
|
+
passthrough.on = stream.on.bind(stream);
|
|
1725
|
+
passthrough.off = stream.off.bind(stream);
|
|
1726
|
+
passthrough.once = stream.once.bind(stream);
|
|
1727
|
+
passthrough.addListener = stream.addListener.bind(stream);
|
|
1728
|
+
passthrough.removeListener = stream.removeListener.bind(stream);
|
|
1729
|
+
passthrough.emit = stream.emit.bind(stream);
|
|
1730
|
+
return passthrough;
|
|
1731
|
+
};
|
|
1732
|
+
const createInk = (options) => {
|
|
1733
|
+
let captureTargets;
|
|
1734
|
+
if (options.patchConsole === "stdio") {
|
|
1735
|
+
captureTargets = {
|
|
1736
|
+
stdout: options.stdout,
|
|
1737
|
+
stderr: options.stderr
|
|
1738
|
+
};
|
|
1739
|
+
options = {
|
|
1740
|
+
...options,
|
|
1741
|
+
stdout: createRenderPassthrough(options.stdout),
|
|
1742
|
+
stderr: createRenderPassthrough(options.stderr)
|
|
1743
|
+
};
|
|
1744
|
+
}
|
|
1745
|
+
const rootNode = createNode("ink-root");
|
|
1746
|
+
rootNode.onComputeLayout = calculateLayout;
|
|
1747
|
+
const isScreenReaderEnabled = options.isScreenReaderEnabled ?? isScreenReader;
|
|
1748
|
+
const interactive = options.interactive ?? (!isInCi && Boolean(options.stdout.isTTY));
|
|
1749
|
+
const terminal = new TerminalSession({
|
|
1750
|
+
stdin: options.stdin,
|
|
1751
|
+
stdout: options.stdout,
|
|
1752
|
+
stderr: options.stderr,
|
|
1753
|
+
colorPolicy: options.colorProfile ?? "auto",
|
|
1754
|
+
onCapabilitiesChange: () => rootNode.onRender?.()
|
|
1755
|
+
});
|
|
1756
|
+
const terminalOsc = {
|
|
1757
|
+
publishProgress: (owner, state, value) => {
|
|
1758
|
+
terminal.publishProgress(owner, state, value);
|
|
1759
|
+
},
|
|
1760
|
+
copyToClipboard: (text, selection) => {
|
|
1761
|
+
terminal.copyToClipboard(text, selection);
|
|
1762
|
+
},
|
|
1763
|
+
publishTitle: (owner, title) => terminal.publishTitle(owner, title),
|
|
1764
|
+
setWorkingDirectory: (directory) => terminal.setWorkingDirectory(directory),
|
|
1765
|
+
notify: (title) => terminal.notify(title),
|
|
1766
|
+
setPointerShape: (shape) => terminal.setPointerShape(shape)
|
|
1767
|
+
};
|
|
1768
|
+
const capabilitiesStore = terminal.capabilities;
|
|
1769
|
+
const renderScheduler = createRenderScheduler(onRender, {
|
|
1770
|
+
unthrottled: options.debug || isScreenReaderEnabled,
|
|
1771
|
+
maxFps: options.maxFps ?? 30
|
|
1772
|
+
});
|
|
1773
|
+
rootNode.onRender = renderScheduler.schedule;
|
|
1774
|
+
rootNode.onImmediateRender = renderScheduler.immediate;
|
|
1775
|
+
rootNode.onStaticChange = handleStaticChange;
|
|
1776
|
+
const accessiblePresenter = isScreenReaderEnabled ? createInlinePresenter(options.stdout, { showCursor: true }) : void 0;
|
|
1777
|
+
let isUnmounted = false;
|
|
1778
|
+
let isUnmounting = false;
|
|
1779
|
+
const isConcurrent = options.concurrent ?? false;
|
|
1780
|
+
let lastOutput = "";
|
|
1781
|
+
let lastOutputToRender = "";
|
|
1782
|
+
let lastOutputHeight = 0;
|
|
1783
|
+
let lastScreen;
|
|
1784
|
+
const windowSize = () => capabilitiesStore.current.size;
|
|
1785
|
+
let lastTerminalWidth = windowSize().columns;
|
|
1786
|
+
let lastTerminalHeight = windowSize().rows;
|
|
1787
|
+
let resizeSettle;
|
|
1788
|
+
let fullStaticOutput = "";
|
|
1789
|
+
let exitResult;
|
|
1790
|
+
let unsubscribeBeforeExit;
|
|
1791
|
+
let restoreConsole;
|
|
1792
|
+
const capturedStdioTails = {
|
|
1793
|
+
stdout: "",
|
|
1794
|
+
stderr: ""
|
|
1795
|
+
};
|
|
1796
|
+
let unsubscribeResize;
|
|
1797
|
+
let kittyProtocolEnabled = false;
|
|
1798
|
+
let kittyFlags;
|
|
1799
|
+
let cancelKittyDetection;
|
|
1800
|
+
let nextRenderCommit;
|
|
1801
|
+
let pauseInput;
|
|
1802
|
+
let resumeInput;
|
|
1803
|
+
const rootTag = isConcurrent ? import_constants.ConcurrentRoot : import_constants.LegacyRoot;
|
|
1804
|
+
const container = reconciler.createContainer(rootNode, rootTag, null, false, null, "id", () => {}, () => {}, () => {}, () => {});
|
|
1805
|
+
const unsubscribeExit = signalExit(unmount, { alwaysLast: false });
|
|
1806
|
+
setAlternateScreen(Boolean(options.alternateScreen));
|
|
1807
|
+
if (isSigilDev) reconciler.injectIntoDevTools();
|
|
1808
|
+
if (options.patchConsole) installConsolePatch();
|
|
1809
|
+
if (interactive) {
|
|
1810
|
+
let seenColumns = lastTerminalWidth;
|
|
1811
|
+
let seenRows = lastTerminalHeight;
|
|
1812
|
+
unsubscribeResize = capabilitiesStore.subscribe(({ size }) => {
|
|
1813
|
+
if (size.columns === seenColumns && size.rows === seenRows) return;
|
|
1814
|
+
seenColumns = size.columns;
|
|
1815
|
+
seenRows = size.rows;
|
|
1816
|
+
resized(size.source);
|
|
1817
|
+
});
|
|
1818
|
+
}
|
|
1819
|
+
initKittyKeyboard();
|
|
1820
|
+
const { promise: exitPromise, resolve: resolveExitPromise, reject: rejectExitPromise } = Promise.withResolvers();
|
|
1821
|
+
exitPromise.catch(noop);
|
|
1822
|
+
function resized(source) {
|
|
1823
|
+
if (resizeSettle !== void 0) {
|
|
1824
|
+
clearTimeout(resizeSettle);
|
|
1825
|
+
resizeSettle = void 0;
|
|
1826
|
+
}
|
|
1827
|
+
if (source === "terminal") {
|
|
1828
|
+
settleResize();
|
|
1829
|
+
return;
|
|
1830
|
+
}
|
|
1831
|
+
resizeSettle = setTimeout(settleResize, RESIZE_SETTLE_MS);
|
|
1832
|
+
}
|
|
1833
|
+
function settleResize() {
|
|
1834
|
+
resizeSettle = void 0;
|
|
1835
|
+
if (isUnmounted || isUnmounting) return;
|
|
1836
|
+
const { columns: currentWidth, rows: currentHeight } = windowSize();
|
|
1837
|
+
if (currentWidth < lastTerminalWidth || currentHeight !== lastTerminalHeight) {
|
|
1838
|
+
clearLiveOutput(rewrapsOnResize() ? currentWidth : void 0);
|
|
1839
|
+
resetLiveOutput();
|
|
1840
|
+
lastOutput = "";
|
|
1841
|
+
lastOutputToRender = "";
|
|
1842
|
+
lastOutputHeight = 0;
|
|
1843
|
+
}
|
|
1844
|
+
lastTerminalWidth = currentWidth;
|
|
1845
|
+
lastTerminalHeight = currentHeight;
|
|
1846
|
+
calculateLayout();
|
|
1847
|
+
emitLayoutListeners(rootNode);
|
|
1848
|
+
onRender();
|
|
1849
|
+
}
|
|
1850
|
+
function rewrapsOnResize() {
|
|
1851
|
+
return !isWindows && capabilitiesStore.current.terminal.name !== "apple-terminal";
|
|
1852
|
+
}
|
|
1853
|
+
function handleAppExit(errorOrResult) {
|
|
1854
|
+
if (isUnmounted || isUnmounting) return;
|
|
1855
|
+
if (isErrorInput(errorOrResult)) {
|
|
1856
|
+
unmount(errorOrResult);
|
|
1857
|
+
return;
|
|
1858
|
+
}
|
|
1859
|
+
exitResult = errorOrResult;
|
|
1860
|
+
unmount();
|
|
1861
|
+
}
|
|
1862
|
+
function setCursorPosition(position) {
|
|
1863
|
+
terminal.setCursor(position);
|
|
1864
|
+
accessiblePresenter?.setCursorPosition(position);
|
|
1865
|
+
}
|
|
1866
|
+
function restoreLastOutput() {
|
|
1867
|
+
if (!interactive) return;
|
|
1868
|
+
if (isScreenReaderEnabled) {
|
|
1869
|
+
accessiblePresenter.setCursorPosition(terminal.cursor.position);
|
|
1870
|
+
accessiblePresenter(lastOutputToRender || lastOutput + "\n");
|
|
1871
|
+
} else if (lastScreen) terminal.present(lastScreen, {
|
|
1872
|
+
fullscreen: lastOutputToRender === lastOutput,
|
|
1873
|
+
forceRewrite: true
|
|
1874
|
+
});
|
|
1875
|
+
}
|
|
1876
|
+
function clearLiveOutput(columns) {
|
|
1877
|
+
if (isScreenReaderEnabled) accessiblePresenter.clear();
|
|
1878
|
+
else terminal.clearFrame({ columns });
|
|
1879
|
+
}
|
|
1880
|
+
function finishLiveOutput() {
|
|
1881
|
+
if (isScreenReaderEnabled) accessiblePresenter.done();
|
|
1882
|
+
else terminal.finishFrame();
|
|
1883
|
+
}
|
|
1884
|
+
function resetLiveOutput() {
|
|
1885
|
+
if (isScreenReaderEnabled) accessiblePresenter.reset();
|
|
1886
|
+
else terminal.resetFrame();
|
|
1887
|
+
}
|
|
1888
|
+
function calculateLayout() {
|
|
1889
|
+
const terminalWidth = windowSize().columns;
|
|
1890
|
+
rootNode.yogaNode.setWidth(terminalWidth);
|
|
1891
|
+
rootNode.yogaNode.calculateLayout(void 0, void 0, Yoga.DIRECTION_LTR);
|
|
1892
|
+
}
|
|
1893
|
+
function handleStaticChange() {
|
|
1894
|
+
fullStaticOutput = "";
|
|
1895
|
+
}
|
|
1896
|
+
function onRender() {
|
|
1897
|
+
renderScheduler.markRendered();
|
|
1898
|
+
if (isUnmounted) return;
|
|
1899
|
+
if (terminal.suspended) {
|
|
1900
|
+
if (nextRenderCommit) {
|
|
1901
|
+
nextRenderCommit.resolve();
|
|
1902
|
+
nextRenderCommit = void 0;
|
|
1903
|
+
}
|
|
1904
|
+
return;
|
|
1905
|
+
}
|
|
1906
|
+
if (resizeSettle !== void 0 && !isUnmounting) {
|
|
1907
|
+
if (nextRenderCommit) {
|
|
1908
|
+
nextRenderCommit.resolve();
|
|
1909
|
+
nextRenderCommit = void 0;
|
|
1910
|
+
}
|
|
1911
|
+
return;
|
|
1912
|
+
}
|
|
1913
|
+
if (nextRenderCommit) {
|
|
1914
|
+
nextRenderCommit.resolve();
|
|
1915
|
+
nextRenderCommit = void 0;
|
|
1916
|
+
}
|
|
1917
|
+
const startTime = performance.now();
|
|
1918
|
+
const rendered = renderFrame(rootNode, isScreenReaderEnabled, {
|
|
1919
|
+
colorProfile: terminal.colorProfile,
|
|
1920
|
+
paintContext: {
|
|
1921
|
+
appearance: capabilitiesStore.current.theme.appearance,
|
|
1922
|
+
palette: capabilitiesStore.current.theme.palette
|
|
1923
|
+
}
|
|
1924
|
+
});
|
|
1925
|
+
const screen = rendered.screen;
|
|
1926
|
+
const output = rendered.accessibleText ?? (screen ? terminal.encode(screen) : "");
|
|
1927
|
+
const outputHeight = rendered.accessibleText === void 0 ? screen?.height ?? 0 : rendered.accessibleText === "" ? 0 : rendered.accessibleText.split("\n").length;
|
|
1928
|
+
const staticBody = rendered.staticAccessibleText ?? (rendered.staticScreen ? terminal.encode(rendered.staticScreen) : "");
|
|
1929
|
+
const staticOutput = staticBody ? `${staticBody}\n` : "";
|
|
1930
|
+
options.onRender?.({ renderTime: performance.now() - startTime });
|
|
1931
|
+
const hasStaticOutput = staticOutput && staticOutput !== "\n";
|
|
1932
|
+
if (options.debug) {
|
|
1933
|
+
if (hasStaticOutput) fullStaticOutput += staticOutput;
|
|
1934
|
+
lastOutput = output;
|
|
1935
|
+
lastOutputToRender = output;
|
|
1936
|
+
lastOutputHeight = outputHeight;
|
|
1937
|
+
options.stdout.write(fullStaticOutput + output);
|
|
1938
|
+
return;
|
|
1939
|
+
}
|
|
1940
|
+
if (!interactive) {
|
|
1941
|
+
if (hasStaticOutput) options.stdout.write(staticOutput);
|
|
1942
|
+
lastOutput = output;
|
|
1943
|
+
lastOutputToRender = output + "\n";
|
|
1944
|
+
lastOutputHeight = outputHeight;
|
|
1945
|
+
return;
|
|
1946
|
+
}
|
|
1947
|
+
if (isScreenReaderEnabled) {
|
|
1948
|
+
const sync = shouldSync();
|
|
1949
|
+
if (sync) options.stdout.write(bsu);
|
|
1950
|
+
if (hasStaticOutput) {
|
|
1951
|
+
const erase = lastOutputHeight > 0 ? ansiEscapes.eraseLines(lastOutputHeight) : "";
|
|
1952
|
+
options.stdout.write(erase + staticOutput);
|
|
1953
|
+
lastOutputHeight = 0;
|
|
1954
|
+
}
|
|
1955
|
+
if (output === lastOutput && !hasStaticOutput) {
|
|
1956
|
+
if (sync) options.stdout.write(esu);
|
|
1957
|
+
return;
|
|
1958
|
+
}
|
|
1959
|
+
const terminalWidth = windowSize().columns;
|
|
1960
|
+
const wrappedOutput = wrapAnsi(output, terminalWidth, {
|
|
1961
|
+
trim: false,
|
|
1962
|
+
hard: true
|
|
1963
|
+
});
|
|
1964
|
+
if (hasStaticOutput) options.stdout.write(wrappedOutput);
|
|
1965
|
+
else {
|
|
1966
|
+
const erase = lastOutputHeight > 0 ? ansiEscapes.eraseLines(lastOutputHeight) : "";
|
|
1967
|
+
options.stdout.write(erase + wrappedOutput);
|
|
1968
|
+
}
|
|
1969
|
+
lastOutput = output;
|
|
1970
|
+
lastOutputToRender = wrappedOutput;
|
|
1971
|
+
lastOutputHeight = wrappedOutput === "" ? 0 : wrappedOutput.split("\n").length;
|
|
1972
|
+
if (sync) options.stdout.write(esu);
|
|
1973
|
+
return;
|
|
1974
|
+
}
|
|
1975
|
+
if (hasStaticOutput) fullStaticOutput += staticOutput;
|
|
1976
|
+
renderInteractiveFrame(output, outputHeight, hasStaticOutput ? staticOutput : "", screen, false);
|
|
1977
|
+
}
|
|
1978
|
+
function render(node) {
|
|
1979
|
+
const tree = /* @__PURE__ */ (0, import_jsx_runtime.jsx)(accessibilityContext.Provider, {
|
|
1980
|
+
value: { isScreenReaderEnabled },
|
|
1981
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TerminalOscContext.Provider, {
|
|
1982
|
+
value: terminalOsc,
|
|
1983
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(App, {
|
|
1984
|
+
stdin: options.stdin,
|
|
1985
|
+
stdout: options.stdout,
|
|
1986
|
+
stderr: options.stderr,
|
|
1987
|
+
exitOnCtrlC: options.exitOnCtrlC,
|
|
1988
|
+
interactive,
|
|
1989
|
+
renderThrottleMs: renderScheduler.intervalMs,
|
|
1990
|
+
terminalInput: terminal.input,
|
|
1991
|
+
writeToStdout,
|
|
1992
|
+
writeToStderr,
|
|
1993
|
+
setCursorPosition,
|
|
1994
|
+
onExit: handleAppExit,
|
|
1995
|
+
onWaitUntilRenderFlush: waitUntilRenderFlush,
|
|
1996
|
+
onSuspendTerminal: suspendTerminal,
|
|
1997
|
+
onRegisterInputControl: registerInputControl,
|
|
1998
|
+
children: node
|
|
1999
|
+
})
|
|
2000
|
+
})
|
|
2001
|
+
});
|
|
2002
|
+
if (isConcurrent) reconciler.updateContainer(tree, container, null, noop);
|
|
2003
|
+
else {
|
|
2004
|
+
reconciler.updateContainerSync(tree, container, null, noop);
|
|
2005
|
+
reconciler.flushSyncWork();
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
function writeToStdout(data) {
|
|
2009
|
+
if (isUnmounted) return;
|
|
2010
|
+
if (terminal.suspended) return;
|
|
2011
|
+
if (options.debug) {
|
|
2012
|
+
options.stdout.write(data + fullStaticOutput + lastOutput);
|
|
2013
|
+
return;
|
|
2014
|
+
}
|
|
2015
|
+
if (!interactive) {
|
|
2016
|
+
options.stdout.write(data);
|
|
2017
|
+
return;
|
|
2018
|
+
}
|
|
2019
|
+
const sync = shouldSync();
|
|
2020
|
+
if (sync) options.stdout.write(bsu);
|
|
2021
|
+
clearLiveOutput();
|
|
2022
|
+
options.stdout.write(data);
|
|
2023
|
+
restoreLastOutput();
|
|
2024
|
+
if (sync) options.stdout.write(esu);
|
|
2025
|
+
}
|
|
2026
|
+
function writeToStderr(data) {
|
|
2027
|
+
if (isUnmounted) return;
|
|
2028
|
+
if (terminal.suspended) return;
|
|
2029
|
+
if (options.debug) {
|
|
2030
|
+
options.stderr.write(data);
|
|
2031
|
+
options.stdout.write(fullStaticOutput + lastOutput);
|
|
2032
|
+
return;
|
|
2033
|
+
}
|
|
2034
|
+
if (!interactive) {
|
|
2035
|
+
options.stderr.write(data);
|
|
2036
|
+
return;
|
|
2037
|
+
}
|
|
2038
|
+
const sync = shouldSync();
|
|
2039
|
+
if (sync) options.stdout.write(bsu);
|
|
2040
|
+
clearLiveOutput();
|
|
2041
|
+
options.stderr.write(data);
|
|
2042
|
+
restoreLastOutput();
|
|
2043
|
+
if (sync) options.stdout.write(esu);
|
|
2044
|
+
}
|
|
2045
|
+
function unmount(error) {
|
|
2046
|
+
if (isUnmounted || isUnmounting) return;
|
|
2047
|
+
isUnmounting = true;
|
|
2048
|
+
if (resizeSettle !== void 0) {
|
|
2049
|
+
clearTimeout(resizeSettle);
|
|
2050
|
+
resizeSettle = void 0;
|
|
2051
|
+
}
|
|
2052
|
+
unsubscribeBeforeExit?.();
|
|
2053
|
+
unsubscribeBeforeExit = void 0;
|
|
2054
|
+
const { canWriteToStdout } = getWritableStreamState(options.stdout);
|
|
2055
|
+
if (canWriteToStdout) flushCapturedStdio();
|
|
2056
|
+
settleThrottle(renderScheduler.throttled, canWriteToStdout);
|
|
2057
|
+
if (canWriteToStdout) {
|
|
2058
|
+
if (!renderScheduler.throttled || !renderScheduler.pending && fullStaticOutput === "") {
|
|
2059
|
+
calculateLayout();
|
|
2060
|
+
onRender();
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
isUnmounted = true;
|
|
2064
|
+
unsubscribeExit();
|
|
2065
|
+
terminal.cleanup();
|
|
2066
|
+
if (typeof restoreConsole === "function") restoreConsole();
|
|
2067
|
+
const finishUnmount = () => {
|
|
2068
|
+
if (typeof unsubscribeResize === "function") unsubscribeResize();
|
|
2069
|
+
if (cancelKittyDetection) cancelKittyDetection();
|
|
2070
|
+
if (canWriteToStdout) {
|
|
2071
|
+
if (kittyProtocolEnabled) writeBestEffort(options.stdout, ansiEscapes.popKittyKeyboard);
|
|
2072
|
+
if (terminal.alternateScreen) {
|
|
2073
|
+
terminal.setAlternateScreen(false);
|
|
2074
|
+
terminal.setCursorAppearance({ visible: true });
|
|
2075
|
+
}
|
|
2076
|
+
if (!interactive) options.stdout.write(options.debug ? "\n" : lastOutput + "\n");
|
|
2077
|
+
else if (!options.debug) finishLiveOutput();
|
|
2078
|
+
}
|
|
2079
|
+
kittyProtocolEnabled = false;
|
|
2080
|
+
instances.delete(captureTargets?.stdout ?? options.stdout);
|
|
2081
|
+
const finalExitResult = exitResult;
|
|
2082
|
+
const resolveOrReject = () => {
|
|
2083
|
+
if (isErrorInput(error)) rejectExitPromise(error);
|
|
2084
|
+
else resolveExitPromise(finalExitResult);
|
|
2085
|
+
};
|
|
2086
|
+
if (error !== void 0 && !isErrorInput(error)) resolveOrReject();
|
|
2087
|
+
else if (canWriteToStdout) options.stdout.write("", resolveOrReject);
|
|
2088
|
+
else setImmediate(resolveOrReject);
|
|
2089
|
+
};
|
|
2090
|
+
const concurrentReconciler = reconciler;
|
|
2091
|
+
if (isConcurrent) {
|
|
2092
|
+
reconciler.updateContainerSync(null, container, null, noop);
|
|
2093
|
+
reconciler.flushSyncWork();
|
|
2094
|
+
concurrentReconciler.flushPassiveEffects?.();
|
|
2095
|
+
finishUnmount();
|
|
2096
|
+
} else {
|
|
2097
|
+
reconciler.updateContainerSync(null, container, null, noop);
|
|
2098
|
+
reconciler.flushSyncWork();
|
|
2099
|
+
finishUnmount();
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
async function waitUntilExit() {
|
|
2103
|
+
if (!unsubscribeBeforeExit) unsubscribeBeforeExit = registerBeforeExit(() => {
|
|
2104
|
+
unmount();
|
|
2105
|
+
});
|
|
2106
|
+
return exitPromise;
|
|
2107
|
+
}
|
|
2108
|
+
async function waitUntilRenderFlush() {
|
|
2109
|
+
if (isUnmounted || isUnmounting) {
|
|
2110
|
+
await awaitExit();
|
|
2111
|
+
return;
|
|
2112
|
+
}
|
|
2113
|
+
await setImmediate$1();
|
|
2114
|
+
if (isUnmounted || isUnmounting) {
|
|
2115
|
+
await awaitExit();
|
|
2116
|
+
return;
|
|
2117
|
+
}
|
|
2118
|
+
if (isConcurrent && hasPendingConcurrentWork()) {
|
|
2119
|
+
await Promise.race([awaitNextRender(), awaitExit()]);
|
|
2120
|
+
if (isUnmounted || isUnmounting) {
|
|
2121
|
+
nextRenderCommit = void 0;
|
|
2122
|
+
await awaitExit();
|
|
2123
|
+
return;
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
reconciler.flushSyncWork();
|
|
2127
|
+
const { canWriteToStdout } = getWritableStreamState(options.stdout);
|
|
2128
|
+
settleThrottle(renderScheduler.throttled, canWriteToStdout);
|
|
2129
|
+
if (canWriteToStdout) {
|
|
2130
|
+
await new Promise((resolve) => {
|
|
2131
|
+
options.stdout.write("", () => {
|
|
2132
|
+
resolve();
|
|
2133
|
+
});
|
|
2134
|
+
});
|
|
2135
|
+
return;
|
|
2136
|
+
}
|
|
2137
|
+
await setImmediate$1();
|
|
2138
|
+
}
|
|
2139
|
+
function clear() {
|
|
2140
|
+
if (interactive && !options.debug) {
|
|
2141
|
+
clearLiveOutput();
|
|
2142
|
+
if (isScreenReaderEnabled) accessiblePresenter.sync(lastOutputToRender || lastOutput + "\n");
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
function installConsolePatch() {
|
|
2146
|
+
if (options.debug) return;
|
|
2147
|
+
const restoreConsoleMethods = patchConsole((stream, data) => {
|
|
2148
|
+
if (options.onCapturedOutput?.(stream, data, "console") === true) return;
|
|
2149
|
+
if (stream === "stdout") writeToStdout(data);
|
|
2150
|
+
if (stream === "stderr") {
|
|
2151
|
+
if (!data.startsWith("The above error occurred")) writeToStderr(data);
|
|
2152
|
+
}
|
|
2153
|
+
});
|
|
2154
|
+
const restoreDirectStdio = patchDirectStdio();
|
|
2155
|
+
restoreConsole = () => {
|
|
2156
|
+
restoreConsoleMethods();
|
|
2157
|
+
restoreDirectStdio?.();
|
|
2158
|
+
};
|
|
2159
|
+
}
|
|
2160
|
+
function patchDirectStdio() {
|
|
2161
|
+
if (!captureTargets) return;
|
|
2162
|
+
const restoreStdout = patchStreamWrite(captureTargets.stdout, (data) => {
|
|
2163
|
+
handleCapturedStdio("stdout", data);
|
|
2164
|
+
});
|
|
2165
|
+
const restoreStderr = patchStreamWrite(captureTargets.stderr, (data) => {
|
|
2166
|
+
handleCapturedStdio("stderr", data);
|
|
2167
|
+
});
|
|
2168
|
+
return () => {
|
|
2169
|
+
restoreStdout();
|
|
2170
|
+
restoreStderr();
|
|
2171
|
+
};
|
|
2172
|
+
}
|
|
2173
|
+
function handleCapturedStdio(stream, data) {
|
|
2174
|
+
if (options.onCapturedOutput?.(stream, data, "stdio") === true) return;
|
|
2175
|
+
const parts = (capturedStdioTails[stream] + data).split(/\r?\n/);
|
|
2176
|
+
capturedStdioTails[stream] = parts.pop() ?? "";
|
|
2177
|
+
if (parts.length === 0) return;
|
|
2178
|
+
const payload = parts.join("\n") + "\n";
|
|
2179
|
+
if (stream === "stdout") writeToStdout(payload);
|
|
2180
|
+
else writeToStderr(payload);
|
|
2181
|
+
}
|
|
2182
|
+
function flushCapturedStdio() {
|
|
2183
|
+
for (const stream of ["stdout", "stderr"]) {
|
|
2184
|
+
const tail = capturedStdioTails[stream];
|
|
2185
|
+
if (tail === "") continue;
|
|
2186
|
+
capturedStdioTails[stream] = "";
|
|
2187
|
+
if (stream === "stdout") writeToStdout(tail + "\n");
|
|
2188
|
+
else writeToStderr(tail + "\n");
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
function registerInputControl(pause, resume) {
|
|
2192
|
+
pauseInput = pause;
|
|
2193
|
+
resumeInput = resume;
|
|
2194
|
+
}
|
|
2195
|
+
async function suspendTerminal(callback) {
|
|
2196
|
+
beginSuspend();
|
|
2197
|
+
if (callback) {
|
|
2198
|
+
try {
|
|
2199
|
+
await callback();
|
|
2200
|
+
} finally {
|
|
2201
|
+
await endSuspend();
|
|
2202
|
+
}
|
|
2203
|
+
return;
|
|
2204
|
+
}
|
|
2205
|
+
const resume = async () => {
|
|
2206
|
+
await endSuspend();
|
|
2207
|
+
};
|
|
2208
|
+
return {
|
|
2209
|
+
resume,
|
|
2210
|
+
[Symbol.asyncDispose]: resume
|
|
2211
|
+
};
|
|
2212
|
+
}
|
|
2213
|
+
function setAlternateScreen(enabled) {
|
|
2214
|
+
terminal.setAlternateScreen(enabled && interactive && Boolean(options.stdout.isTTY), { hideCursor: true });
|
|
2215
|
+
}
|
|
2216
|
+
function shouldSync() {
|
|
2217
|
+
return Boolean(options.stdout.isTTY) && interactive;
|
|
2218
|
+
}
|
|
2219
|
+
async function awaitExit() {
|
|
2220
|
+
try {
|
|
2221
|
+
await exitPromise;
|
|
2222
|
+
} catch {}
|
|
2223
|
+
}
|
|
2224
|
+
function hasPendingConcurrentWork() {
|
|
2225
|
+
const concurrentContainer = container;
|
|
2226
|
+
return (concurrentContainer.pendingLanes ?? 0) !== 0 && concurrentContainer.callbackNode !== void 0 && concurrentContainer.callbackNode !== null;
|
|
2227
|
+
}
|
|
2228
|
+
async function awaitNextRender() {
|
|
2229
|
+
nextRenderCommit ??= Promise.withResolvers();
|
|
2230
|
+
return nextRenderCommit.promise;
|
|
2231
|
+
}
|
|
2232
|
+
function renderInteractiveFrame(output, outputHeight, staticOutput, screen, hasOverflow) {
|
|
2233
|
+
if (!screen) return;
|
|
2234
|
+
const hasStaticOutput = staticOutput !== "";
|
|
2235
|
+
const isTTY = Boolean(options.stdout.isTTY);
|
|
2236
|
+
const viewportRows = isTTY ? windowSize().rows : 24;
|
|
2237
|
+
if (isTTY && outputHeight > viewportRows) {
|
|
2238
|
+
const lines = output.split("\n");
|
|
2239
|
+
output = lines.slice(lines.length - viewportRows).join("\n");
|
|
2240
|
+
outputHeight = viewportRows;
|
|
2241
|
+
screen = bottomRows(screen, viewportRows);
|
|
2242
|
+
}
|
|
2243
|
+
const isFullscreen = isTTY && outputHeight >= viewportRows;
|
|
2244
|
+
const outputToRender = isFullscreen ? output : output + "\n";
|
|
2245
|
+
if (shouldClearTerminalForFrame({
|
|
2246
|
+
isTTY,
|
|
2247
|
+
viewportRows,
|
|
2248
|
+
previousOutputHeight: lastOutputHeight,
|
|
2249
|
+
nextOutputHeight: outputHeight,
|
|
2250
|
+
isUnmounting
|
|
2251
|
+
})) {
|
|
2252
|
+
const sync = shouldSync();
|
|
2253
|
+
if (sync) options.stdout.write(bsu);
|
|
2254
|
+
options.stdout.write(ansiEscapes.clearTerminal + fullStaticOutput + outputToRender);
|
|
2255
|
+
lastOutput = output;
|
|
2256
|
+
lastOutputToRender = outputToRender;
|
|
2257
|
+
lastOutputHeight = outputHeight;
|
|
2258
|
+
lastScreen = screen;
|
|
2259
|
+
terminal.resetFrame();
|
|
2260
|
+
if (sync) options.stdout.write(esu);
|
|
2261
|
+
return;
|
|
2262
|
+
}
|
|
2263
|
+
const willPresent = terminal.willPresent(screen, {
|
|
2264
|
+
fullscreen: isFullscreen,
|
|
2265
|
+
forceRewrite: hasStaticOutput || hasOverflow
|
|
2266
|
+
});
|
|
2267
|
+
const sync = shouldSync() && willPresent;
|
|
2268
|
+
if (sync) terminal.write(bsu);
|
|
2269
|
+
if (hasStaticOutput) {
|
|
2270
|
+
terminal.clearFrame();
|
|
2271
|
+
terminal.write(staticOutput);
|
|
2272
|
+
}
|
|
2273
|
+
terminal.present(screen, {
|
|
2274
|
+
fullscreen: isFullscreen,
|
|
2275
|
+
forceRewrite: hasStaticOutput || hasOverflow
|
|
2276
|
+
});
|
|
2277
|
+
if (sync) terminal.write(esu);
|
|
2278
|
+
lastOutput = output;
|
|
2279
|
+
lastOutputToRender = outputToRender;
|
|
2280
|
+
lastOutputHeight = outputHeight;
|
|
2281
|
+
lastScreen = screen;
|
|
2282
|
+
}
|
|
2283
|
+
function initKittyKeyboard() {
|
|
2284
|
+
if (!options.kittyKeyboard) return;
|
|
2285
|
+
const opts = options.kittyKeyboard;
|
|
2286
|
+
const mode = opts.mode ?? "auto";
|
|
2287
|
+
if (mode === "disabled") return;
|
|
2288
|
+
const flags = opts.flags ?? ["disambiguateEscapeCodes"];
|
|
2289
|
+
if (mode === "enabled") {
|
|
2290
|
+
if (isTty(options.stdin) && options.stdout.isTTY) enableKittyProtocol(flags);
|
|
2291
|
+
return;
|
|
2292
|
+
}
|
|
2293
|
+
if (!interactive || !isTty(options.stdin) || !options.stdout.isTTY) return;
|
|
2294
|
+
cancelKittyDetection = detectKittySupport(options.stdin, options.stdout, () => {
|
|
2295
|
+
cancelKittyDetection = void 0;
|
|
2296
|
+
if (!isUnmounted) enableKittyProtocol(flags);
|
|
2297
|
+
});
|
|
2298
|
+
}
|
|
2299
|
+
function enableKittyProtocol(flags) {
|
|
2300
|
+
options.stdout.write(ansiEscapes.pushKittyKeyboard(resolveFlags(flags)));
|
|
2301
|
+
kittyProtocolEnabled = true;
|
|
2302
|
+
kittyFlags = flags;
|
|
2303
|
+
}
|
|
2304
|
+
function beginSuspend() {
|
|
2305
|
+
terminal.beginSuspension();
|
|
2306
|
+
if (!interactive || isUnmounted || isUnmounting) return;
|
|
2307
|
+
try {
|
|
2308
|
+
const { canWriteToStdout } = getWritableStreamState(options.stdout);
|
|
2309
|
+
settleThrottle(renderScheduler.throttled, canWriteToStdout);
|
|
2310
|
+
if (canWriteToStdout) flushCapturedStdio();
|
|
2311
|
+
if (canWriteToStdout) {
|
|
2312
|
+
clearLiveOutput();
|
|
2313
|
+
finishLiveOutput();
|
|
2314
|
+
if (kittyProtocolEnabled) writeBestEffort(options.stdout, ansiEscapes.popKittyKeyboard);
|
|
2315
|
+
if (terminal.alternateScreen) writeBestEffort(options.stdout, ansiEscapes.exitAlternativeScreen);
|
|
2316
|
+
}
|
|
2317
|
+
pauseInput?.();
|
|
2318
|
+
} catch (error) {
|
|
2319
|
+
terminal.resume();
|
|
2320
|
+
try {
|
|
2321
|
+
resumeInput?.();
|
|
2322
|
+
} catch {}
|
|
2323
|
+
throw error;
|
|
2324
|
+
}
|
|
2325
|
+
}
|
|
2326
|
+
async function endSuspend() {
|
|
2327
|
+
if (!terminal.suspended) return;
|
|
2328
|
+
terminal.resume();
|
|
2329
|
+
resumeInput?.();
|
|
2330
|
+
if (!interactive || isUnmounted || isUnmounting) return;
|
|
2331
|
+
const { canWriteToStdout } = getWritableStreamState(options.stdout);
|
|
2332
|
+
if (canWriteToStdout) {
|
|
2333
|
+
if (terminal.alternateScreen) writeBestEffort(options.stdout, ansiEscapes.enterAlternativeScreen);
|
|
2334
|
+
if (kittyProtocolEnabled && kittyFlags) writeBestEffort(options.stdout, ansiEscapes.pushKittyKeyboard(resolveFlags(kittyFlags)));
|
|
2335
|
+
}
|
|
2336
|
+
lastOutput = "";
|
|
2337
|
+
lastOutputToRender = "";
|
|
2338
|
+
lastOutputHeight = 0;
|
|
2339
|
+
resetLiveOutput();
|
|
2340
|
+
try {
|
|
2341
|
+
calculateLayout();
|
|
2342
|
+
onRender();
|
|
2343
|
+
await waitUntilRenderFlush();
|
|
2344
|
+
} catch {}
|
|
2345
|
+
}
|
|
2346
|
+
return {
|
|
2347
|
+
render,
|
|
2348
|
+
unmount,
|
|
2349
|
+
waitUntilExit,
|
|
2350
|
+
waitUntilRenderFlush,
|
|
2351
|
+
clear,
|
|
2352
|
+
copyToClipboard: (text, selection) => terminal.copyToClipboard(text, selection),
|
|
2353
|
+
setProgress: (state, value) => terminal.setProgress(state, value)
|
|
2354
|
+
};
|
|
2355
|
+
};
|
|
2356
|
+
//#endregion
|
|
2357
|
+
//#region src/render.ts
|
|
2358
|
+
/**
|
|
2359
|
+
Mount a component and render the output.
|
|
2360
|
+
*/
|
|
2361
|
+
const render = (node, options) => {
|
|
2362
|
+
const inkOptions = {
|
|
2363
|
+
stdout: process.stdout,
|
|
2364
|
+
stdin: process.stdin,
|
|
2365
|
+
stderr: process.stderr,
|
|
2366
|
+
debug: false,
|
|
2367
|
+
exitOnCtrlC: true,
|
|
2368
|
+
patchConsole: true,
|
|
2369
|
+
maxFps: 30,
|
|
2370
|
+
concurrent: false,
|
|
2371
|
+
alternateScreen: false,
|
|
2372
|
+
...getOptions(options)
|
|
2373
|
+
};
|
|
2374
|
+
const instance = getInstance(inkOptions.stdout, () => createInk(inkOptions));
|
|
2375
|
+
instance.render(node);
|
|
2376
|
+
return {
|
|
2377
|
+
rerender: instance.render,
|
|
2378
|
+
unmount() {
|
|
2379
|
+
instance.unmount();
|
|
2380
|
+
},
|
|
2381
|
+
waitUntilExit: instance.waitUntilExit,
|
|
2382
|
+
waitUntilRenderFlush: instance.waitUntilRenderFlush,
|
|
2383
|
+
cleanup() {
|
|
2384
|
+
instance.unmount();
|
|
2385
|
+
},
|
|
2386
|
+
clear: instance.clear,
|
|
2387
|
+
copyToClipboard: instance.copyToClipboard,
|
|
2388
|
+
setProgress: instance.setProgress
|
|
2389
|
+
};
|
|
2390
|
+
};
|
|
2391
|
+
const getOptions = (stdout = {}) => {
|
|
2392
|
+
if (stdout instanceof Stream) return {
|
|
2393
|
+
stdout,
|
|
2394
|
+
stdin: process.stdin
|
|
2395
|
+
};
|
|
2396
|
+
return stdout;
|
|
2397
|
+
};
|
|
2398
|
+
const getInstance = (stdout, createInstance) => {
|
|
2399
|
+
const instance = instances.get(stdout);
|
|
2400
|
+
if (instance === void 0) {
|
|
2401
|
+
const newInstance = createInstance();
|
|
2402
|
+
instances.set(stdout, newInstance);
|
|
2403
|
+
return newInstance;
|
|
2404
|
+
}
|
|
2405
|
+
process.stderr.write("Warning: render() was called again for the same stdout before the previous Ink instance was unmounted. Reusing stdout across multiple render() calls is unsupported. Call unmount() first.\n");
|
|
2406
|
+
return instance;
|
|
2407
|
+
};
|
|
2408
|
+
//#endregion
|
|
2409
|
+
//#region src/render-to-string.ts
|
|
2410
|
+
/**
|
|
2411
|
+
Render a React element to a string synchronously. Unlike `render()`, this function does not write to stdout, does not set up any terminal event listeners, and returns the rendered output as a string.
|
|
2412
|
+
|
|
2413
|
+
Useful for generating documentation, writing output to files, testing, or any scenario where you need the rendered output as a string without starting a persistent terminal application.
|
|
2414
|
+
|
|
2415
|
+
**Notes:**
|
|
2416
|
+
|
|
2417
|
+
- Terminal-specific hooks (`useInput`, `useStdin`, `useStdout`, `useStderr`, `useApp`, `useFocus`, `useFocusManager`) return default no-op values since there is no terminal session. They will not throw, but they will not function as in a live terminal.
|
|
2418
|
+
- `useEffect` callbacks will execute during rendering (due to synchronous rendering mode), but state updates they trigger will not affect the returned output, which reflects the initial render.
|
|
2419
|
+
- `useLayoutEffect` callbacks fire synchronously during commit, so state updates they trigger **will** be reflected in the output.
|
|
2420
|
+
- The `<Static>` component is supported — its output is prepended to the dynamic output.
|
|
2421
|
+
- If a component throws during rendering, the error is propagated to the caller after cleanup.
|
|
2422
|
+
|
|
2423
|
+
@example
|
|
2424
|
+
```
|
|
2425
|
+
import {renderToString, Text, Box} from 'ink';
|
|
2426
|
+
|
|
2427
|
+
const output = renderToString(
|
|
2428
|
+
<Box padding={1}>
|
|
2429
|
+
<Text color="green">Hello World</Text>
|
|
2430
|
+
</Box>,
|
|
2431
|
+
{columns: 40}
|
|
2432
|
+
);
|
|
2433
|
+
|
|
2434
|
+
console.log(output);
|
|
2435
|
+
```
|
|
2436
|
+
*/
|
|
2437
|
+
const renderToString = (node, options) => {
|
|
2438
|
+
const columns = options?.columns ?? 80;
|
|
2439
|
+
const colorProfile = options?.colorProfile ?? colorProfileFromLevel(detectColorLevel());
|
|
2440
|
+
const rootNode = createNode("ink-root");
|
|
2441
|
+
let capturedStaticOutput = "";
|
|
2442
|
+
rootNode.onComputeLayout = () => {
|
|
2443
|
+
rootNode.yogaNode.setWidth(columns);
|
|
2444
|
+
rootNode.yogaNode.calculateLayout(void 0, void 0, Yoga.DIRECTION_LTR);
|
|
2445
|
+
};
|
|
2446
|
+
rootNode.onImmediateRender = () => {
|
|
2447
|
+
const { staticScreen } = renderFrame(rootNode, false, { colorProfile });
|
|
2448
|
+
const staticOutput = staticScreen ? `${serializeScreen(staticScreen, {
|
|
2449
|
+
colorProfile,
|
|
2450
|
+
styles: colorProfile !== "none"
|
|
2451
|
+
})}\n` : "";
|
|
2452
|
+
if (staticOutput && staticOutput !== "\n") capturedStaticOutput += staticOutput;
|
|
2453
|
+
};
|
|
2454
|
+
let uncaughtError;
|
|
2455
|
+
const container = reconciler.createContainer(rootNode, import_constants.LegacyRoot, null, false, null, "render-to-string", (error) => {
|
|
2456
|
+
uncaughtError ??= error;
|
|
2457
|
+
}, () => {}, () => {}, () => {});
|
|
2458
|
+
reconciler.updateContainerSync(node, container, null, () => {});
|
|
2459
|
+
reconciler.flushSyncWork();
|
|
2460
|
+
const { screen } = renderFrame(rootNode, false, { colorProfile });
|
|
2461
|
+
const output = screen ? serializeScreen(screen, {
|
|
2462
|
+
colorProfile,
|
|
2463
|
+
styles: colorProfile !== "none"
|
|
2464
|
+
}) : "";
|
|
2465
|
+
reconciler.updateContainerSync(null, container, null, () => {});
|
|
2466
|
+
reconciler.flushSyncWork();
|
|
2467
|
+
if (uncaughtError !== void 0) throw uncaughtError instanceof Error ? uncaughtError : new Error(String(uncaughtError));
|
|
2468
|
+
const normalizedStaticOutput = capturedStaticOutput.endsWith("\n") ? capturedStaticOutput.slice(0, -1) : capturedStaticOutput;
|
|
2469
|
+
if (normalizedStaticOutput && output) return normalizedStaticOutput + "\n" + output;
|
|
2470
|
+
return normalizedStaticOutput || output;
|
|
2471
|
+
};
|
|
2472
|
+
//#endregion
|
|
2473
|
+
//#region src/components/AnsiText.tsx
|
|
2474
|
+
/** @jsxImportSource react */
|
|
2475
|
+
/**
|
|
2476
|
+
* Renders explicitly trusted ANSI-styled output as structured terminal cells.
|
|
2477
|
+
* Ordinary `Text` continues to strip terminal control sequences.
|
|
2478
|
+
*/
|
|
2479
|
+
function AnsiText({ children, wrap = "wrap", "aria-label": ariaLabel, "aria-hidden": ariaHidden = false }) {
|
|
2480
|
+
const { isScreenReaderEnabled } = (0, import_react.useContext)(accessibilityContext);
|
|
2481
|
+
if (isScreenReaderEnabled) {
|
|
2482
|
+
if (ariaHidden) return null;
|
|
2483
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: ariaLabel ?? stripAnsi(children) });
|
|
2484
|
+
}
|
|
2485
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ink-text", {
|
|
2486
|
+
style: {
|
|
2487
|
+
flexGrow: 0,
|
|
2488
|
+
flexShrink: 1,
|
|
2489
|
+
flexDirection: "row",
|
|
2490
|
+
textWrap: wrap
|
|
2491
|
+
},
|
|
2492
|
+
internal_ansi: true,
|
|
2493
|
+
children
|
|
2494
|
+
});
|
|
2495
|
+
}
|
|
2496
|
+
//#endregion
|
|
2497
|
+
//#region src/components/Static.tsx
|
|
2498
|
+
/** @jsxImportSource react */
|
|
2499
|
+
/**
|
|
2500
|
+
`<Static>` component permanently renders its output above everything else. It's useful for displaying activity like completed tasks or logs—things that don't change after they're rendered (hence the name "Static").
|
|
2501
|
+
|
|
2502
|
+
It's preferred to use `<Static>` for use cases like these when you can't know or control the number of items that need to be rendered.
|
|
2503
|
+
|
|
2504
|
+
For example, [Tap](https://github.com/tapjs/node-tap) uses `<Static>` to display a list of completed tests. [Gatsby](https://github.com/gatsbyjs/gatsby) uses it to display a list of generated pages while still displaying a live progress bar.
|
|
2505
|
+
*/
|
|
2506
|
+
function Static(props) {
|
|
2507
|
+
const { items, children: render, style: customStyle } = props;
|
|
2508
|
+
const [index, setIndex] = (0, import_react.useState)(0);
|
|
2509
|
+
const itemsToRender = (0, import_react.useMemo)(() => {
|
|
2510
|
+
return items.slice(index);
|
|
2511
|
+
}, [items, index]);
|
|
2512
|
+
(0, import_react.useLayoutEffect)(() => {
|
|
2513
|
+
setIndex(items.length);
|
|
2514
|
+
}, [items.length]);
|
|
2515
|
+
const children = itemsToRender.map((item, itemIndex) => {
|
|
2516
|
+
return render(item, index + itemIndex);
|
|
2517
|
+
});
|
|
2518
|
+
const style = (0, import_react.useMemo)(() => ({
|
|
2519
|
+
position: "absolute",
|
|
2520
|
+
flexDirection: "column",
|
|
2521
|
+
...customStyle
|
|
2522
|
+
}), [customStyle]);
|
|
2523
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ink-box", {
|
|
2524
|
+
internal_static: true,
|
|
2525
|
+
style,
|
|
2526
|
+
children
|
|
2527
|
+
});
|
|
2528
|
+
}
|
|
2529
|
+
//#endregion
|
|
2530
|
+
//#region src/components/Transform.tsx
|
|
2531
|
+
/** @jsxImportSource react */
|
|
2532
|
+
/**
|
|
2533
|
+
Transform a string representation of React components before they're written to output. For example, you might want to apply a gradient to text, add a clickable link, or create some text effects. These use cases can't accept React nodes as input; they expect a string. That's what the <Transform> component does: it gives you an output string of its child components and lets you transform it in any way.
|
|
2534
|
+
*/
|
|
2535
|
+
function Transform({ children, transform, accessibilityLabel }) {
|
|
2536
|
+
const { isScreenReaderEnabled } = (0, import_react.useContext)(accessibilityContext);
|
|
2537
|
+
if (children === void 0 || children === null) return null;
|
|
2538
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ink-text", {
|
|
2539
|
+
style: {
|
|
2540
|
+
flexGrow: 0,
|
|
2541
|
+
flexShrink: 1,
|
|
2542
|
+
flexDirection: "row"
|
|
2543
|
+
},
|
|
2544
|
+
internal_transform: transform,
|
|
2545
|
+
children: isScreenReaderEnabled && accessibilityLabel ? accessibilityLabel : children
|
|
2546
|
+
});
|
|
2547
|
+
}
|
|
2548
|
+
//#endregion
|
|
2549
|
+
//#region src/hooks/use-stdout.ts
|
|
2550
|
+
/**
|
|
2551
|
+
A React hook that returns the stdout stream where Ink renders your app.
|
|
2552
|
+
*/
|
|
2553
|
+
const useStdout = () => (0, import_react.useContext)(StdoutContext);
|
|
2554
|
+
//#endregion
|
|
2555
|
+
//#region src/components/Hyperlink.tsx
|
|
2556
|
+
/** @jsxImportSource react */
|
|
2557
|
+
/**
|
|
2558
|
+
A clickable OSC 8 hyperlink — the counterpart to the router's `<Link>`, which
|
|
2559
|
+
navigates between screens. On terminals without hyperlink support it falls
|
|
2560
|
+
back to `text (url)`.
|
|
2561
|
+
|
|
2562
|
+
```tsx
|
|
2563
|
+
<Hyperlink url="https://example.com">Documentation</Hyperlink>
|
|
2564
|
+
```
|
|
2565
|
+
*/
|
|
2566
|
+
function Hyperlink({ url, fallback = true, children, ...textProps }) {
|
|
2567
|
+
const { stdout } = useStdout();
|
|
2568
|
+
if (!detectHyperlinkSupport(stdout)) return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
|
|
2569
|
+
...textProps,
|
|
2570
|
+
children: [children, fallback ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
|
|
2571
|
+
dimColor: true,
|
|
2572
|
+
children: [
|
|
2573
|
+
" (",
|
|
2574
|
+
url,
|
|
2575
|
+
")"
|
|
2576
|
+
]
|
|
2577
|
+
}) : null]
|
|
2578
|
+
});
|
|
2579
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Transform, {
|
|
2580
|
+
transform: (text) => link(text, url),
|
|
2581
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
|
|
2582
|
+
...textProps,
|
|
2583
|
+
children
|
|
2584
|
+
})
|
|
2585
|
+
});
|
|
2586
|
+
}
|
|
2587
|
+
//#endregion
|
|
2588
|
+
//#region src/components/Newline.tsx
|
|
2589
|
+
/**
|
|
2590
|
+
Adds one or more newline (`\n`) characters. Must be used within `<Text>` components.
|
|
2591
|
+
*/
|
|
2592
|
+
function Newline({ count = 1 }) {
|
|
2593
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ink-text", { children: "\n".repeat(count) });
|
|
2594
|
+
}
|
|
2595
|
+
//#endregion
|
|
2596
|
+
//#region src/components/Spacer.tsx
|
|
2597
|
+
/** @jsxImportSource react */
|
|
2598
|
+
/**
|
|
2599
|
+
A flexible space that expands along the major axis of its containing layout.
|
|
2600
|
+
|
|
2601
|
+
It's useful as a shortcut for filling all the available space between elements.
|
|
2602
|
+
*/
|
|
2603
|
+
function Spacer() {
|
|
2604
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, { flexGrow: 1 });
|
|
2605
|
+
}
|
|
2606
|
+
//#endregion
|
|
2607
|
+
//#region src/virtual-scroll.ts
|
|
2608
|
+
const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
|
|
2609
|
+
const wholeRows = (value) => Math.max(0, Math.floor(value)) || 0;
|
|
2610
|
+
/**
|
|
2611
|
+
Compute which items intersect a viewport over a vertical stack of items.
|
|
2612
|
+
*/
|
|
2613
|
+
const virtualScrollWindow = (options) => {
|
|
2614
|
+
const count = wholeRows(options.count);
|
|
2615
|
+
const viewportHeight = wholeRows(options.viewportHeight);
|
|
2616
|
+
const tops = [];
|
|
2617
|
+
let totalHeight = 0;
|
|
2618
|
+
for (let index = 0; index < count; index++) {
|
|
2619
|
+
tops.push(totalHeight);
|
|
2620
|
+
totalHeight += wholeRows(options.itemHeight(index));
|
|
2621
|
+
}
|
|
2622
|
+
const bottomOf = (index) => index + 1 < count ? tops[index + 1] : totalHeight;
|
|
2623
|
+
const maxScrollTop = Math.max(0, totalHeight - viewportHeight);
|
|
2624
|
+
let scrollTop = clamp(Math.floor(options.scrollTop) || 0, 0, maxScrollTop);
|
|
2625
|
+
const focused = options.focusedIndex;
|
|
2626
|
+
if (focused !== void 0 && Number.isInteger(focused) && focused >= 0 && focused < count) {
|
|
2627
|
+
const top = tops[focused];
|
|
2628
|
+
const bottom = bottomOf(focused);
|
|
2629
|
+
if (top < scrollTop) scrollTop = top;
|
|
2630
|
+
else if (bottom > scrollTop + viewportHeight) scrollTop = Math.min(top, bottom - viewportHeight);
|
|
2631
|
+
scrollTop = clamp(scrollTop, 0, maxScrollTop);
|
|
2632
|
+
}
|
|
2633
|
+
const viewportBottom = scrollTop + viewportHeight;
|
|
2634
|
+
let start = 0;
|
|
2635
|
+
while (start < count && bottomOf(start) <= scrollTop) start++;
|
|
2636
|
+
let end = start;
|
|
2637
|
+
while (end < count && tops[end] < viewportBottom) end++;
|
|
2638
|
+
return {
|
|
2639
|
+
start,
|
|
2640
|
+
end,
|
|
2641
|
+
scrollTop,
|
|
2642
|
+
maxScrollTop,
|
|
2643
|
+
totalHeight,
|
|
2644
|
+
offset: start < end ? tops[start] - scrollTop : 0,
|
|
2645
|
+
hiddenAbove: scrollTop,
|
|
2646
|
+
hiddenBelow: Math.max(0, totalHeight - viewportBottom)
|
|
2647
|
+
};
|
|
2648
|
+
};
|
|
2649
|
+
//#endregion
|
|
2650
|
+
//#region src/hooks/use-virtual-scroll.ts
|
|
2651
|
+
/**
|
|
2652
|
+
A React hook that owns the scroll position of a windowed list and returns which
|
|
2653
|
+
items to render for it. The position is clamped to the scrollable range and,
|
|
2654
|
+
while `focusedIndex` is set, moved as little as necessary to keep that item
|
|
2655
|
+
fully visible. Render the items in `[start, end)` inside an `overflowY="hidden"`
|
|
2656
|
+
box of `viewportHeight` rows, shifted up by `offset` rows.
|
|
2657
|
+
|
|
2658
|
+
`<VirtualList>` wraps this hook; use it directly to draw your own chrome such
|
|
2659
|
+
as overflow markers or a scrollbar around the window.
|
|
2660
|
+
|
|
2661
|
+
@example
|
|
2662
|
+
```tsx
|
|
2663
|
+
import { Box, Text, useVirtualScroll } from "@alchemy.run/sigil";
|
|
2664
|
+
|
|
2665
|
+
const Example = ({ lines, cursor }: { lines: string[]; cursor: number }) => {
|
|
2666
|
+
const { start, end, offset, hiddenAbove, hiddenBelow } = useVirtualScroll({
|
|
2667
|
+
count: lines.length,
|
|
2668
|
+
itemHeight: () => 1,
|
|
2669
|
+
viewportHeight: 10,
|
|
2670
|
+
focusedIndex: cursor,
|
|
2671
|
+
});
|
|
2672
|
+
|
|
2673
|
+
return (
|
|
2674
|
+
<Box flexDirection="column">
|
|
2675
|
+
<Text dimColor>{hiddenAbove > 0 ? `↑ ${hiddenAbove} more` : ""}</Text>
|
|
2676
|
+
<Box flexDirection="column" height={10} overflowY="hidden">
|
|
2677
|
+
<Box flexDirection="column" flexShrink={0} marginTop={offset}>
|
|
2678
|
+
{lines.slice(start, end).map((line, index) => (
|
|
2679
|
+
<Text key={start + index} inverse={start + index === cursor}>
|
|
2680
|
+
{line}
|
|
2681
|
+
</Text>
|
|
2682
|
+
))}
|
|
2683
|
+
</Box>
|
|
2684
|
+
</Box>
|
|
2685
|
+
<Text dimColor>{hiddenBelow > 0 ? `↓ ${hiddenBelow} more` : ""}</Text>
|
|
2686
|
+
</Box>
|
|
2687
|
+
);
|
|
2688
|
+
};
|
|
2689
|
+
```
|
|
2690
|
+
*/
|
|
2691
|
+
const useVirtualScroll = (options) => {
|
|
2692
|
+
const [position, setPosition] = (0, import_react.useState)(0);
|
|
2693
|
+
const window = virtualScrollWindow({
|
|
2694
|
+
...options,
|
|
2695
|
+
scrollTop: position
|
|
2696
|
+
});
|
|
2697
|
+
(0, import_react.useEffect)(() => {
|
|
2698
|
+
if (window.scrollTop !== position) setPosition(window.scrollTop);
|
|
2699
|
+
}, [window.scrollTop, position]);
|
|
2700
|
+
const scrollTo = (0, import_react.useCallback)((top) => {
|
|
2701
|
+
setPosition(top);
|
|
2702
|
+
}, []);
|
|
2703
|
+
const scrollBy = (0, import_react.useCallback)((delta) => {
|
|
2704
|
+
setPosition((current) => current + delta);
|
|
2705
|
+
}, []);
|
|
2706
|
+
return {
|
|
2707
|
+
...window,
|
|
2708
|
+
scrollTo,
|
|
2709
|
+
scrollBy
|
|
2710
|
+
};
|
|
2711
|
+
};
|
|
2712
|
+
//#endregion
|
|
2713
|
+
//#region src/components/VirtualList.tsx
|
|
2714
|
+
/** @jsxImportSource react */
|
|
2715
|
+
const rootOf = (node) => {
|
|
2716
|
+
let current = node;
|
|
2717
|
+
while (current?.parentNode) current = current.parentNode;
|
|
2718
|
+
return current?.nodeName === "ink-root" ? current : void 0;
|
|
2719
|
+
};
|
|
2720
|
+
/**
|
|
2721
|
+
A vertically windowed list: only the items intersecting the viewport are
|
|
2722
|
+
rendered, inside a clipped box that scrolls by whole rows. Items may have
|
|
2723
|
+
different heights, and the item at the top edge may be partially visible.
|
|
2724
|
+
|
|
2725
|
+
Until the first layout pass has measured the viewport, every item is rendered
|
|
2726
|
+
inside the clipped box so the first frame already looks right.
|
|
2727
|
+
*/
|
|
2728
|
+
function VirtualList({ items, itemHeight, renderItem, getKey, focusedIndex, height }) {
|
|
2729
|
+
const ref = (0, import_react.useRef)(null);
|
|
2730
|
+
const [measured, setMeasured] = (0, import_react.useState)();
|
|
2731
|
+
const measure = () => {
|
|
2732
|
+
const next = ref.current?.yogaNode?.getComputedHeight();
|
|
2733
|
+
if (next !== void 0) setMeasured((previous) => previous === next ? previous : next);
|
|
2734
|
+
};
|
|
2735
|
+
(0, import_react.useLayoutEffect)(() => {
|
|
2736
|
+
if (height === void 0) measure();
|
|
2737
|
+
});
|
|
2738
|
+
(0, import_react.useEffect)(() => {
|
|
2739
|
+
if (height !== void 0) return;
|
|
2740
|
+
const root = rootOf(ref.current);
|
|
2741
|
+
return root ? addLayoutListener(root, measure) : void 0;
|
|
2742
|
+
});
|
|
2743
|
+
const viewportHeight = height ?? measured;
|
|
2744
|
+
const windowed = viewportHeight !== void 0;
|
|
2745
|
+
const window = useVirtualScroll({
|
|
2746
|
+
count: items.length,
|
|
2747
|
+
itemHeight: (index) => itemHeight(items[index], index),
|
|
2748
|
+
viewportHeight: viewportHeight ?? 0,
|
|
2749
|
+
focusedIndex: windowed ? focusedIndex : void 0
|
|
2750
|
+
});
|
|
2751
|
+
const start = windowed ? window.start : 0;
|
|
2752
|
+
const end = windowed ? window.end : items.length;
|
|
2753
|
+
const offset = windowed ? window.offset : 0;
|
|
2754
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
|
|
2755
|
+
ref,
|
|
2756
|
+
flexDirection: "column",
|
|
2757
|
+
overflowY: "hidden",
|
|
2758
|
+
...height === void 0 ? {
|
|
2759
|
+
height: window.totalHeight,
|
|
2760
|
+
minHeight: 0,
|
|
2761
|
+
flexShrink: 1
|
|
2762
|
+
} : {
|
|
2763
|
+
height,
|
|
2764
|
+
flexShrink: 0
|
|
2765
|
+
},
|
|
2766
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
|
|
2767
|
+
flexDirection: "column",
|
|
2768
|
+
flexShrink: 0,
|
|
2769
|
+
marginTop: offset,
|
|
2770
|
+
children: items.slice(start, end).map((item, sliceIndex) => {
|
|
2771
|
+
const index = start + sliceIndex;
|
|
2772
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
|
|
2773
|
+
flexDirection: "column",
|
|
2774
|
+
flexShrink: 0,
|
|
2775
|
+
children: renderItem(item, index)
|
|
2776
|
+
}, getKey ? getKey(item, index) : index);
|
|
2777
|
+
})
|
|
2778
|
+
})
|
|
2779
|
+
});
|
|
2780
|
+
}
|
|
2781
|
+
//#endregion
|
|
2782
|
+
//#region src/hooks/use-capabilities.ts
|
|
2783
|
+
/**
|
|
2784
|
+
Returns everything knowable about the terminal: size, identity, platform,
|
|
2785
|
+
color depth, theme, and feature support.
|
|
2786
|
+
|
|
2787
|
+
A thin wrapper over the framework-free capabilities store (`getCapabilities`):
|
|
2788
|
+
environment-derived facts are available immediately; facts only the terminal
|
|
2789
|
+
itself can answer fill in after a lazy one-time query, and re-mounting
|
|
2790
|
+
consumers refreshes the dynamic facts (theme colors, pixel geometry).
|
|
2791
|
+
Re-renders on terminal resize and whenever query answers arrive.
|
|
2792
|
+
*/
|
|
2793
|
+
const useCapabilities = () => {
|
|
2794
|
+
const { stdout } = useStdout();
|
|
2795
|
+
const { stdin } = useStdinContext();
|
|
2796
|
+
const store = getCapabilities(stdin, stdout);
|
|
2797
|
+
const capabilities = (0, import_react.useSyncExternalStore)(store.subscribe, () => store.current);
|
|
2798
|
+
(0, import_react.useEffect)(() => {
|
|
2799
|
+
store.query();
|
|
2800
|
+
}, [store]);
|
|
2801
|
+
return capabilities;
|
|
2802
|
+
};
|
|
2803
|
+
/**
|
|
2804
|
+
Calls `onChange` whenever the terminal changes: resizes (including in-band
|
|
2805
|
+
pixel geometry), color scheme switches, window focus, and query answers
|
|
2806
|
+
arriving. The React wrapper over `capabilities.subscribe()` for side effects —
|
|
2807
|
+
for rendering, use `useCapabilities` instead.
|
|
2808
|
+
|
|
2809
|
+
The callback always sees the latest render's closure and changing it does not
|
|
2810
|
+
resubscribe. Both the new and previous snapshot are passed, so handlers can
|
|
2811
|
+
react to the specific change:
|
|
2812
|
+
|
|
2813
|
+
```tsx
|
|
2814
|
+
useCapabilitiesChange((next, previous) => {
|
|
2815
|
+
if (next.theme.appearance !== previous.theme.appearance) {
|
|
2816
|
+
// re-theme
|
|
2817
|
+
}
|
|
2818
|
+
});
|
|
2819
|
+
```
|
|
2820
|
+
*/
|
|
2821
|
+
const useCapabilitiesChange = (onChange) => {
|
|
2822
|
+
const { stdout } = useStdout();
|
|
2823
|
+
const { stdin } = useStdinContext();
|
|
2824
|
+
const store = getCapabilities(stdin, stdout);
|
|
2825
|
+
const handleChange = (0, import_react.useEffectEvent)(onChange);
|
|
2826
|
+
(0, import_react.useEffect)(() => {
|
|
2827
|
+
let previous = store.current;
|
|
2828
|
+
const unsubscribe = store.subscribe((next) => {
|
|
2829
|
+
const before = previous;
|
|
2830
|
+
previous = next;
|
|
2831
|
+
handleChange(next, before);
|
|
2832
|
+
});
|
|
2833
|
+
store.query();
|
|
2834
|
+
return unsubscribe;
|
|
2835
|
+
}, [store]);
|
|
2836
|
+
};
|
|
2837
|
+
//#endregion
|
|
2838
|
+
//#region src/hooks/use-paste.ts
|
|
2839
|
+
/**
|
|
2840
|
+
A React hook that calls `handler` whenever the user pastes text in the terminal. Bracketed paste mode (`\x1b[?2004h`) is automatically enabled while the hook is active, so pasted text arrives as a single string rather than being misinterpreted as individual key presses.
|
|
2841
|
+
|
|
2842
|
+
`usePaste` and `useInput` can be used together in the same component. They operate on separate event channels, so paste content is never forwarded to `useInput` handlers when `usePaste` is active.
|
|
2843
|
+
|
|
2844
|
+
```
|
|
2845
|
+
import {useInput, usePaste} from 'ink';
|
|
2846
|
+
|
|
2847
|
+
const MyInput = () => {
|
|
2848
|
+
useInput((input, key) => {
|
|
2849
|
+
// Only receives typed characters and key events, not pasted text.
|
|
2850
|
+
if (key.return) {
|
|
2851
|
+
// Submit
|
|
2852
|
+
}
|
|
2853
|
+
});
|
|
2854
|
+
|
|
2855
|
+
usePaste((text) => {
|
|
2856
|
+
// Receives the full pasted string, including newlines.
|
|
2857
|
+
console.log('Pasted:', text);
|
|
2858
|
+
});
|
|
2859
|
+
|
|
2860
|
+
return …
|
|
2861
|
+
};
|
|
2862
|
+
```
|
|
2863
|
+
*/
|
|
2864
|
+
const usePaste = (handler, options = {}) => {
|
|
2865
|
+
const { setRawMode, setBracketedPasteMode, internal_eventEmitter } = useStdinContext();
|
|
2866
|
+
(0, import_react.useEffect)(() => {
|
|
2867
|
+
if (options.isActive === false) return;
|
|
2868
|
+
setRawMode(true);
|
|
2869
|
+
setBracketedPasteMode(true);
|
|
2870
|
+
return () => {
|
|
2871
|
+
setRawMode(false);
|
|
2872
|
+
setBracketedPasteMode(false);
|
|
2873
|
+
};
|
|
2874
|
+
}, [
|
|
2875
|
+
options.isActive,
|
|
2876
|
+
setRawMode,
|
|
2877
|
+
setBracketedPasteMode
|
|
2878
|
+
]);
|
|
2879
|
+
const handlePaste = (0, import_react.useEffectEvent)((text) => {
|
|
2880
|
+
reconciler.discreteUpdates(() => {
|
|
2881
|
+
handler(text);
|
|
2882
|
+
});
|
|
2883
|
+
});
|
|
2884
|
+
(0, import_react.useEffect)(() => {
|
|
2885
|
+
if (options.isActive === false) return;
|
|
2886
|
+
internal_eventEmitter.on("paste", handlePaste);
|
|
2887
|
+
return () => {
|
|
2888
|
+
internal_eventEmitter.removeListener("paste", handlePaste);
|
|
2889
|
+
};
|
|
2890
|
+
}, [options.isActive, internal_eventEmitter]);
|
|
2891
|
+
};
|
|
2892
|
+
//#endregion
|
|
2893
|
+
//#region src/hooks/use-app.ts
|
|
2894
|
+
/**
|
|
2895
|
+
A React hook that returns app lifecycle methods like `exit()` and `waitUntilRenderFlush()`.
|
|
2896
|
+
*/
|
|
2897
|
+
const useApp = () => (0, import_react.useContext)(AppContext);
|
|
2898
|
+
//#endregion
|
|
2899
|
+
//#region src/hooks/use-stderr.ts
|
|
2900
|
+
/**
|
|
2901
|
+
A React hook that returns the stderr stream.
|
|
2902
|
+
*/
|
|
2903
|
+
const useStderr = () => (0, import_react.useContext)(StderrContext);
|
|
2904
|
+
//#endregion
|
|
2905
|
+
//#region src/hooks/use-focus-manager.ts
|
|
2906
|
+
/**
|
|
2907
|
+
A React hook that returns methods to enable or disable focus management for all components or manually switch focus to the next or previous components.
|
|
2908
|
+
*/
|
|
2909
|
+
const useFocusManager = () => {
|
|
2910
|
+
const focusContext = (0, import_react.useContext)(FocusContext);
|
|
2911
|
+
return {
|
|
2912
|
+
enableFocus: focusContext.enableFocus,
|
|
2913
|
+
disableFocus: focusContext.disableFocus,
|
|
2914
|
+
focusNext: focusContext.focusNext,
|
|
2915
|
+
focusPrevious: focusContext.focusPrevious,
|
|
2916
|
+
focus: focusContext.focus,
|
|
2917
|
+
activeId: focusContext.activeId
|
|
2918
|
+
};
|
|
2919
|
+
};
|
|
2920
|
+
//#endregion
|
|
2921
|
+
//#region src/hooks/use-is-screen-reader-enabled.ts
|
|
2922
|
+
/**
|
|
2923
|
+
A React hook that returns whether a screen reader is enabled.
|
|
2924
|
+
This is useful when you want to render different output for screen readers.
|
|
2925
|
+
*/
|
|
2926
|
+
const useIsScreenReaderEnabled = () => {
|
|
2927
|
+
const { isScreenReaderEnabled } = (0, import_react.useContext)(accessibilityContext);
|
|
2928
|
+
return isScreenReaderEnabled;
|
|
2929
|
+
};
|
|
2930
|
+
//#endregion
|
|
2931
|
+
//#region src/hooks/use-cursor.ts
|
|
2932
|
+
/**
|
|
2933
|
+
A React hook that returns methods to control the terminal cursor position.
|
|
2934
|
+
|
|
2935
|
+
Setting a cursor position makes the cursor visible at the specified coordinates (relative to the Ink output origin). This is useful for IME (Input Method Editor) support, where the composing character is displayed at the cursor location.
|
|
2936
|
+
|
|
2937
|
+
Pass `undefined` to hide the cursor.
|
|
2938
|
+
*/
|
|
2939
|
+
const useCursor = () => {
|
|
2940
|
+
const context = (0, import_react.useContext)(CursorContext);
|
|
2941
|
+
const positionRef = (0, import_react.useRef)(void 0);
|
|
2942
|
+
const setCursorPosition = (0, import_react.useCallback)((position) => {
|
|
2943
|
+
positionRef.current = position;
|
|
2944
|
+
}, []);
|
|
2945
|
+
(0, import_react.useInsertionEffect)(() => {
|
|
2946
|
+
context.setCursorPosition(positionRef.current);
|
|
2947
|
+
return () => {
|
|
2948
|
+
context.setCursorPosition(void 0);
|
|
2949
|
+
};
|
|
2950
|
+
});
|
|
2951
|
+
return { setCursorPosition };
|
|
2952
|
+
};
|
|
2953
|
+
//#endregion
|
|
2954
|
+
//#region src/hooks/use-animation.ts
|
|
2955
|
+
const defaultAnimationInterval = 100;
|
|
2956
|
+
const maximumTimerInterval = 2147483647;
|
|
2957
|
+
const zeroAnimState = {
|
|
2958
|
+
frame: 0,
|
|
2959
|
+
time: 0,
|
|
2960
|
+
delta: 0
|
|
2961
|
+
};
|
|
2962
|
+
/**
|
|
2963
|
+
A React hook that drives animations. Returns a frame counter, elapsed time, frame delta, and a reset function. All animations share a single timer internally, so multiple animated components consolidate into one render cycle.
|
|
2964
|
+
|
|
2965
|
+
@example
|
|
2966
|
+
```
|
|
2967
|
+
import {Text, useAnimation} from 'ink';
|
|
2968
|
+
|
|
2969
|
+
const Spinner = () => {
|
|
2970
|
+
const {frame} = useAnimation({interval: 80});
|
|
2971
|
+
const characters = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
2972
|
+
|
|
2973
|
+
return <Text>{characters[frame % characters.length]}</Text>;
|
|
2974
|
+
};
|
|
2975
|
+
```
|
|
2976
|
+
*/
|
|
2977
|
+
function useAnimation(options) {
|
|
2978
|
+
const { interval = defaultAnimationInterval, isActive = true } = options ?? {};
|
|
2979
|
+
const safeInterval = normalizeAnimationInterval(interval);
|
|
2980
|
+
const { subscribe, renderThrottleMs } = (0, import_react.useContext)(animationContext);
|
|
2981
|
+
const [resetKey, setResetKey] = (0, import_react.useState)(0);
|
|
2982
|
+
const [animState, setAnimState] = (0, import_react.useState)(zeroAnimState);
|
|
2983
|
+
const nextRenderTimeRef = (0, import_react.useRef)(0);
|
|
2984
|
+
const lastRenderTimeRef = (0, import_react.useRef)(0);
|
|
2985
|
+
const previousOptionsRef = (0, import_react.useRef)({
|
|
2986
|
+
isActive,
|
|
2987
|
+
safeInterval,
|
|
2988
|
+
resetKey
|
|
2989
|
+
});
|
|
2990
|
+
const previousOptions = previousOptionsRef.current;
|
|
2991
|
+
const shouldReset = isActive && (safeInterval !== previousOptions.safeInterval || !previousOptions.isActive || resetKey !== previousOptions.resetKey);
|
|
2992
|
+
const reset = (0, import_react.useCallback)(() => {
|
|
2993
|
+
setResetKey((k) => k + 1);
|
|
2994
|
+
}, []);
|
|
2995
|
+
(0, import_react.useLayoutEffect)(() => {
|
|
2996
|
+
if (!isActive) return;
|
|
2997
|
+
setAnimState(zeroAnimState);
|
|
2998
|
+
let startTime = 0;
|
|
2999
|
+
const { startTime: subscriberStartTime, unsubscribe } = subscribe((currentTime) => {
|
|
3000
|
+
if (renderThrottleMs > 0 && currentTime < nextRenderTimeRef.current) return;
|
|
3001
|
+
const elapsed = currentTime - startTime;
|
|
3002
|
+
const nextDelta = currentTime - lastRenderTimeRef.current;
|
|
3003
|
+
lastRenderTimeRef.current = currentTime;
|
|
3004
|
+
nextRenderTimeRef.current = currentTime + renderThrottleMs;
|
|
3005
|
+
setAnimState({
|
|
3006
|
+
frame: Math.floor(elapsed / safeInterval),
|
|
3007
|
+
time: elapsed,
|
|
3008
|
+
delta: nextDelta
|
|
3009
|
+
});
|
|
3010
|
+
}, safeInterval);
|
|
3011
|
+
startTime = subscriberStartTime;
|
|
3012
|
+
lastRenderTimeRef.current = subscriberStartTime;
|
|
3013
|
+
nextRenderTimeRef.current = startTime + renderThrottleMs;
|
|
3014
|
+
return unsubscribe;
|
|
3015
|
+
}, [
|
|
3016
|
+
safeInterval,
|
|
3017
|
+
isActive,
|
|
3018
|
+
subscribe,
|
|
3019
|
+
renderThrottleMs,
|
|
3020
|
+
resetKey
|
|
3021
|
+
]);
|
|
3022
|
+
(0, import_react.useLayoutEffect)(() => {
|
|
3023
|
+
previousOptionsRef.current = {
|
|
3024
|
+
isActive,
|
|
3025
|
+
safeInterval,
|
|
3026
|
+
resetKey
|
|
3027
|
+
};
|
|
3028
|
+
}, [
|
|
3029
|
+
isActive,
|
|
3030
|
+
safeInterval,
|
|
3031
|
+
resetKey
|
|
3032
|
+
]);
|
|
3033
|
+
if (shouldReset) return {
|
|
3034
|
+
...zeroAnimState,
|
|
3035
|
+
reset
|
|
3036
|
+
};
|
|
3037
|
+
return {
|
|
3038
|
+
...animState,
|
|
3039
|
+
reset
|
|
3040
|
+
};
|
|
3041
|
+
}
|
|
3042
|
+
function normalizeAnimationInterval(interval) {
|
|
3043
|
+
if (!Number.isFinite(interval)) return defaultAnimationInterval;
|
|
3044
|
+
return Math.min(maximumTimerInterval, Math.max(1, interval));
|
|
3045
|
+
}
|
|
3046
|
+
//#endregion
|
|
3047
|
+
//#region src/hooks/use-terminal-osc.ts
|
|
3048
|
+
const useProgress = ({ state, value }) => {
|
|
3049
|
+
const terminal = (0, import_react.useContext)(TerminalOscContext);
|
|
3050
|
+
const owner = (0, import_react.useRef)(void 0);
|
|
3051
|
+
owner.current ??= Symbol("terminal-progress");
|
|
3052
|
+
(0, import_react.useEffect)(() => {
|
|
3053
|
+
terminal.publishProgress(owner.current, state, value);
|
|
3054
|
+
}, [
|
|
3055
|
+
state,
|
|
3056
|
+
terminal,
|
|
3057
|
+
value
|
|
3058
|
+
]);
|
|
3059
|
+
(0, import_react.useEffect)(() => {
|
|
3060
|
+
return () => {
|
|
3061
|
+
terminal.publishProgress(owner.current, "inactive");
|
|
3062
|
+
};
|
|
3063
|
+
}, [terminal]);
|
|
3064
|
+
};
|
|
3065
|
+
const useClipboard = () => {
|
|
3066
|
+
const terminal = (0, import_react.useContext)(TerminalOscContext);
|
|
3067
|
+
return (0, import_react.useCallback)((text, selection) => terminal.copyToClipboard(text, selection), [terminal]);
|
|
3068
|
+
};
|
|
3069
|
+
const useTitle = (title) => {
|
|
3070
|
+
const terminal = (0, import_react.useContext)(TerminalOscContext);
|
|
3071
|
+
const owner = (0, import_react.useRef)(void 0);
|
|
3072
|
+
owner.current ??= Symbol("terminal-title");
|
|
3073
|
+
(0, import_react.useEffect)(() => {
|
|
3074
|
+
terminal.publishTitle(owner.current, title);
|
|
3075
|
+
}, [terminal, title]);
|
|
3076
|
+
(0, import_react.useEffect)(() => () => terminal.publishTitle(owner.current, void 0), [terminal]);
|
|
3077
|
+
};
|
|
3078
|
+
const useWorkingDirectory = (directory) => {
|
|
3079
|
+
const terminal = (0, import_react.useContext)(TerminalOscContext);
|
|
3080
|
+
(0, import_react.useEffect)(() => {
|
|
3081
|
+
terminal.setWorkingDirectory(directory);
|
|
3082
|
+
}, [directory, terminal]);
|
|
3083
|
+
};
|
|
3084
|
+
const useNotification = () => {
|
|
3085
|
+
const terminal = (0, import_react.useContext)(TerminalOscContext);
|
|
3086
|
+
return (0, import_react.useCallback)((title) => terminal.notify(title), [terminal]);
|
|
3087
|
+
};
|
|
3088
|
+
const usePointerShape = (shape) => {
|
|
3089
|
+
const terminal = (0, import_react.useContext)(TerminalOscContext);
|
|
3090
|
+
(0, import_react.useEffect)(() => {
|
|
3091
|
+
terminal.setPointerShape(shape);
|
|
3092
|
+
}, [shape, terminal]);
|
|
3093
|
+
};
|
|
3094
|
+
//#endregion
|
|
3095
|
+
//#region src/hooks/use-window-size.ts
|
|
3096
|
+
/**
|
|
3097
|
+
A React hook that returns the current terminal window dimensions and re-renders the component whenever the terminal is resized.
|
|
3098
|
+
|
|
3099
|
+
Reads the capabilities store, so on terminals that send in-band size reports
|
|
3100
|
+
(mode 2048) the dimensions are the emulator's own, arriving after it has
|
|
3101
|
+
rewrapped its screen; elsewhere they are the stream's `columns`/`rows`.
|
|
3102
|
+
Subscribing to dimensions does not initiate capability queries. In-band reports
|
|
3103
|
+
are used when another consumer has explicitly enabled capability discovery.
|
|
3104
|
+
*/
|
|
3105
|
+
const useWindowSize = () => {
|
|
3106
|
+
const { stdin } = useStdinContext();
|
|
3107
|
+
const { stdout } = useStdout();
|
|
3108
|
+
const store = getCapabilities(stdin, stdout);
|
|
3109
|
+
const { size } = (0, import_react.useSyncExternalStore)(store.subscribe, () => store.current);
|
|
3110
|
+
return (0, import_react.useMemo)(() => ({
|
|
3111
|
+
columns: size.columns,
|
|
3112
|
+
rows: size.rows
|
|
3113
|
+
}), [size.columns, size.rows]);
|
|
3114
|
+
};
|
|
3115
|
+
//#endregion
|
|
3116
|
+
//#region src/hooks/use-box-metrics.ts
|
|
3117
|
+
const emptyMetrics = {
|
|
3118
|
+
width: 0,
|
|
3119
|
+
height: 0,
|
|
3120
|
+
left: 0,
|
|
3121
|
+
top: 0
|
|
3122
|
+
};
|
|
3123
|
+
const findRootNode = (node) => {
|
|
3124
|
+
if (!node) return;
|
|
3125
|
+
if (!node.parentNode) return node.nodeName === "ink-root" ? node : void 0;
|
|
3126
|
+
return findRootNode(node.parentNode);
|
|
3127
|
+
};
|
|
3128
|
+
/**
|
|
3129
|
+
A React hook that returns the current layout metrics for a tracked box element.
|
|
3130
|
+
It updates when layout changes (for example terminal resize, sibling/content changes, or position changes).
|
|
3131
|
+
|
|
3132
|
+
The hook returns `{width: 0, height: 0, left: 0, top: 0}` until the first layout pass completes. It also returns zeros when the tracked ref is detached.
|
|
3133
|
+
|
|
3134
|
+
Use `hasMeasured` to detect when the currently tracked element has been measured.
|
|
3135
|
+
|
|
3136
|
+
@example
|
|
3137
|
+
```tsx
|
|
3138
|
+
import {useRef} from 'react';
|
|
3139
|
+
import {Box, Text, useBoxMetrics} from 'ink';
|
|
3140
|
+
|
|
3141
|
+
const Example = () => {
|
|
3142
|
+
const ref = useRef(null);
|
|
3143
|
+
const {width, height, left, top, hasMeasured} = useBoxMetrics(ref);
|
|
3144
|
+
return (
|
|
3145
|
+
<Box ref={ref}>
|
|
3146
|
+
<Text>
|
|
3147
|
+
{hasMeasured ? `${width}x${height} at ${left},${top}` : 'Measuring...'}
|
|
3148
|
+
</Text>
|
|
3149
|
+
</Box>
|
|
3150
|
+
);
|
|
3151
|
+
};
|
|
3152
|
+
```
|
|
3153
|
+
*/
|
|
3154
|
+
const useBoxMetrics = (ref) => {
|
|
3155
|
+
const [metrics, setMetrics] = (0, import_react.useState)(emptyMetrics);
|
|
3156
|
+
const [hasMeasured, setHasMeasured] = (0, import_react.useState)(false);
|
|
3157
|
+
const updateMetrics = (0, import_react.useCallback)(() => {
|
|
3158
|
+
const layout = ref.current?.yogaNode?.getComputedLayout() ?? emptyMetrics;
|
|
3159
|
+
setMetrics((previousMetrics) => {
|
|
3160
|
+
return previousMetrics.width !== layout.width || previousMetrics.height !== layout.height || previousMetrics.left !== layout.left || previousMetrics.top !== layout.top ? layout : previousMetrics;
|
|
3161
|
+
});
|
|
3162
|
+
setHasMeasured(Boolean(ref.current));
|
|
3163
|
+
}, [ref]);
|
|
3164
|
+
(0, import_react.useEffect)(updateMetrics);
|
|
3165
|
+
(0, import_react.useEffect)(() => {
|
|
3166
|
+
const rootNode = findRootNode(ref.current);
|
|
3167
|
+
if (!rootNode) return;
|
|
3168
|
+
return addLayoutListener(rootNode, updateMetrics);
|
|
3169
|
+
});
|
|
3170
|
+
return (0, import_react.useMemo)(() => ({
|
|
3171
|
+
...metrics,
|
|
3172
|
+
hasMeasured
|
|
3173
|
+
}), [metrics, hasMeasured]);
|
|
3174
|
+
};
|
|
3175
|
+
//#endregion
|
|
3176
|
+
//#region src/measure-element.ts
|
|
3177
|
+
/**
|
|
3178
|
+
Measure the layout metrics of a particular `<Box>` element.
|
|
3179
|
+
Returns an object with `x`, `y`, `width`, and `height` properties.
|
|
3180
|
+
|
|
3181
|
+
`x` and `y` are the element's position within the live layout region, computed by walking up the layout tree and accumulating each ancestor's offset. These are layout-tree coordinates, not terminal viewport coordinates. To compare them with mouse events, convert the event coordinates using the live region's viewport position. This is necessary even in alternate-screen mode when output, such as `<Static>` content, appears above the live region.
|
|
3182
|
+
|
|
3183
|
+
Note: `measureElement()` returns `{x: 0, y: 0, width: 0, height: 0}` when called during render (before layout is calculated). Call it from post-render code, such as `useEffect`, `useLayoutEffect`, input handlers, or timer callbacks. When content changes, pass the relevant dependency to your effect so it re-measures after each update.
|
|
3184
|
+
*/
|
|
3185
|
+
const measureElement = (node) => {
|
|
3186
|
+
const { yogaNode } = node;
|
|
3187
|
+
if (!yogaNode) return {
|
|
3188
|
+
x: 0,
|
|
3189
|
+
y: 0,
|
|
3190
|
+
width: 0,
|
|
3191
|
+
height: 0
|
|
3192
|
+
};
|
|
3193
|
+
let x = yogaNode.getComputedLeft();
|
|
3194
|
+
let y = yogaNode.getComputedTop();
|
|
3195
|
+
let current = node.parentNode;
|
|
3196
|
+
while (current) {
|
|
3197
|
+
if (current.yogaNode) {
|
|
3198
|
+
x += current.yogaNode.getComputedLeft();
|
|
3199
|
+
y += current.yogaNode.getComputedTop();
|
|
3200
|
+
}
|
|
3201
|
+
current = current.parentNode;
|
|
3202
|
+
}
|
|
3203
|
+
return {
|
|
3204
|
+
x,
|
|
3205
|
+
y,
|
|
3206
|
+
width: yogaNode.getComputedWidth(),
|
|
3207
|
+
height: yogaNode.getComputedHeight()
|
|
3208
|
+
};
|
|
3209
|
+
};
|
|
3210
|
+
//#endregion
|
|
3211
|
+
export { AnsiText, Box, Hyperlink, Newline, Spacer, Static, Text, Transform, VirtualList, applyTerminalQuery, capabilities, createSupportsColor, detectCapabilities, detectColorLevel, detectHyperlinkSupport, detectTerminal, detectUnicodeSupport, getCapabilities, getTerminalQuery, kittyFlags, kittyModifiers, measureElement, queryTerminal, refreshTerminalQuery, render, renderToString, useAnimation, useApp, useBoxMetrics, useCapabilities, useCapabilitiesChange, useClipboard, useCursor, useFocus, useFocusManager, useInput, useIsScreenReaderEnabled, useNotification, usePaste, usePointerShape, useProgress, useStderr, useStdin, useStdout, useTitle, useVirtualScroll, useWindowSize, useWorkingDirectory };
|