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