Spaces:
Running
Running
File size: 3,977 Bytes
060ec76 a405934 cc5a7ba a405934 cc5a7ba 060ec76 a405934 cc5a7ba 060ec76 a405934 cc5a7ba a405934 cc5a7ba 060ec76 cc5a7ba a405934 cc5a7ba a405934 cc5a7ba 060ec76 cc5a7ba 060ec76 cc5a7ba a405934 cc5a7ba 060ec76 cc5a7ba 060ec76 cc5a7ba a405934 cc5a7ba a405934 cc5a7ba a35908e 060ec76 cc5a7ba a35908e cc5a7ba 060ec76 a405934 060ec76 a405934 cc5a7ba a405934 060ec76 a405934 | 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 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 | // 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",
}}
/>
);
}
|