CodeIDE-Dev / src /Terminal.js
FrederickSundeep's picture
commit initial 12-12-2025 0002
7f234e3
// 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 (
<div
id={containerId}
style={{
width: "100%",
height: "180px",
background: "#1e1e1e",
borderTop: "1px solid #333",
}}
/>
);
}