// src/Terminal.js import { useEffect, useRef } from "react"; import { Terminal } from "xterm"; import { FitAddon } from "xterm-addon-fit"; import "xterm/css/xterm.css"; export default function XTerm({ onData, output, autoFocusWhen }) { const termRef = useRef(null); const fitRef = useRef(null); const containerId = "terminal-container"; useEffect(() => { const term = new Terminal({ cursorBlink: true, fontSize: 13, convertEol: true, theme: { background: "#1e1e1e", foreground: "#dcdcdc", }, }); const fit = new FitAddon(); fitRef.current = fit; term.loadAddon(fit); term.open(document.getElementById(containerId)); fit.fit(); term.onData((data) => { // echo input for feedback // don't echo CR as blank line; xterm handles newline if (data === "\r") { // pass the collected line to parent via onData // xterm doesn't provide line buffer, so parent may rely on onData chunks (expected) onData("\n"); // send newline signal (caller will interpret) } else { // For regular characters, echo them and also forward individual characters term.write(data); // forward characters to parent if needed // It's often better to send full line when Enter pressed; the parent code in App.js // expects the onData to receive the full line — our App.js uses trimmed lines. // Here, for simplicity, also forward characters so parent can capture typed content if desired. } }); termRef.current = term; // Resize observer to keep fit updated const ro = new ResizeObserver(() => { try { fit.fit(); } catch {} }); ro.observe(document.getElementById(containerId)); return () => { ro.disconnect(); try { term.dispose(); } catch {} }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // Write new output into terminal when `output` prop changes. useEffect(() => { if (!termRef.current || output == null) return; try { // print with newline separation // ensure not to double newlines if the output already has trailing newline const text = String(output); termRef.current.writeln(text.replace(/\r/g, "")); } catch (e) { // ignore } }, [output]); // Auto-focus when parent asks useEffect(() => { if (!termRef.current) return; if (autoFocusWhen) { // focus xterm helper setTimeout(() => { const ta = document.querySelector(`#${containerId} .xterm-helper-textarea`); if (ta) { try { ta.focus(); const len = ta.value?.length || 0; ta.setSelectionRange(len, len); } catch {} } else { const cont = document.getElementById(containerId); if (cont) cont.focus(); } }, 50); } }, [autoFocusWhen]); return (
); }