react-xterm-shell 0.1.1 → 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 +19 -3
- package/dist/index.d.ts +14 -0
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/types.ts +11 -0
- package/src/use-xterm.ts +11 -2
package/README.md
CHANGED
|
@@ -96,10 +96,23 @@ function RemoteShell({ socket }: { socket: WebSocket }) {
|
|
|
96
96
|
|
|
97
97
|
socket.onmessage = (event) => terminal.write(event.data);
|
|
98
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
|
+
|
|
99
108
|
return <XTerm terminal={terminal} className="h-[420px]" />;
|
|
100
109
|
}
|
|
101
110
|
```
|
|
102
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
|
+
|
|
103
116
|
## API
|
|
104
117
|
|
|
105
118
|
### `useXTerm(options?) => XTermHandle`
|
|
@@ -119,9 +132,12 @@ function RemoteShell({ socket }: { socket: WebSocket }) {
|
|
|
119
132
|
| `addons` | `ITerminalAddon[]` | — | Extra addons (e.g. a search addon). |
|
|
120
133
|
|
|
121
134
|
The returned `XTermHandle` has `attach` (the callback ref for `<XTerm>`), a live
|
|
122
|
-
`term` getter, and `write` / `clear` / `focus` / `fit
|
|
123
|
-
|
|
124
|
-
`
|
|
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.
|
|
125
141
|
|
|
126
142
|
### `<XTerm terminal={handle} className? style? />`
|
|
127
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",
|
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]
|