127 lines
2.7 KiB
TypeScript
127 lines
2.7 KiB
TypeScript
"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<HTMLDivElement>(null);
|
|
const xtermRef = useRef<any>(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:
|
|
// - <Terminal> component mounts
|
|
// - `useEffect` runs, `mounted = true`
|
|
// - `init_term()` function begins work
|
|
// - before init_term() finishes, the user navigates away and <Terminal> 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 <div ref={terminalRef} style={{ height: "100%", width: "100%" }} />;
|
|
});
|
|
|
|
async function init_term(
|
|
ref: RefObject<HTMLDivElement>,
|
|
|
|
// 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;
|
|
}
|