Spaces:
Running
Running
File size: 3,128 Bytes
e3e0a75 7f234e3 e3e0a75 7f234e3 e3e0a75 7f234e3 e3e0a75 7f234e3 e3e0a75 7f234e3 e3e0a75 7f234e3 e3e0a75 7f234e3 e3e0a75 7f234e3 e3e0a75 7f234e3 e3e0a75 7f234e3 e3e0a75 7f234e3 e3e0a75 7f234e3 e3e0a75 7f234e3 e3e0a75 7f234e3 e3e0a75 7f234e3 e3e0a75 7f234e3 e3e0a75 7f234e3 e3e0a75 7f234e3 e3e0a75 7f234e3 e3e0a75 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 | // 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",
}}
/>
);
}
|