@nuvin/ink 6.6.7 → 7.0.1-alpha

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