ink 6.7.0 → 6.8.0

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 (45) hide show
  1. package/build/ansi-tokenizer.d.ts +38 -0
  2. package/build/ansi-tokenizer.js +316 -0
  3. package/build/ansi-tokenizer.js.map +1 -0
  4. package/build/components/App.d.ts +1 -1
  5. package/build/components/App.js +60 -29
  6. package/build/components/App.js.map +1 -1
  7. package/build/components/AppContext.d.ts +5 -1
  8. package/build/components/AppContext.js.map +1 -1
  9. package/build/components/Cursor.d.ts +83 -0
  10. package/build/components/Cursor.js +53 -0
  11. package/build/components/Cursor.js.map +1 -0
  12. package/build/dom.js +5 -4
  13. package/build/dom.js.map +1 -1
  14. package/build/index.d.ts +2 -0
  15. package/build/index.js +1 -0
  16. package/build/index.js.map +1 -1
  17. package/build/ink.d.ts +7 -3
  18. package/build/ink.js +151 -50
  19. package/build/ink.js.map +1 -1
  20. package/build/input-parser.d.ts +7 -0
  21. package/build/input-parser.js +154 -0
  22. package/build/input-parser.js.map +1 -0
  23. package/build/layout.d.ts +7 -0
  24. package/build/layout.js +33 -0
  25. package/build/layout.js.map +1 -0
  26. package/build/output.d.ts +1 -0
  27. package/build/output.js +38 -5
  28. package/build/output.js.map +1 -1
  29. package/build/reconciler.js +12 -2
  30. package/build/reconciler.js.map +1 -1
  31. package/build/render-to-string.d.ts +38 -0
  32. package/build/render-to-string.js +115 -0
  33. package/build/render-to-string.js.map +1 -0
  34. package/build/render.d.ts +12 -1
  35. package/build/render.js.map +1 -1
  36. package/build/sanitize-ansi.d.ts +2 -0
  37. package/build/sanitize-ansi.js +27 -0
  38. package/build/sanitize-ansi.js.map +1 -0
  39. package/build/squash-text-nodes.js +2 -1
  40. package/build/squash-text-nodes.js.map +1 -1
  41. package/build/utils.d.ts +2 -0
  42. package/build/utils.js +4 -0
  43. package/build/utils.js.map +1 -0
  44. package/package.json +10 -7
  45. package/readme.md +125 -13
