|
@@ -0,0 +1,115 @@
|
|
|
|
|
+import { IDisposable, Terminal as XtermTerminal } from "@xterm/xterm";
|
|
|
|
|
+import "@xterm/xterm/css/xterm.css";
|
|
|
|
|
+import React, { useEffect, useRef } from "react";
|
|
|
|
|
+import { VscTerminal } from "react-icons/vsc";
|
|
|
|
|
+import { useSelector } from "react-redux";
|
|
|
|
|
+import { FitAddon } from "xterm-addon-fit";
|
|
|
|
|
+import Socket from "#/services/socket";
|
|
|
|
|
+import { RootState } from "#/store";
|
|
|
|
|
+import ActionType from "#/types/ActionType";
|
|
|
|
|
+import ObservationType from "#/types/ObservationType";
|
|
|
|
|
+
|
|
|
|
|
+class JsonWebsocketAddon {
|
|
|
|
|
+ _disposables: IDisposable[];
|
|
|
|
|
+
|
|
|
|
|
+ constructor() {
|
|
|
|
|
+ this._disposables = [];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ activate(terminal: XtermTerminal) {
|
|
|
|
|
+ this._disposables.push(
|
|
|
|
|
+ terminal.onData((data) => {
|
|
|
|
|
+ const payload = JSON.stringify({ action: "terminal", data });
|
|
|
|
|
+ Socket.send(payload);
|
|
|
|
|
+ }),
|
|
|
|
|
+ );
|
|
|
|
|
+ Socket.addEventListener("message", (event) => {
|
|
|
|
|
+ const { action, args, observation, content } = JSON.parse(event.data);
|
|
|
|
|
+ if (action === ActionType.RUN) {
|
|
|
|
|
+ terminal.writeln(args.command);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (observation === ObservationType.RUN) {
|
|
|
|
|
+ content.split("\n").forEach((line: string) => {
|
|
|
|
|
+ terminal.writeln(line);
|
|
|
|
|
+ });
|
|
|
|
|
+ terminal.write("\n$ ");
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ dispose() {
|
|
|
|
|
+ this._disposables.forEach((d) => d.dispose());
|
|
|
|
|
+ Socket.removeEventListener("message", () => {});
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * The terminal's content is set by write messages. To avoid complicated state logic,
|
|
|
|
|
+ * we keep the terminal persistently open as a child of <App /> and hidden when not in use.
|
|
|
|
|
+ */
|
|
|
|
|
+
|
|
|
|
|
+function Terminal(): JSX.Element {
|
|
|
|
|
+ const terminalRef = useRef<HTMLDivElement>(null);
|
|
|
|
|
+ const { commands } = useSelector((state: RootState) => state.cmd);
|
|
|
|
|
+
|
|
|
|
|
+ useEffect(() => {
|
|
|
|
|
+ const terminal = new XtermTerminal({
|
|
|
|
|
+ // This value is set to the appropriate value by the
|
|
|
|
|
+ // `fitAddon.fit()` call below.
|
|
|
|
|
+ // If not set here, the terminal does not respect the width
|
|
|
|
|
+ // of its parent element. This causes a bug where the terminal
|
|
|
|
|
+ // is too large and switching tabs causes a layout shift.
|
|
|
|
|
+ cols: 0,
|
|
|
|
|
+ fontFamily: "Menlo, Monaco, 'Courier New', monospace",
|
|
|
|
|
+ fontSize: 14,
|
|
|
|
|
+ theme: {
|
|
|
|
|
+ background: "#262626",
|
|
|
|
|
+ },
|
|
|
|
|
+ });
|
|
|
|
|
+ terminal.write("$ ");
|
|
|
|
|
+
|
|
|
|
|
+ const fitAddon = new FitAddon();
|
|
|
|
|
+ terminal.loadAddon(fitAddon);
|
|
|
|
|
+
|
|
|
|
|
+ terminal.open(terminalRef.current as HTMLDivElement);
|
|
|
|
|
+
|
|
|
|
|
+ // Without this timeout, `fitAddon.fit()` throws the error
|
|
|
|
|
+ // "this._renderer.value is undefined"
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ fitAddon.fit();
|
|
|
|
|
+ }, 1);
|
|
|
|
|
+
|
|
|
|
|
+ const jsonWebsocketAddon = new JsonWebsocketAddon();
|
|
|
|
|
+ terminal.loadAddon(jsonWebsocketAddon);
|
|
|
|
|
+
|
|
|
|
|
+ // FIXME, temporary solution to display the terminal,
|
|
|
|
|
+ // but it will rerender the terminal every time the commands change
|
|
|
|
|
+ commands.forEach((command) => {
|
|
|
|
|
+ if (command.type === "input") {
|
|
|
|
|
+ terminal.writeln(command.content);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ command.content.split("\n").forEach((line: string) => {
|
|
|
|
|
+ terminal.writeln(line);
|
|
|
|
|
+ });
|
|
|
|
|
+ terminal.write("\n$ ");
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ return () => {
|
|
|
|
|
+ terminal.dispose();
|
|
|
|
|
+ };
|
|
|
|
|
+ }, [commands]);
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div className="flex flex-col h-full">
|
|
|
|
|
+ <div className="flex items-center gap-2 px-4 py-2 text-sm border-b border-neutral-600">
|
|
|
|
|
+ <VscTerminal />
|
|
|
|
|
+ Terminal
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div className="grow p-2 flex min-h-0">
|
|
|
|
|
+ <div ref={terminalRef} className="h-full w-full" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ );
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export default Terminal;
|