@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.
Files changed (223) hide show
  1. package/README.md +499 -313
  2. package/THIRD_PARTY_NOTICES.md +98 -23
  3. package/dist/Text-DV9CuzAT.d.ts +452 -0
  4. package/dist/ansi.d.ts +217 -0
  5. package/dist/ansi.js +87 -0
  6. package/dist/capabilities.d.ts +5 -0
  7. package/dist/capabilities.js +3 -0
  8. package/dist/cell-_ZVhbfl0.js +44 -0
  9. package/dist/color-CkbalRqK.js +2 -0
  10. package/dist/color-policy-BAC9-TZX.js +592 -0
  11. package/dist/color-policy-CCxuHIdD.d.ts +22 -0
  12. package/dist/color-profile-CyeHnG1T.d.ts +97 -0
  13. package/dist/color-profile-DHhQHY55.js +36 -0
  14. package/dist/color.d.ts +21 -0
  15. package/dist/color.js +3 -0
  16. package/dist/cursor-position-D2LAkRG0.d.ts +7 -0
  17. package/dist/detect-B3dL4Q11.js +374 -0
  18. package/dist/detect-Db6GbKOm.d.ts +195 -0
  19. package/dist/{devtools-QpCMm9JH.mjs → devtools-DbthxoD1.js} +23 -24
  20. package/dist/env-YVw64yZS.js +9 -0
  21. package/dist/escapes-CB_6CWOE.d.ts +72 -0
  22. package/dist/geometry-BxXOzJgo.d.ts +11 -0
  23. package/dist/index-D48vQhhe.d.ts +21 -0
  24. package/dist/index-DDVME65c.d.ts +919 -0
  25. package/dist/index.d.ts +1310 -0
  26. package/dist/index.js +3211 -0
  27. package/dist/jsx-dev-runtime.d.ts +2 -0
  28. package/dist/jsx-dev-runtime.js +227 -0
  29. package/dist/jsx-runtime-XtwIVFsg.js +255 -0
  30. package/dist/jsx-runtime.d.ts +2 -0
  31. package/dist/jsx-runtime.js +8 -0
  32. package/dist/osc-BFKKSqpg.js +71 -0
  33. package/dist/osc-Cn0fw77g.d.ts +23 -0
  34. package/dist/paint-Cx-zC_sX.d.ts +81 -0
  35. package/dist/query-BNc2B8GD.d.ts +152 -0
  36. package/dist/react-CTZ_8dwh.js +1197 -0
  37. package/dist/react.d.ts +2 -0
  38. package/dist/react.js +49 -0
  39. package/dist/router.d.ts +392 -0
  40. package/dist/router.js +711 -0
  41. package/dist/sample-Cqw1bjUL.js +445 -0
  42. package/dist/screen-CgC2WlVM.d.ts +49 -0
  43. package/dist/screen-CiPytswf.js +342 -0
  44. package/dist/screen.d.ts +5 -0
  45. package/dist/screen.js +5 -0
  46. package/dist/semantic-text-style-DIMzC7xt.js +91 -0
  47. package/dist/serialize-BTkAZgw1.js +79 -0
  48. package/dist/session-DDQ5V300.js +723 -0
  49. package/dist/sgr-BhwaWAJB.js +246 -0
  50. package/dist/store-C1P5fOUi.d.ts +72 -0
  51. package/dist/string-width-CijQwpIk.js +69 -0
  52. package/dist/strip-BvU4toXG.js +6 -0
  53. package/dist/terminal.d.ts +121 -0
  54. package/dist/terminal.js +2 -0
  55. package/dist/tokenize-AjqbvtiT.js +1242 -0
  56. package/dist/tokenize-Dx1y_l5H.d.ts +57 -0
  57. package/dist/truncate-D31fhU6i.js +562 -0
  58. package/dist/use-focus-B5npZrJU.js +18736 -0
  59. package/dist/yoga-5jKhYCJC.js +3465 -0
  60. package/dist/yoga.d.ts +2 -0
  61. package/dist/yoga.js +2 -0
  62. package/package.json +89 -29
  63. package/src/ansi/chalk.ts +138 -0
  64. package/src/ansi/cursor.ts +46 -0
  65. package/src/ansi/east-asian-width.ts +259 -0
  66. package/src/ansi/escapes.ts +120 -0
  67. package/src/ansi/graphemes.ts +8 -0
  68. package/src/ansi/hyperlink.ts +44 -0
  69. package/src/ansi/index.ts +27 -0
  70. package/src/ansi/osc.ts +77 -0
  71. package/src/ansi/sgr.ts +234 -0
  72. package/src/ansi/slice.ts +43 -0
  73. package/src/ansi/string-width.ts +121 -0
  74. package/src/ansi/strip.ts +13 -0
  75. package/src/ansi/tokenize.ts +380 -0
  76. package/src/ansi/truncate.ts +196 -0
  77. package/src/ansi/wrap.ts +765 -0
  78. package/src/ansi-tokenizer.ts +510 -0
  79. package/src/capabilities/color-policy.ts +34 -0
  80. package/src/capabilities/detect.ts +608 -0
  81. package/src/capabilities/index.ts +37 -0
  82. package/src/capabilities/query.ts +679 -0
  83. package/src/capabilities/store.ts +394 -0
  84. package/src/code-excerpt.ts +39 -0
  85. package/src/color/index.ts +3 -0
  86. package/src/color/paint.ts +169 -0
  87. package/src/color/palette.ts +48 -0
  88. package/src/color/sample.ts +323 -0
  89. package/src/color.ts +1 -0
  90. package/src/components/AccessibilityContext.ts +5 -0
  91. package/src/components/AnimationContext.ts +24 -0
  92. package/src/components/AnsiText.tsx +42 -0
  93. package/src/components/App.tsx +878 -0
  94. package/src/components/AppContext.ts +111 -0
  95. package/src/components/BackgroundContext.ts +7 -0
  96. package/src/components/Box.tsx +100 -0
  97. package/src/components/CursorContext.ts +19 -0
  98. package/src/components/ErrorBoundary.tsx +39 -0
  99. package/src/components/ErrorOverview.tsx +135 -0
  100. package/src/components/FocusContext.ts +30 -0
  101. package/src/components/Hyperlink.tsx +56 -0
  102. package/src/components/Newline.tsx +16 -0
  103. package/src/components/Spacer.tsx +11 -0
  104. package/src/components/Static.tsx +60 -0
  105. package/src/components/StderrContext.ts +24 -0
  106. package/src/components/StdinContext.ts +48 -0
  107. package/src/components/StdoutContext.ts +26 -0
  108. package/src/components/TerminalOscContext.ts +25 -0
  109. package/src/components/Text.tsx +122 -0
  110. package/src/components/Transform.tsx +38 -0
  111. package/src/components/VirtualList.tsx +128 -0
  112. package/src/cursor-position.ts +103 -0
  113. package/src/devtools.ts +103 -0
  114. package/src/dom.ts +301 -0
  115. package/src/env.ts +12 -0
  116. package/src/get-max-width.ts +11 -0
  117. package/src/global.d.ts +38 -0
  118. package/src/glyphs.ts +99 -0
  119. package/src/hooks/use-animation.ts +142 -0
  120. package/src/hooks/use-app.ts +8 -0
  121. package/src/hooks/use-box-metrics.ts +134 -0
  122. package/src/hooks/use-capabilities.ts +73 -0
  123. package/src/hooks/use-cursor.ts +33 -0
  124. package/src/hooks/use-focus-manager.ts +62 -0
  125. package/src/hooks/use-focus.ts +82 -0
  126. package/src/hooks/use-input.ts +267 -0
  127. package/src/hooks/use-is-screen-reader-enabled.ts +12 -0
  128. package/src/hooks/use-paste.ts +78 -0
  129. package/src/hooks/use-stderr.ts +8 -0
  130. package/src/hooks/use-stdin.ts +10 -0
  131. package/src/hooks/use-stdout.ts +8 -0
  132. package/src/hooks/use-terminal-osc.ts +59 -0
  133. package/src/hooks/use-virtual-scroll.ts +84 -0
  134. package/src/hooks/use-window-size.ts +37 -0
  135. package/src/index.ts +96 -0
  136. package/src/ink.tsx +1452 -0
  137. package/src/input-parser.ts +303 -0
  138. package/src/instances.ts +9 -0
  139. package/src/jsx-dev-runtime.ts +3 -0
  140. package/src/jsx-runtime.ts +6 -0
  141. package/src/kitty-keyboard.ts +185 -0
  142. package/src/measure-element.ts +62 -0
  143. package/src/measure-text.ts +31 -0
  144. package/src/paint-tree.ts +220 -0
  145. package/src/parse-keypress.ts +515 -0
  146. package/src/parse-stack-line.ts +138 -0
  147. package/src/patch-console.ts +106 -0
  148. package/src/quick-lru.ts +85 -0
  149. package/src/react.ts +53 -0
  150. package/src/reconciler.ts +476 -0
  151. package/src/render-background.ts +59 -0
  152. package/src/render-border.ts +167 -0
  153. package/src/render-frame.ts +83 -0
  154. package/src/render-to-string.ts +146 -0
  155. package/src/render.ts +284 -0
  156. package/src/router/components.tsx +343 -0
  157. package/src/router/context.ts +41 -0
  158. package/src/router/history.ts +194 -0
  159. package/src/router/hooks.tsx +391 -0
  160. package/src/router/index.ts +34 -0
  161. package/src/router/matcher.ts +571 -0
  162. package/src/sanitize-ansi.ts +33 -0
  163. package/src/screen/ansi.ts +184 -0
  164. package/src/screen/canvas.ts +160 -0
  165. package/src/screen/cell.ts +138 -0
  166. package/src/screen/color-profile.ts +47 -0
  167. package/src/screen/geometry.ts +9 -0
  168. package/src/screen/index.ts +6 -0
  169. package/src/screen/screen.ts +305 -0
  170. package/src/screen/serialize.ts +129 -0
  171. package/src/screen.ts +1 -0
  172. package/src/semantic-text-style.ts +118 -0
  173. package/src/signal-exit.ts +106 -0
  174. package/src/squash-text-nodes.ts +37 -0
  175. package/src/stream.ts +28 -0
  176. package/src/structured-text.ts +325 -0
  177. package/src/styles.ts +753 -0
  178. package/src/terminal/index.ts +2 -0
  179. package/src/terminal/inline-presenter.ts +120 -0
  180. package/src/terminal/input.ts +92 -0
  181. package/src/terminal/render-scheduler.ts +37 -0
  182. package/src/terminal/screen-presenter.ts +242 -0
  183. package/src/terminal/session.ts +408 -0
  184. package/src/terminal-size.ts +58 -0
  185. package/src/terminal.ts +1 -0
  186. package/src/testing/browser.ts +588 -0
  187. package/src/testing/emulators.ts +205 -0
  188. package/src/testing/explorer-app/index.html +12 -0
  189. package/src/testing/explorer-app/main.ts +381 -0
  190. package/src/testing/explorer-app/style.css +194 -0
  191. package/src/testing/explorer-app/tsconfig.json +15 -0
  192. package/src/testing/explorer-app/vite-env.d.ts +1 -0
  193. package/src/testing/index.ts +26 -0
  194. package/src/testing/keys.ts +56 -0
  195. package/src/testing/live.ts +85 -0
  196. package/src/testing/matchers.ts +70 -0
  197. package/src/testing/public.ts +94 -0
  198. package/src/testing/terminal.ts +361 -0
  199. package/src/testing/vitest.ts +157 -0
  200. package/src/throttle.ts +73 -0
  201. package/src/transform-adapter.ts +14 -0
  202. package/src/types.ts +10 -0
  203. package/src/virtual-scroll.ts +133 -0
  204. package/src/wrap-text.ts +54 -0
  205. package/src/yoga/config.ts +57 -0
  206. package/src/yoga/core/absoluteLayout.ts +626 -0
  207. package/src/yoga/core/baseline.ts +66 -0
  208. package/src/yoga/core/cache.ts +136 -0
  209. package/src/yoga/core/calculateLayout.ts +2926 -0
  210. package/src/yoga/core/config.ts +104 -0
  211. package/src/yoga/core/flexLine.ts +177 -0
  212. package/src/yoga/core/helpers.ts +293 -0
  213. package/src/yoga/core/layoutResults.ts +167 -0
  214. package/src/yoga/core/node.ts +611 -0
  215. package/src/yoga/core/numeric.ts +44 -0
  216. package/src/yoga/core/pixelGrid.ts +151 -0
  217. package/src/yoga/core/style.ts +887 -0
  218. package/src/yoga/core/types.ts +224 -0
  219. package/src/yoga/generated/YGEnums.ts +263 -0
  220. package/src/yoga/index.ts +19 -0
  221. package/src/yoga/node.ts +1140 -0
  222. package/dist/index.d.mts +0 -2379
  223. package/dist/index.mjs +0 -10072
