From f7e15941ee7d06f93f271def234bc96d0e6cdcfa Mon Sep 17 00:00:00 2001 From: Ishaan Dey Date: Mon, 29 Apr 2024 21:36:33 -0400 Subject: [PATCH] terminal improvements + styling --- backend/server/dist/index.js | 26 ++- backend/server/src/index.ts | 31 ++- backend/server/src/terminal.ts | 8 +- frontend/components/editor/index.tsx | 6 +- .../{terminal.tsx => terminal/index.tsx} | 12 +- frontend/components/editor/terminal/xterm.css | 219 ++++++++++++++++++ 6 files changed, 270 insertions(+), 32 deletions(-) rename frontend/components/editor/{terminal.tsx => terminal/index.tsx} (90%) create mode 100644 frontend/components/editor/terminal/xterm.css diff --git a/backend/server/dist/index.js b/backend/server/dist/index.js index e13479f..43ebdcc 100644 --- a/backend/server/dist/index.js +++ b/backend/server/dist/index.js @@ -77,7 +77,6 @@ io.on("connection", (socket) => __awaiter(void 0, void 0, void 0, function* () { fs_1.default.writeFile(filePath, file.data, function (err) { if (err) throw err; - // console.log("Saved File:", file.id) }); }); socket.emit("loaded", sandboxFiles.files); @@ -85,7 +84,6 @@ io.on("connection", (socket) => __awaiter(void 0, void 0, void 0, function* () { const file = sandboxFiles.fileData.find((f) => f.id === fileId); if (!file) return; - // console.log("get file " + file.id + ": ", file.data.slice(0, 10) + "...") callback(file.data); }); // todo: send diffs + debounce for efficiency @@ -94,7 +92,6 @@ io.on("connection", (socket) => __awaiter(void 0, void 0, void 0, function* () { if (!file) return; file.data = body; - // console.log("save file " + file.id + ": ", file.data) fs_1.default.writeFile(path_1.default.join(dirName, file.id), body, function (err) { if (err) throw err; @@ -139,15 +136,19 @@ io.on("connection", (socket) => __awaiter(void 0, void 0, void 0, function* () { cols: 100, cwd: path_1.default.join(dirName, "projects", data.id), }); - pty.onData((data) => { + const onData = pty.onData((data) => { console.log(data); socket.emit("terminalResponse", { // data: Buffer.from(data, "utf-8").toString("base64"), data, }); }); - pty.onExit((code) => console.log("exit :(", code)); - terminals[id] = pty; + const onExit = pty.onExit((code) => console.log("exit :(", code)); + terminals[id] = { + terminal: pty, + onData, + onExit, + }; }); socket.on("terminalData", (id, data) => { // socket.on("terminalData", (data: string) => { @@ -159,13 +160,22 @@ io.on("connection", (socket) => __awaiter(void 0, void 0, void 0, function* () { return; } try { - terminals[id].write(data); + terminals[id].terminal.write(data); } catch (e) { console.log("Error writing to terminal", e); } }); - socket.on("disconnect", () => { }); + socket.on("disconnect", () => { + Object.entries(terminals).forEach((t) => { + const { terminal, onData, onExit } = t[1]; + if (os_1.default.platform() !== "win32") + terminal.kill(); + onData.dispose(); + onExit.dispose(); + delete terminals[t[0]]; + }); + }); })); httpServer.listen(port, () => { console.log(`Server running on port ${port}`); diff --git a/backend/server/src/index.ts b/backend/server/src/index.ts index 37f4b09..2f99f8b 100644 --- a/backend/server/src/index.ts +++ b/backend/server/src/index.ts @@ -10,7 +10,7 @@ import { z } from "zod" import { User } from "./types" import { createFile, getSandboxFiles, renameFile, saveFile } from "./utils" import { Pty } from "./terminal" -import { IPty, spawn } from "node-pty" +import { IDisposable, IPty, spawn } from "node-pty" dotenv.config() @@ -24,7 +24,9 @@ const io = new Server(httpServer, { }, }) -const terminals: { [id: string]: IPty } = {} +const terminals: { + [id: string]: { terminal: IPty; onData: IDisposable; onExit: IDisposable } +} = {} const dirName = path.join(__dirname, "..") @@ -83,7 +85,6 @@ io.on("connection", async (socket) => { fs.mkdirSync(path.dirname(filePath), { recursive: true }) fs.writeFile(filePath, file.data, function (err) { if (err) throw err - // console.log("Saved File:", file.id) }) }) @@ -93,7 +94,6 @@ io.on("connection", async (socket) => { const file = sandboxFiles.fileData.find((f) => f.id === fileId) if (!file) return - // console.log("get file " + file.id + ": ", file.data.slice(0, 10) + "...") callback(file.data) }) @@ -102,7 +102,6 @@ io.on("connection", async (socket) => { const file = sandboxFiles.fileData.find((f) => f.id === fileId) if (!file) return file.data = body - // console.log("save file " + file.id + ": ", file.data) fs.writeFile(path.join(dirName, file.id), body, function (err) { if (err) throw err @@ -158,7 +157,7 @@ io.on("connection", async (socket) => { cwd: path.join(dirName, "projects", data.id), }) - pty.onData((data) => { + const onData = pty.onData((data) => { console.log(data) socket.emit("terminalResponse", { // data: Buffer.from(data, "utf-8").toString("base64"), @@ -166,9 +165,13 @@ io.on("connection", async (socket) => { }) }) - pty.onExit((code) => console.log("exit :(", code)) + const onExit = pty.onExit((code) => console.log("exit :(", code)) - terminals[id] = pty + terminals[id] = { + terminal: pty, + onData, + onExit, + } }) socket.on("terminalData", (id: string, data: string) => { @@ -183,13 +186,21 @@ io.on("connection", async (socket) => { } try { - terminals[id].write(data) + terminals[id].terminal.write(data) } catch (e) { console.log("Error writing to terminal", e) } }) - socket.on("disconnect", () => {}) + socket.on("disconnect", () => { + Object.entries(terminals).forEach((t) => { + const { terminal, onData, onExit } = t[1] + if (os.platform() !== "win32") terminal.kill() + onData.dispose() + onExit.dispose() + delete terminals[t[0]] + }) + }) }) httpServer.listen(port, () => { diff --git a/backend/server/src/terminal.ts b/backend/server/src/terminal.ts index 8717e75..d1bc371 100644 --- a/backend/server/src/terminal.ts +++ b/backend/server/src/terminal.ts @@ -44,9 +44,9 @@ export class Pty { // kill() { // console.log("killing terminal") - // if (os.platform() !== "win32") { - // this.ptyProcess.kill() - // return - // } + // if (os.platform() !== "win32") { + // this.ptyProcess.kill() + // return + // } // } } diff --git a/frontend/components/editor/index.tsx b/frontend/components/editor/index.tsx index a272b29..d3a8cb2 100644 --- a/frontend/components/editor/index.tsx +++ b/frontend/components/editor/index.tsx @@ -30,8 +30,6 @@ import EditorTerminal from "./terminal" import { Terminal } from "@xterm/xterm" import { FitAddon } from "@xterm/addon-fit" -import { decodeTerminalResponse } from "@/lib/utils" - export default function CodeEditor({ userId, sandboxId, @@ -284,11 +282,11 @@ export default function CodeEditor({ minSize={20} className="p-2 flex flex-col" > -
+
Node Console
-
+
{socket ? : null}
diff --git a/frontend/components/editor/terminal.tsx b/frontend/components/editor/terminal/index.tsx similarity index 90% rename from frontend/components/editor/terminal.tsx rename to frontend/components/editor/terminal/index.tsx index 257a9a1..fffc95b 100644 --- a/frontend/components/editor/terminal.tsx +++ b/frontend/components/editor/terminal/index.tsx @@ -2,6 +2,7 @@ import { Terminal } from "@xterm/xterm" import { FitAddon } from "@xterm/addon-fit" +import "./xterm.css" import { useEffect, useRef, useState } from "react" import { Socket } from "socket.io-client" @@ -14,7 +15,10 @@ export default function EditorTerminal({ socket }: { socket: Socket }) { if (!terminalRef.current) return const terminal = new Terminal({ - cursorBlink: false, + cursorBlink: true, + theme: { + background: "#262626", + }, }) setTerm(terminal) @@ -67,9 +71,5 @@ export default function EditorTerminal({ socket }: { socket: Socket }) { } }, [term, terminalRef.current]) - return ( -
-
-
- ) + return
} diff --git a/frontend/components/editor/terminal/xterm.css b/frontend/components/editor/terminal/xterm.css new file mode 100644 index 0000000..386a214 --- /dev/null +++ b/frontend/components/editor/terminal/xterm.css @@ -0,0 +1,219 @@ +/** + * Copyright (c) 2014 The xterm.js authors. All rights reserved. + * Copyright (c) 2012-2013, Christopher Jeffrey (MIT License) + * https://github.com/chjj/term.js + * @license MIT + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * Originally forked from (with the author's permission): + * Fabrice Bellard's javascript vt100 for jslinux: + * http://bellard.org/jslinux/ + * Copyright (c) 2011 Fabrice Bellard + * The original design remains. The terminal itself + * has been extended to include xterm CSI codes, among + * other features. + */ + +/** + * Default styles for xterm.js + */ + + .xterm { + cursor: text; + position: relative; + user-select: none; + -ms-user-select: none; + -webkit-user-select: none; + padding: 8px; +} + +.xterm.focus, +.xterm:focus { + outline: none; +} + +.xterm .xterm-helpers { + position: absolute; + top: 0; + /** + * The z-index of the helpers must be higher than the canvases in order for + * IMEs to appear on top. + */ + z-index: 5; +} + +.xterm .xterm-helper-textarea { + padding: 0; + border: 0; + margin: 0; + /* Move textarea out of the screen to the far left, so that the cursor is not visible */ + position: absolute; + opacity: 0; + left: -9999em; + top: 0; + width: 0; + height: 0; + z-index: -5; + /** Prevent wrapping so the IME appears against the textarea at the correct position */ + white-space: nowrap; + overflow: hidden; + resize: none; +} + +.xterm .composition-view { + /* TODO: Composition position got messed up somewhere */ + background: transparent; + color: #FFF; + display: none; + position: absolute; + white-space: nowrap; + z-index: 1; +} + +.xterm .composition-view.active { + display: block; +} + +.xterm .xterm-viewport { + /* On OS X this is required in order for the scroll bar to appear fully opaque */ + background-color: transparent; + overflow-y: scroll; + cursor: default; + position: absolute; + right: 0; + left: 0; + top: 0; + bottom: 0; +} + +.xterm .xterm-screen { + position: relative; +} + +.xterm .xterm-screen canvas { + position: absolute; + left: 0; + top: 0; +} + +.xterm .xterm-scroll-area { + visibility: hidden; +} + +.xterm-char-measure-element { + display: inline-block; + visibility: hidden; + position: absolute; + top: 0; + left: -9999em; + line-height: normal; +} + +.xterm.enable-mouse-events { + /* When mouse events are enabled (eg. tmux), revert to the standard pointer cursor */ + cursor: default; +} + +.xterm.xterm-cursor-pointer, +.xterm .xterm-cursor-pointer { + cursor: pointer; +} + +.xterm.column-select.focus { + /* Column selection mode */ + cursor: crosshair; +} + +.xterm .xterm-accessibility:not(.debug), +.xterm .xterm-message { + position: absolute; + left: 0; + top: 0; + bottom: 0; + right: 0; + z-index: 10; + color: transparent; + pointer-events: none; +} + +.xterm .xterm-accessibility-tree:not(.debug) *::selection { +color: transparent; +} + +.xterm .xterm-accessibility-tree { +user-select: text; +white-space: pre; +} + +.xterm .live-region { + position: absolute; + left: -9999px; + width: 1px; + height: 1px; + overflow: hidden; +} + +.xterm-dim { + /* Dim should not apply to background, so the opacity of the foreground color is applied + * explicitly in the generated class and reset to 1 here */ + opacity: 1 !important; +} + +.xterm-underline-1 { text-decoration: underline; } +.xterm-underline-2 { text-decoration: double underline; } +.xterm-underline-3 { text-decoration: wavy underline; } +.xterm-underline-4 { text-decoration: dotted underline; } +.xterm-underline-5 { text-decoration: dashed underline; } + +.xterm-overline { + text-decoration: overline; +} + +.xterm-overline.xterm-underline-1 { text-decoration: overline underline; } +.xterm-overline.xterm-underline-2 { text-decoration: overline double underline; } +.xterm-overline.xterm-underline-3 { text-decoration: overline wavy underline; } +.xterm-overline.xterm-underline-4 { text-decoration: overline dotted underline; } +.xterm-overline.xterm-underline-5 { text-decoration: overline dashed underline; } + +.xterm-strikethrough { + text-decoration: line-through; +} + +.xterm-screen .xterm-decoration-container .xterm-decoration { +z-index: 6; +position: absolute; +} + +.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer { +z-index: 7; +} + +.xterm-decoration-overview-ruler { + z-index: 8; + position: absolute; + top: 0; + right: 0; + pointer-events: none; +} + +.xterm-decoration-top { + z-index: 2; + position: relative; +} \ No newline at end of file