react-xterm-shell 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 +110 -0
- package/dist/index.d.ts +92 -0
- package/dist/index.js +125 -0
- package/dist/index.js.map +1 -0
- package/package.json +78 -0
- package/src/context.tsx +28 -0
- package/src/index.ts +13 -0
- package/src/types.ts +55 -0
- package/src/use-xterm.ts +142 -0
- package/src/xterm.tsx +16 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Noah Wardlow
|
|
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,110 @@
|
|
|
1
|
+
# react-xterm-shell
|
|
2
|
+
|
|
3
|
+
> **Beta** — under active development; the API may change between minor versions until 1.0.
|
|
4
|
+
|
|
5
|
+
A small React shell around [xterm.js](https://xtermjs.org/). It is a **React shell around xterm, not a React renderer for terminal cells** — xterm owns the grid, parsing, and rendering; this gives you the lifecycle, a stable imperative controller, automatic fitting, and opt-in addons as ordinary React.
|
|
6
|
+
|
|
7
|
+
- `useXTerm()` — creates and owns the xterm instance behind a stable controller.
|
|
8
|
+
- `<XTerm />` — the DOM mount point.
|
|
9
|
+
- `TerminalProvider` / `useTerminalController()` — drive the terminal from chrome (toolbars) without prop drilling.
|
|
10
|
+
- Automatic sizing via `FitAddon` + `ResizeObserver`.
|
|
11
|
+
- Opt-in `webgl` (with DOM fallback), `web-links`, and `unicode11` addons, on by default.
|
|
12
|
+
|
|
13
|
+
It does **not** include a transport — wire `onData`/`onResize` to your own WebSocket/PTY backend. That keeps the wrapper reusable across rosbridge, a PTY service, SSH, or a local shell.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install react-xterm-shell @xterm/xterm react@^19
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
React 19 and `@xterm/xterm` are peer dependencies. Import the xterm stylesheet once in your app:
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import "@xterm/xterm/css/xterm.css";
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Quick start
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
import { useXTerm, XTerm } from "react-xterm-shell";
|
|
31
|
+
import "@xterm/xterm/css/xterm.css";
|
|
32
|
+
|
|
33
|
+
function TerminalPanel({ socket }: { socket: WebSocket }) {
|
|
34
|
+
const terminal = useXTerm({
|
|
35
|
+
onData: (data) => socket.send(JSON.stringify({ type: "input", data })),
|
|
36
|
+
onResize: ({ cols, rows }) =>
|
|
37
|
+
socket.send(JSON.stringify({ type: "resize", cols, rows })),
|
|
38
|
+
theme: { background: "#1a1b26", foreground: "#a9b1d6" }
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Stream server output into the terminal:
|
|
42
|
+
socket.onmessage = (e) => terminal.write(e.data);
|
|
43
|
+
|
|
44
|
+
return <XTerm terminal={terminal} style={{ width: "100%", height: 420 }} />;
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Composition
|
|
49
|
+
|
|
50
|
+
`useXTerm` returns a stable controller, so a toolbar can drive it imperatively
|
|
51
|
+
without re-rendering as bytes stream:
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
import { useXTerm, XTerm, TerminalProvider, useTerminalController } from "react-xterm-shell";
|
|
55
|
+
|
|
56
|
+
function Toolbar() {
|
|
57
|
+
const term = useTerminalController();
|
|
58
|
+
return <button onClick={term.clear}>Clear</button>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function Panel() {
|
|
62
|
+
const terminal = useXTerm({ onData: send });
|
|
63
|
+
return (
|
|
64
|
+
<TerminalProvider value={terminal}>
|
|
65
|
+
<Toolbar />
|
|
66
|
+
<XTerm terminal={terminal} className="h-[420px]" />
|
|
67
|
+
</TerminalProvider>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## API
|
|
73
|
+
|
|
74
|
+
### `useXTerm(options?) => XTermHandle`
|
|
75
|
+
|
|
76
|
+
| Option | Type | Default | Notes |
|
|
77
|
+
|--------|------|---------|-------|
|
|
78
|
+
| `onData` | `(data: string) => void` | — | User keystrokes (xterm `onData`). |
|
|
79
|
+
| `onResize` | `(size: { cols, rows }) => void` | — | Fires when the grid resizes. |
|
|
80
|
+
| `theme` | `ITheme` | — | xterm color theme. |
|
|
81
|
+
| `fontSize` | `number` | `13` | |
|
|
82
|
+
| `scrollback` | `number` | `1000` | |
|
|
83
|
+
| `cursorBlink` | `boolean` | `true` | |
|
|
84
|
+
| `webgl` | `boolean` | `true` | GL renderer; falls back to DOM on context loss. |
|
|
85
|
+
| `webLinks` | `boolean` | `true` | Clickable URLs. |
|
|
86
|
+
| `unicode11` | `boolean` | `true` | Correct width for box-drawing / emoji. |
|
|
87
|
+
| `options` | `ITerminalOptions` | — | Merged over the above. |
|
|
88
|
+
| `addons` | `ITerminalAddon[]` | — | Extra addons (e.g. a search addon). |
|
|
89
|
+
|
|
90
|
+
The returned `XTermHandle` has `attach` (the callback ref for `<XTerm>`), a live
|
|
91
|
+
`term` getter, and `write` / `clear` / `focus` / `fit`. The handle is stable
|
|
92
|
+
across renders; callbacks are read through refs, so passing fresh `onData` /
|
|
93
|
+
`onResize` each render does not remount the terminal.
|
|
94
|
+
|
|
95
|
+
### `<XTerm terminal={handle} className? style? />`
|
|
96
|
+
|
|
97
|
+
Renders the mount element. The `terminal.attach` callback ref is the whole
|
|
98
|
+
integration surface.
|
|
99
|
+
|
|
100
|
+
## Addon notes
|
|
101
|
+
|
|
102
|
+
`@xterm/addon-fit`, `-web-links`, `-unicode11`, and `-webgl` are bundled as
|
|
103
|
+
dependencies and pinned to the xterm-5 line. WebGL loads after `open()` and is
|
|
104
|
+
guarded: if the GL context is unavailable or lost, it disposes and the DOM
|
|
105
|
+
renderer takes over. Disable any of them via the `webgl` / `webLinks` /
|
|
106
|
+
`unicode11` options.
|
|
107
|
+
|
|
108
|
+
## License
|
|
109
|
+
|
|
110
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { Terminal, ITheme, ITerminalOptions, ITerminalAddon } from '@xterm/xterm';
|
|
2
|
+
export { ITheme, Terminal } from '@xterm/xterm';
|
|
3
|
+
import * as react from 'react';
|
|
4
|
+
import { ReactNode } from 'react';
|
|
5
|
+
|
|
6
|
+
interface UseXTermOptions {
|
|
7
|
+
/** Called with user keystrokes (xterm `onData`). Wire this to your transport. */
|
|
8
|
+
onData?: (data: string) => void;
|
|
9
|
+
/** Called when the rendered grid resizes (xterm `onResize`). */
|
|
10
|
+
onResize?: (size: {
|
|
11
|
+
cols: number;
|
|
12
|
+
rows: number;
|
|
13
|
+
}) => void;
|
|
14
|
+
/** xterm color theme. */
|
|
15
|
+
theme?: ITheme;
|
|
16
|
+
/** Font size in px. Default 13. */
|
|
17
|
+
fontSize?: number;
|
|
18
|
+
/** Scrollback lines retained. Default 1000. */
|
|
19
|
+
scrollback?: number;
|
|
20
|
+
/** Whether the cursor blinks. Default true. */
|
|
21
|
+
cursorBlink?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Built-in addons, all enabled by default:
|
|
24
|
+
* - `webgl`: GL renderer with automatic DOM-renderer fallback on context loss.
|
|
25
|
+
* - `webLinks`: clickable URLs in output.
|
|
26
|
+
* - `unicode11`: correct width for box-drawing, spinners, and emoji.
|
|
27
|
+
*/
|
|
28
|
+
webgl?: boolean;
|
|
29
|
+
webLinks?: boolean;
|
|
30
|
+
unicode11?: boolean;
|
|
31
|
+
/** Extra xterm options merged over the ones above. */
|
|
32
|
+
options?: ITerminalOptions;
|
|
33
|
+
/** Additional addons to load (e.g. a search addon). */
|
|
34
|
+
addons?: ITerminalAddon[];
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Imperative controller for the terminal. Stable across renders, so passing it
|
|
38
|
+
* around (or into a context) never re-renders consumers as output streams. `term`
|
|
39
|
+
* is a live getter that returns the instance once attached, `null` before.
|
|
40
|
+
*/
|
|
41
|
+
interface TerminalController {
|
|
42
|
+
readonly term: Terminal | null;
|
|
43
|
+
write: (data: string | Uint8Array) => void;
|
|
44
|
+
clear: () => void;
|
|
45
|
+
focus: () => void;
|
|
46
|
+
fit: () => void;
|
|
47
|
+
}
|
|
48
|
+
type XTermHandle = TerminalController & {
|
|
49
|
+
/**
|
|
50
|
+
* Callback ref for the mount element. Creates the terminal on mount and
|
|
51
|
+
* disposes it when React clears the ref on unmount.
|
|
52
|
+
*/
|
|
53
|
+
attach: (el: HTMLDivElement | null) => void;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* A React shell around xterm.js. xterm owns the terminal grid and rendering;
|
|
58
|
+
* this hook owns the instance lifecycle and exposes a stable imperative
|
|
59
|
+
* controller so the surrounding tree can drive it without re-rendering as
|
|
60
|
+
* output streams.
|
|
61
|
+
*
|
|
62
|
+
* The instance is created lazily in the `attach` callback ref and disposed by
|
|
63
|
+
* the cleanup it returns. Sizing is automatic via a `ResizeObserver` + `FitAddon`.
|
|
64
|
+
*
|
|
65
|
+
* Import the stylesheet once in your app: `import "@xterm/xterm/css/xterm.css"`.
|
|
66
|
+
*/
|
|
67
|
+
declare function useXTerm(opts?: UseXTermOptions): XTermHandle;
|
|
68
|
+
|
|
69
|
+
interface XTermProps {
|
|
70
|
+
/** The handle returned by `useXTerm`. */
|
|
71
|
+
terminal: XTermHandle;
|
|
72
|
+
className?: string;
|
|
73
|
+
style?: React.CSSProperties;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Thin DOM mount point for an xterm instance. The `useXTerm` callback ref is the
|
|
77
|
+
* entire integration surface; this component just renders the host element.
|
|
78
|
+
*/
|
|
79
|
+
declare function XTerm({ terminal, className, style }: Readonly<XTermProps>): react.JSX.Element;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Provides a terminal controller to descendants so chrome (toolbars, buttons)
|
|
83
|
+
* can drive the terminal imperatively without prop drilling.
|
|
84
|
+
*/
|
|
85
|
+
declare function TerminalProvider({ value, children }: Readonly<{
|
|
86
|
+
value: TerminalController;
|
|
87
|
+
children: ReactNode;
|
|
88
|
+
}>): react.JSX.Element;
|
|
89
|
+
/** Read the terminal controller from the nearest `TerminalProvider`. */
|
|
90
|
+
declare function useTerminalController(): TerminalController;
|
|
91
|
+
|
|
92
|
+
export { type TerminalController, TerminalProvider, type UseXTermOptions, XTerm, type XTermHandle, type XTermProps, useTerminalController, useXTerm };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { createContext, useRef, useEffect, useCallback, useMemo, useContext } from 'react';
|
|
2
|
+
import { Terminal } from '@xterm/xterm';
|
|
3
|
+
import { FitAddon } from '@xterm/addon-fit';
|
|
4
|
+
import { WebLinksAddon } from '@xterm/addon-web-links';
|
|
5
|
+
import { Unicode11Addon } from '@xterm/addon-unicode11';
|
|
6
|
+
import { WebglAddon } from '@xterm/addon-webgl';
|
|
7
|
+
import { jsx } from 'react/jsx-runtime';
|
|
8
|
+
|
|
9
|
+
// src/use-xterm.ts
|
|
10
|
+
var DEFAULT_FONT_SIZE = 13;
|
|
11
|
+
var DEFAULT_SCROLLBACK = 1e3;
|
|
12
|
+
function useXTerm(opts = {}) {
|
|
13
|
+
const {
|
|
14
|
+
onData,
|
|
15
|
+
onResize,
|
|
16
|
+
theme,
|
|
17
|
+
fontSize,
|
|
18
|
+
scrollback,
|
|
19
|
+
cursorBlink,
|
|
20
|
+
webgl = true,
|
|
21
|
+
webLinks = true,
|
|
22
|
+
unicode11 = true,
|
|
23
|
+
options,
|
|
24
|
+
addons
|
|
25
|
+
} = opts;
|
|
26
|
+
const termRef = useRef(null);
|
|
27
|
+
const fitRef = useRef(null);
|
|
28
|
+
const cleanupRef = useRef(null);
|
|
29
|
+
const onDataRef = useRef(onData);
|
|
30
|
+
const onResizeRef = useRef(onResize);
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
onDataRef.current = onData;
|
|
33
|
+
onResizeRef.current = onResize;
|
|
34
|
+
}, [onData, onResize]);
|
|
35
|
+
const extraAddons = useRef(addons);
|
|
36
|
+
extraAddons.current = addons;
|
|
37
|
+
const attach = useCallback(
|
|
38
|
+
(el) => {
|
|
39
|
+
if (!el) {
|
|
40
|
+
cleanupRef.current?.();
|
|
41
|
+
cleanupRef.current = null;
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (termRef.current) return;
|
|
45
|
+
const term = new Terminal({
|
|
46
|
+
cursorBlink: cursorBlink ?? true,
|
|
47
|
+
fontSize: fontSize ?? DEFAULT_FONT_SIZE,
|
|
48
|
+
theme,
|
|
49
|
+
scrollback: scrollback ?? DEFAULT_SCROLLBACK,
|
|
50
|
+
allowProposedApi: true,
|
|
51
|
+
...options
|
|
52
|
+
});
|
|
53
|
+
const fit = new FitAddon();
|
|
54
|
+
term.loadAddon(fit);
|
|
55
|
+
if (webLinks) term.loadAddon(new WebLinksAddon());
|
|
56
|
+
if (unicode11) {
|
|
57
|
+
term.loadAddon(new Unicode11Addon());
|
|
58
|
+
term.unicode.activeVersion = "11";
|
|
59
|
+
}
|
|
60
|
+
for (const addon of extraAddons.current ?? []) {
|
|
61
|
+
term.loadAddon(addon);
|
|
62
|
+
}
|
|
63
|
+
term.open(el);
|
|
64
|
+
if (webgl) {
|
|
65
|
+
try {
|
|
66
|
+
const webglAddon = new WebglAddon();
|
|
67
|
+
webglAddon.onContextLoss(() => webglAddon.dispose());
|
|
68
|
+
term.loadAddon(webglAddon);
|
|
69
|
+
} catch {
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
fit.fit();
|
|
73
|
+
const disposables = [
|
|
74
|
+
term.onData((data) => onDataRef.current?.(data)),
|
|
75
|
+
term.onResize((size) => onResizeRef.current?.(size))
|
|
76
|
+
];
|
|
77
|
+
const resizeObserver = new ResizeObserver(() => fit.fit());
|
|
78
|
+
resizeObserver.observe(el);
|
|
79
|
+
termRef.current = term;
|
|
80
|
+
fitRef.current = fit;
|
|
81
|
+
cleanupRef.current = () => {
|
|
82
|
+
resizeObserver.disconnect();
|
|
83
|
+
disposables.forEach((d) => d.dispose());
|
|
84
|
+
term.dispose();
|
|
85
|
+
termRef.current = null;
|
|
86
|
+
fitRef.current = null;
|
|
87
|
+
};
|
|
88
|
+
},
|
|
89
|
+
[theme, fontSize, scrollback, cursorBlink, webgl, webLinks, unicode11, options]
|
|
90
|
+
);
|
|
91
|
+
return useMemo(
|
|
92
|
+
() => ({
|
|
93
|
+
get term() {
|
|
94
|
+
return termRef.current;
|
|
95
|
+
},
|
|
96
|
+
write: (data) => termRef.current?.write(data),
|
|
97
|
+
clear: () => termRef.current?.clear(),
|
|
98
|
+
focus: () => termRef.current?.focus(),
|
|
99
|
+
fit: () => fitRef.current?.fit(),
|
|
100
|
+
attach
|
|
101
|
+
}),
|
|
102
|
+
[attach]
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
function XTerm({ terminal, className, style }) {
|
|
106
|
+
return /* @__PURE__ */ jsx("div", { ref: terminal.attach, className, style });
|
|
107
|
+
}
|
|
108
|
+
var TerminalContext = createContext(null);
|
|
109
|
+
function TerminalProvider({
|
|
110
|
+
value,
|
|
111
|
+
children
|
|
112
|
+
}) {
|
|
113
|
+
return /* @__PURE__ */ jsx(TerminalContext.Provider, { value, children });
|
|
114
|
+
}
|
|
115
|
+
function useTerminalController() {
|
|
116
|
+
const ctx = useContext(TerminalContext);
|
|
117
|
+
if (!ctx) {
|
|
118
|
+
throw new Error("useTerminalController must be used within a <TerminalProvider>");
|
|
119
|
+
}
|
|
120
|
+
return ctx;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export { TerminalProvider, XTerm, useTerminalController, useXTerm };
|
|
124
|
+
//# sourceMappingURL=index.js.map
|
|
125
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/use-xterm.ts","../src/xterm.tsx","../src/context.tsx"],"names":["jsx"],"mappings":"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","file":"index.js","sourcesContent":["import { useCallback, useEffect, useMemo, useRef } from \"react\";\nimport { Terminal } from \"@xterm/xterm\";\nimport { FitAddon } from \"@xterm/addon-fit\";\nimport { WebLinksAddon } from \"@xterm/addon-web-links\";\nimport { Unicode11Addon } from \"@xterm/addon-unicode11\";\nimport { WebglAddon } from \"@xterm/addon-webgl\";\n\nimport type { UseXTermOptions, XTermHandle } from \"./types\";\n\nconst DEFAULT_FONT_SIZE = 13;\nconst DEFAULT_SCROLLBACK = 1000;\n\n/**\n * A React shell around xterm.js. xterm owns the terminal grid and rendering;\n * this hook owns the instance lifecycle and exposes a stable imperative\n * controller so the surrounding tree can drive it without re-rendering as\n * output streams.\n *\n * The instance is created lazily in the `attach` callback ref and disposed by\n * the cleanup it returns. Sizing is automatic via a `ResizeObserver` + `FitAddon`.\n *\n * Import the stylesheet once in your app: `import \"@xterm/xterm/css/xterm.css\"`.\n */\nexport function useXTerm(opts: UseXTermOptions = {}): XTermHandle {\n const {\n onData,\n onResize,\n theme,\n fontSize,\n scrollback,\n cursorBlink,\n webgl = true,\n webLinks = true,\n unicode11 = true,\n options,\n addons\n } = opts;\n\n const termRef = useRef<Terminal | null>(null);\n const fitRef = useRef<FitAddon | null>(null);\n const cleanupRef = useRef<(() => void) | null>(null);\n\n // Latest callbacks live in refs so `attach` does not depend on their identity.\n // Otherwise a parent passing fresh callbacks each render would change `attach`,\n // and the callback ref would dispose and recreate the terminal (wiping\n // scrollback). The xterm handlers read the refs, so no stale-closure risk.\n const onDataRef = useRef(onData);\n const onResizeRef = useRef(onResize);\n useEffect(() => {\n onDataRef.current = onData;\n onResizeRef.current = onResize;\n }, [onData, onResize]);\n\n // Snapshot the addon set + extra addons so identity churn does not remount.\n const extraAddons = useRef(addons);\n extraAddons.current = addons;\n\n const attach = useCallback(\n (el: HTMLDivElement | null) => {\n if (!el) {\n cleanupRef.current?.();\n cleanupRef.current = null;\n return;\n }\n if (termRef.current) return;\n\n const term = new Terminal({\n cursorBlink: cursorBlink ?? true,\n fontSize: fontSize ?? DEFAULT_FONT_SIZE,\n theme,\n scrollback: scrollback ?? DEFAULT_SCROLLBACK,\n allowProposedApi: true,\n ...options\n });\n\n const fit = new FitAddon();\n term.loadAddon(fit);\n\n if (webLinks) term.loadAddon(new WebLinksAddon());\n if (unicode11) {\n term.loadAddon(new Unicode11Addon());\n term.unicode.activeVersion = \"11\";\n }\n for (const addon of extraAddons.current ?? []) {\n term.loadAddon(addon);\n }\n\n term.open(el);\n\n // WebGL must load after open(); fall back to the DOM renderer if the GL\n // context is unavailable or lost.\n if (webgl) {\n try {\n const webglAddon = new WebglAddon();\n webglAddon.onContextLoss(() => webglAddon.dispose());\n term.loadAddon(webglAddon);\n } catch {\n // No WebGL (headless/software GL); the DOM renderer remains active.\n }\n }\n\n fit.fit();\n\n const disposables = [\n term.onData((data) => onDataRef.current?.(data)),\n term.onResize((size) => onResizeRef.current?.(size))\n ];\n\n const resizeObserver = new ResizeObserver(() => fit.fit());\n resizeObserver.observe(el);\n\n termRef.current = term;\n fitRef.current = fit;\n\n cleanupRef.current = () => {\n resizeObserver.disconnect();\n disposables.forEach((d) => d.dispose());\n term.dispose();\n termRef.current = null;\n fitRef.current = null;\n };\n },\n [theme, fontSize, scrollback, cursorBlink, webgl, webLinks, unicode11, options]\n );\n\n // One stable handle. `term` is a live getter — spreading it into a static\n // object would freeze it to the null it holds before attach. Methods read the\n // refs, so the handle is stable while the instance comes and goes.\n return useMemo<XTermHandle>(\n () => ({\n get term() {\n return termRef.current;\n },\n write: (data) => termRef.current?.write(data),\n clear: () => termRef.current?.clear(),\n focus: () => termRef.current?.focus(),\n fit: () => fitRef.current?.fit(),\n attach\n }),\n [attach]\n );\n}\n","import type { XTermHandle } from \"./types\";\n\nexport interface XTermProps {\n /** The handle returned by `useXTerm`. */\n terminal: XTermHandle;\n className?: string;\n style?: React.CSSProperties;\n}\n\n/**\n * Thin DOM mount point for an xterm instance. The `useXTerm` callback ref is the\n * entire integration surface; this component just renders the host element.\n */\nexport function XTerm({ terminal, className, style }: Readonly<XTermProps>) {\n return <div ref={terminal.attach} className={className} style={style} />;\n}\n","import { createContext, useContext } from \"react\";\nimport type { ReactNode } from \"react\";\n\nimport type { TerminalController } from \"./types\";\n\nconst TerminalContext = createContext<TerminalController | null>(null);\n\n/**\n * Provides a terminal controller to descendants so chrome (toolbars, buttons)\n * can drive the terminal imperatively without prop drilling.\n */\nexport function TerminalProvider({\n value,\n children\n}: Readonly<{ value: TerminalController; children: ReactNode }>) {\n return (\n <TerminalContext.Provider value={value}>{children}</TerminalContext.Provider>\n );\n}\n\n/** Read the terminal controller from the nearest `TerminalProvider`. */\nexport function useTerminalController(): TerminalController {\n const ctx = useContext(TerminalContext);\n if (!ctx) {\n throw new Error(\"useTerminalController must be used within a <TerminalProvider>\");\n }\n return ctx;\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "react-xterm-shell",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A small React shell around xterm.js: a useXTerm hook + XTerm component with a stable imperative controller, automatic fitting, and opt-in webgl/web-links/unicode11 addons.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"./package.json": "./package.json"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
"src"
|
|
21
|
+
],
|
|
22
|
+
"sideEffects": false,
|
|
23
|
+
"keywords": [
|
|
24
|
+
"xterm",
|
|
25
|
+
"xterm.js",
|
|
26
|
+
"react",
|
|
27
|
+
"terminal",
|
|
28
|
+
"pty",
|
|
29
|
+
"webgl",
|
|
30
|
+
"hook",
|
|
31
|
+
"component"
|
|
32
|
+
],
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"author": "Noah Wardlow",
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "https://github.com/noah-wardlow/react-xterm-shell"
|
|
38
|
+
},
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/noah-wardlow/react-xterm-shell/issues"
|
|
41
|
+
},
|
|
42
|
+
"homepage": "https://github.com/noah-wardlow/react-xterm-shell#readme",
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "tsup",
|
|
45
|
+
"dev": "tsup --watch",
|
|
46
|
+
"release": "semantic-release",
|
|
47
|
+
"typecheck": "tsc --noEmit",
|
|
48
|
+
"prepublishOnly": "pnpm run typecheck && pnpm run build"
|
|
49
|
+
},
|
|
50
|
+
"publishConfig": {
|
|
51
|
+
"access": "public",
|
|
52
|
+
"provenance": true
|
|
53
|
+
},
|
|
54
|
+
"peerDependencies": {
|
|
55
|
+
"@xterm/xterm": ">=5",
|
|
56
|
+
"react": ">=19"
|
|
57
|
+
},
|
|
58
|
+
"dependencies": {
|
|
59
|
+
"@xterm/addon-fit": "^0.10.0",
|
|
60
|
+
"@xterm/addon-unicode11": "^0.8.0",
|
|
61
|
+
"@xterm/addon-web-links": "^0.11.0",
|
|
62
|
+
"@xterm/addon-webgl": "^0.18.0"
|
|
63
|
+
},
|
|
64
|
+
"devDependencies": {
|
|
65
|
+
"@semantic-release/changelog": "^6.0.3",
|
|
66
|
+
"@semantic-release/git": "^10.0.1",
|
|
67
|
+
"@types/react": "^19.0.0",
|
|
68
|
+
"@xterm/xterm": "^5.5.0",
|
|
69
|
+
"react": "^19.2.0",
|
|
70
|
+
"semantic-release": "^25.0.3",
|
|
71
|
+
"tsup": "^8.4.0",
|
|
72
|
+
"typescript": "~5.8.2"
|
|
73
|
+
},
|
|
74
|
+
"engines": {
|
|
75
|
+
"node": ">=22"
|
|
76
|
+
},
|
|
77
|
+
"packageManager": "pnpm@10.30.0"
|
|
78
|
+
}
|
package/src/context.tsx
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { createContext, useContext } from "react";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
import type { TerminalController } from "./types";
|
|
5
|
+
|
|
6
|
+
const TerminalContext = createContext<TerminalController | null>(null);
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Provides a terminal controller to descendants so chrome (toolbars, buttons)
|
|
10
|
+
* can drive the terminal imperatively without prop drilling.
|
|
11
|
+
*/
|
|
12
|
+
export function TerminalProvider({
|
|
13
|
+
value,
|
|
14
|
+
children
|
|
15
|
+
}: Readonly<{ value: TerminalController; children: ReactNode }>) {
|
|
16
|
+
return (
|
|
17
|
+
<TerminalContext.Provider value={value}>{children}</TerminalContext.Provider>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Read the terminal controller from the nearest `TerminalProvider`. */
|
|
22
|
+
export function useTerminalController(): TerminalController {
|
|
23
|
+
const ctx = useContext(TerminalContext);
|
|
24
|
+
if (!ctx) {
|
|
25
|
+
throw new Error("useTerminalController must be used within a <TerminalProvider>");
|
|
26
|
+
}
|
|
27
|
+
return ctx;
|
|
28
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export { useXTerm } from "./use-xterm";
|
|
2
|
+
export { XTerm } from "./xterm";
|
|
3
|
+
export type { XTermProps } from "./xterm";
|
|
4
|
+
export { TerminalProvider, useTerminalController } from "./context";
|
|
5
|
+
export type {
|
|
6
|
+
UseXTermOptions,
|
|
7
|
+
TerminalController,
|
|
8
|
+
XTermHandle
|
|
9
|
+
} from "./types";
|
|
10
|
+
|
|
11
|
+
// Convenience re-export so consumers can type a theme without a direct
|
|
12
|
+
// @xterm/xterm import.
|
|
13
|
+
export type { ITheme, Terminal } from "@xterm/xterm";
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ITheme,
|
|
3
|
+
ITerminalOptions,
|
|
4
|
+
ITerminalAddon,
|
|
5
|
+
Terminal
|
|
6
|
+
} from "@xterm/xterm";
|
|
7
|
+
|
|
8
|
+
export interface UseXTermOptions {
|
|
9
|
+
/** Called with user keystrokes (xterm `onData`). Wire this to your transport. */
|
|
10
|
+
onData?: (data: string) => void;
|
|
11
|
+
/** Called when the rendered grid resizes (xterm `onResize`). */
|
|
12
|
+
onResize?: (size: { cols: number; rows: number }) => void;
|
|
13
|
+
/** xterm color theme. */
|
|
14
|
+
theme?: ITheme;
|
|
15
|
+
/** Font size in px. Default 13. */
|
|
16
|
+
fontSize?: number;
|
|
17
|
+
/** Scrollback lines retained. Default 1000. */
|
|
18
|
+
scrollback?: number;
|
|
19
|
+
/** Whether the cursor blinks. Default true. */
|
|
20
|
+
cursorBlink?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Built-in addons, all enabled by default:
|
|
23
|
+
* - `webgl`: GL renderer with automatic DOM-renderer fallback on context loss.
|
|
24
|
+
* - `webLinks`: clickable URLs in output.
|
|
25
|
+
* - `unicode11`: correct width for box-drawing, spinners, and emoji.
|
|
26
|
+
*/
|
|
27
|
+
webgl?: boolean;
|
|
28
|
+
webLinks?: boolean;
|
|
29
|
+
unicode11?: boolean;
|
|
30
|
+
/** Extra xterm options merged over the ones above. */
|
|
31
|
+
options?: ITerminalOptions;
|
|
32
|
+
/** Additional addons to load (e.g. a search addon). */
|
|
33
|
+
addons?: ITerminalAddon[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Imperative controller for the terminal. Stable across renders, so passing it
|
|
38
|
+
* around (or into a context) never re-renders consumers as output streams. `term`
|
|
39
|
+
* is a live getter that returns the instance once attached, `null` before.
|
|
40
|
+
*/
|
|
41
|
+
export interface TerminalController {
|
|
42
|
+
readonly term: Terminal | null;
|
|
43
|
+
write: (data: string | Uint8Array) => void;
|
|
44
|
+
clear: () => void;
|
|
45
|
+
focus: () => void;
|
|
46
|
+
fit: () => void;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type XTermHandle = TerminalController & {
|
|
50
|
+
/**
|
|
51
|
+
* Callback ref for the mount element. Creates the terminal on mount and
|
|
52
|
+
* disposes it when React clears the ref on unmount.
|
|
53
|
+
*/
|
|
54
|
+
attach: (el: HTMLDivElement | null) => void;
|
|
55
|
+
};
|
package/src/use-xterm.ts
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef } from "react";
|
|
2
|
+
import { Terminal } from "@xterm/xterm";
|
|
3
|
+
import { FitAddon } from "@xterm/addon-fit";
|
|
4
|
+
import { WebLinksAddon } from "@xterm/addon-web-links";
|
|
5
|
+
import { Unicode11Addon } from "@xterm/addon-unicode11";
|
|
6
|
+
import { WebglAddon } from "@xterm/addon-webgl";
|
|
7
|
+
|
|
8
|
+
import type { UseXTermOptions, XTermHandle } from "./types";
|
|
9
|
+
|
|
10
|
+
const DEFAULT_FONT_SIZE = 13;
|
|
11
|
+
const DEFAULT_SCROLLBACK = 1000;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A React shell around xterm.js. xterm owns the terminal grid and rendering;
|
|
15
|
+
* this hook owns the instance lifecycle and exposes a stable imperative
|
|
16
|
+
* controller so the surrounding tree can drive it without re-rendering as
|
|
17
|
+
* output streams.
|
|
18
|
+
*
|
|
19
|
+
* The instance is created lazily in the `attach` callback ref and disposed by
|
|
20
|
+
* the cleanup it returns. Sizing is automatic via a `ResizeObserver` + `FitAddon`.
|
|
21
|
+
*
|
|
22
|
+
* Import the stylesheet once in your app: `import "@xterm/xterm/css/xterm.css"`.
|
|
23
|
+
*/
|
|
24
|
+
export function useXTerm(opts: UseXTermOptions = {}): XTermHandle {
|
|
25
|
+
const {
|
|
26
|
+
onData,
|
|
27
|
+
onResize,
|
|
28
|
+
theme,
|
|
29
|
+
fontSize,
|
|
30
|
+
scrollback,
|
|
31
|
+
cursorBlink,
|
|
32
|
+
webgl = true,
|
|
33
|
+
webLinks = true,
|
|
34
|
+
unicode11 = true,
|
|
35
|
+
options,
|
|
36
|
+
addons
|
|
37
|
+
} = opts;
|
|
38
|
+
|
|
39
|
+
const termRef = useRef<Terminal | null>(null);
|
|
40
|
+
const fitRef = useRef<FitAddon | null>(null);
|
|
41
|
+
const cleanupRef = useRef<(() => void) | null>(null);
|
|
42
|
+
|
|
43
|
+
// Latest callbacks live in refs so `attach` does not depend on their identity.
|
|
44
|
+
// Otherwise a parent passing fresh callbacks each render would change `attach`,
|
|
45
|
+
// and the callback ref would dispose and recreate the terminal (wiping
|
|
46
|
+
// scrollback). The xterm handlers read the refs, so no stale-closure risk.
|
|
47
|
+
const onDataRef = useRef(onData);
|
|
48
|
+
const onResizeRef = useRef(onResize);
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
onDataRef.current = onData;
|
|
51
|
+
onResizeRef.current = onResize;
|
|
52
|
+
}, [onData, onResize]);
|
|
53
|
+
|
|
54
|
+
// Snapshot the addon set + extra addons so identity churn does not remount.
|
|
55
|
+
const extraAddons = useRef(addons);
|
|
56
|
+
extraAddons.current = addons;
|
|
57
|
+
|
|
58
|
+
const attach = useCallback(
|
|
59
|
+
(el: HTMLDivElement | null) => {
|
|
60
|
+
if (!el) {
|
|
61
|
+
cleanupRef.current?.();
|
|
62
|
+
cleanupRef.current = null;
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (termRef.current) return;
|
|
66
|
+
|
|
67
|
+
const term = new Terminal({
|
|
68
|
+
cursorBlink: cursorBlink ?? true,
|
|
69
|
+
fontSize: fontSize ?? DEFAULT_FONT_SIZE,
|
|
70
|
+
theme,
|
|
71
|
+
scrollback: scrollback ?? DEFAULT_SCROLLBACK,
|
|
72
|
+
allowProposedApi: true,
|
|
73
|
+
...options
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const fit = new FitAddon();
|
|
77
|
+
term.loadAddon(fit);
|
|
78
|
+
|
|
79
|
+
if (webLinks) term.loadAddon(new WebLinksAddon());
|
|
80
|
+
if (unicode11) {
|
|
81
|
+
term.loadAddon(new Unicode11Addon());
|
|
82
|
+
term.unicode.activeVersion = "11";
|
|
83
|
+
}
|
|
84
|
+
for (const addon of extraAddons.current ?? []) {
|
|
85
|
+
term.loadAddon(addon);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
term.open(el);
|
|
89
|
+
|
|
90
|
+
// WebGL must load after open(); fall back to the DOM renderer if the GL
|
|
91
|
+
// context is unavailable or lost.
|
|
92
|
+
if (webgl) {
|
|
93
|
+
try {
|
|
94
|
+
const webglAddon = new WebglAddon();
|
|
95
|
+
webglAddon.onContextLoss(() => webglAddon.dispose());
|
|
96
|
+
term.loadAddon(webglAddon);
|
|
97
|
+
} catch {
|
|
98
|
+
// No WebGL (headless/software GL); the DOM renderer remains active.
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
fit.fit();
|
|
103
|
+
|
|
104
|
+
const disposables = [
|
|
105
|
+
term.onData((data) => onDataRef.current?.(data)),
|
|
106
|
+
term.onResize((size) => onResizeRef.current?.(size))
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
const resizeObserver = new ResizeObserver(() => fit.fit());
|
|
110
|
+
resizeObserver.observe(el);
|
|
111
|
+
|
|
112
|
+
termRef.current = term;
|
|
113
|
+
fitRef.current = fit;
|
|
114
|
+
|
|
115
|
+
cleanupRef.current = () => {
|
|
116
|
+
resizeObserver.disconnect();
|
|
117
|
+
disposables.forEach((d) => d.dispose());
|
|
118
|
+
term.dispose();
|
|
119
|
+
termRef.current = null;
|
|
120
|
+
fitRef.current = null;
|
|
121
|
+
};
|
|
122
|
+
},
|
|
123
|
+
[theme, fontSize, scrollback, cursorBlink, webgl, webLinks, unicode11, options]
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
// One stable handle. `term` is a live getter — spreading it into a static
|
|
127
|
+
// object would freeze it to the null it holds before attach. Methods read the
|
|
128
|
+
// refs, so the handle is stable while the instance comes and goes.
|
|
129
|
+
return useMemo<XTermHandle>(
|
|
130
|
+
() => ({
|
|
131
|
+
get term() {
|
|
132
|
+
return termRef.current;
|
|
133
|
+
},
|
|
134
|
+
write: (data) => termRef.current?.write(data),
|
|
135
|
+
clear: () => termRef.current?.clear(),
|
|
136
|
+
focus: () => termRef.current?.focus(),
|
|
137
|
+
fit: () => fitRef.current?.fit(),
|
|
138
|
+
attach
|
|
139
|
+
}),
|
|
140
|
+
[attach]
|
|
141
|
+
);
|
|
142
|
+
}
|
package/src/xterm.tsx
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { XTermHandle } from "./types";
|
|
2
|
+
|
|
3
|
+
export interface XTermProps {
|
|
4
|
+
/** The handle returned by `useXTerm`. */
|
|
5
|
+
terminal: XTermHandle;
|
|
6
|
+
className?: string;
|
|
7
|
+
style?: React.CSSProperties;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Thin DOM mount point for an xterm instance. The `useXTerm` callback ref is the
|
|
12
|
+
* entire integration surface; this component just renders the host element.
|
|
13
|
+
*/
|
|
14
|
+
export function XTerm({ terminal, className, style }: Readonly<XTermProps>) {
|
|
15
|
+
return <div ref={terminal.attach} className={className} style={style} />;
|
|
16
|
+
}
|