You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

209 lines
5.2 KiB

4 weeks ago
export class TerminalConsole {
constructor(t) {
this.t = t;
this.term = null;
this.fitAddon = null;
this.socket = null;
this.currentPrompt = "PS>";
this.commandBuffer = "";
this.footerStateEl = null;
this.connectionStateEl = null;
this.resizeHandler = this.handleResize.bind(this);
}
mount() {
const terminalRoot = document.getElementById("terminal");
this.footerStateEl = document.getElementById("footerState");
this.connectionStateEl = document.getElementById("connectionState");
if (!terminalRoot) {
return;
}
this.term = new Terminal({
cursorBlink: true,
fontFamily: 'Consolas, "IBM Plex Mono", monospace',
fontSize: 15,
lineHeight: 1.25,
letterSpacing: 0.2,
theme: {
background: "#081426",
foreground: "#eef5ff",
cursor: "#7af1d0",
cursorAccent: "#081426",
selectionBackground: "rgba(122, 241, 208, 0.18)",
black: "#081426",
red: "#ff8d8d",
green: "#7cf6d1",
yellow: "#ffd479",
blue: "#8dbaff",
magenta: "#d6a4ff",
cyan: "#73d5ff",
white: "#eef5ff",
brightBlack: "#5d718f",
brightRed: "#ffb0b0",
brightGreen: "#a2ffe3",
brightYellow: "#ffe4a6",
brightBlue: "#b3d1ff",
brightMagenta: "#e6c1ff",
brightCyan: "#9be6ff",
brightWhite: "#ffffff",
},
});
this.fitAddon = new FitAddon.FitAddon();
this.term.loadAddon(this.fitAddon);
this.term.open(terminalRoot);
this.fitAddon.fit();
this.bindTerminalInput();
this.setConnectionState("socket.connecting", "status-live");
this.connect();
window.addEventListener("resize", this.resizeHandler);
}
unmount() {
window.removeEventListener("resize", this.resizeHandler);
if (this.socket) {
this.socket.close();
this.socket = null;
}
if (this.term) {
this.term.dispose();
this.term = null;
}
this.fitAddon = null;
this.commandBuffer = "";
this.currentPrompt = "PS>";
this.footerStateEl = null;
this.connectionStateEl = null;
}
updateTranslations(t) {
this.t = t;
if (this.socket?.readyState === WebSocket.OPEN) {
this.setConnectionState("socket.connected", "status-ok");
return;
}
this.setConnectionState("socket.connecting", "status-live");
}
handleResize() {
this.fitAddon?.fit();
}
bindTerminalInput() {
this.term.onData((data) => {
if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
return;
}
if (data === "\r") {
const command = this.commandBuffer.trim();
this.term.write("\r\n");
if (command === "clear" || command === "cls") {
this.commandBuffer = "";
this.term.clear();
this.writePrompt();
return;
}
this.socket.send(JSON.stringify({
type: "input",
data: this.commandBuffer,
}));
this.commandBuffer = "";
return;
}
if (data === "\u007f") {
if (this.commandBuffer.length > 0) {
this.commandBuffer = this.commandBuffer.slice(0, -1);
this.term.write("\b \b");
}
return;
}
if (data === "\u0003") {
this.commandBuffer = "";
this.term.write("^C\r\n");
this.writePrompt();
return;
}
if (data < " ") {
return;
}
this.commandBuffer += data;
this.term.write(data);
});
}
writePrompt() {
this.term.write(this.currentPrompt + " ");
}
setConnectionState(labelKey, className) {
const label = this.t(labelKey);
if (this.connectionStateEl) {
this.connectionStateEl.textContent = label;
}
if (this.footerStateEl) {
this.footerStateEl.textContent = label;
this.footerStateEl.className = className;
}
}
connect() {
const protocol = window.location.protocol === "https:" ? "wss" : "ws";
this.socket = new WebSocket(protocol + "://" + window.location.host + "/legacy/ws");
this.socket.addEventListener("open", () => {
this.setConnectionState("socket.connected", "status-ok");
});
this.socket.addEventListener("message", (event) => {
const message = JSON.parse(event.data);
if (message.prompt) {
this.currentPrompt = message.prompt;
}
if (message.type === "welcome") {
this.term.write(message.data || "");
this.writePrompt();
return;
}
if (message.type === "output") {
if (message.data) {
this.term.write(message.data);
}
this.writePrompt();
return;
}
if (message.type === "error") {
this.term.writeln("");
this.term.writeln(`${this.t("socket.errorPrefix")}: ${message.data || "unknown websocket error"}`);
this.writePrompt();
}
});
this.socket.addEventListener("close", () => {
this.setConnectionState("socket.disconnected", "status-bad");
this.term?.writeln("");
this.term?.writeln(this.t("socket.closed"));
});
this.socket.addEventListener("error", () => {
this.setConnectionState("socket.error", "status-bad");
});
}
}