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",
      }}
    />
  );
}