@textui/kit 0.1.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/LICENSE +21 -0
- package/README.md +93 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/jsx-dev-runtime.d.ts +2 -0
- package/dist/jsx-dev-runtime.d.ts.map +1 -0
- package/dist/jsx-dev-runtime.js +1 -0
- package/dist/jsx-runtime.d.ts +9 -0
- package/dist/jsx-runtime.d.ts.map +1 -0
- package/dist/jsx-runtime.js +8 -0
- package/dist/render.d.ts +43 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +41 -0
- package/package.json +64 -0
- package/src/index.ts +18 -0
- package/src/jsx-dev-runtime.ts +1 -0
- package/src/jsx-runtime.ts +8 -0
- package/src/render.ts +92 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Softov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# @textui/kit
|
|
2
|
+
|
|
3
|
+
Build a terminal UI in TypeScript. One install, one import.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install @textui/kit
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
import { render, Box, Text, useState, useInput } from '@textui/kit';
|
|
11
|
+
|
|
12
|
+
function App() {
|
|
13
|
+
const [count, setCount] = useState(0);
|
|
14
|
+
useInput((e) => { if (e.name === '+') { setCount((c) => c + 1); return true; } });
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<Box border="round" padding={1} direction="column">
|
|
18
|
+
<Text bold>Count: {count}</Text>
|
|
19
|
+
<Text dim>Press + to increment, ctrl+c to quit</Text>
|
|
20
|
+
</Box>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const { waitUntilExit } = render(<App />);
|
|
25
|
+
await waitUntilExit();
|
|
26
|
+
console.log('App exited');
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## What this package is
|
|
30
|
+
|
|
31
|
+
The runtime ([`@textui/core`](https://github.com/softov/textui/tree/main/packages/core)), a terminal to put it on
|
|
32
|
+
([`@textui/terminal`](https://github.com/softov/textui/tree/main/packages/terminal)), and `render`. Every other name here is
|
|
33
|
+
re-exported from those two - importing from them directly is the same thing
|
|
34
|
+
with a longer name.
|
|
35
|
+
|
|
36
|
+
`render` mounts and keeps running. It returns a handle rather than a promise,
|
|
37
|
+
so the application is up before the next line:
|
|
38
|
+
|
|
39
|
+
| | |
|
|
40
|
+
|---|---|
|
|
41
|
+
| `app` | Commands, themes, focus, the store - everything hello world did not need |
|
|
42
|
+
| `waitUntilExit()` | Resolves when the application stops, however it stopped |
|
|
43
|
+
| `unmount()` | Stop, put the terminal back, resolve `waitUntilExit` |
|
|
44
|
+
| `rerender(node)` | Swap the root |
|
|
45
|
+
|
|
46
|
+
For one frame and no terminal - a report, `--help`, a test - use `renderOnce`
|
|
47
|
+
or `renderToString` instead. Those return; this one runs.
|
|
48
|
+
|
|
49
|
+
## ctrl+c
|
|
50
|
+
|
|
51
|
+
Handled, and it has to be handled as a **key rather than a signal**. The
|
|
52
|
+
terminal is in raw mode from the moment the app starts, and raw mode is
|
|
53
|
+
exactly the mode where ctrl+c stops being SIGINT and becomes the byte `0x03` -
|
|
54
|
+
so process signal handlers never fire.
|
|
55
|
+
|
|
56
|
+
`exitOnCtrlC: false` gives you the key instead, which is what an editor with
|
|
57
|
+
unsaved work wants.
|
|
58
|
+
|
|
59
|
+
## Components
|
|
60
|
+
|
|
61
|
+
The catalog is a separate install:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
npm install @textui/widgets
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
```tsx
|
|
68
|
+
import { Card, Badge } from '@textui/widgets';
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Nothing to register. `<Card/>` compiles to a node that carries the imported
|
|
72
|
+
function, and the runtime uses that in preference to any registry - so
|
|
73
|
+
importing a component *is* registering it. That is why the catalog is not
|
|
74
|
+
bundled here: a screen made of `Box` and `Text` should not carry eighty
|
|
75
|
+
components it never mentions.
|
|
76
|
+
|
|
77
|
+
The one case that needs a registry is a screen named in data, where a string
|
|
78
|
+
has to resolve to something:
|
|
79
|
+
|
|
80
|
+
```tsx
|
|
81
|
+
import { registerBuiltins } from '@textui/widgets';
|
|
82
|
+
|
|
83
|
+
render(<App />, { onBoot: registerBuiltins });
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Runtime
|
|
87
|
+
|
|
88
|
+
No dependencies outside the two packages it re-exports, and no `node:` imports
|
|
89
|
+
of its own. Node 22+ and Bun.
|
|
90
|
+
|
|
91
|
+
## Documentation
|
|
92
|
+
|
|
93
|
+
<https://softov.github.io/textui/>
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One package to build a terminal UI with.
|
|
3
|
+
*
|
|
4
|
+
* The runtime, a terminal to put it on, and `render`. Nothing of its own
|
|
5
|
+
* except that function - every other name here is re-exported from
|
|
6
|
+
* `@textui/core` or `@textui/terminal`, and importing from those directly is
|
|
7
|
+
* the same thing with a longer name.
|
|
8
|
+
*
|
|
9
|
+
* The component catalog is deliberately **not** here. `@textui/widgets` is a
|
|
10
|
+
* separate install because an imported component registers itself just by
|
|
11
|
+
* being imported, so nothing needs to know about it in advance - and a
|
|
12
|
+
* hello world made of `Box` and `Text` should not carry eighty components it
|
|
13
|
+
* never mentions.
|
|
14
|
+
*/
|
|
15
|
+
export * from '@textui/core';
|
|
16
|
+
export * from '@textui/terminal';
|
|
17
|
+
export { render } from './render.js';
|
|
18
|
+
export type { RenderOptions, RenderHandle } from './render.js';
|
|
19
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,cAAc,cAAc,CAAC;AAC7B,cAAc,kBAAkB,CAAC;AACjC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One package to build a terminal UI with.
|
|
3
|
+
*
|
|
4
|
+
* The runtime, a terminal to put it on, and `render`. Nothing of its own
|
|
5
|
+
* except that function - every other name here is re-exported from
|
|
6
|
+
* `@textui/core` or `@textui/terminal`, and importing from those directly is
|
|
7
|
+
* the same thing with a longer name.
|
|
8
|
+
*
|
|
9
|
+
* The component catalog is deliberately **not** here. `@textui/widgets` is a
|
|
10
|
+
* separate install because an imported component registers itself just by
|
|
11
|
+
* being imported, so nothing needs to know about it in advance - and a
|
|
12
|
+
* hello world made of `Box` and `Text` should not carry eighty components it
|
|
13
|
+
* never mentions.
|
|
14
|
+
*/
|
|
15
|
+
export * from '@textui/core';
|
|
16
|
+
export * from '@textui/terminal';
|
|
17
|
+
export { render } from './render.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jsx-dev-runtime.d.ts","sourceRoot":"","sources":["../src/jsx-dev-runtime.ts"],"names":[],"mappings":"AAAA,cAAc,8BAA8B,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '@textui/core/jsx-dev-runtime';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* So `jsxImportSource: "textui"` works.
|
|
3
|
+
*
|
|
4
|
+
* Without this a JSX file has to point its transform at `@textui/core`, which
|
|
5
|
+
* means installing a second package to compile a file that imports one - and
|
|
6
|
+
* under a strict node_modules layout it is not even resolvable.
|
|
7
|
+
*/
|
|
8
|
+
export * from '@textui/core/jsx-runtime';
|
|
9
|
+
//# sourceMappingURL=jsx-runtime.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jsx-runtime.d.ts","sourceRoot":"","sources":["../src/jsx-runtime.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,cAAc,0BAA0B,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* So `jsxImportSource: "textui"` works.
|
|
3
|
+
*
|
|
4
|
+
* Without this a JSX file has to point its transform at `@textui/core`, which
|
|
5
|
+
* means installing a second package to compile a file that imports one - and
|
|
6
|
+
* under a strict node_modules layout it is not even resolvable.
|
|
7
|
+
*/
|
|
8
|
+
export * from '@textui/core/jsx-runtime';
|
package/dist/render.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { ComponentNode, CreateAppOptions, TerminalAdapter, TextUIApp } from '@textui/core';
|
|
2
|
+
import type { NodeAdapterOptions } from '@textui/terminal';
|
|
3
|
+
/**
|
|
4
|
+
* Mount a component on the terminal and keep it there.
|
|
5
|
+
*
|
|
6
|
+
* `createApp(...).start()` is what this is, with the two lines of ceremony
|
|
7
|
+
* that every program repeated - build a terminal, hand it the root - moved
|
|
8
|
+
* behind the call. The app is still on the handle, because everything past
|
|
9
|
+
* hello world is reached through it.
|
|
10
|
+
*
|
|
11
|
+
* One-shot rendering is `renderOnce` and `renderToString`. This one runs.
|
|
12
|
+
*/
|
|
13
|
+
export interface RenderOptions extends Omit<CreateAppOptions, 'terminal' | 'root'> {
|
|
14
|
+
/** Render onto this instead of the process's terminal. */
|
|
15
|
+
terminal?: TerminalAdapter;
|
|
16
|
+
stdin?: NodeAdapterOptions['stdin'];
|
|
17
|
+
stdout?: NodeAdapterOptions['stdout'];
|
|
18
|
+
/**
|
|
19
|
+
* Unmount on ctrl+c. On by default.
|
|
20
|
+
*
|
|
21
|
+
* It has to be handled as a key, not a signal. The terminal is in raw mode
|
|
22
|
+
* from the moment the app starts, and raw mode is precisely the mode where
|
|
23
|
+
* ctrl+c stops being SIGINT and becomes the byte 0x03 - so the process
|
|
24
|
+
* signal handlers never fire, and an application that does not read the key
|
|
25
|
+
* cannot be quit from the keyboard at all.
|
|
26
|
+
*
|
|
27
|
+
* Turn it off to handle the key yourself: an editor with unsaved work should
|
|
28
|
+
* ask rather than obey.
|
|
29
|
+
*/
|
|
30
|
+
exitOnCtrlC?: boolean;
|
|
31
|
+
}
|
|
32
|
+
export interface RenderHandle {
|
|
33
|
+
/** Commands, themes, focus, the store - everything hello world did not need. */
|
|
34
|
+
app: TextUIApp;
|
|
35
|
+
/** Resolves when the application stops, however it stopped. */
|
|
36
|
+
waitUntilExit(): Promise<void>;
|
|
37
|
+
/** Stop, put the terminal back, and resolve `waitUntilExit`. */
|
|
38
|
+
unmount(): Promise<void>;
|
|
39
|
+
/** Swap the root for another node. */
|
|
40
|
+
rerender(node: ComponentNode): void;
|
|
41
|
+
}
|
|
42
|
+
export declare function render(node: ComponentNode, options?: RenderOptions): RenderHandle;
|
|
43
|
+
//# sourceMappingURL=render.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,gBAAgB,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEhG,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAE3D;;;;;;;;;GASG;AAEH,MAAM,WAAW,aAAc,SAAQ,IAAI,CAAC,gBAAgB,EAAE,UAAU,GAAG,MAAM,CAAC;IAChF,0DAA0D;IAC1D,QAAQ,CAAC,EAAE,eAAe,CAAC;IAC3B,KAAK,CAAC,EAAE,kBAAkB,CAAC,OAAO,CAAC,CAAC;IACpC,MAAM,CAAC,EAAE,kBAAkB,CAAC,QAAQ,CAAC,CAAC;IACtC;;;;;;;;;;;OAWG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,MAAM,WAAW,YAAY;IAC3B,gFAAgF;IAChF,GAAG,EAAE,SAAS,CAAC;IACf,+DAA+D;IAC/D,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/B,gEAAgE;IAChE,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACzB,sCAAsC;IACtC,QAAQ,CAAC,IAAI,EAAE,aAAa,GAAG,IAAI,CAAC;CACrC;AAED,wBAAgB,MAAM,CAAC,IAAI,EAAE,aAAa,EAAE,OAAO,GAAE,aAAkB,GAAG,YAAY,CA4CrF"}
|
package/dist/render.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { createApp, WRITER_KEY } from '@textui/core';
|
|
2
|
+
import { createNodeTerminal, createWriter } from '@textui/terminal';
|
|
3
|
+
export function render(node, options = {}) {
|
|
4
|
+
const { terminal: given, stdin, stdout, exitOnCtrlC = true, ...rest } = options;
|
|
5
|
+
const terminal = given ?? createNodeTerminal({
|
|
6
|
+
...(stdin ? { stdin } : {}),
|
|
7
|
+
...(stdout ? { stdout } : {}),
|
|
8
|
+
});
|
|
9
|
+
const app = createApp({ ...rest, terminal, root: node });
|
|
10
|
+
// Every application wrote this line and none of them had a choice about it:
|
|
11
|
+
// without a writer the app renders frames and puts none of them anywhere.
|
|
12
|
+
// Provide it again afterwards to use your own.
|
|
13
|
+
app.services.provide(WRITER_KEY, createWriter(terminal.capabilities()));
|
|
14
|
+
let settle;
|
|
15
|
+
let fail;
|
|
16
|
+
const exited = new Promise((resolve, reject) => { settle = resolve; fail = reject; });
|
|
17
|
+
let stopping = null;
|
|
18
|
+
const unmount = () => {
|
|
19
|
+
stopping ??= app.stop().then(() => { settle?.(); }, (err) => { fail?.(err); });
|
|
20
|
+
return stopping;
|
|
21
|
+
};
|
|
22
|
+
// Before `start`, so this sees the key first. That ordering is the whole
|
|
23
|
+
// difference between "ctrl+c quits" and "ctrl+c quits unless something else
|
|
24
|
+
// got there", and a program you cannot leave is worse than one that leaves
|
|
25
|
+
// too eagerly - `exitOnCtrlC: false` is there for the other case.
|
|
26
|
+
if (exitOnCtrlC) {
|
|
27
|
+
terminal.onInput((event) => {
|
|
28
|
+
if (event.type === 'key' && event.ctrl && event.name === 'c')
|
|
29
|
+
void unmount();
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
app.start().catch((err) => { fail?.(err); });
|
|
33
|
+
return {
|
|
34
|
+
app,
|
|
35
|
+
waitUntilExit: () => exited,
|
|
36
|
+
unmount,
|
|
37
|
+
rerender(next) {
|
|
38
|
+
app.setRoot(next);
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@textui/kit",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "One install for TextUI - the runtime, a terminal, and render",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"terminal",
|
|
7
|
+
"tui",
|
|
8
|
+
"cli",
|
|
9
|
+
"jsx",
|
|
10
|
+
"ui",
|
|
11
|
+
"console",
|
|
12
|
+
"text-ui",
|
|
13
|
+
"ink-alternative",
|
|
14
|
+
"no-dependencies"
|
|
15
|
+
],
|
|
16
|
+
"homepage": "https://softov.github.io/textui/",
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/softov/textui/issues"
|
|
19
|
+
},
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/softov/textui.git",
|
|
23
|
+
"directory": "packages/facade"
|
|
24
|
+
},
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"author": "Softov <softov@brbyte.com>",
|
|
27
|
+
"type": "module",
|
|
28
|
+
"sideEffects": false,
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=22"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist",
|
|
34
|
+
"src",
|
|
35
|
+
"README.md",
|
|
36
|
+
"LICENSE"
|
|
37
|
+
],
|
|
38
|
+
"exports": {
|
|
39
|
+
".": {
|
|
40
|
+
"types": "./dist/index.d.ts",
|
|
41
|
+
"import": "./dist/index.js"
|
|
42
|
+
},
|
|
43
|
+
"./jsx-runtime": {
|
|
44
|
+
"types": "./dist/jsx-runtime.d.ts",
|
|
45
|
+
"import": "./dist/jsx-runtime.js"
|
|
46
|
+
},
|
|
47
|
+
"./jsx-dev-runtime": {
|
|
48
|
+
"types": "./dist/jsx-dev-runtime.d.ts",
|
|
49
|
+
"import": "./dist/jsx-dev-runtime.js"
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"@textui/terminal": "^0.1.0",
|
|
54
|
+
"@textui/core": "^0.1.0"
|
|
55
|
+
},
|
|
56
|
+
"publishConfig": {
|
|
57
|
+
"access": "public"
|
|
58
|
+
},
|
|
59
|
+
"scripts": {
|
|
60
|
+
"build": "tsc -p tsconfig.json",
|
|
61
|
+
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json",
|
|
62
|
+
"test": "vitest run"
|
|
63
|
+
}
|
|
64
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One package to build a terminal UI with.
|
|
3
|
+
*
|
|
4
|
+
* The runtime, a terminal to put it on, and `render`. Nothing of its own
|
|
5
|
+
* except that function - every other name here is re-exported from
|
|
6
|
+
* `@textui/core` or `@textui/terminal`, and importing from those directly is
|
|
7
|
+
* the same thing with a longer name.
|
|
8
|
+
*
|
|
9
|
+
* The component catalog is deliberately **not** here. `@textui/widgets` is a
|
|
10
|
+
* separate install because an imported component registers itself just by
|
|
11
|
+
* being imported, so nothing needs to know about it in advance - and a
|
|
12
|
+
* hello world made of `Box` and `Text` should not carry eighty components it
|
|
13
|
+
* never mentions.
|
|
14
|
+
*/
|
|
15
|
+
export * from '@textui/core';
|
|
16
|
+
export * from '@textui/terminal';
|
|
17
|
+
export { render } from './render.js';
|
|
18
|
+
export type { RenderOptions, RenderHandle } from './render.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '@textui/core/jsx-dev-runtime';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* So `jsxImportSource: "textui"` works.
|
|
3
|
+
*
|
|
4
|
+
* Without this a JSX file has to point its transform at `@textui/core`, which
|
|
5
|
+
* means installing a second package to compile a file that imports one - and
|
|
6
|
+
* under a strict node_modules layout it is not even resolvable.
|
|
7
|
+
*/
|
|
8
|
+
export * from '@textui/core/jsx-runtime';
|
package/src/render.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { createApp, WRITER_KEY } from '@textui/core';
|
|
2
|
+
import type { ComponentNode, CreateAppOptions, TerminalAdapter, TextUIApp } from '@textui/core';
|
|
3
|
+
import { createNodeTerminal, createWriter } from '@textui/terminal';
|
|
4
|
+
import type { NodeAdapterOptions } from '@textui/terminal';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Mount a component on the terminal and keep it there.
|
|
8
|
+
*
|
|
9
|
+
* `createApp(...).start()` is what this is, with the two lines of ceremony
|
|
10
|
+
* that every program repeated - build a terminal, hand it the root - moved
|
|
11
|
+
* behind the call. The app is still on the handle, because everything past
|
|
12
|
+
* hello world is reached through it.
|
|
13
|
+
*
|
|
14
|
+
* One-shot rendering is `renderOnce` and `renderToString`. This one runs.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export interface RenderOptions extends Omit<CreateAppOptions, 'terminal' | 'root'> {
|
|
18
|
+
/** Render onto this instead of the process's terminal. */
|
|
19
|
+
terminal?: TerminalAdapter;
|
|
20
|
+
stdin?: NodeAdapterOptions['stdin'];
|
|
21
|
+
stdout?: NodeAdapterOptions['stdout'];
|
|
22
|
+
/**
|
|
23
|
+
* Unmount on ctrl+c. On by default.
|
|
24
|
+
*
|
|
25
|
+
* It has to be handled as a key, not a signal. The terminal is in raw mode
|
|
26
|
+
* from the moment the app starts, and raw mode is precisely the mode where
|
|
27
|
+
* ctrl+c stops being SIGINT and becomes the byte 0x03 - so the process
|
|
28
|
+
* signal handlers never fire, and an application that does not read the key
|
|
29
|
+
* cannot be quit from the keyboard at all.
|
|
30
|
+
*
|
|
31
|
+
* Turn it off to handle the key yourself: an editor with unsaved work should
|
|
32
|
+
* ask rather than obey.
|
|
33
|
+
*/
|
|
34
|
+
exitOnCtrlC?: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface RenderHandle {
|
|
38
|
+
/** Commands, themes, focus, the store - everything hello world did not need. */
|
|
39
|
+
app: TextUIApp;
|
|
40
|
+
/** Resolves when the application stops, however it stopped. */
|
|
41
|
+
waitUntilExit(): Promise<void>;
|
|
42
|
+
/** Stop, put the terminal back, and resolve `waitUntilExit`. */
|
|
43
|
+
unmount(): Promise<void>;
|
|
44
|
+
/** Swap the root for another node. */
|
|
45
|
+
rerender(node: ComponentNode): void;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function render(node: ComponentNode, options: RenderOptions = {}): RenderHandle {
|
|
49
|
+
const { terminal: given, stdin, stdout, exitOnCtrlC = true, ...rest } = options;
|
|
50
|
+
const terminal = given ?? createNodeTerminal({
|
|
51
|
+
...(stdin ? { stdin } : {}),
|
|
52
|
+
...(stdout ? { stdout } : {}),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const app = createApp({ ...rest, terminal, root: node });
|
|
56
|
+
|
|
57
|
+
// Every application wrote this line and none of them had a choice about it:
|
|
58
|
+
// without a writer the app renders frames and puts none of them anywhere.
|
|
59
|
+
// Provide it again afterwards to use your own.
|
|
60
|
+
app.services.provide(WRITER_KEY, createWriter(terminal.capabilities()));
|
|
61
|
+
|
|
62
|
+
let settle: (() => void) | undefined;
|
|
63
|
+
let fail: ((err: unknown) => void) | undefined;
|
|
64
|
+
const exited = new Promise<void>((resolve, reject) => { settle = resolve; fail = reject; });
|
|
65
|
+
let stopping: Promise<void> | null = null;
|
|
66
|
+
|
|
67
|
+
const unmount = (): Promise<void> => {
|
|
68
|
+
stopping ??= app.stop().then(() => { settle?.(); }, (err: unknown) => { fail?.(err); });
|
|
69
|
+
return stopping;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// Before `start`, so this sees the key first. That ordering is the whole
|
|
73
|
+
// difference between "ctrl+c quits" and "ctrl+c quits unless something else
|
|
74
|
+
// got there", and a program you cannot leave is worse than one that leaves
|
|
75
|
+
// too eagerly - `exitOnCtrlC: false` is there for the other case.
|
|
76
|
+
if (exitOnCtrlC) {
|
|
77
|
+
terminal.onInput((event) => {
|
|
78
|
+
if (event.type === 'key' && event.ctrl && event.name === 'c') void unmount();
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
app.start().catch((err: unknown) => { fail?.(err); });
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
app,
|
|
86
|
+
waitUntilExit: () => exited,
|
|
87
|
+
unmount,
|
|
88
|
+
rerender(next) {
|
|
89
|
+
app.setRoot(next);
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|