FrederickSundeep commited on
Commit
060ec76
·
1 Parent(s): f4f6dff

commit initial 09-12-2025 33

Browse files
Files changed (2) hide show
  1. src/App.js +9 -9
  2. src/Terminal.js +96 -19
src/App.js CHANGED
@@ -580,21 +580,21 @@ const [interactivePromptShown, setInteractivePromptShown] = useState(false);
580
  <div style={{ marginBottom: 8 }}>
581
  <div style={{ fontSize: 12, color: "#ccc", marginBottom: 6 }}>Terminal</div>
582
 
583
- <XTerm
584
  output={output}
585
  onData={(line) => {
586
- const text = (line || "").replace(/\r$/, "");
587
- if (!text) return;
588
- // only auto-send when we previously showed a prompt OR are awaiting input
589
- if (interactivePromptShown || awaitingInput) {
590
- runCodeWithUpdatedInput(text);
591
- } else {
592
- // not an interactive session — you can still echo into small input if desired
593
- setTerminalInput((prev) => (prev ? prev + text : text));
594
  }
 
 
595
  }}
596
  />
597
 
 
598
  {/* When interactive program detected and waiting for user input */}
599
  {awaitingInput && (
600
  <div style={{ marginTop: 8, padding: 8, background: "#252526", border: "1px solid #333", borderRadius: 6 }}>
 
580
  <div style={{ marginBottom: 8 }}>
581
  <div style={{ fontSize: 12, color: "#ccc", marginBottom: 6 }}>Terminal</div>
582
 
583
+ <XTerm
584
  output={output}
585
  onData={(line) => {
586
+ // line is a clean string (no CR)
587
+ const trimmed = (line || "").replace(/\r$/, "");
588
+ if (!trimmed && !awaitingInput) {
589
+ // nothing typed
590
+ return;
 
 
 
591
  }
592
+ // Always delegate to the unified runner which appends to accumulated stdin
593
+ runCodeWithUpdatedInput(trimmed);
594
  }}
595
  />
596
 
597
+
598
  {/* When interactive program detected and waiting for user input */}
599
  {awaitingInput && (
600
  <div style={{ marginTop: 8, padding: 8, background: "#252526", border: "1px solid #333", borderRadius: 6 }}>
src/Terminal.js CHANGED
@@ -1,60 +1,137 @@
 
1
  import { useEffect, useRef } from "react";
2
  import { Terminal } from "xterm";
3
  import { FitAddon } from "xterm-addon-fit";
4
  import "xterm/css/xterm.css";
5
 
 
 
 
 
 
6
  export default function XTerm({ onData, output }) {
 
7
  const termRef = useRef(null);
8
- const fitAddon = new FitAddon();
 
9
 
10
  useEffect(() => {
11
  const term = new Terminal({
12
  cursorBlink: true,
13
  fontSize: 14,
 
 
14
  theme: {
15
  background: "#1e1e1e",
16
  foreground: "#ffffff",
17
  },
18
  });
19
 
20
- termRef.current = term;
21
- term.loadAddon(fitAddon);
22
 
23
- term.open(document.getElementById("terminal-container"));
 
24
  fitAddon.fit();
25
 
 
 
 
 
26
  term.onData((data) => {
27
- // Echo user input (like real terminals)
 
28
  term.write(data);
29
 
30
- // When ENTER pressed, send full line to parent
31
- if (data === "\r") {
32
- const line = term._core.buffer.xtermBuffer.active.getLine(
33
- term.buffer.active.cursorY
34
- ).translateToString().trim();
35
- onData(line);
 
 
 
 
 
 
 
 
 
36
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  });
38
 
39
- return () => term.dispose();
40
- }, []);
 
 
 
 
 
 
 
 
41
 
42
- // Print backend output into terminal
43
  useEffect(() => {
44
- if (output) {
45
- termRef.current?.writeln("\r\n" + output);
46
- }
 
 
 
 
 
 
 
 
47
  }, [output]);
48
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  return (
50
  <div
51
- id="terminal-container"
52
  style={{
53
  width: "100%",
54
  height: "180px",
55
  background: "#1e1e1e",
56
  borderTop: "1px solid #333",
 
57
  }}
58
- ></div>
59
  );
60
  }
 
1
+ // src/Terminal.js
2
  import { useEffect, useRef } from "react";
3
  import { Terminal } from "xterm";
4
  import { FitAddon } from "xterm-addon-fit";
5
  import "xterm/css/xterm.css";
6
 
7
+ /**
8
+ * Props:
9
+ * - onData(line: string) -> called when user presses Enter with the typed line (no trailing \r)
10
+ * - output (string) -> append output to the terminal when it changes
11
+ */
12
  export default function XTerm({ onData, output }) {
13
+ const containerId = "terminal-container";
14
  const termRef = useRef(null);
15
+ const bufferRef = useRef(""); // collects user typed chars until Enter
16
+ const fitRef = useRef(null);
17
 
18
  useEffect(() => {
19
  const term = new Terminal({
20
  cursorBlink: true,
21
  fontSize: 14,
22
+ disableStdin: false,
23
+ convertEol: true,
24
  theme: {
25
  background: "#1e1e1e",
26
  foreground: "#ffffff",
27
  },
28
  });
29
 
30
+ const fitAddon = new FitAddon();
31
+ fitRef.current = fitAddon;
32
 
33
+ term.loadAddon(fitAddon);
34
+ term.open(document.getElementById(containerId));
35
  fitAddon.fit();
36
 
37
+ // Keep a reference
38
+ termRef.current = term;
39
+
40
+ // echo typed characters and detect Enter
41
  term.onData((data) => {
42
+ // xterm sends strings including characters and control chars like '\r'
43
+ // append to visible terminal
44
  term.write(data);
45
 
46
+ // common Enter is '\r' (CR)
47
+ if (data === "\r" || data === "\n") {
48
+ // capture current buffer as line, trim trailing CR/LF
49
+ const line = (bufferRef.current || "").replace(/\r?\n$/, "");
50
+ bufferRef.current = ""; // reset buffer
51
+ // echo newline if not already
52
+ // (we already wrote the '\r' above)
53
+ // call parent handler
54
+ try {
55
+ if (typeof onData === "function" && line !== null) onData(line);
56
+ } catch (e) {
57
+ // swallow
58
+ console.error("XTerm onData handler threw:", e);
59
+ }
60
+ return;
61
  }
62
+
63
+ // backspace handling: if user presses backspace key, it may come as '\x7f' or '\b'
64
+ if (data === "\x7f" || data === "\b") {
65
+ bufferRef.current = bufferRef.current.slice(0, -1);
66
+ return;
67
+ }
68
+
69
+ // other control sequences ignore
70
+ if (data.charCodeAt(0) < 32) {
71
+ // ignore other ctrl chars
72
+ return;
73
+ }
74
+
75
+ // normal characters: append to buffer
76
+ bufferRef.current += data;
77
  });
78
 
79
+ // expose a simple focus method on container element for external focusing
80
+ const container = document.getElementById(containerId);
81
+ if (container) container.tabIndex = 0;
82
+
83
+ return () => {
84
+ try {
85
+ term.dispose();
86
+ } catch {}
87
+ };
88
+ }, [onData]);
89
 
90
+ // Append new output when `output` prop changes
91
  useEffect(() => {
92
+ const term = termRef.current;
93
+ if (!term || !output) return;
94
+ // write a newline and the output text (preserves newlines)
95
+ term.writeln("");
96
+ // if output includes multiple lines, write each
97
+ const lines = output.split(/\r?\n/);
98
+ lines.forEach((ln, idx) => {
99
+ // avoid extra blank at very start
100
+ if (idx === 0 && ln === "") return;
101
+ term.writeln(ln);
102
+ });
103
  }, [output]);
104
 
105
+ // helper to focus the hidden xterm textarea
106
+ const focus = () => {
107
+ // xterm's helper textarea is what receives keyboard input
108
+ const ta = document.querySelector(`#${containerId} .xterm-helper-textarea`);
109
+ if (ta) {
110
+ ta.focus();
111
+ const len = ta.value?.length ?? 0;
112
+ try { ta.setSelectionRange(len, len); } catch {}
113
+ } else {
114
+ const cont = document.getElementById(containerId);
115
+ if (cont) cont.focus();
116
+ }
117
+ };
118
+
119
+ // expose a global method in-case parent wants to call it (not ideal but handy)
120
+ useEffect(() => {
121
+ window.__xterm_focus = focus;
122
+ return () => { try { delete window.__xterm_focus } catch {} };
123
+ }, []);
124
+
125
  return (
126
  <div
127
+ id={containerId}
128
  style={{
129
  width: "100%",
130
  height: "180px",
131
  background: "#1e1e1e",
132
  borderTop: "1px solid #333",
133
+ outline: "none",
134
  }}
135
+ />
136
  );
137
  }