@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 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/>
@@ -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,2 @@
1
+ export * from '@textui/core/jsx-dev-runtime';
2
+ //# sourceMappingURL=jsx-dev-runtime.d.ts.map
@@ -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';
@@ -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
+ }