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 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`. The handle is stable
123
- across renders; callbacks are read through refs, so passing fresh `onData` /
124
- `onResize` each render does not remount the terminal.
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.1.1",
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.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]