react-xterm-shell 0.1.0 → 0.2.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/README.md +63 -16
- package/dist/index.d.ts +14 -0
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/types.ts +11 -0
- package/src/use-xterm.ts +11 -2
package/README.md
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
# react-xterm-shell
|
|
2
2
|
|
|
3
|
+
[npm package](https://www.npmjs.com/package/react-xterm-shell)
|
|
4
|
+
|
|
3
5
|
> **Beta** — under active development; the API may change between minor versions until 1.0.
|
|
4
6
|
|
|
5
7
|
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
8
|
|
|
7
9
|
- `useXTerm()` — creates and owns the xterm instance behind a stable controller.
|
|
8
10
|
- `<XTerm />` — the DOM mount point.
|
|
9
|
-
- `TerminalProvider` / `useTerminalController()` — drive the terminal from
|
|
11
|
+
- `TerminalProvider` / `useTerminalController()` — drive the terminal from surrounding UI without prop drilling.
|
|
10
12
|
- Automatic sizing via `FitAddon` + `ResizeObserver`.
|
|
11
13
|
- Opt-in `webgl` (with DOM fallback), `web-links`, and `unicode11` addons, on by default.
|
|
12
14
|
|
|
@@ -27,48 +29,90 @@ import "@xterm/xterm/css/xterm.css";
|
|
|
27
29
|
## Quick start
|
|
28
30
|
|
|
29
31
|
```tsx
|
|
30
|
-
import {
|
|
32
|
+
import { useEffect, useRef } from "react";
|
|
33
|
+
import { useXTerm, XTerm, type XTermHandle } from "react-xterm-shell";
|
|
31
34
|
import "@xterm/xterm/css/xterm.css";
|
|
32
35
|
|
|
33
|
-
function TerminalPanel(
|
|
36
|
+
export function TerminalPanel() {
|
|
37
|
+
const terminalRef = useRef<XTermHandle | null>(null);
|
|
34
38
|
const terminal = useXTerm({
|
|
35
|
-
onData: (data) =>
|
|
36
|
-
onResize: ({ cols, rows }) =>
|
|
37
|
-
socket.send(JSON.stringify({ type: "resize", cols, rows })),
|
|
39
|
+
onData: (data) => terminalRef.current?.write(data === "\r" ? "\r\n$ " : data),
|
|
38
40
|
theme: { background: "#1a1b26", foreground: "#a9b1d6" }
|
|
39
41
|
});
|
|
42
|
+
terminalRef.current = terminal;
|
|
40
43
|
|
|
41
|
-
|
|
42
|
-
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
terminal.write("react-xterm-shell\r\n$ ");
|
|
46
|
+
terminal.focus();
|
|
47
|
+
}, [terminal]);
|
|
43
48
|
|
|
44
49
|
return <XTerm terminal={terminal} style={{ width: "100%", height: 420 }} />;
|
|
45
50
|
}
|
|
46
51
|
```
|
|
47
52
|
|
|
48
|
-
##
|
|
53
|
+
## External controls
|
|
49
54
|
|
|
50
|
-
`useXTerm` returns a stable controller, so
|
|
55
|
+
`useXTerm` returns a stable controller, so surrounding UI can drive it imperatively
|
|
51
56
|
without re-rendering as bytes stream:
|
|
52
57
|
|
|
53
58
|
```tsx
|
|
54
59
|
import { useXTerm, XTerm, TerminalProvider, useTerminalController } from "react-xterm-shell";
|
|
55
60
|
|
|
56
|
-
function
|
|
61
|
+
function SessionActions() {
|
|
57
62
|
const term = useTerminalController();
|
|
58
|
-
return
|
|
63
|
+
return (
|
|
64
|
+
<div>
|
|
65
|
+
<button onClick={() => term.write("deploy --target staging\r\n")}>Insert command</button>
|
|
66
|
+
<button onClick={term.clear}>Clear</button>
|
|
67
|
+
<button onClick={term.focus}>Focus</button>
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
59
70
|
}
|
|
60
71
|
|
|
61
72
|
function Panel() {
|
|
62
73
|
const terminal = useXTerm({ onData: send });
|
|
63
74
|
return (
|
|
64
75
|
<TerminalProvider value={terminal}>
|
|
65
|
-
<
|
|
76
|
+
<SessionActions />
|
|
66
77
|
<XTerm terminal={terminal} className="h-[420px]" />
|
|
67
78
|
</TerminalProvider>
|
|
68
79
|
);
|
|
69
80
|
}
|
|
70
81
|
```
|
|
71
82
|
|
|
83
|
+
## Backend wiring
|
|
84
|
+
|
|
85
|
+
The package does not include a transport. Connect `onData` and `onResize` to a
|
|
86
|
+
PTY, SSH, container, or WebSocket service, and stream backend output into
|
|
87
|
+
`terminal.write()`:
|
|
88
|
+
|
|
89
|
+
```tsx
|
|
90
|
+
function RemoteShell({ socket }: { socket: WebSocket }) {
|
|
91
|
+
const terminal = useXTerm({
|
|
92
|
+
onData: (data) => socket.send(JSON.stringify({ type: "input", data })),
|
|
93
|
+
onResize: ({ cols, rows }) =>
|
|
94
|
+
socket.send(JSON.stringify({ type: "resize", cols, rows }))
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
socket.onmessage = (event) => terminal.write(event.data);
|
|
98
|
+
|
|
99
|
+
// Re-send the size once the socket is open. The mount-time `onResize` fires
|
|
100
|
+
// before the socket connects, so that first resize is dropped — without this
|
|
101
|
+
// the PTY stays at its connect-time default and the shell renders narrower
|
|
102
|
+
// than the pane.
|
|
103
|
+
socket.onopen = () => {
|
|
104
|
+
const size = terminal.getDimensions();
|
|
105
|
+
if (size) socket.send(JSON.stringify({ type: "resize", ...size }));
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
return <XTerm terminal={terminal} className="h-[420px]" />;
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
> **Sizing gotcha:** the terminal fits at mount, so the first `onResize` fires
|
|
113
|
+
> *before* your transport is connected and that size is lost. Always re-send the
|
|
114
|
+
> size on (re)connect using `getDimensions()`, as shown above.
|
|
115
|
+
|
|
72
116
|
## API
|
|
73
117
|
|
|
74
118
|
### `useXTerm(options?) => XTermHandle`
|
|
@@ -88,9 +132,12 @@ function Panel() {
|
|
|
88
132
|
| `addons` | `ITerminalAddon[]` | — | Extra addons (e.g. a search addon). |
|
|
89
133
|
|
|
90
134
|
The returned `XTermHandle` has `attach` (the callback ref for `<XTerm>`), a live
|
|
91
|
-
`term` getter, and `write` / `clear` / `focus` / `fit
|
|
92
|
-
|
|
93
|
-
`
|
|
135
|
+
`term` getter, and `write` / `clear` / `reset` / `focus` / `fit` /
|
|
136
|
+
`getDimensions` (`clear` keeps the prompt line; `reset` blanks the screen and
|
|
137
|
+
drops scrollback; `getDimensions` returns the current `{ cols, rows }` or `null`
|
|
138
|
+
before attach — see the sizing gotcha under [Backend wiring](#backend-wiring)).
|
|
139
|
+
The handle is stable across renders; callbacks are read through refs, so passing
|
|
140
|
+
fresh `onData` / `onResize` each render does not remount the terminal.
|
|
94
141
|
|
|
95
142
|
### `<XTerm terminal={handle} className? style? />`
|
|
96
143
|
|
package/dist/index.d.ts
CHANGED
|
@@ -41,9 +41,23 @@ interface UseXTermOptions {
|
|
|
41
41
|
interface TerminalController {
|
|
42
42
|
readonly term: Terminal | null;
|
|
43
43
|
write: (data: string | Uint8Array) => void;
|
|
44
|
+
/** Clear scrollback, keeping the current prompt line (xterm `clear`). */
|
|
44
45
|
clear: () => void;
|
|
46
|
+
/** Full reset to a blank screen, dropping scrollback (xterm `reset`). */
|
|
47
|
+
reset: () => void;
|
|
45
48
|
focus: () => void;
|
|
46
49
|
fit: () => void;
|
|
50
|
+
/**
|
|
51
|
+
* Current grid size, or `null` before the terminal is attached. Use this to
|
|
52
|
+
* re-send the size to your backend when the transport (re)connects: the
|
|
53
|
+
* initial `onResize` fires synchronously at mount — before your socket is
|
|
54
|
+
* open — so that first size is otherwise dropped and the PTY stays at its
|
|
55
|
+
* connect-time default.
|
|
56
|
+
*/
|
|
57
|
+
getDimensions: () => {
|
|
58
|
+
cols: number;
|
|
59
|
+
rows: number;
|
|
60
|
+
} | null;
|
|
47
61
|
}
|
|
48
62
|
type XTermHandle = TerminalController & {
|
|
49
63
|
/**
|
package/dist/index.js
CHANGED
|
@@ -69,11 +69,11 @@ function useXTerm(opts = {}) {
|
|
|
69
69
|
} catch {
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
|
-
fit.fit();
|
|
73
72
|
const disposables = [
|
|
74
73
|
term.onData((data) => onDataRef.current?.(data)),
|
|
75
74
|
term.onResize((size) => onResizeRef.current?.(size))
|
|
76
75
|
];
|
|
76
|
+
fit.fit();
|
|
77
77
|
const resizeObserver = new ResizeObserver(() => fit.fit());
|
|
78
78
|
resizeObserver.observe(el);
|
|
79
79
|
termRef.current = term;
|
|
@@ -95,8 +95,13 @@ function useXTerm(opts = {}) {
|
|
|
95
95
|
},
|
|
96
96
|
write: (data) => termRef.current?.write(data),
|
|
97
97
|
clear: () => termRef.current?.clear(),
|
|
98
|
+
reset: () => termRef.current?.reset(),
|
|
98
99
|
focus: () => termRef.current?.focus(),
|
|
99
100
|
fit: () => fitRef.current?.fit(),
|
|
101
|
+
getDimensions: () => {
|
|
102
|
+
const term = termRef.current;
|
|
103
|
+
return term ? { cols: term.cols, rows: term.rows } : null;
|
|
104
|
+
},
|
|
100
105
|
attach
|
|
101
106
|
}),
|
|
102
107
|
[attach]
|
package/dist/index.js.map
CHANGED
|
@@ -1 +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"]}
|
|
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 // Register handlers before the first fit() so the mount-time resize is\n // delivered to onResize. If fit() runs first, that initial resize event\n // fires with no listener attached and the starting grid size is lost —\n // leaving a transport that only syncs on onResize stuck at the default.\n const disposables = [\n term.onData((data) => onDataRef.current?.(data)),\n term.onResize((size) => onResizeRef.current?.(size))\n ];\n\n fit.fit();\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 reset: () => termRef.current?.reset(),\n focus: () => termRef.current?.focus(),\n fit: () => fitRef.current?.fit(),\n getDimensions: () => {\n const term = termRef.current;\n return term ? { cols: term.cols, rows: term.rows } : null;\n },\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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-xterm-shell",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
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
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"author": "Noah Wardlow",
|
|
35
35
|
"repository": {
|
|
36
36
|
"type": "git",
|
|
37
|
-
"url": "https://github.com/noah-wardlow/react-xterm-shell"
|
|
37
|
+
"url": "git+https://github.com/noah-wardlow/react-xterm-shell.git"
|
|
38
38
|
},
|
|
39
39
|
"bugs": {
|
|
40
40
|
"url": "https://github.com/noah-wardlow/react-xterm-shell/issues"
|
package/src/types.ts
CHANGED
|
@@ -41,9 +41,20 @@ export interface UseXTermOptions {
|
|
|
41
41
|
export interface TerminalController {
|
|
42
42
|
readonly term: Terminal | null;
|
|
43
43
|
write: (data: string | Uint8Array) => void;
|
|
44
|
+
/** Clear scrollback, keeping the current prompt line (xterm `clear`). */
|
|
44
45
|
clear: () => void;
|
|
46
|
+
/** Full reset to a blank screen, dropping scrollback (xterm `reset`). */
|
|
47
|
+
reset: () => void;
|
|
45
48
|
focus: () => void;
|
|
46
49
|
fit: () => void;
|
|
50
|
+
/**
|
|
51
|
+
* Current grid size, or `null` before the terminal is attached. Use this to
|
|
52
|
+
* re-send the size to your backend when the transport (re)connects: the
|
|
53
|
+
* initial `onResize` fires synchronously at mount — before your socket is
|
|
54
|
+
* open — so that first size is otherwise dropped and the PTY stays at its
|
|
55
|
+
* connect-time default.
|
|
56
|
+
*/
|
|
57
|
+
getDimensions: () => { cols: number; rows: number } | null;
|
|
47
58
|
}
|
|
48
59
|
|
|
49
60
|
export type XTermHandle = TerminalController & {
|
package/src/use-xterm.ts
CHANGED
|
@@ -99,13 +99,17 @@ export function useXTerm(opts: UseXTermOptions = {}): XTermHandle {
|
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
fit
|
|
103
|
-
|
|
102
|
+
// Register handlers before the first fit() so the mount-time resize is
|
|
103
|
+
// delivered to onResize. If fit() runs first, that initial resize event
|
|
104
|
+
// fires with no listener attached and the starting grid size is lost —
|
|
105
|
+
// leaving a transport that only syncs on onResize stuck at the default.
|
|
104
106
|
const disposables = [
|
|
105
107
|
term.onData((data) => onDataRef.current?.(data)),
|
|
106
108
|
term.onResize((size) => onResizeRef.current?.(size))
|
|
107
109
|
];
|
|
108
110
|
|
|
111
|
+
fit.fit();
|
|
112
|
+
|
|
109
113
|
const resizeObserver = new ResizeObserver(() => fit.fit());
|
|
110
114
|
resizeObserver.observe(el);
|
|
111
115
|
|
|
@@ -133,8 +137,13 @@ export function useXTerm(opts: UseXTermOptions = {}): XTermHandle {
|
|
|
133
137
|
},
|
|
134
138
|
write: (data) => termRef.current?.write(data),
|
|
135
139
|
clear: () => termRef.current?.clear(),
|
|
140
|
+
reset: () => termRef.current?.reset(),
|
|
136
141
|
focus: () => termRef.current?.focus(),
|
|
137
142
|
fit: () => fitRef.current?.fit(),
|
|
143
|
+
getDimensions: () => {
|
|
144
|
+
const term = termRef.current;
|
|
145
|
+
return term ? { cols: term.cols, rows: term.rows } : null;
|
|
146
|
+
},
|
|
138
147
|
attach
|
|
139
148
|
}),
|
|
140
149
|
[attach]
|