"use client"; import { useEffect, useRef, useImperativeHandle, forwardRef, RefObject, } from "react"; import "@xterm/xterm/css/xterm.css"; export type TerminalRef = { write: (data: string) => void; clear: () => void; focus: () => void; }; export const Terminal = forwardRef< TerminalRef, { onData: (data: String) => void; } >(function Terminal(props, ref) { const terminalRef = useRef(null); const xtermRef = useRef(null); useImperativeHandle(ref, () => ({ write: (data: string) => { if (xtermRef.current) { xtermRef.current.write(data); } }, clear: () => { if (xtermRef.current) { xtermRef.current.clear(); } }, focus: () => { if (xtermRef.current) { xtermRef.current.focus(); } }, })); useEffect(() => { // Set to false when this component is unmounted. // // Here's what this flag prevents: // - component mounts // - `useEffect` runs, `mounted = true` // - `init_term()` function begins work // - before init_term() finishes, the user navigates away and to unmounts // - `useEffect` cleans up, `mounted = false` // - `init_term()` ccompletes, and we attempt to set `xtermRef.current`, causing issues let mounted = true; init_term(terminalRef, props.onData, () => mounted) .then((term) => { if (!mounted) return; xtermRef.current = term; }) .catch((err) => { console.error("Failed to initialize terminal:", err); }); return () => { mounted = false; if (xtermRef.current) { xtermRef.current.dispose(); xtermRef.current = null; } }; }, [props.onData]); return
; }); async function init_term( ref: RefObject, // Called when the terminal receives data onData: (data: String) => void, isMounted: () => boolean ) { if (!ref.current) return; const { Terminal } = await import("@xterm/xterm"); if (!isMounted()) return; const term = new Terminal({ //"fontFamily": "Fantasque", rows: 24, fontSize: 16, tabStopWidth: 8, cursorBlink: false, cursorStyle: "block", cursorInactiveStyle: "none", theme: { background: "#1D1F21", foreground: "#F8F8F8", cursor: "#F8F8F2", black: "#282828", blue: "#0087AF", brightBlack: "#555555", brightBlue: "#87DFFF", brightCyan: "#28D1E7", brightGreen: "#A8FF60", brightMagenta: "#985EFF", brightRed: "#FFAA00", brightWhite: "#D0D0D0", brightYellow: "#F1FF52", cyan: "#87DFEB", green: "#B4EC85", magenta: "#BD99FF", red: "#ff2f00ff", white: "#F8F8F8", yellow: "#FFFFB6", }, }); term.open(ref.current); term.onData(onData); console.log("Terminal ready"); return term; }