@@ -0,0 +1,83 @@
1
+ import React, { type RefObject } from 'react';
2
+ import { type DOMElement, type CursorAnchorMode } from '../dom.js';
3
+ export type Props = {
4
+ /**
5
+ Optional reference to anchor cursor coordinates to a different element.
6
+
7
+ By default, `anchorRef` uses `anchor="textEnd"` behavior to follow the rendered end of that element's text, including wrapping and wide characters.
8
+
9
+ Use this for inputs where the cursor should stay at the visible end of text.
10
+
11
+ If `anchorRef` is set but currently unresolved, Ink hides the cursor for that frame unless `anchor="flow"` is used.
12
+
13
+ If multiple `<Cursor>` components are rendered in one frame, the last rendered one controls terminal cursor position.
14
+ */
15
+ readonly anchorRef?: RefObject<DOMElement | null>;
16
+ /**
17
+ Anchor mode used to resolve cursor coordinates.
18
+
19
+ - `'flow'`: Anchor to `<Cursor />` position in layout flow.
20
+ Use this when you place `<Cursor />` exactly where it should appear.
21
+
22
+ - `'origin'`: Anchor to content origin (top-left) of `anchorRef` or parent when no `anchorRef` is provided.
23
+ Use this for manual `x/y` positioning.
24
+
25
+ - `'textEnd'`: Anchor to rendered end of text for `anchorRef` or parent when no `anchorRef` is provided.
26
+ Use this when cursor should follow wrapped text.
27
+
28
+ Defaults to `'flow'` when `anchorRef` is omitted and `'textEnd'` when `anchorRef` is provided.
29
+
30
+ `'flow'` is the default without `anchorRef` to avoid coupling cursor position to surrounding sibling text changes.
31
+ */
32
+ readonly anchor?: CursorAnchorMode;
33
+ /**
34
+ Horizontal offset from resolved anchor position.
35
+ */
36
+ readonly x?: number;
37
+ /**
38
+ Vertical offset from resolved anchor position.
39
+ */
40
+ readonly y?: number;
41
+ };
42
+ /**
43
+ Declaratively position the terminal cursor relative to a container.
44
+
45
+ Use this component when building reusable inputs where absolute root coordinates are inconvenient.
46
+
47
+ `<Cursor>` must not be rendered inside `<Text>`.
48
+
49
+ @example
50
+ ```jsx
51
+ import {Box, Cursor, Text} from 'ink';
52
+ import {useRef} from 'react';
53
+
54
+ const prompt = '> ';
55
+ const value = 'hello';
56
+
57
+ const Example = () => {
58
+ return (
59
+ <Box flexDirection="row">
60
+ <Text>{prompt}</Text>
61
+ <Text>{value}</Text>
62
+ <Cursor />
63
+ </Box>
64
+ );
65
+ };
66
+ ```
67
+
68
+ ```jsx
69
+ const ExampleWithAnchor = () => {
70
+ const lineReference = useRef();
71
+
72
+ return (
73
+ <Box flexDirection="column">
74
+ <Box ref={lineReference}>
75
+ <Text>{`${prompt}${value}`}</Text>
76
+ </Box>
77
+ <Cursor anchorRef={lineReference} />
78
+ </Box>
79
+ );
80
+ };
81
+ ```
82
+ */
83
+ export default function Cursor({ anchorRef, anchor, x, y }: Props): React.JSX.Element;
@@ -0,0 +1,53 @@
1
+ import React from 'react';
2
+ /**
3
+ Declaratively position the terminal cursor relative to a container.
4
+
5
+ Use this component when building reusable inputs where absolute root coordinates are inconvenient.
6
+
7
+ `<Cursor>` must not be rendered inside `<Text>`.
8
+
9
+ @example
10
+ ```jsx
11
+ import {Box, Cursor, Text} from 'ink';
12
+ import {useRef} from 'react';
13
+
14
+ const prompt = '> ';
15
+ const value = 'hello';
16
+
17
+ const Example = () => {
18
+ return (
19
+ <Box flexDirection="row">
20
+ <Text>{prompt}</Text>
21
+ <Text>{value}</Text>
22
+ <Cursor />
23
+ </Box>
24
+ );
25
+ };
26
+ ```
27
+
28
+ ```jsx
29
+ const ExampleWithAnchor = () => {
30
+ const lineReference = useRef();
31
+
32
+ return (
33
+ <Box flexDirection="column">
34
+ <Box ref={lineReference}>
35
+ <Text>{`${prompt}${value}`}</Text>
36
+ </Box>
37
+ <Cursor anchorRef={lineReference} />
38
+ </Box>
39
+ );
40
+ };
41
+ ```
42
+ */
43
+ export default function Cursor({ anchorRef, anchor, x = 0, y = 0 }) {
44
+ const normalizedAnchorReference = anchorRef ?? undefined;
45
+ const normalizedAnchor = anchor ?? (normalizedAnchorReference ? 'textEnd' : 'flow');
46
+ return (React.createElement("ink-cursor", { internal_cursor: {
47
+ anchorRef: normalizedAnchorReference,
48
+ anchor: normalizedAnchor,
49
+ x,
50
+ y,
51
+ } }));
52
+ }
53
+ //# sourceMappingURL=Cursor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Cursor.js","sourceRoot":"","sources":["../../src/components/Cursor.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAuB,MAAM,OAAO,CAAC;AAmD5C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAwCE;AACF,MAAM,CAAC,OAAO,UAAU,MAAM,CAAC,EAAC,SAAS,EAAE,MAAM,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAQ;IACtE,MAAM,yBAAyB,GAC9B,SAAS,IAAI,SAAS,CAAC;IACxB,MAAM,gBAAgB,GACrB,MAAM,IAAI,CAAC,yBAAyB,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;IAE5D,OAAO,CACN,oCACC,eAAe,EAAE;YAChB,SAAS,EAAE,yBAAyB;YACpC,MAAM,EAAE,gBAAgB;YACxB,CAAC;YACD,CAAC;SACD,GACA,CACF,CAAC;AACH,CAAC"}
package/build/dom.js CHANGED
@@ -42,11 +42,12 @@ export const insertBeforeNode = (node, newChildNode, beforeChildNode) => {
42
42
  if (newChildNode.yogaNode) {
43
43
  node.yogaNode?.insertChild(newChildNode.yogaNode, index);
44
44
  }
45
- return;
46
45
  }
