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.
- package/build/ansi-tokenizer.d.ts +38 -0
- package/build/ansi-tokenizer.js +316 -0
- package/build/ansi-tokenizer.js.map +1 -0
- package/build/components/App.d.ts +1 -1
- package/build/components/App.js +60 -29
- package/build/components/App.js.map +1 -1
- package/build/components/AppContext.d.ts +5 -1
- package/build/components/AppContext.js.map +1 -1
- package/build/components/Cursor.d.ts +83 -0
- package/build/components/Cursor.js +53 -0
- package/build/components/Cursor.js.map +1 -0
- package/build/dom.js +5 -4
- package/build/dom.js.map +1 -1
- package/build/index.d.ts +2 -0
- package/build/index.js +1 -0
- package/build/index.js.map +1 -1
- package/build/ink.d.ts +7 -3
- package/build/ink.js +151 -50
- package/build/ink.js.map +1 -1
- package/build/input-parser.d.ts +7 -0
- package/build/input-parser.js +154 -0
- package/build/input-parser.js.map +1 -0
- package/build/layout.d.ts +7 -0
- package/build/layout.js +33 -0
- package/build/layout.js.map +1 -0
- package/build/output.d.ts +1 -0
- package/build/output.js +38 -5
- package/build/output.js.map +1 -1
- package/build/reconciler.js +12 -2
- package/build/reconciler.js.map +1 -1
- package/build/render-to-string.d.ts +38 -0
- package/build/render-to-string.js +115 -0
- package/build/render-to-string.js.map +1 -0
- package/build/render.d.ts +12 -1
- package/build/render.js.map +1 -1
- package/build/sanitize-ansi.d.ts +2 -0
- package/build/sanitize-ansi.js +27 -0
- package/build/sanitize-ansi.js.map +1 -0
- package/build/squash-text-nodes.js +2 -1
- package/build/squash-text-nodes.js.map +1 -1
- package/build/utils.d.ts +2 -0
- package/build/utils.js +4 -0
- package/build/utils.js.map +1 -0
- package/package.json +10 -7
- package/readme.md +125 -13
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import Yoga from 'yoga-layout';
|
|
2
|
+
import { LegacyRoot } from 'react-reconciler/constants.js';
|
|
3
|
+
import reconciler from './reconciler.js';
|
|
4
|
+
import renderer from './renderer.js';
|
|
5
|
+
import { createNode } from './dom.js';
|
|
6
|
+
/**
|
|
7
|
+
Render a React element to a string synchronously. Unlike `render()`, this function does not write to stdout, does not set up any terminal event listeners, and returns the rendered output as a string.
|
|
8
|
+
|
|
9
|
+
Useful for generating documentation, writing output to files, testing, or any scenario where you need the rendered output as a string without starting a persistent terminal application.
|
|
10
|
+
|
|
11
|
+
**Notes:**
|
|
12
|
+
|
|
13
|
+
- Terminal-specific hooks (`useInput`, `useStdin`, `useStdout`, `useStderr`, `useApp`, `useFocus`, `useFocusManager`) return default no-op values since there is no terminal session. They will not throw, but they will not function as in a live terminal.
|
|
14
|
+
- `useEffect` callbacks will execute during rendering (due to synchronous rendering mode), but state updates they trigger will not affect the returned output, which reflects the initial render.
|
|
15
|
+
- `useLayoutEffect` callbacks fire synchronously during commit, so state updates they trigger **will** be reflected in the output.
|
|
16
|
+
- The `<Static>` component is supported — its output is prepended to the dynamic output.
|
|
17
|
+
- If a component throws during rendering, the error is propagated to the caller after cleanup.
|
|
18
|
+
|
|
19
|
+
@example
|
|
20
|
+
```
|
|
21
|
+
import {renderToString, Text, Box} from 'ink';
|
|
22
|
+
|
|
23
|
+
const output = renderToString(
|
|
24
|
+
<Box padding={1}>
|
|
25
|
+
<Text color="green">Hello World</Text>
|
|
26
|
+
</Box>,
|
|
27
|
+
{columns: 40}
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
console.log(output);
|
|
31
|
+
```
|
|
32
|
+
*/
|
|
33
|
+
const renderToString = (node, options) => {
|
|
34
|
+
const columns = options?.columns ?? 80;
|
|
35
|
+
// Create a standalone root node — no stdout, stdin, or terminal bindings
|
|
36
|
+
const rootNode = createNode('ink-root');
|
|
37
|
+
// Capture static output from intermediate renders.
|
|
38
|
+
// The <Static> component uses useLayoutEffect to clear its children after
|
|
39
|
+
// the first commit. The reconciler's resetAfterCommit calls onImmediateRender
|
|
40
|
+
// when static content is dirty (and returns early, skipping the normal
|
|
41
|
+
// onRender callback), giving us a chance to capture it before it's cleared
|
|
42
|
+
// by the subsequent re-render.
|
|
43
|
+
let capturedStaticOutput = '';
|
|
44
|
+
rootNode.onComputeLayout = () => {
|
|
45
|
+
rootNode.yogaNode.setWidth(columns);
|
|
46
|
+
rootNode.yogaNode.calculateLayout(undefined, undefined, Yoga.DIRECTION_LTR);
|
|
47
|
+
};
|
|
48
|
+
rootNode.onImmediateRender = () => {
|
|
49
|
+
const { staticOutput } = renderer(rootNode, false);
|
|
50
|
+
if (staticOutput && staticOutput !== '\n') {
|
|
51
|
+
capturedStaticOutput += staticOutput;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
// Capture the first uncaught error so we can re-throw it after cleanup.
|
|
55
|
+
// React's reconciler catches component errors internally and reports them
|
|
56
|
+
// via onUncaughtError rather than letting them propagate. For a synchronous
|
|
57
|
+
// utility like renderToString, callers expect errors to throw.
|
|
58
|
+
let uncaughtError;
|
|
59
|
+
// Create a reconciler container in legacy (synchronous) mode.
|
|
60
|
+
// The four trailing callbacks are: onUncaughtError, onCaughtError,
|
|
61
|
+
// onRecoverableError, and onHostTransitionComplete.
|
|
62
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
63
|
+
const container = reconciler.createContainer(rootNode, LegacyRoot, null, false, null, 'render-to-string', (error) => {
|
|
64
|
+
uncaughtError ??= error;
|
|
65
|
+
}, () => { }, () => { }, () => { });
|
|
66
|
+
let teardownSucceeded = false;
|
|
67
|
+
try {
|
|
68
|
+
// Synchronously render the React tree into the container
|
|
69
|
+
reconciler.updateContainerSync(node, container, null, () => { });
|
|
70
|
+
reconciler.flushSyncWork();
|
|
71
|
+
// Yoga layout has already been calculated by onComputeLayout during commit.
|
|
72
|
+
// Render the DOM tree to a string — this captures the dynamic (non-static) output.
|
|
73
|
+
const { output } = renderer(rootNode, false);
|
|
74
|
+
// Tear down: unmount the tree so the reconciler cleans up child nodes
|
|
75
|
+
// and runs effect cleanup functions. Child Yoga nodes are freed by the
|
|
76
|
+
// reconciler's removeChildFromContainer → cleanupYogaNode → freeRecursive.
|
|
77
|
+
reconciler.updateContainerSync(null, container, null, () => { });
|
|
78
|
+
reconciler.flushSyncWork();
|
|
79
|
+
teardownSucceeded = true;
|
|
80
|
+
// Free the root yoga node itself (children already freed by reconciler)
|
|
81
|
+
rootNode.yogaNode.free();
|
|
82
|
+
// Re-throw after full cleanup so callers see the original error.
|
|
83
|
+
if (uncaughtError !== undefined) {
|
|
84
|
+
throw uncaughtError instanceof Error
|
|
85
|
+
? uncaughtError
|
|
86
|
+
: new Error(String(uncaughtError));
|
|
87
|
+
}
|
|
88
|
+
// The renderer appends a trailing newline to static output for terminal
|
|
89
|
+
// rendering (so dynamic output starts on a fresh line). Strip it here
|
|
90
|
+
// so renderToString returns clean output.
|
|
91
|
+
const normalizedStaticOutput = capturedStaticOutput.endsWith('\n')
|
|
92
|
+
? capturedStaticOutput.slice(0, -1)
|
|
93
|
+
: capturedStaticOutput;
|
|
94
|
+
if (normalizedStaticOutput && output) {
|
|
95
|
+
return normalizedStaticOutput + '\n' + output;
|
|
96
|
+
}
|
|
97
|
+
return normalizedStaticOutput || output;
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
// Ensure native Yoga memory is freed even if rendering or teardown threw.
|
|
101
|
+
// Yoga nodes are WASM-backed and not garbage collected.
|
|
102
|
+
if (!teardownSucceeded && rootNode.yogaNode) {
|
|
103
|
+
try {
|
|
104
|
+
// If reconciler teardown failed, some child nodes may not have been
|
|
105
|
+
// freed. Use freeRecursive to clean up the entire tree as best-effort.
|
|
106
|
+
rootNode.yogaNode.freeRecursive();
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// Best-effort: node may already be partially freed
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
export default renderToString;
|
|
115
|
+
//# sourceMappingURL=render-to-string.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render-to-string.js","sourceRoot":"","sources":["../src/render-to-string.ts"],"names":[],"mappings":"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"}
|
package/build/render.d.ts
CHANGED
|
@@ -94,7 +94,18 @@ export type Instance = {
|
|
|
94
94
|
*/
|
|
95
95
|
unmount: Ink['unmount'];
|
|
96
96
|
/**
|
|
97
|
-
Returns a promise that
|
|
97
|
+
Returns a promise that settles when the app is unmounted.
|
|
98
|
+
|
|
99
|
+
It resolves with the value passed to `exit(value)` and rejects with the error passed to `exit(error)`.
|
|
100
|
+
|
|
101
|
+
@example
|
|
102
|
+
```jsx
|
|
103
|
+
const {unmount, waitUntilExit} = render(<MyApp />);
|
|
104
|
+
|
|
105
|
+
setTimeout(unmount, 1000);
|
|
106
|
+
|
|
107
|
+
await waitUntilExit(); // resolves after `unmount()` is called
|
|
108
|
+
```
|
|
98
109
|
*/
|
|
99
110
|
waitUntilExit: Ink['waitUntilExit'];
|
|
100
111
|
cleanup: () => void;
|
package/build/render.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"render.js","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,MAAM,EAAC,MAAM,aAAa,CAAC;AACnC,OAAO,OAAO,MAAM,cAAc,CAAC;AAEnC,OAAO,GAAqD,MAAM,UAAU,CAAC;AAC7E,OAAO,SAAS,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"render.js","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,MAAM,EAAC,MAAM,aAAa,CAAC;AACnC,OAAO,OAAO,MAAM,cAAc,CAAC;AAEnC,OAAO,GAAqD,MAAM,UAAU,CAAC;AAC7E,OAAO,SAAS,MAAM,gBAAgB,CAAC;AAqIvC;;EAEE;AACF,MAAM,MAAM,GAAG,CACd,IAAe,EACf,OAA4C,EACjC,EAAE;IACb,MAAM,UAAU,GAAe;QAC9B,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,KAAK,EAAE,OAAO,CAAC,KAAK;QACpB,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,KAAK,EAAE,KAAK;QACZ,WAAW,EAAE,IAAI;QACjB,YAAY,EAAE,IAAI;QAClB,MAAM,EAAE,EAAE;QACV,oBAAoB,EAAE,KAAK;QAC3B,UAAU,EAAE,KAAK;QACjB,GAAG,UAAU,CAAC,OAAO,CAAC;KACtB,CAAC;IAEF,MAAM,QAAQ,GAAQ,WAAW,CAChC,UAAU,CAAC,MAAM,EACjB,GAAG,EAAE,CAAC,IAAI,GAAG,CAAC,UAAU,CAAC,EACzB,UAAU,CAAC,UAAU,IAAI,KAAK,CAC9B,CAAC;IAEF,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IAEtB,OAAO;QACN,QAAQ,EAAE,QAAQ,CAAC,MAAM;QACzB,OAAO;YACN,QAAQ,CAAC,OAAO,EAAE,CAAC;QACpB,CAAC;QACD,aAAa,EAAE,QAAQ,CAAC,aAAa;QACrC,OAAO,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC;QAClD,KAAK,EAAE,QAAQ,CAAC,KAAK;KACrB,CAAC;AACH,CAAC,CAAC;AAEF,eAAe,MAAM,CAAC;AAEtB,MAAM,UAAU,GAAG,CAClB,SAAyD,EAAE,EAC3C,EAAE;IAClB,IAAI,MAAM,YAAY,MAAM,EAAE,CAAC;QAC9B,OAAO;YACN,MAAM;YACN,KAAK,EAAE,OAAO,CAAC,KAAK;SACpB,CAAC;IACH,CAAC;IAED,OAAO,MAAM,CAAC;AACf,CAAC,CAAC;AAEF,MAAM,WAAW,GAAG,CACnB,MAA0B,EAC1B,cAAyB,EACzB,UAAmB,EACb,EAAE;IACR,IAAI,QAAQ,GAAG,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IAErC,IAAI,CAAC,QAAQ,EAAE,CAAC;QACf,QAAQ,GAAG,cAAc,EAAE,CAAC;QAC5B,SAAS,CAAC,GAAG,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IACjC,CAAC;SAAM,IAAI,QAAQ,CAAC,YAAY,KAAK,UAAU,EAAE,CAAC;QACjD,OAAO,CAAC,IAAI,CACX,iDAAiD,UAAU,gEAAgE,QAAQ,CAAC,YAAY,IAAI;YACnJ,6HAA6H,CAC9H,CAAC;IACH,CAAC;IAED,OAAO,QAAQ,CAAC;AACjB,CAAC,CAAC"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { hasAnsiControlCharacters, tokenizeAnsi } from './ansi-tokenizer.js';
|
|
2
|
+
const sgrParametersRegex = /^[\d:;]*$/;
|
|
3
|
+
// Strip ANSI escape sequences that would conflict with Ink's layout.
|
|
4
|
+
// Preserved: SGR sequences (colors, bold, etc. - end with 'm') and
|
|
5
|
+
// OSC sequences (hyperlinks, etc. - ESC ] or C1 OSC).
|
|
6
|
+
// Stripped: cursor movement, screen clearing, and other control sequences.
|
|
7
|
+
const sanitizeAnsi = (text) => {
|
|
8
|
+
if (!hasAnsiControlCharacters(text)) {
|
|
9
|
+
return text;
|
|
10
|
+
}
|
|
11
|
+
let output = '';
|
|
12
|
+
for (const token of tokenizeAnsi(text)) {
|
|
13
|
+
if (token.type === 'text' || token.type === 'osc') {
|
|
14
|
+
output += token.value;
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
if (token.type === 'csi' &&
|
|
18
|
+
token.finalCharacter === 'm' &&
|
|
19
|
+
token.intermediateString === '' &&
|
|
20
|
+
sgrParametersRegex.test(token.parameterString)) {
|
|
21
|
+
output += token.value;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return output;
|
|
25
|
+
};
|
|
26
|
+
export default sanitizeAnsi;
|
|
27
|
+
//# sourceMappingURL=sanitize-ansi.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sanitize-ansi.js","sourceRoot":"","sources":["../src/sanitize-ansi.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,wBAAwB,EAAE,YAAY,EAAC,MAAM,qBAAqB,CAAC;AAE3E,MAAM,kBAAkB,GAAG,WAAW,CAAC;AAEvC,qEAAqE;AACrE,mEAAmE;AACnE,sDAAsD;AACtD,2EAA2E;AAC3E,MAAM,YAAY,GAAG,CAAC,IAAY,EAAU,EAAE;IAC7C,IAAI,CAAC,wBAAwB,CAAC,IAAI,CAAC,EAAE,CAAC;QACrC,OAAO,IAAI,CAAC;IACb,CAAC;IAED,IAAI,MAAM,GAAG,EAAE,CAAC;IAEhB,KAAK,MAAM,KAAK,IAAI,YAAY,CAAC,IAAI,CAAC,EAAE,CAAC;QACxC,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,IAAI,KAAK,CAAC,IAAI,KAAK,KAAK,EAAE,CAAC;YACnD,MAAM,IAAI,KAAK,CAAC,KAAK,CAAC;YACtB,SAAS;QACV,CAAC;QAED,IACC,KAAK,CAAC,IAAI,KAAK,KAAK;YACpB,KAAK,CAAC,cAAc,KAAK,GAAG;YAC5B,KAAK,CAAC,kBAAkB,KAAK,EAAE;YAC/B,kBAAkB,CAAC,IAAI,CAAC,KAAK,CAAC,eAAe,CAAC,EAC7C,CAAC;YACF,MAAM,IAAI,KAAK,CAAC,KAAK,CAAC;QACvB,CAAC;IACF,CAAC;IAED,OAAO,MAAM,CAAC;AACf,CAAC,CAAC;AAEF,eAAe,YAAY,CAAC"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import sanitizeAnsi from './sanitize-ansi.js';
|
|
1
2
|
// Squashing text nodes allows to combine multiple text nodes into one and write
|
|
2
3
|
// to `Output` instance only once. For example, <Text>hello{' '}world</Text>
|
|
3
4
|
// is actually 3 text nodes, which would result 3 writes to `Output`.
|
|
@@ -29,7 +30,7 @@ const squashTextNodes = (node) => {
|
|
|
29
30
|
}
|
|
30
31
|
text += nodeText;
|
|
31
32
|
}
|
|
32
|
-
return text;
|
|
33
|
+
return sanitizeAnsi(text);
|
|
33
34
|
};
|
|
34
35
|
export default squashTextNodes;
|
|
35
36
|
//# sourceMappingURL=squash-text-nodes.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"squash-text-nodes.js","sourceRoot":"","sources":["../src/squash-text-nodes.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"squash-text-nodes.js","sourceRoot":"","sources":["../src/squash-text-nodes.ts"],"names":[],"mappings":"AACA,OAAO,YAAY,MAAM,oBAAoB,CAAC;AAE9C,gFAAgF;AAChF,4EAA4E;AAC5E,qEAAqE;AACrE,EAAE;AACF,kGAAkG;AAClG,wFAAwF;AACxF,MAAM,eAAe,GAAG,CAAC,IAAgB,EAAU,EAAE;IACpD,IAAI,IAAI,GAAG,EAAE,CAAC;IAEd,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,IAAI,CAAC,UAAU,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,CAAC;QAC7D,MAAM,SAAS,GAAG,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;QAEzC,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;YAC7B,SAAS;QACV,CAAC;QAED,IAAI,QAAQ,GAAG,EAAE,CAAC;QAElB,IAAI,SAAS,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;YACpC,QAAQ,GAAG,SAAS,CAAC,SAAS,CAAC;QAChC,CAAC;aAAM,CAAC;YACP,IACC,SAAS,CAAC,QAAQ,KAAK,UAAU;gBACjC,SAAS,CAAC,QAAQ,KAAK,kBAAkB,EACxC,CAAC;gBACF,QAAQ,GAAG,eAAe,CAAC,SAAS,CAAC,CAAC;YACvC,CAAC;YAED,oFAAoF;YACpF,iFAAiF;YACjF,IACC,QAAQ,CAAC,MAAM,GAAG,CAAC;gBACnB,OAAO,SAAS,CAAC,kBAAkB,KAAK,UAAU,EACjD,CAAC;gBACF,QAAQ,GAAG,SAAS,CAAC,kBAAkB,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;YAC1D,CAAC;QACF,CAAC;QAED,IAAI,IAAI,QAAQ,CAAC;IAClB,CAAC;IAED,OAAO,YAAY,CAAC,IAAI,CAAC,CAAC;AAC3B,CAAC,CAAC;AAEF,eAAe,eAAe,CAAC"}
|
package/build/utils.d.ts
ADDED
package/build/utils.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,cAAc,CAAC;AAEnC,MAAM,KAAK,GAAG,GAAG,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,MAAM,CAAC;AAElD,OAAO,EAAC,KAAK,EAAC,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ink",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.8.0",
|
|
4
4
|
"description": "React for CLI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": "vadimdemedes/ink",
|
|
@@ -22,8 +22,9 @@
|
|
|
22
22
|
"build": "tsc",
|
|
23
23
|
"prepare": "npm run build",
|
|
24
24
|
"test": "tsc --noEmit && xo && FORCE_COLOR=true ava",
|
|
25
|
-
"example": "NODE_NO_WARNINGS=1 node --
|
|
26
|
-
"benchmark": "NODE_NO_WARNINGS=1 node --
|
|
25
|
+
"example": "NODE_NO_WARNINGS=1 node --import=tsx",
|
|
26
|
+
"benchmark": "NODE_NO_WARNINGS=1 node --import=tsx",
|
|
27
|
+
"inspect": "react-devtools"
|
|
27
28
|
},
|
|
28
29
|
"files": [
|
|
29
30
|
"build"
|
|
@@ -59,7 +60,7 @@
|
|
|
59
60
|
"react-reconciler": "^0.33.0",
|
|
60
61
|
"scheduler": "^0.27.0",
|
|
61
62
|
"signal-exit": "^3.0.7",
|
|
62
|
-
"slice-ansi": "^
|
|
63
|
+
"slice-ansi": "^8.0.0",
|
|
63
64
|
"stack-utils": "^2.0.6",
|
|
64
65
|
"string-width": "^8.1.1",
|
|
65
66
|
"terminal-size": "^4.0.1",
|
|
@@ -95,16 +96,18 @@
|
|
|
95
96
|
"prettier": "^3.8.1",
|
|
96
97
|
"react": "^19.2.4",
|
|
97
98
|
"react-devtools-core": "^7.0.1",
|
|
99
|
+
"react-devtools": "^7.0.1",
|
|
100
|
+
"react-router": "^7.13.0",
|
|
98
101
|
"sinon": "^21.0.0",
|
|
99
102
|
"strip-ansi": "^7.1.0",
|
|
100
|
-
"
|
|
103
|
+
"tsx": "^4.21.0",
|
|
101
104
|
"typescript": "^5.8.3",
|
|
102
105
|
"xo": "^0.59.3"
|
|
103
106
|
},
|
|
104
107
|
"peerDependencies": {
|
|
105
108
|
"@types/react": ">=19.0.0",
|
|
106
109
|
"react": ">=19.0.0",
|
|
107
|
-
"react-devtools-core": "
|
|
110
|
+
"react-devtools-core": ">=6.1.2"
|
|
108
111
|
},
|
|
109
112
|
"peerDependenciesMeta": {
|
|
110
113
|
"@types/react": {
|
|
@@ -127,7 +130,7 @@
|
|
|
127
130
|
"tsx": "module"
|
|
128
131
|
},
|
|
129
132
|
"nodeArguments": [
|
|
130
|
-
"--
|
|
133
|
+
"--import=tsx"
|
|
131
134
|
]
|
|
132
135
|
},
|
|
133
136
|
"xo": {
|
package/readme.md
CHANGED
|
@@ -69,8 +69,6 @@ render(<Counter />);
|
|
|
69
69
|
|
|
70
70
|
<img src="media/demo.svg" width="600">
|
|
71
71
|
|
|
72
|
-
Feel free to play around with the code and fork this Repl at [https://repl.it/@vadimdemedes/ink-counter-demo](https://repl.it/@vadimdemedes/ink-counter-demo).
|
|
73
|
-
|
|
74
72
|
## Who's Using Ink?
|
|
75
73
|
|
|
76
74
|
- [Claude Code](https://github.com/anthropics/claude-code) - An agentic coding tool made by Anthropic.
|
|
@@ -134,6 +132,7 @@ Feel free to play around with the code and fork this Repl at [https://repl.it/@v
|
|
|
134
132
|
## Contents
|
|
135
133
|
|
|
136
134
|
- [Getting Started](#getting-started)
|
|
135
|
+
- [App Lifecycle](#app-lifecycle)
|
|
137
136
|
- [Components](#components)
|
|
138
137
|
- [`<Text>`](#text)
|
|
139
138
|
- [`<Box>`](#box)
|
|
@@ -156,7 +155,9 @@ Feel free to play around with the code and fork this Repl at [https://repl.it/@v
|
|
|
156
155
|
- [Screen Reader Support](#screen-reader-support)
|
|
157
156
|
- [Useful Components](#useful-components)
|
|
158
157
|
- [Useful Hooks](#useful-hooks)
|
|
158
|
+
- [Recipes](#recipes)
|
|
159
159
|
- [Examples](#examples)
|
|
160
|
+
- [Continuous Integration](#continuous-integration)
|
|
160
161
|
|
|
161
162
|
## Getting Started
|
|
162
163
|
|
|
@@ -223,6 +224,22 @@ Think of it as if every `<div>` in the browser had `display: flex`.
|
|
|
223
224
|
See [`<Box>`](#box) built-in component below for documentation on how to use Flexbox layouts in Ink.
|
|
224
225
|
Note that all text must be wrapped in a [`<Text>`](#text) component.
|
|
225
226
|
|
|
227
|
+
## App Lifecycle
|
|
228
|
+
|
|
229
|
+
An Ink app is a Node.js process, so it stays alive only while there is active work in the event loop (timers, pending promises, [`useInput`](#useinputinputhandler-options) listening on `stdin`, etc.). If your component tree has no async work, the app will render once and exit immediately.
|
|
230
|
+
|
|
231
|
+
To exit the app, press **Ctrl+C** (enabled by default via [`exitOnCtrlC`](#exitonctrlc)), call [`exit()`](#exiterrororresult) from [`useApp`](#useapp) inside a component, or call [`unmount()`](#unmount) on the object returned by [`render()`](#rendertree-options).
|
|
232
|
+
|
|
233
|
+
Use [`waitUntilExit()`](#waituntilexit) to run code after the app is unmounted:
|
|
234
|
+
|
|
235
|
+
```jsx
|
|
236
|
+
const {waitUntilExit} = render(<MyApp />);
|
|
237
|
+
|
|
238
|
+
await waitUntilExit();
|
|
239
|
+
|
|
240
|
+
console.log('App exited');
|
|
241
|
+
```
|
|
242
|
+
|
|
226
243
|
## Components
|
|
227
244
|
|
|
228
245
|
### `<Text>`
|
|
@@ -1420,12 +1437,11 @@ For example, to implement a hanging indent component, you can indent all the lin
|
|
|
1420
1437
|
```jsx
|
|
1421
1438
|
import {render, Transform} from 'ink';
|
|
1422
1439
|
|
|
1423
|
-
const HangingIndent = ({
|
|
1440
|
+
const HangingIndent = ({indent = 4, children}) => (
|
|
1424
1441
|
<Transform
|
|
1425
1442
|
transform={(line, index) =>
|
|
1426
1443
|
index === 0 ? line : ' '.repeat(indent) + line
|
|
1427
1444
|
}
|
|
1428
|
-
{...props}
|
|
1429
1445
|
>
|
|
1430
1446
|
{children}
|
|
1431
1447
|
</Transform>
|
|
@@ -1439,9 +1455,8 @@ const text =
|
|
|
1439
1455
|
'of my hands only. I lived there two years and two months. At ' +
|
|
1440
1456
|
'present I am a sojourner in civilized life again.';
|
|
1441
1457
|
|
|
1442
|
-
// Other text properties are allowed as well
|
|
1443
1458
|
render(
|
|
1444
|
-
<HangingIndent
|
|
1459
|
+
<HangingIndent indent={4}>
|
|
1445
1460
|
{text}
|
|
1446
1461
|
</HangingIndent>
|
|
1447
1462
|
);
|
|
@@ -1585,6 +1600,16 @@ Default: `false`
|
|
|
1585
1600
|
If the Page Up or Page Down key was pressed, the corresponding property will be `true`.
|
|
1586
1601
|
For example, if the user presses Page Down, `key.pageDown` equals `true`.
|
|
1587
1602
|
|
|
1603
|
+
###### key.home
|
|
1604
|
+
|
|
1605
|
+
###### key.end
|
|
1606
|
+
|
|
1607
|
+
Type: `boolean`\
|
|
1608
|
+
Default: `false`
|
|
1609
|
+
|
|
1610
|
+
If the Home or End key was pressed, the corresponding property will be `true`.
|
|
1611
|
+
For example, if the user presses End, `key.end` equals `true`.
|
|
1612
|
+
|
|
1588
1613
|
###### key.meta
|
|
1589
1614
|
|
|
1590
1615
|
Type: `boolean`\
|
|
@@ -1643,17 +1668,20 @@ Useful when there are multiple `useInput` hooks used at once to avoid handling t
|
|
|
1643
1668
|
|
|
1644
1669
|
`useApp` is a React hook that exposes a method to manually exit the app (unmount).
|
|
1645
1670
|
|
|
1646
|
-
#### exit(
|
|
1671
|
+
#### exit(errorOrResult?)
|
|
1647
1672
|
|
|
1648
1673
|
Type: `Function`
|
|
1649
1674
|
|
|
1650
1675
|
Exit (unmount) the whole Ink app.
|
|
1651
1676
|
|
|
1652
|
-
#####
|
|
1677
|
+
##### errorOrResult
|
|
1653
1678
|
|
|
1654
|
-
Type: `Error`
|
|
1679
|
+
Type: `Error | unknown`
|
|
1655
1680
|
|
|
1656
|
-
Optional
|
|
1681
|
+
Optional value that controls how [`waitUntilExit`](waituntilexit) settles:
|
|
1682
|
+
- `exit()` resolves with `undefined`.
|
|
1683
|
+
- `exit(error)` rejects when `error` is an `Error`.
|
|
1684
|
+
- `exit(value)` resolves with `value`.
|
|
1657
1685
|
|
|
1658
1686
|
```js
|
|
1659
1687
|
import {useApp} from 'ink';
|
|
@@ -2079,7 +2107,7 @@ Mount a component and render the output.
|
|
|
2079
2107
|
|
|
2080
2108
|
##### tree
|
|
2081
2109
|
|
|
2082
|
-
Type: `
|
|
2110
|
+
Type: `ReactNode`
|
|
2083
2111
|
|
|
2084
2112
|
##### options
|
|
2085
2113
|
|
|
@@ -2132,6 +2160,13 @@ Default: `undefined`
|
|
|
2132
2160
|
|
|
2133
2161
|
Runs the given callback after each render and re-render with a metrics object.
|
|
2134
2162
|
|
|
2163
|
+
###### isScreenReaderEnabled
|
|
2164
|
+
|
|
2165
|
+
Type: `boolean`\
|
|
2166
|
+
Default: `process.env['INK_SCREEN_READER'] === 'true'`
|
|
2167
|
+
|
|
2168
|
+
Enable screen reader support. See [Screen Reader Support](#screen-reader-support).
|
|
2169
|
+
|
|
2135
2170
|
###### debug
|
|
2136
2171
|
|
|
2137
2172
|
Type: `boolean`\
|
|
@@ -2234,6 +2269,56 @@ When the kitty keyboard protocol is enabled, input handling changes in several w
|
|
|
2234
2269
|
- `Escape` key vs `Ctrl+[` - these are disambiguated.
|
|
2235
2270
|
- **Event types.** With the `reportEventTypes` flag, key press, repeat, and release events are distinguished via `key.eventType`.
|
|
2236
2271
|
|
|
2272
|
+
#### renderToString(tree, options?)
|
|
2273
|
+
|
|
2274
|
+
Returns: `string`
|
|
2275
|
+
|
|
2276
|
+
Render a React element to a string synchronously. Unlike `render()`, this function does not write to stdout, does not set up any terminal event listeners, and returns the rendered output as a string.
|
|
2277
|
+
|
|
2278
|
+
Useful for generating documentation, writing output to files, testing, or any scenario where you need the rendered output as a string without starting a persistent terminal application.
|
|
2279
|
+
|
|
2280
|
+
```jsx
|
|
2281
|
+
import {renderToString, Text, Box} from 'ink';
|
|
2282
|
+
|
|
2283
|
+
const output = renderToString(
|
|
2284
|
+
<Box padding={1}>
|
|
2285
|
+
<Text color="green">Hello World</Text>
|
|
2286
|
+
</Box>,
|
|
2287
|
+
);
|
|
2288
|
+
|
|
2289
|
+
console.log(output);
|
|
2290
|
+
```
|
|
2291
|
+
|
|
2292
|
+
**Notes:**
|
|
2293
|
+
|
|
2294
|
+
- Terminal-specific hooks (`useInput`, `useStdin`, `useStdout`, `useStderr`, `useApp`, `useFocus`, `useFocusManager`) return default no-op values since there is no terminal session. They will not throw, but they will not function as in a live terminal.
|
|
2295
|
+
- `useEffect` callbacks will execute during rendering (due to synchronous rendering mode), but state updates they trigger will not affect the returned output, which reflects the initial render.
|
|
2296
|
+
- `useLayoutEffect` callbacks fire synchronously during commit, so state updates they trigger **will** be reflected in the output.
|
|
2297
|
+
- The `<Static>` component is supported — its output is prepended to the dynamic output.
|
|
2298
|
+
- If a component throws during rendering, the error is propagated to the caller after cleanup.
|
|
2299
|
+
|
|
2300
|
+
##### tree
|
|
2301
|
+
|
|
2302
|
+
Type: `ReactNode`
|
|
2303
|
+
|
|
2304
|
+
##### options
|
|
2305
|
+
|
|
2306
|
+
Type: `object`
|
|
2307
|
+
|
|
2308
|
+
###### columns
|
|
2309
|
+
|
|
2310
|
+
Type: `number`\
|
|
2311
|
+
Default: `80`
|
|
2312
|
+
|
|
2313
|
+
Width of the virtual terminal in columns. Controls where text wrapping occurs.
|
|
2314
|
+
|
|
2315
|
+
```jsx
|
|
2316
|
+
const output = renderToString(<Text>{'A'.repeat(100)}</Text>, {
|
|
2317
|
+
columns: 40,
|
|
2318
|
+
});
|
|
2319
|
+
// Text wraps at 40 columns
|
|
2320
|
+
```
|
|
2321
|
+
|
|
2237
2322
|
#### Instance
|
|
2238
2323
|
|
|
2239
2324
|
This is the object that `render()` returns.
|
|
@@ -2244,7 +2329,7 @@ Replace the previous root node with a new one or update the props of the current
|
|
|
2244
2329
|
|
|
2245
2330
|
###### tree
|
|
2246
2331
|
|
|
2247
|
-
Type: `
|
|
2332
|
+
Type: `ReactNode`
|
|
2248
2333
|
|
|
2249
2334
|
```jsx
|
|
2250
2335
|
// Update props of the root node
|
|
@@ -2267,7 +2352,9 @@ unmount();
|
|
|
2267
2352
|
|
|
2268
2353
|
##### waitUntilExit()
|
|
2269
2354
|
|
|
2270
|
-
Returns a promise that
|
|
2355
|
+
Returns a promise that settles when the app is unmounted.
|
|
2356
|
+
|
|
2357
|
+
It resolves with the value passed to `exit(value)` and rejects with the error passed to `exit(error)`.
|
|
2271
2358
|
|
|
2272
2359
|
```jsx
|
|
2273
2360
|
const {unmount, waitUntilExit} = render(<MyApp />);
|
|
@@ -2277,6 +2364,12 @@ setTimeout(unmount, 1000);
|
|
|
2277
2364
|
await waitUntilExit(); // resolves after `unmount()` is called
|
|
2278
2365
|
```
|
|
2279
2366
|
|
|
2367
|
+
##### cleanup()
|
|
2368
|
+
|
|
2369
|
+
Delete the internal Ink instance associated with the current `stdout`.
|
|
2370
|
+
This is mostly useful for advanced cases (for example, tests) where you need `render()` to create a fresh instance for the same stream.
|
|
2371
|
+
This does not unmount the current app.
|
|
2372
|
+
|
|
2280
2373
|
##### clear()
|
|
2281
2374
|
|
|
2282
2375
|
Clear output.
|
|
@@ -2491,11 +2584,16 @@ For a practical example of building an accessible component, see the [ARIA examp
|
|
|
2491
2584
|
- [ink-scroll-list](https://github.com/ByteLandTechnology/ink-scroll-list) - Scrollable list.
|
|
2492
2585
|
- [ink-stepper](https://github.com/archcorsair/ink-stepper) - Step-by-step wizard.
|
|
2493
2586
|
- [ink-virtual-list](https://github.com/archcorsair/ink-virtual-list) - Virtualized list that renders only visible items for performance.
|
|
2587
|
+
- [ink-color-picker](https://github.com/sina-byn/ink-color-picker) - Color picker.
|
|
2494
2588
|
|
|
2495
2589
|
## Useful Hooks
|
|
2496
2590
|
|
|
2497
2591
|
- [ink-use-stdout-dimensions](https://github.com/cameronhunter/ink-monorepo/tree/master/packages/ink-use-stdout-dimensions) - Subscribe to stdout dimensions.
|
|
2498
2592
|
|
|
2593
|
+
## Recipes
|
|
2594
|
+
|
|
2595
|
+
- [Routing with React Router](recipes/routing.md) - Navigate between routes using `MemoryRouter`.
|
|
2596
|
+
|
|
2499
2597
|
## Examples
|
|
2500
2598
|
|
|
2501
2599
|
The [`examples`](/examples) directory contains a set of real examples. You can run them with:
|
|
@@ -2517,6 +2615,20 @@ npm run example examples/[example name]
|
|
|
2517
2615
|
- [Write to stderr](examples/use-stderr/use-stderr.tsx) - Write to stderr, bypassing main Ink output.
|
|
2518
2616
|
- [Static](examples/static/static.tsx) - Use the `<Static>` component to render permanent output.
|
|
2519
2617
|
- [Child process](examples/subprocess-output) - Renders output from a child process.
|
|
2618
|
+
- [Router](examples/router/router.tsx) - Navigate between routes using React Router's `MemoryRouter`.
|
|
2619
|
+
|
|
2620
|
+
## Continuous Integration
|
|
2621
|
+
|
|
2622
|
+
When running on CI (detected via the `CI` environment variable), Ink adapts its rendering:
|
|
2623
|
+
|
|
2624
|
+
- Only the last frame is rendered on exit, instead of continuously updating the terminal. This is because most CI environments don't support the ANSI escape sequences used to overwrite previous output.
|
|
2625
|
+
- Terminal resize events are not listened to.
|
|
2626
|
+
|
|
2627
|
+
If your CI environment supports full terminal rendering and you want to opt out of this behavior, set `CI=false`:
|
|
2628
|
+
|
|
2629
|
+
```sh
|
|
2630
|
+
CI=false node my-cli.js
|
|
2631
|
+
```
|
|
2520
2632
|
|
|
2521
2633
|
## Maintainers
|
|
2522
2634
|
|