@@ -0,0 +1,878 @@
1
+ /** @jsxImportSource react */
2
+ import { EventEmitter } from "node:events";
3
+
4
+ import React, {
5
+ type ReactNode,
6
+ useState,
7
+ useRef,
8
+ useCallback,
9
+ useMemo,
10
+ useEffect,
11
+ useInsertionEffect,
12
+ } from "react";
13
+
14
+ import { cliCursor } from "#/ansi/cursor.ts";
15
+ import { ansiEscapes, CSI, ESC } from "#/ansi/escapes.ts";
16
+ import {
17
+ ensureTerminalQuery,
18
+ getTerminalQueryPromise,
19
+ refreshTerminalQuery,
20
+ type TerminalQueryResult,
21
+ } from "#/capabilities/query.ts";
22
+ import { registerTerminalIntegration } from "#/capabilities/store.ts";
23
+ import { animationContext as AnimationContext } from "#/components/AnimationContext.ts";
24
+ import { AppContext, type SuspendTerminal } from "#/components/AppContext.ts";
25
+ import { CursorContext } from "#/components/CursorContext.ts";
26
+ import { ErrorBoundary } from "#/components/ErrorBoundary.tsx";
27
+ import { FocusContext } from "#/components/FocusContext.ts";
28
+ import { StderrContext } from "#/components/StderrContext.ts";
29
+ import { StdinContext } from "#/components/StdinContext.ts";
30
+ import { StdoutContext } from "#/components/StdoutContext.ts";
31
+ import { type CursorPosition } from "#/cursor-position.ts";
32
+ import { getRawModeStream, type OutputStream } from "#/stream.ts";
33
+ import type { TerminalInput } from "#/terminal/input.ts";
34
+
35
+ const tab = "\t";
36
+ const shiftTab = `${CSI}Z`;
37
+ const escape = ESC;
38
+
39
+ type AnimationSubscriber = {
40
+ readonly callback: (currentTime: number) => void;
41
+ readonly interval: number;
42
+ readonly startTime: number;
43
+ nextDueTime: number;
44
+ };
45
+
46
+ type Props = {
47
+ readonly children: ReactNode;
48
+ readonly stdin: NodeJS.ReadableStream;
49
+ readonly stdout: OutputStream;
50
+ readonly stderr: OutputStream;
51
+ readonly writeToStdout: (data: string) => void;
52
+ readonly writeToStderr: (data: string) => void;
53
+ readonly exitOnCtrlC: boolean;
54
+ readonly onExit: (errorOrResult?: unknown) => void;
55
+ readonly onWaitUntilRenderFlush: () => Promise<void>;
56
+ readonly onSuspendTerminal: SuspendTerminal;
57
+ readonly onRegisterInputControl: (pauseInput: () => void, resumeInput: () => void) => void;
58
+ readonly setCursorPosition: (position: CursorPosition | undefined) => void;
59
+ readonly interactive: boolean;
60
+ readonly renderThrottleMs: number;
61
+ readonly terminalInput: TerminalInput;
62
+ };
63
+
64
+ type Focusable = {
65
+ readonly id: string;
66
+ readonly isActive: boolean;
67
+ };
68
+
69
+ // Root component for all Ink apps
70
+ // It renders stdin and stdout contexts, so that children can access them if needed
71
+ // It also handles Ctrl+C exiting and cursor visibility
72
+ export function App({
73
+ children,
74
+ stdin,
75
+ stdout,
76
+ stderr,
77
+ writeToStdout,
78
+ writeToStderr,
79
+ exitOnCtrlC,
80
+ onExit,
81
+ onWaitUntilRenderFlush,
82
+ onSuspendTerminal,
83
+ onRegisterInputControl,
84
+ setCursorPosition,
85
+ interactive,
86
+ renderThrottleMs,
87
+ terminalInput,
88
+ }: Props): React.ReactNode {
89
+ const [isFocusEnabled, setIsFocusEnabled] = useState(true);
90
+ const [activeFocusId, setActiveFocusId] = useState<string | undefined>(undefined);
91
+ // Focusables array is managed internally via setFocusables callback pattern
92
+ // eslint-disable-next-line react/hook-use-state
93
+ const [, setFocusables] = useState<Focusable[]>([]);
94
+ // Track focusables count for tab navigation check (avoids stale closure)
95
+ const focusablesCountRef = useRef(0);
96
+ const animationSubscribersRef = useRef(
97
+ new Map<(currentTime: number) => void, AnimationSubscriber>(),
98
+ );
99
+ const animationTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
100
+ // Count how many components enabled raw mode to avoid disabling
101
+ // raw mode until all components don't need it anymore
102
+ const rawModeEnabledCount = useRef(0);
103
+ const pendingDisableRawModeRef = useRef(false);
104
+ // Count how many components enabled bracketed paste mode
105
+ const bracketedPasteModeEnabledCount = useRef(0);
106
+ // eslint-disable-next-line @typescript-eslint/naming-convention
107
+ const internal_eventEmitter = useRef(new EventEmitter());
108
+ // Each useInput hook adds a listener, so the count can legitimately exceed the default limit of 10.
109
+ internal_eventEmitter.current.setMaxListeners(Infinity);
110
+ // Store the currently attached readable listener to avoid stale closure issues
111
+ const readableListenerRef = useRef<(() => void) | undefined>(undefined);
112
+ const isMountedRef = useRef(true);
113
+ const pendingInputFlushRef = useRef<NodeJS.Timeout | undefined>(undefined);
114
+ // Small delay to let chunked escape sequences complete before flushing as literal input.
115
+ const pendingInputFlushDelayMilliseconds = 20;
116
+
117
+ const clearPendingInputFlush = useCallback((): void => {
118
+ if (!pendingInputFlushRef.current) {
119
+ return;
120
+ }
121
+
122
+ clearTimeout(pendingInputFlushRef.current);
123
+ pendingInputFlushRef.current = undefined;
124
+ }, []);
125
+
126
+ const clearAnimationTimer = useCallback((): void => {
127
+ if (!animationTimerRef.current) {
128
+ return;
129
+ }
130
+
131
+ clearTimeout(animationTimerRef.current);
132
+ animationTimerRef.current = undefined;
133
+ }, []);
134
+
135
+ const scheduleAnimationTick = useCallback((): void => {
136
+ clearAnimationTimer();
137
+
138
+ if (animationSubscribersRef.current.size === 0) {
139
+ return;
140
+ }
141
+
142
+ let nextDueTime = Number.POSITIVE_INFINITY;
143
+
144
+ for (const subscriber of animationSubscribersRef.current.values()) {
145
+ // One shared timer is enough as long as it wakes at the earliest
146
+ // subscriber deadline and lets slower animations skip that tick.
147
+ nextDueTime = Math.min(nextDueTime, subscriber.nextDueTime);
148
+ }
149
+
150
+ const delay = Math.max(0, nextDueTime - performance.now());
151
+ animationTimerRef.current = setTimeout(() => {
152
+ animationTimerRef.current = undefined;
153
+ const currentTime = performance.now();
154
+
155
+ for (const subscriber of animationSubscribersRef.current.values()) {
156
+ if (currentTime < subscriber.nextDueTime) {
157
+ continue;
158
+ }
159
+
160
+ subscriber.callback(currentTime);
161
+ const elapsedTime = currentTime - subscriber.startTime;
162
+ const elapsedFrames = Math.floor(elapsedTime / subscriber.interval) + 1;
163
+ // Advance from elapsed time rather than callback count so delayed
164
+ // ticks catch up instead of stretching the animation timeline.
165
+ subscriber.nextDueTime = subscriber.startTime + elapsedFrames * subscriber.interval;
166
+ }
167
+
168
+ scheduleAnimationTick();
169
+ }, delay);
170
+ // Keep the timer ref'd while animations are active so `useAnimation()`
171
+ // can drive process lifetime in both interactive and non-interactive apps.
172
+ }, [clearAnimationTimer]);
173
+
174
+ const animationSubscribe = useCallback(
175
+ (
176
+ callback: (currentTime: number) => void,
177
+ interval: number,
178
+ ): { readonly startTime: number; readonly unsubscribe: () => void } => {
179
+ const startTime = performance.now();
180
+ // The scheduler owns the start timestamp so hooks can derive frames from
181
+ // the exact same origin that determines each subscriber's due time.
182
+ animationSubscribersRef.current.set(callback, {
183
+ callback,
184
+ interval,
185
+ startTime,
186
+ nextDueTime: startTime + interval,
187
+ });
188
+ scheduleAnimationTick();
189
+
190
+ return {
191
+ startTime,
192
+ unsubscribe() {
193
+ animationSubscribersRef.current.delete(callback);
194
+
195
+ if (animationSubscribersRef.current.size === 0) {
196
+ clearAnimationTimer();
197
+ return;
198
+ }
199
+
200
+ scheduleAnimationTick();
201
+ },
202
+ };
203
+ },
204
+ [clearAnimationTimer, scheduleAnimationTick],
205
+ );
206
+
207
+ useEffect(() => {
208
+ return () => {
209
+ clearAnimationTimer();
210
+ };
211
+ }, [clearAnimationTimer]);
212
+
213
+ const rawModeStdin = getRawModeStream(stdin);
214
+ const isRawModeSupported = rawModeStdin !== undefined;
215
+
216
+ const detachReadableListener = useCallback((): void => {
217
+ if (!readableListenerRef.current) {
218
+ return;
219
+ }
220
+
221
+ stdin.removeListener("readable", readableListenerRef.current);
222
+ readableListenerRef.current = undefined;
223
+ }, [stdin]);
224
+
225
+ const clearInputState = useCallback((): void => {
226
+ terminalInput.reset();
227
+ clearPendingInputFlush();
228
+ detachReadableListener();
229
+ }, [clearPendingInputFlush, detachReadableListener, terminalInput]);
230
+
231
+ const disableRawMode = useCallback((): void => {
232
+ if (!rawModeStdin) {
233
+ return;
234
+ }
235
+
236
+ pendingDisableRawModeRef.current = false;
237
+ rawModeStdin.setRawMode(false);
238
+ rawModeStdin.unref?.();
239
+ rawModeEnabledCount.current = 0;
240
+ clearInputState();
241
+ }, [rawModeStdin, clearInputState]);
242
+
243
+ const handleExit = useCallback(
244
+ (errorOrResult?: unknown): void => {
245
+ if (
246
+ isRawModeSupported &&
247
+ (rawModeEnabledCount.current > 0 || pendingDisableRawModeRef.current)
248
+ ) {
249
+ disableRawMode();
250
+ }
251
+
252
+ onExit(errorOrResult);
253
+ },
254
+ [isRawModeSupported, disableRawMode, onExit],
255
+ );
256
+
257
+ const handleInput = useCallback(
258
+ (input: string): void => {
259
+ // Exit on Ctrl+C
260
+ // eslint-disable-next-line unicorn/no-hex-escape
261
+ if (input === "\x03" && exitOnCtrlC) {
262
+ handleExit();
263
+ return;
264
+ }
265
+
266
+ // Reset focus when there's an active focused component on Esc
267
+ if (input === escape && isFocusEnabled) {
268
+ setActiveFocusId(undefined);
269
+ }
270
+ },
271
+ [exitOnCtrlC, handleExit, isFocusEnabled],
272
+ );
273
+
274
+ const emitInput = useCallback(
275
+ (input: string): void => {
276
+ handleInput(input);
277
+ internal_eventEmitter.current.emit("input", input);
278
+ },
279
+ [handleInput],
280
+ );
281
+
282
+ const schedulePendingInputFlush = useCallback((): void => {
283
+ clearPendingInputFlush();
284
+ pendingInputFlushRef.current = setTimeout(() => {
285
+ pendingInputFlushRef.current = undefined;
286
+ const pendingEscape = terminalInput.flushPendingEscape();
287
+ if (!pendingEscape) {
288
+ return;
289
+ }
290
+
291
+ emitInput(pendingEscape);
292
+ }, pendingInputFlushDelayMilliseconds);
293
+ }, [clearPendingInputFlush, emitInput, terminalInput]);
294
+
295
+ const handleReadable = useCallback((): void => {
296
+ clearPendingInputFlush();
297
+ let chunk;
298
+ // eslint-disable-next-line @typescript-eslint/no-restricted-types
299
+ while ((chunk = stdin.read() as string | null) !== null) {
300
+ const inputEvents = terminalInput.push(chunk);
301
+ for (const event of inputEvents) {
302
+ if (typeof event === "string") {
303
+ emitInput(event);
304
+ } else {
305
+ // Keep paste on a separate channel from `useInput` so key handlers
306
+ // don't need to branch on mixed key-vs-paste event shapes.
307
+ if (internal_eventEmitter.current.listenerCount("paste") === 0) {
308
+ emitInput(event.paste);
309
+ continue;
310
+ }
311
+
312
+ internal_eventEmitter.current.emit("paste", event.paste);
313
+ }
314
+ }
315
+ }
316
+
317
+ if (terminalInput.hasPendingEscape()) {
318
+ schedulePendingInputFlush();
319
+ }
320
+ }, [stdin, emitInput, clearPendingInputFlush, schedulePendingInputFlush, terminalInput]);
321
+
322
+ const attachReadableListener = useCallback((): void => {
323
+ if (readableListenerRef.current) {
324
+ return;
325
+ }
326
+
327
+ // Store the listener reference to avoid stale closure when removing
328
+ readableListenerRef.current = handleReadable;
329
+ stdin.addListener("readable", handleReadable);
330
+ }, [stdin, handleReadable]);
331
+
332
+ const handleSetRawMode = useCallback(
333
+ (isEnabled: boolean): void => {
334
+ if (!rawModeStdin) {
335
+ if (stdin === process.stdin) {
336
+ throw new Error(
337
+ "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",
338
+ );
339
+ } else {
340
+ throw new Error(
341
+ "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",
342
+ );
343
+ }
344
+ }
345
+
346
+ rawModeStdin.setEncoding("utf8");
347
+
348
+ if (isEnabled) {
349
+ if (rawModeEnabledCount.current === 0) {
350
+ // A same-render component swap may have detached input handling while
351
+ // leaving terminal raw mode enabled until the queued disable runs.
352
+ const isRawModeAlreadyEnabled = pendingDisableRawModeRef.current;
353
+ pendingDisableRawModeRef.current = false;
354
+
355
+ if (!isRawModeAlreadyEnabled) {
356
+ rawModeStdin.ref?.();
357
+ rawModeStdin.setRawMode(true);
358
+ }
359
+
360
+ attachReadableListener();
361
+ }
362
+
363
+ rawModeEnabledCount.current++;
364
+ return;
365
+ }
366
+
367
+ if (rawModeEnabledCount.current === 0) {
368
+ return;
369
+ }
370
+
371
+ if (--rawModeEnabledCount.current === 0) {
372
+ // Stop owning input immediately so pending parser state cannot leak into
373
+ // a replacement `useInput` component mounted in the same React update.
374
+ clearInputState();
375
+
376
+ // Defer only the terminal raw-mode teardown so a same-render replacement
377
+ // can keep the process ref and raw mode active without a disable/enable cycle.
378
+ pendingDisableRawModeRef.current = true;
379
+ queueMicrotask(() => {
380
+ if (!pendingDisableRawModeRef.current) {
381
+ return;
382
+ }
383
+
384
+ disableRawMode();
385
+ });
386
+ }
387
+ },
388
+ [rawModeStdin, stdin, attachReadableListener, clearInputState, disableRawMode],
389
+ );
390
+
391
+ const handleQueryTerminal = useCallback(
392
+ async ({ refresh = false } = {}): Promise<TerminalQueryResult | undefined> => {
393
+ if (!interactive || !isRawModeSupported || !stdout.isTTY) {
394
+ return undefined;
395
+ }
396
+
397
+ // Without a refresh, an existing (or in-flight) query answers directly.
398
+ if (!refresh) {
399
+ const existing = getTerminalQueryPromise(stdout);
400
+ if (existing) {
401
+ return existing;
402
+ }
403
+ }
404
+
405
+ // Own the input stream for the duration of the query: raw mode on so
406
+ // responses arrive unbuffered, and the readable listener detached so they
407
+ // flow to the query's own data listener instead of being parsed as key
408
+ // presses. queryTerminal buffers real user input typed during the query
409
+ // and unshifts it back; reattaching the readable listener delivers it.
410
+ //
411
+ // Note: if kitty keyboard auto-detection is in flight at the same time,
412
+ // both data listeners see the same bytes; that detector answers the same
413
+ // CSI ? u probe this query sends, so it converges to the same result.
414
+ handleSetRawMode(true);
415
+ detachReadableListener();
416
+ try {
417
+ return await (refresh
418
+ ? refreshTerminalQuery(stdin, stdout)
419
+ : ensureTerminalQuery(stdin, stdout));
420
+ } finally {
421
+ // A slow terminal response may arrive after this renderer has been
422
+ // replaced. Do not leave its reader attached to consume input meant
423
+ // for a subsequent renderer.
424
+ if (isMountedRef.current) {
425
+ attachReadableListener();
426
+ }
427
+ handleSetRawMode(false);
428
+ }
429
+ },
430
+ [
431
+ interactive,
432
+ isRawModeSupported,
433
+ stdin,
434
+ stdout,
435
+ handleSetRawMode,
436
+ detachReadableListener,
437
+ attachReadableListener,
438
+ ],
439
+ );
440
+
441
+ // While the store has push reporting enabled, someone must actually read
442
+ // stdin so the reports reach `ingest` — even in apps without a single
443
+ // `useInput`. Holding raw mode (ref-counted, like `useInput` does) keeps
444
+ // the readable pipeline attached for the duration.
445
+ const reportFeedActiveRef = useRef(false);
446
+ const handleSetReportFeed = useCallback(
447
+ (active: boolean): void => {
448
+ if (active === reportFeedActiveRef.current || !isRawModeSupported) {
449
+ return;
450
+ }
451
+
452
+ reportFeedActiveRef.current = active;
453
+ handleSetRawMode(active);
454
+ },
455
+ [isRawModeSupported, handleSetRawMode],
456
+ );
457
+
458
+ // Hand the capabilities store this instance's integration: queries go
459
+ // through the input pipeline above instead of competing with it, and the
460
+ // report feed keeps that pipeline running while reports are enabled. An
461
+ // insertion effect runs before every passive effect — parent and child —
462
+ // so a child calling store.query() from its own mount effect always finds
463
+ // the integration already registered.
464
+ useInsertionEffect(() => {
465
+ const unregister = registerTerminalIntegration(stdout, {
466
+ runQuery: handleQueryTerminal,
467
+ setReportFeed: handleSetReportFeed,
468
+ });
469
+
470
+ return () => {
471
+ unregister();
472
+ handleSetReportFeed(false);
473
+ };
474
+ }, [stdout, handleQueryTerminal, handleSetReportFeed]);
475
+
476
+ const handleSetBracketedPasteMode = useCallback(
477
+ (isEnabled: boolean): void => {
478
+ if (!stdout.isTTY) {
479
+ return;
480
+ }
481
+
482
+ if (isEnabled) {
483
+ if (bracketedPasteModeEnabledCount.current === 0) {
484
+ stdout.write(ansiEscapes.enableBracketedPaste);
485
+ }
486
+
487
+ bracketedPasteModeEnabledCount.current++;
488
+ return;
489
+ }
490
+
491
+ if (bracketedPasteModeEnabledCount.current === 0) {
492
+ return;
493
+ }
494
+
495
+ if (--bracketedPasteModeEnabledCount.current === 0) {
496
+ stdout.write(ansiEscapes.disableBracketedPaste);
497
+ }
498
+ },
499
+ [stdout],
500
+ );
501
+
502
+ // Remembers which input modes were active so resumeInput can reinstate exactly
503
+ // those after a terminal suspension, without touching the ref counts (the React
504
+ // components still "own" raw mode/bracketed paste across the suspension).
505
+ const suspendedInputStateRef = useRef({
506
+ rawMode: false,
507
+ bracketedPaste: false,
508
+ });
509
+
510
+ const pauseInput = useCallback((): void => {
511
+ const wasRawMode = isRawModeSupported && rawModeEnabledCount.current > 0;
512
+ const wasBracketedPaste = bracketedPasteModeEnabledCount.current > 0;
513
+ suspendedInputStateRef.current = {
514
+ rawMode: wasRawMode,
515
+ bracketedPaste: wasBracketedPaste,
516
+ };
517
+
518
+ if (wasBracketedPaste && stdout.isTTY) {
519
+ try {
520
+ stdout.write(ansiEscapes.disableBracketedPaste);
521
+ } catch {}
522
+ }
523
+
524
+ if (wasRawMode) {
525
+ rawModeStdin?.setRawMode(false);
526
+ rawModeStdin?.unref?.();
527
+ clearInputState();
528
+ }
529
+ }, [isRawModeSupported, rawModeStdin, stdout, clearInputState]);
530
+
531
+ const resumeInput = useCallback((): void => {
532
+ const { rawMode, bracketedPaste } = suspendedInputStateRef.current;
533
+
534
+ if (rawMode) {
535
+ rawModeStdin?.setEncoding("utf8");
536
+ rawModeStdin?.ref?.();
537
+ rawModeStdin?.setRawMode(true);
538
+ attachReadableListener();
539
+ }
540
+
541
+ if (bracketedPaste && stdout.isTTY) {
542
+ try {
543
+ stdout.write(ansiEscapes.enableBracketedPaste);
544
+ } catch {}
545
+ }
546
+ }, [rawModeStdin, stdout, attachReadableListener]);
547
+
548
+ // Register input pause/resume in an insertion effect: it runs before every
549
+ // passive effect (parent and child), so a child that calls suspendTerminal()
550
+ // from its own effect always finds the input control already registered. A
551
+ // normal effect would run too late (child effects fire before the parent's).
552
+ useInsertionEffect(() => {
553
+ onRegisterInputControl(pauseInput, resumeInput);
554
+ }, [onRegisterInputControl, pauseInput, resumeInput]);
555
+
556
+ // Focus navigation helpers
557
+ const findNextFocusable = useCallback(
558
+ (
559
+ currentFocusables: Focusable[],
560
+ currentActiveFocusId: string | undefined,
561
+ ): string | undefined => {
562
+ const activeIndex = currentFocusables.findIndex((focusable) => {
563
+ return focusable.id === currentActiveFocusId;
564
+ });
565
+
566
+ for (let index = activeIndex + 1; index < currentFocusables.length; index++) {
567
+ const focusable = currentFocusables[index];
568
+
569
+ if (focusable?.isActive) {
570
+ return focusable.id;
571
+ }
572
+ }
573
+
574
+ return;
575
+ },
576
+ [],
577
+ );
578
+
579
+ const findPreviousFocusable = useCallback(
580
+ (
581
+ currentFocusables: Focusable[],
582
+ currentActiveFocusId: string | undefined,
583
+ ): string | undefined => {
584
+ const activeIndex = currentFocusables.findIndex((focusable) => {
585
+ return focusable.id === currentActiveFocusId;
586
+ });
587
+
588
+ for (let index = activeIndex - 1; index >= 0; index--) {
589
+ const focusable = currentFocusables[index];
590
+
591
+ if (focusable?.isActive) {
592
+ return focusable.id;
593
+ }
594
+ }
595
+
596
+ return;
597
+ },
598
+ [],
599
+ );
600
+
601
+ const focusNext = useCallback((): void => {
602
+ setFocusables((currentFocusables) => {
603
+ setActiveFocusId((currentActiveFocusId) => {
604
+ const firstFocusableId = currentFocusables.find((focusable) => focusable.isActive)?.id;
605
+ const nextFocusableId = findNextFocusable(currentFocusables, currentActiveFocusId);
606
+
607
+ return nextFocusableId ?? firstFocusableId;
608
+ });
609
+ return currentFocusables;
610
+ });
611
+ }, [findNextFocusable]);
612
+
613
+ const focusPrevious = useCallback((): void => {
614
+ setFocusables((currentFocusables) => {
615
+ setActiveFocusId((currentActiveFocusId) => {
616
+ const lastFocusableId = currentFocusables.findLast((focusable) => focusable.isActive)?.id;
617
+ const previousFocusableId = findPreviousFocusable(currentFocusables, currentActiveFocusId);
618
+
619
+ return previousFocusableId ?? lastFocusableId;
620
+ });
621
+ return currentFocusables;
622
+ });
623
+ }, [findPreviousFocusable]);
624
+
625
+ // Handle tab navigation via effect that subscribes to input events
626
+ useEffect(() => {
627
+ const handleTabNavigation = (input: string): void => {
628
+ if (!isFocusEnabled || focusablesCountRef.current === 0) return;
629
+
630
+ if (input === tab) {
631
+ focusNext();
632
+ }
633
+
634
+ if (input === shiftTab) {
635
+ focusPrevious();
636
+ }
637
+ };
638
+
639
+ internal_eventEmitter.current.on("input", handleTabNavigation);
640
+ const emitter = internal_eventEmitter.current;
641
+
642
+ return () => {
643
+ emitter.off("input", handleTabNavigation);
644
+ };
645
+ }, [isFocusEnabled, focusNext, focusPrevious]);
646
+
647
+ const enableFocus = useCallback((): void => {
648
+ setIsFocusEnabled(true);
649
+ }, []);
650
+
651
+ const disableFocus = useCallback((): void => {
652
+ setIsFocusEnabled(false);
653
+ }, []);
654
+
655
+ const focus = useCallback((id: string): void => {
656
+ setFocusables((currentFocusables) => {
657
+ const hasFocusableId = currentFocusables.some((focusable) => focusable?.id === id);
658
+
659
+ if (hasFocusableId) {
660
+ setActiveFocusId(id);
661
+ }
662
+
663
+ return currentFocusables;
664
+ });
665
+ }, []);
666
+
667
+ const addFocusable = useCallback((id: string, { autoFocus }: { autoFocus: boolean }): void => {
668
+ setFocusables((currentFocusables) => {
669
+ focusablesCountRef.current = currentFocusables.length + 1;
670
+
671
+ return [
672
+ ...currentFocusables,
673
+ {
674
+ id,
675
+ isActive: true,
676
+ },
677
+ ];
678
+ });
679
+
680
+ if (autoFocus) {
681
+ setActiveFocusId((currentActiveFocusId) => {
682
+ if (!currentActiveFocusId) {
683
+ return id;
684
+ }
685
+
686
+ return currentActiveFocusId;
687
+ });
688
+ }
689
+ }, []);
690
+
691
+ const removeFocusable = useCallback((id: string): void => {
692
+ setActiveFocusId((currentActiveFocusId) => {
693
+ if (currentActiveFocusId === id) {
694
+ return;
695
+ }
696
+
697
+ return currentActiveFocusId;
698
+ });
699
+
700
+ setFocusables((currentFocusables) => {
701
+ const filtered = currentFocusables.filter((focusable) => {
702
+ return focusable.id !== id;
703
+ });
704
+ focusablesCountRef.current = filtered.length;
705
+
706
+ return filtered;
707
+ });
708
+ }, []);
709
+
710
+ const activateFocusable = useCallback((id: string): void => {
711
+ setFocusables((currentFocusables) =>
712
+ currentFocusables.map((focusable) => {
713
+ if (focusable.id !== id) {
714
+ return focusable;
715
+ }
716
+
717
+ return {
718
+ id,
719
+ isActive: true,
720
+ };
721
+ }),
722
+ );
723
+ }, []);
724
+
725
+ const deactivateFocusable = useCallback((id: string): void => {
726
+ setActiveFocusId((currentActiveFocusId) => {
727
+ if (currentActiveFocusId === id) {
728
+ return;
729
+ }
730
+
731
+ return currentActiveFocusId;
732
+ });
733
+
734
+ setFocusables((currentFocusables) =>
735
+ currentFocusables.map((focusable) => {
736
+ if (focusable.id !== id) {
737
+ return focusable;
738
+ }
739
+
740
+ return {
741
+ id,
742
+ isActive: false,
743
+ };
744
+ }),
745
+ );
746
+ }, []);
747
+
748
+ // Handle cursor visibility, raw mode, and bracketed paste mode cleanup on unmount
749
+ useEffect(() => {
750
+ isMountedRef.current = true;
751
+
752
+ return () => {
753
+ isMountedRef.current = false;
754
+ const canWriteToStdout = !stdout.destroyed && !stdout.writableEnded;
755
+
756
+ if (interactive && canWriteToStdout) {
757
+ cliCursor.show(stdout);
758
+ }
759
+
760
+ if (
761
+ isRawModeSupported &&
762
+ (rawModeEnabledCount.current > 0 || pendingDisableRawModeRef.current)
763
+ ) {
764
+ disableRawMode();
765
+ }
766
+
767
+ if (bracketedPasteModeEnabledCount.current > 0) {
768
+ if (stdout.isTTY && canWriteToStdout) {
769
+ stdout.write(ansiEscapes.disableBracketedPaste);
770
+ }
771
+
772
+ bracketedPasteModeEnabledCount.current = 0;
773
+ }
774
+ };
775
+ }, [stdout, isRawModeSupported, disableRawMode, interactive]);
776
+
777
+ // Memoize context values to prevent unnecessary re-renders
778
+ const appContextValue = useMemo(
779
+ () => ({
780
+ exit: handleExit,
781
+ waitUntilRenderFlush: onWaitUntilRenderFlush,
782
+ suspendTerminal: onSuspendTerminal,
783
+ }),
784
+ [handleExit, onWaitUntilRenderFlush, onSuspendTerminal],
785
+ );
786
+
787
+ const stdinContextValue = useMemo(
788
+ () => ({
789
+ stdin,
790
+ setRawMode: handleSetRawMode,
791
+ setBracketedPasteMode: handleSetBracketedPasteMode,
792
+ isRawModeSupported,
793
+ // eslint-disable-next-line @typescript-eslint/naming-convention
794
+ internal_exitOnCtrlC: exitOnCtrlC,
795
+ // eslint-disable-next-line @typescript-eslint/naming-convention
796
+ internal_eventEmitter: internal_eventEmitter.current,
797
+ }),
798
+ [stdin, handleSetRawMode, handleSetBracketedPasteMode, isRawModeSupported, exitOnCtrlC],
799
+ );
800
+
801
+ const stdoutContextValue = useMemo(
802
+ () => ({
803
+ stdout,
804
+ write: writeToStdout,
805
+ }),
806
+ [stdout, writeToStdout],
807
+ );
808
+
809
+ const stderrContextValue = useMemo(
810
+ () => ({
811
+ stderr,
812
+ write: writeToStderr,
813
+ }),
814
+ [stderr, writeToStderr],
815
+ );
816
+
817
+ const cursorContextValue = useMemo(
818
+ () => ({
819
+ setCursorPosition,
820
+ }),
821
+ [setCursorPosition],
822
+ );
823
+
824
+ const focusContextValue = useMemo(
825
+ () => ({
826
+ activeId: activeFocusId,
827
+ add: addFocusable,
828
+ remove: removeFocusable,
829
+ activate: activateFocusable,
830
+ deactivate: deactivateFocusable,
831
+ enableFocus,
832
+ disableFocus,
833
+ focusNext,
834
+ focusPrevious,
835
+ focus,
836
+ }),
837
+ [
838
+ activeFocusId,
839
+ addFocusable,
840
+ removeFocusable,
841
+ activateFocusable,
842
+ deactivateFocusable,
843
+ enableFocus,
844
+ disableFocus,
845
+ focusNext,
846
+ focusPrevious,
847
+ focus,
848
+ ],
849
+ );
850
+
851
+ const animationContextValue = useMemo(
852
+ () => ({
853
+ renderThrottleMs,
854
+ subscribe: animationSubscribe,
855
+ }),
856
+ [animationSubscribe, renderThrottleMs],
857
+ );
858
+
859
+ return (
860
+ <AppContext.Provider value={appContextValue}>
861
+ <StdinContext.Provider value={stdinContextValue}>
862
+ <StdoutContext.Provider value={stdoutContextValue}>
863
+ <StderrContext.Provider value={stderrContextValue}>
864
+ <FocusContext.Provider value={focusContextValue}>
865
+ <AnimationContext.Provider value={animationContextValue}>
866
+ <CursorContext.Provider value={cursorContextValue}>
867
+ <ErrorBoundary onError={handleExit}>{children}</ErrorBoundary>
868
+ </CursorContext.Provider>
869
+ </AnimationContext.Provider>
870
+ </FocusContext.Provider>
871
+ </StderrContext.Provider>
872
+ </StdoutContext.Provider>
873
+ </StdinContext.Provider>
874
+ </AppContext.Provider>
875
+ );
876
+ }
877
+
878
+ App.displayName = "InternalApp";