CodeIDE / src /Terminal.js
FrederickSundeep's picture
commit initial 09-12-2025 46
cc5a7ba
// src/Terminal.js
import { useEffect, useRef } from "react";
import { Terminal } from "xterm";
import { FitAddon } from "xterm-addon-fit";
import "xterm/css/xterm.css";
/**
* Props:
* - onData(line: string) -> called when user presses Enter with the typed line (no trailing \r)
* - output (string) -> append output to the terminal when it changes
*/
export default function XTerm({ onData, output }) {
const containerId = "terminal-container";
const termRef = useRef(null);
const bufferRef = useRef(""); // collects user typed chars until Enter
const fitRef = useRef(null);
useEffect(() => {
const term = new Terminal({
cursorBlink: true,
fontSize: 14,
disableStdin: false,
convertEol: true,
theme: {
background: "#1e1e1e",
foreground: "#ffffff",
},
});
const fitAddon = new FitAddon();
fitRef.current = fitAddon;
term.loadAddon(fitAddon);
term.open(document.getElementById(containerId));
fitAddon.fit();
// Keep a reference
termRef.current = term;
// echo typed characters and detect Enter
term.onData((data) => {
// xterm sends strings including characters and control chars like '\r'
// append to visible terminal
term.write(data);
// common Enter is '\r' (CR)
if (data === "\r" || data === "\n") {
// capture current buffer as line, trim trailing CR/LF
const line = (bufferRef.current || "").replace(/\r?\n$/, "");
bufferRef.current = ""; // reset buffer
// echo newline if not already
// (we already wrote the '\r' above)
// call parent handler
try {
if (typeof onData === "function" && line !== null) onData(line);
} catch (e) {
// swallow
console.error("XTerm onData handler threw:", e);
}
return;
}
// backspace handling: if user presses backspace key, it may come as '\x7f' or '\b'
if (data === "\x7f" || data === "\b") {
bufferRef.current = bufferRef.current.slice(0, -1);
return;
}
// other control sequences ignore
if (data.charCodeAt(0) < 32) {
// ignore other ctrl chars
return;
}
// normal characters: append to buffer
bufferRef.current += data;
});
// expose a simple focus method on container element for external focusing
const container = document.getElementById(containerId);
if (container) container.tabIndex = 0;
return () => {
try {
term.dispose();
} catch {}
};
}, [onData]);
// Append new output when `output` prop changes
useEffect(() => {
const term = termRef.current;
if (!term || !output) return;
// write a newline and the output text (preserves newlines)
term.writeln("");
// if output includes multiple lines, write each
const lines = output.split(/\r?\n/);
lines.forEach((ln, idx) => {
// avoid extra blank at very start
if (idx === 0 && ln === "") return;
term.writeln(ln);
});
}, [output]);
// helper to focus the hidden xterm textarea
const focus = () => {
// xterm's helper textarea is what receives keyboard input
const ta = document.querySelector(`#${containerId} .xterm-helper-textarea`);
if (ta) {
ta.focus();
const len = ta.value?.length ?? 0;
try { ta.setSelectionRange(len, len); } catch {}
} else {
const cont = document.getElementById(containerId);
if (cont) cont.focus();
}
};
// expose a global method in-case parent wants to call it (not ideal but handy)
useEffect(() => {
window.__xterm_focus = focus;
return () => { try { delete window.__xterm_focus } catch {} };
}, []);
return (
<div
id={containerId}
style={{
width: "100%",
height: "180px",
background: "#1e1e1e",
borderTop: "1px solid #333",
outline: "none",
}}
/>
);
}