47
- node.childNodes.push(newChildNode);
48
- if (newChildNode.yogaNode) {
49
- node.yogaNode?.insertChild(newChildNode.yogaNode, node.yogaNode.getChildCount());
46
+ else {
47
+ node.childNodes.push(newChildNode);
48
+ if (newChildNode.yogaNode) {
49
+ node.yogaNode?.insertChild(newChildNode.yogaNode, node.yogaNode.getChildCount());
50
+ }
50
51
  }
51
52
  if (node.nodeName === 'ink-text' || node.nodeName === 'ink-virtual-text') {
52
53
  markNodeAsDirty(node);
package/build/dom.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"dom.js","sourceRoot":"","sources":["../src/dom.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"dom.js","sourceRoot":"","sources":["../src/dom.ts"],"names":[],"mappings":"AAAA,OAAO,IAA6B,MAAM,aAAa,CAAC;AACxD,OAAO,WAAW,MAAM,mBAAmB,CAAC;AAE5C,OAAO,QAAQ,MAAM,gBAAgB,CAAC;AACtC,OAAO,eAAe,MAAM,wBAAwB,CAAC;AAoFrD,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,QAAsB,EAAc,EAAE;IAChE,MAAM,IAAI,GAAe;QACxB,QAAQ;QACR,KAAK,EAAE,EAAE;QACT,UAAU,EAAE,EAAE;QACd,UAAU,EAAE,EAAE;QACd,UAAU,EAAE,SAAS;QACrB,QAAQ,EAAE,QAAQ,KAAK,kBAAkB,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE;QAC1E,gEAAgE;QAChE,sBAAsB,EAAE,EAAE;KAC1B,CAAC;IAEF,IAAI,QAAQ,KAAK,UAAU,EAAE,CAAC;QAC7B,IAAI,CAAC,QAAQ,EAAE,cAAc,CAAC,eAAe,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;IACjE,CAAC;IAED,OAAO,IAAI,CAAC;AACb,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,eAAe,GAAG,CAC9B,IAAgB,EAChB,SAAqB,EACd,EAAE;IACT,IAAI,SAAS,CAAC,UAAU,EAAE,CAAC;QAC1B,eAAe,CAAC,SAAS,CAAC,UAAU,EAAE,SAAS,CAAC,CAAC;IAClD,CAAC;IAED,SAAS,CAAC,UAAU,GAAG,IAAI,CAAC;IAC5B,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IAEhC,IAAI,SAAS,CAAC,QAAQ,EAAE,CAAC;QACxB,IAAI,CAAC,QAAQ,EAAE,WAAW,CACzB,SAAS,CAAC,QAAQ,EAClB,IAAI,CAAC,QAAQ,CAAC,aAAa,EAAE,CAC7B,CAAC;IACH,CAAC;IAED,IAAI,IAAI,CAAC,QAAQ,KAAK,UAAU,IAAI,IAAI,CAAC,QAAQ,KAAK,kBAAkB,EAAE,CAAC;QAC1E,eAAe,CAAC,IAAI,CAAC,CAAC;IACvB,CAAC;AACF,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAC/B,IAAgB,EAChB,YAAqB,EACrB,eAAwB,EACjB,EAAE;IACT,IAAI,YAAY,CAAC,UAAU,EAAE,CAAC;QAC7B,eAAe,CAAC,YAAY,CAAC,UAAU,EAAE,YAAY,CAAC,CAAC;IACxD,CAAC;IAED,YAAY,CAAC,UAAU,GAAG,IAAI,CAAC;IAE/B,MAAM,KAAK,GAAG,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC;IACvD,IAAI,KAAK,IAAI,CAAC,EAAE,CAAC;QAChB,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,EAAE,YAAY,CAAC,CAAC;QAC/C,IAAI,YAAY,CAAC,QAAQ,EAAE,CAAC;YAC3B,IAAI,CAAC,QAAQ,EAAE,WAAW,CAAC,YAAY,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;QAC1D,CAAC;IACF,CAAC;SAAM,CAAC;QACP,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;QAEnC,IAAI,YAAY,CAAC,QAAQ,EAAE,CAAC;YAC3B,IAAI,CAAC,QAAQ,EAAE,WAAW,CACzB,YAAY,CAAC,QAAQ,EACrB,IAAI,CAAC,QAAQ,CAAC,aAAa,EAAE,CAC7B,CAAC;QACH,CAAC;IACF,CAAC;IAED,IAAI,IAAI,CAAC,QAAQ,KAAK,UAAU,IAAI,IAAI,CAAC,QAAQ,KAAK,kBAAkB,EAAE,CAAC;QAC1E,eAAe,CAAC,IAAI,CAAC,CAAC;IACvB,CAAC;AACF,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,eAAe,GAAG,CAC9B,IAAgB,EAChB,UAAmB,EACZ,EAAE;IACT,IAAI,UAAU,CAAC,QAAQ,EAAE,CAAC;QACzB,UAAU,CAAC,UAAU,EAAE,QAAQ,EAAE,WAAW,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;IACnE,CAAC;IAED,UAAU,CAAC,UAAU,GAAG,SAAS,CAAC;IAElC,MAAM,KAAK,GAAG,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;IAClD,IAAI,KAAK,IAAI,CAAC,EAAE,CAAC;QAChB,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;IAClC,CAAC;IAED,IAAI,IAAI,CAAC,QAAQ,KAAK,UAAU,IAAI,IAAI,CAAC,QAAQ,KAAK,kBAAkB,EAAE,CAAC;QAC1E,eAAe,CAAC,IAAI,CAAC,CAAC;IACvB,CAAC;AACF,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,YAAY,GAAG,CAC3B,IAAgB,EAChB,GAAW,EACX,KAAuB,EAChB,EAAE;IACT,IAAI,GAAG,KAAK,wBAAwB,EAAE,CAAC;QACtC,IAAI,CAAC,sBAAsB,GAAG,KAA6C,CAAC;QAC5E,OAAO;IACR,CAAC;IAED,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;AAC9B,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,IAAa,EAAE,KAAa,EAAQ,EAAE;IAC9D,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;AACpB,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,IAAY,EAAY,EAAE;IACxD,MAAM,IAAI,GAAa;QACtB,QAAQ,EAAE,OAAO;QACjB,SAAS,EAAE,IAAI;QACf,QAAQ,EAAE,SAAS;QACnB,UAAU,EAAE,SAAS;QACrB,KAAK,EAAE,EAAE;KACT,CAAC;IAEF,gBAAgB,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IAE7B,OAAO,IAAI,CAAC;AACb,CAAC,CAAC;AAEF,MAAM,eAAe,GAAG,UACvB,IAAa,EACb,KAAa;IAEb,MAAM,IAAI,GACT,IAAI,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC;IAEpE,MAAM,UAAU,GAAG,WAAW,CAAC,IAAI,CAAC,CAAC;IAErC,4CAA4C;IAC5C,IAAI,UAAU,CAAC,KAAK,IAAI,KAAK,EAAE,CAAC;QAC/B,OAAO,UAAU,CAAC;IACnB,CAAC;IAED,sEAAsE;IACtE,0EAA0E;IAC1E,IAAI,UAAU,CAAC,KAAK,IAAI,CAAC,IAAI,KAAK,GAAG,CAAC,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;QACrD,OAAO,UAAU,CAAC;IACnB,CAAC;IAED,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,EAAE,QAAQ,IAAI,MAAM,CAAC;IAChD,MAAM,WAAW,GAAG,QAAQ,CAAC,IAAI,EAAE,KAAK,EAAE,QAAQ,CAAC,CAAC;IAEpD,OAAO,WAAW,CAAC,WAAW,CAAC,CAAC;AACjC,CAAC,CAAC;AAEF,MAAM,mBAAmB,GAAG,CAAC,IAAc,EAAwB,EAAE;IACpE,IAAI,CAAC,IAAI,EAAE,UAAU,EAAE,CAAC;QACvB,OAAO,SAAS,CAAC;IAClB,CAAC;IAED,OAAO,IAAI,CAAC,QAAQ,IAAI,mBAAmB,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;AAC9D,CAAC,CAAC;AAEF,MAAM,eAAe,GAAG,CAAC,IAAc,EAAQ,EAAE;IAChD,mEAAmE;IACnE,MAAM,QAAQ,GAAG,mBAAmB,CAAC,IAAI,CAAC,CAAC;IAC3C,QAAQ,EAAE,SAAS,EAAE,CAAC;AACvB,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,IAAc,EAAE,IAAY,EAAQ,EAAE;IACtE,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC9B,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC;IACrB,CAAC;IAED,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;IACtB,eAAe,CAAC,IAAI,CAAC,CAAC;AACvB,CAAC,CAAC"}
package/build/index.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  export type { RenderOptions, Instance } from './render.js';
2
2
  export { default as render } from './render.js';
3
+ export type { RenderToStringOptions } from './render-to-string.js';
4
+ export { default as renderToString } from './render-to-string.js';
3
5
  export type { Props as BoxProps } from './components/Box.js';
4
6
  export { default as Box } from './components/Box.js';
5
7
  export type { Props as TextProps } from './components/Text.js';
package/build/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export { default as render } from './render.js';
2
+ export { default as renderToString } from './render-to-string.js';
2
3
  export { default as Box } from './components/Box.js';
3
4
  export { default as Text } from './components/Text.js';
4
5
  export { default as Static } from './components/Static.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,OAAO,IAAI,MAAM,EAAC,MAAM,aAAa,CAAC;AAE9C,OAAO,EAAC,OAAO,IAAI,GAAG,EAAC,MAAM,qBAAqB,CAAC;AAEnD,OAAO,EAAC,OAAO,IAAI,IAAI,EAAC,MAAM,sBAAsB,CAAC;AAMrD,OAAO,EAAC,OAAO,IAAI,MAAM,EAAC,MAAM,wBAAwB,CAAC;AAEzD,OAAO,EAAC,OAAO,IAAI,SAAS,EAAC,MAAM,2BAA2B,CAAC;AAE/D,OAAO,EAAC,OAAO,IAAI,OAAO,EAAC,MAAM,yBAAyB,CAAC;AAC3D,OAAO,EAAC,OAAO,IAAI,MAAM,EAAC,MAAM,wBAAwB,CAAC;AAEzD,OAAO,EAAC,OAAO,IAAI,QAAQ,EAAC,MAAM,sBAAsB,CAAC;AACzD,OAAO,EAAC,OAAO,IAAI,MAAM,EAAC,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAC,OAAO,IAAI,QAAQ,EAAC,MAAM,sBAAsB,CAAC;AACzD,OAAO,EAAC,OAAO,IAAI,SAAS,EAAC,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAAC,OAAO,IAAI,SAAS,EAAC,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAAC,OAAO,IAAI,QAAQ,EAAC,MAAM,sBAAsB,CAAC;AACzD,OAAO,EAAC,OAAO,IAAI,eAAe,EAAC,MAAM,8BAA8B,CAAC;AACxE,OAAO,EAAC,OAAO,IAAI,wBAAwB,EAAC,MAAM,yCAAyC,CAAC;AAC5F,OAAO,EAAC,OAAO,IAAI,SAAS,EAAC,MAAM,uBAAuB,CAAC;AAE3D,OAAO,EAAC,OAAO,IAAI,cAAc,EAAC,MAAM,sBAAsB,CAAC;AAE/D,OAAO,EAAC,UAAU,EAAE,cAAc,EAAC,MAAM,qBAAqB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,OAAO,IAAI,MAAM,EAAC,MAAM,aAAa,CAAC;AAE9C,OAAO,EAAC,OAAO,IAAI,cAAc,EAAC,MAAM,uBAAuB,CAAC;AAEhE,OAAO,EAAC,OAAO,IAAI,GAAG,EAAC,MAAM,qBAAqB,CAAC;AAEnD,OAAO,EAAC,OAAO,IAAI,IAAI,EAAC,MAAM,sBAAsB,CAAC;AAMrD,OAAO,EAAC,OAAO,IAAI,MAAM,EAAC,MAAM,wBAAwB,CAAC;AAEzD,OAAO,EAAC,OAAO,IAAI,SAAS,EAAC,MAAM,2BAA2B,CAAC;AAE/D,OAAO,EAAC,OAAO,IAAI,OAAO,EAAC,MAAM,yBAAyB,CAAC;AAC3D,OAAO,EAAC,OAAO,IAAI,MAAM,EAAC,MAAM,wBAAwB,CAAC;AAEzD,OAAO,EAAC,OAAO,IAAI,QAAQ,EAAC,MAAM,sBAAsB,CAAC;AACzD,OAAO,EAAC,OAAO,IAAI,MAAM,EAAC,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAC,OAAO,IAAI,QAAQ,EAAC,MAAM,sBAAsB,CAAC;AACzD,OAAO,EAAC,OAAO,IAAI,SAAS,EAAC,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAAC,OAAO,IAAI,SAAS,EAAC,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAAC,OAAO,IAAI,QAAQ,EAAC,MAAM,sBAAsB,CAAC;AACzD,OAAO,EAAC,OAAO,IAAI,eAAe,EAAC,MAAM,8BAA8B,CAAC;AACxE,OAAO,EAAC,OAAO,IAAI,wBAAwB,EAAC,MAAM,yCAAyC,CAAC;AAC5F,OAAO,EAAC,OAAO,IAAI,SAAS,EAAC,MAAM,uBAAuB,CAAC;AAE3D,OAAO,EAAC,OAAO,IAAI,cAAc,EAAC,MAAM,sBAAsB,CAAC;AAE/D,OAAO,EAAC,UAAU,EAAE,cAAc,EAAC,MAAM,qBAAqB,CAAC"}
package/build/ink.d.ts CHANGED
@@ -19,7 +19,7 @@ export type Options = {
19
19
  patchConsole: boolean;
20
20
  onRender?: (metrics: RenderMetrics) => void;
21
21
  isScreenReaderEnabled?: boolean;
22
- waitUntilExit?: () => Promise<void>;
22
+ waitUntilExit?: () => Promise<unknown>;
23
23
  maxFps?: number;
24
24
  incrementalRendering?: boolean;
25
25
  /**
@@ -49,6 +49,7 @@ export default class Ink {
49
49
  private readonly throttledLog;
50
50
  private readonly isScreenReaderEnabled;
51
51
  private isUnmounted;
52
+ private isUnmounting;
52
53
  private lastOutput;
53
54
  private lastOutputToRender;
54
55
  private lastOutputHeight;
@@ -57,18 +58,21 @@ export default class Ink {
57
58
  private readonly rootNode;
58
59
  private fullStaticOutput;
59
60
  private exitPromise?;
61
+ private exitResult;
60
62
  private beforeExitHandler?;
61
63
  private restoreConsole?;
62
64
  private readonly unsubscribeResize?;
63
65
  private readonly throttledOnRender?;
66
+ private hasPendingThrottledRender;
64
67
  private kittyProtocolEnabled;
65
68
  private cancelKittyDetection?;
66
69
  constructor(options: Options);
67
70
  getTerminalWidth: () => number;
68
71
  resized: () => void;
69
- resolveExitPromise: () => void;
72
+ resolveExitPromise: (result?: unknown) => void;
70
73
  rejectExitPromise: (reason?: Error) => void;
71
74
  unsubscribeExit: () => void;
75
+ handleAppExit: (errorOrResult?: unknown) => void;
72
76
  setCursorPosition: (position: CursorPosition | undefined) => void;
73
77
  restoreLastOutput: () => void;
74
78
  calculateLayout: () => void;
@@ -77,7 +81,7 @@ export default class Ink {
77
81
  writeToStdout(data: string): void;
78
82
  writeToStderr(data: string): void;
79
83
  unmount(error?: Error | number | null): void;
80
- waitUntilExit(): Promise<void>;
84
+ waitUntilExit(): Promise<unknown>;
81
85
  clear(): void;
82
86
  patchConsole(): void;
83
87
  private initKittyKeyboard;
package/build/ink.js CHANGED
@@ -10,6 +10,7 @@ import { LegacyRoot, ConcurrentRoot } from 'react-reconciler/constants.js';
10
10
  import Yoga from 'yoga-layout';
11
11
  import wrapAnsi from 'wrap-ansi';
12
12
  import terminalSize from 'terminal-size';
13
+ import { isDev } from './utils.js';
13
14
  import reconciler from './reconciler.js';
14
15
  import render from './renderer.js';
15
16
  import * as dom from './dom.js';
@@ -20,6 +21,65 @@ import App from './components/App.js';
20
21
  import { accessibilityContext as AccessibilityContext } from './components/AccessibilityContext.js';
21
22
  import { resolveFlags, } from './kitty-keyboard.js';
22
23
  const noop = () => { };
24
+ const kittyQueryEscapeByte = 0x1b;
25
+ const kittyQueryOpenBracketByte = 0x5b;
26
+ const kittyQueryQuestionMarkByte = 0x3f;
27
+ const kittyQueryLetterByte = 0x75;
28
+ const zeroByte = 0x30;
29
+ const nineByte = 0x39;
30
+ const isDigitByte = (byte) => byte >= zeroByte && byte <= nineByte;
31
+ const matchKittyQueryResponse = (buffer, startIndex) => {
32
+ if (buffer[startIndex] !== kittyQueryEscapeByte ||
33
+ buffer[startIndex + 1] !== kittyQueryOpenBracketByte ||
34
+ buffer[startIndex + 2] !== kittyQueryQuestionMarkByte) {
35
+ return undefined;
36
+ }
37
+ let index = startIndex + 3;
38
+ const digitsStartIndex = index;
39
+ while (index < buffer.length && isDigitByte(buffer[index])) {
40
+ index++;
41
+ }
42
+ if (index === digitsStartIndex) {
43
+ return undefined;
44
+ }
45
+ if (index === buffer.length) {
46
+ return { state: 'partial' };
47
+ }
48
+ if (buffer[index] === kittyQueryLetterByte) {
49
+ return { state: 'complete', endIndex: index };
50
+ }
51
+ return undefined;
52
+ };
53
+ const hasCompleteKittyQueryResponse = (buffer) => {
54
+ for (let index = 0; index < buffer.length; index++) {
55
+ const match = matchKittyQueryResponse(buffer, index);
56
+ if (match?.state === 'complete') {
57
+ return true;
58
+ }
59
+ }
60
+ return false;
61
+ };
62
+ const stripKittyQueryResponsesAndTrailingPartial = (buffer) => {
63
+ const keptBytes = [];
64
+ let index = 0;
65
+ while (index < buffer.length) {
66
+ const match = matchKittyQueryResponse(buffer, index);
67
+ if (match?.state === 'complete') {
68
+ index = match.endIndex + 1;
69
+ continue;
70
+ }
71
+ if (match?.state === 'partial') {
72
+ break;
73
+ }
74
+ keptBytes.push(buffer[index]);
75
+ index++;
76
+ }
77
+ return keptBytes;
78
+ };
79
+ const isErrorInput = (value) => {
80
+ return (value instanceof Error ||
81
+ Object.prototype.toString.call(value) === '[object Error]');
82
+ };
23
83
  export default class Ink {
24
84
  /**
25
85
  Whether this instance is using concurrent rendering mode.
@@ -32,6 +92,7 @@ export default class Ink {
32
92
  isScreenReaderEnabled;
33
93
  // Ignore last render after unmounting a tree to prevent empty output before exit
34
94
  isUnmounted;
95
+ isUnmounting;
35
96
  lastOutput;
36
97
  lastOutputToRender;
37
98
  lastOutputHeight;
@@ -42,10 +103,12 @@ export default class Ink {
42
103
  // so that it's rerendered every time, not just new static parts, like in non-debug mode
43
104
  fullStaticOutput;
44
105
  exitPromise;
106
+ exitResult;
45
107
  beforeExitHandler;
46
108
  restoreConsole;
47
109
  unsubscribeResize;
48
110
  throttledOnRender;
111
+ hasPendingThrottledRender = false;
49
112
  kittyProtocolEnabled = false;
50
113
  cancelKittyDetection;
51
114
  constructor(options) {
@@ -68,7 +131,10 @@ export default class Ink {
68
131
  leading: true,
69
132
  trailing: true,
70
133
  });
71
- this.rootNode.onRender = throttled;
134
+ this.rootNode.onRender = () => {
135
+ this.hasPendingThrottledRender = true;
136
+ throttled();
137
+ };
72
138
  this.throttledOnRender = throttled;
73
139
  }
74
140
  this.rootNode.onImmediateRender = this.onRender;
@@ -94,6 +160,7 @@ export default class Ink {
94
160
  });
95
161
  // Ignore last render after unmounting a tree to prevent empty output before exit
96
162
  this.isUnmounted = false;
163
+ this.isUnmounting = false;
97
164
  // Store concurrent mode setting
98
165
  this.isConcurrent = options.concurrent ?? false;
99
166
  // Store last output to only rerender when needed
@@ -110,14 +177,9 @@ export default class Ink {
110
177
  this.container = reconciler.createContainer(this.rootNode, rootTag, null, false, null, 'id', () => { }, () => { }, () => { }, () => { });
111
178
  // Unmount when process exits
112
179
  this.unsubscribeExit = signalExit(this.unmount, { alwaysLast: false });
113
- if (process.env['DEV'] === 'true') {
114
- reconciler.injectIntoDevTools({
115
- bundleType: 0,
116
- // Reporting React DOM's version, not Ink's
117
- // See https://github.com/facebook/react/issues/16666#issuecomment-532639905
118
- version: '16.13.1',
119
- rendererPackageName: 'ink',
120
- });
180
+ if (isDev()) {
181
+ // @ts-expect-error outdated types
182
+ reconciler.injectIntoDevTools();
121
183
  }
122
184
  if (options.patchConsole) {
123
185
  this.patchConsole();
@@ -154,6 +216,17 @@ export default class Ink {
154
216
  resolveExitPromise = () => { };
155
217
  rejectExitPromise = () => { };
156
218
  unsubscribeExit = () => { };
219
+ handleAppExit = (errorOrResult) => {
220
+ if (this.isUnmounted || this.isUnmounting) {
221
+ return;
222
+ }
223
+ if (isErrorInput(errorOrResult)) {
224
+ this.unmount(errorOrResult);
225
+ return;
226
+ }
227
+ this.exitResult = errorOrResult;
228
+ this.unmount();
229
+ };
157
230
  setCursorPosition = (position) => {
158
231
  this.cursorPosition = position;
159
232
  this.log.setCursorPosition(position);
@@ -170,6 +243,7 @@ export default class Ink {
170
243
  this.rootNode.yogaNode.calculateLayout(undefined, undefined, Yoga.DIRECTION_LTR);
171
244
  };
172
245
  onRender = () => {
246
+ this.hasPendingThrottledRender = false;
173
247
  if (this.isUnmounted) {
174
248
  return;
175
249
  }
@@ -283,7 +357,7 @@ export default class Ink {
283
357
  };
284
358
  render(node) {
285
359
  const tree = (React.createElement(AccessibilityContext.Provider, { value: { isScreenReaderEnabled: this.isScreenReaderEnabled } },
286
- React.createElement(App, { stdin: this.options.stdin, stdout: this.options.stdout, stderr: this.options.stderr, exitOnCtrlC: this.options.exitOnCtrlC, writeToStdout: this.writeToStdout, writeToStderr: this.writeToStderr, setCursorPosition: this.setCursorPosition, onExit: this.unmount }, node)));
360
+ React.createElement(App, { stdin: this.options.stdin, stdout: this.options.stdout, stderr: this.options.stderr, exitOnCtrlC: this.options.exitOnCtrlC, writeToStdout: this.writeToStdout, writeToStderr: this.writeToStderr, setCursorPosition: this.setCursorPosition, onExit: this.handleAppExit }, node)));
287
361
  if (this.options.concurrent) {
288
362
  // Concurrent mode: use updateContainer (async scheduling)
289
363
  reconciler.updateContainer(tree, this.container, null, noop);
@@ -343,19 +417,44 @@ export default class Ink {
343
417
  }
344
418
  // eslint-disable-next-line @typescript-eslint/ban-types
345
419
  unmount(error) {
346
- if (this.isUnmounted) {
420
+ if (this.isUnmounted || this.isUnmounting) {
347
421
  return;
348
422
  }
423
+ this.isUnmounting = true;
349
424
  if (this.beforeExitHandler) {
350
425
  process.off('beforeExit', this.beforeExitHandler);
351
426
  this.beforeExitHandler = undefined;
352
427
  }
353
- // Flush any pending throttled render to ensure the final frame is rendered
354
- if (this.throttledOnRender) {
355
- this.throttledOnRender.flush();
428
+ const stdout = this.options.stdout;
429
+ const canWriteToStdout = !stdout.destroyed && !stdout.writableEnded && (stdout.writable ?? true);
430
+ const settleThrottle = (throttled) => {
431
+ if (typeof throttled.flush !== 'function') {
432
+ return;
433
+ }
434
+ if (canWriteToStdout) {
435
+ throttled.flush();
436
+ }
437
+ else if (typeof throttled.cancel === 'function') {
438
+ throttled.cancel();
439
+ }
440
+ };
441
+ // Clear any pending throttled render timer on unmount. When stdout is writable,
442
+ // flush so the final frame is emitted; otherwise cancel to avoid delayed callbacks.
443
+ settleThrottle(this.throttledOnRender ?? {});
444
+ if (canWriteToStdout) {
445
+ // If throttling is enabled and there is already a pending render, flushing above
446
+ // is sufficient. Also avoid calling onRender() again when static output already
447
+ // exists, as that can duplicate <Static> children output on exit (see issue #397).
448
+ const shouldRenderFinalFrame = !this.throttledOnRender ||
449
+ (!this.hasPendingThrottledRender && this.fullStaticOutput === '');
450
+ if (shouldRenderFinalFrame) {
451
+ this.calculateLayout();
452
+ this.onRender();
453
+ }
356
454
  }
357
- this.calculateLayout();
358
- this.onRender();
455
+ // Mark as unmounted after the final render but before stdout writes
456
+ // that could re-enter exit() via synchronous write callbacks.
457
+ this.isUnmounted = true;
359
458
  this.unsubscribeExit();
360
459
  if (typeof this.restoreConsole === 'function') {
361
460
  this.restoreConsole();
@@ -363,33 +462,33 @@ export default class Ink {
363
462
  if (typeof this.unsubscribeResize === 'function') {
364
463
  this.unsubscribeResize();
365
464
  }
366
- // Flush any pending throttled log writes
367
- const throttledLog = this.throttledLog;
368
- if (typeof throttledLog.flush === 'function') {
369
- throttledLog.flush();
370
- }
371
465
  // Cancel any in-progress auto-detection before checking protocol state
372
466
  if (this.cancelKittyDetection) {
373
467
  this.cancelKittyDetection();
374
468
  }
375
- if (this.kittyProtocolEnabled) {
376
- try {
377
- this.options.stdout.write('\u001B[<u');
469
+ // Flush any pending throttled log writes if possible, otherwise cancel to
470
+ // prevent delayed callbacks from writing to a closed stream.
471
+ const throttledLog = this.throttledLog;
472
+ settleThrottle(throttledLog);
473
+ if (canWriteToStdout) {
474
+ if (this.kittyProtocolEnabled) {
475
+ try {
476
+ this.options.stdout.write('\u001B[<u');
477
+ }
478
+ catch {
479
+ // Best-effort: stdout may already be destroyed during shutdown
480
+ }
378
481
  }
379
- catch {
380
- // Best-effort: stdout may already be destroyed during shutdown
482
+ // CIs don't handle erasing ansi escapes well, so it's better to
483
+ // only render last frame of non-static output
484
+ if (isInCi) {
485
+ this.options.stdout.write(this.lastOutput + '\n');
486
+ }
487
+ else if (!this.options.debug) {
488
+ this.log.done();
381
489
  }
382
- this.kittyProtocolEnabled = false;
383
- }
384
- // CIs don't handle erasing ansi escapes well, so it's better to
385
- // only render last frame of non-static output
386
- if (isInCi) {
387
- this.options.stdout.write(this.lastOutput + '\n');
388
- }
389
- else if (!this.options.debug) {
390
- this.log.done();
391
490
  }
392
- this.isUnmounted = true;
491
+ this.kittyProtocolEnabled = false;
393
492
  if (this.options.concurrent) {
394
493
  // Concurrent mode: use updateContainer (async scheduling)
395
494
  reconciler.updateContainer(null, this.container, null, noop);
@@ -408,20 +507,22 @@ export default class Ink {
408
507
  // When called from signal-exit during process shutdown (error is a
409
508
  // number or null rather than undefined/Error), resolve synchronously
410
509
  // because the event loop is draining and async callbacks won't fire.
510
+ const { exitResult } = this;
411
511
  const resolveOrReject = () => {
412
- if (error instanceof Error) {
512
+ if (isErrorInput(error)) {
413
513
  this.rejectExitPromise(error);
414
514
  }
415
515
  else {
416
- this.resolveExitPromise();
516
+ this.resolveExitPromise(exitResult);
417
517
  }
418
518
  };
419
- const isProcessExiting = error !== undefined && !(error instanceof Error);
519
+ const isProcessExiting = error !== undefined && !isErrorInput(error);
520
+ const hasWritableState = stdout._writableState !== undefined ||
521
+ stdout.writableLength !== undefined;
420
522
  if (isProcessExiting) {
421
523
  resolveOrReject();
422
524
  }
423
- else if (this.options.stdout._writableState !== undefined ||
424
- this.options.stdout.writableLength !== undefined) {
525
+ else if (canWriteToStdout && hasWritableState) {
425
526
  this.options.stdout.write('', resolveOrReject);
426
527
  }
427
528
  else {
@@ -495,7 +596,7 @@ export default class Ink {
495
596
  }
496
597
  confirmKittySupport(flags) {
497
598
  const { stdin, stdout } = this.options;
498
- let responseBuffer = '';
599
+ let responseBuffer = [];
499
600
  const cleanup = () => {
500
601
  this.cancelKittyDetection = undefined;
501
602
  clearTimeout(timer);
@@ -503,18 +604,18 @@ export default class Ink {
503
604
  // Re-emit any buffered data that wasn't the protocol response,
504
605
  // so it isn't lost from Ink's normal input pipeline.
505
606
  // Clear responseBuffer afterwards to make cleanup idempotent.
506
- // eslint-disable-next-line no-control-regex
507
- const remaining = responseBuffer.replace(/\u001B\[\?\d+u/, '');
508
- responseBuffer = '';
509
- if (remaining) {
607
+ const remaining = stripKittyQueryResponsesAndTrailingPartial(responseBuffer);
608
+ responseBuffer = [];
609
+ if (remaining.length > 0) {
510
610
  stdin.unshift(Buffer.from(remaining));
511
611
  }
512
612
  };
513
613
  const onData = (data) => {
514
- responseBuffer +=
515
- typeof data === 'string' ? data : Buffer.from(data).toString();
516
- // eslint-disable-next-line no-control-regex
517
- if (/\u001B\[\?\d+u/.test(responseBuffer)) {
614
+ const chunk = typeof data === 'string' ? Buffer.from(data) : data;
615
+ for (const byte of chunk) {
616
+ responseBuffer.push(byte);
617
+ }
618
+ if (hasCompleteKittyQueryResponse(responseBuffer)) {
518
619
  cleanup();
519
620
  if (!this.isUnmounted) {
520
621
  this.enableKittyProtocol(flags);