document.addEventListener("DOMContentLoaded", function () { const input = document.getElementById("command-input"); const terminal = document.getElementById("terminal"); let interactiveWS = null; let interactiveMode = false; let lastKeySentTime = 0; const throttleInterval = 50; // milliseconds // Holds the user’s input on the current line. let currentLineBuffer = ""; let currentLineElem = null; const ansi_up = new AnsiUp; input.addEventListener("keydown", function (event) { if (!interactiveMode && event.key === "Enter") { const command = input.value.trim(); if (command === "start-interactive") { startInteractiveSession(); input.value = ""; event.preventDefault(); return; } // Otherwise, normal HTTP submission… } }); function startInteractiveSession() { interactiveWS = new WebSocket("ws://" + location.host + "/terminal"); interactiveMode = true; terminal.insertAdjacentHTML('beforeend', "\n--- Interactive session started ---\n"); input.style.display = "none"; // Hide normal input. currentLineBuffer = ""; createCurrentLineElem(); interactiveWS.binaryType = "arraybuffer"; interactiveWS.onmessage = function (event) { // When the shell returns output (for example, after Enter), process it. let text = new TextDecoder("utf-8").decode(event.data); text = removeOSCTitle(text); processPTYOutput(text); }; interactiveWS.onclose = function () { interactiveMode = false; terminal.insertAdjacentHTML('beforeend', "\n--- Interactive session ended ---\n"); input.style.display = "block"; input.focus(); document.removeEventListener("keydown", handleInteractiveKey, true); }; interactiveWS.onerror = function (err) { terminal.insertAdjacentHTML('beforeend', "\n--- Error in interactive session ---\n"); console.error("Interactive WS error:", err); interactiveMode = false; input.style.display = "block"; document.removeEventListener("keydown", handleInteractiveKey, true); }; // Listen for all key events while in interactive mode. document.addEventListener("keydown", handleInteractiveKey, true); } // Process output received from the PTY. // We expect output to include completed lines (with newline) as command results. function processPTYOutput(text) { // Split output into lines. const parts = text.split("\n"); for (let i = 0; i < parts.length; i++) { const processedPart = processBackspaces(parts[i]); if (i < parts.length - 1) { // Completed line. currentLineBuffer += processedPart; const finishedLine = document.createElement("div"); finishedLine.innerHTML = ansi_up.ansi_to_html(currentLineBuffer); terminal.appendChild(finishedLine); currentLineBuffer = ""; createCurrentLineElem(); } else { // Incomplete line. currentLineBuffer += processedPart; } } updateCurrentLine(); } // Update the current line element with the current buffer and a blinking cursor. function updateCurrentLine() { if (currentLineElem) { currentLineElem.innerHTML = ansi_up.ansi_to_html(currentLineBuffer); updateCursor(); } terminal.scrollTop = terminal.scrollHeight; } // Create (or recreate) the current line element. function createCurrentLineElem() { if (currentLineElem && currentLineElem.parentNode) { currentLineElem.parentNode.removeChild(currentLineElem); } currentLineElem = document.createElement("span"); currentLineElem.id = "current-line"; // Ensure the current line element is inline so the cursor stays on the same line. currentLineElem.style.display = "inline"; terminal.appendChild(currentLineElem); } // Append a blinking cursor to the current line. function updateCursor() { const existingCursor = currentLineElem.querySelector(".cursor"); if (existingCursor) { existingCursor.parentNode.removeChild(existingCursor); } currentLineElem.insertAdjacentHTML('beforeend', '█'); } // Throttled key handler that updates the local current line buffer and sends keys to the PTY. function handleInteractiveKey(event) { if (!interactiveMode || !interactiveWS || interactiveWS.readyState !== WebSocket.OPEN) return; const now = Date.now(); if (now - lastKeySentTime < throttleInterval) { event.preventDefault(); return; } lastKeySentTime = now; let data = ""; if (event.key === "Enter") { data = "\n"; // Append newline to the local buffer and clear it currentLineBuffer += "\n"; updateCurrentLine(); currentLineBuffer = ""; } else if (event.key === "Backspace") { data = "\x7f"; // DEL // Update the local buffer: remove the last character. currentLineBuffer = currentLineBuffer.slice(0, -1); updateCurrentLine(); } else if (event.key.length === 1) { data = event.key; currentLineBuffer += event.key; updateCurrentLine(); } else { return; } interactiveWS.send(data); event.preventDefault(); } // Remove OSC sequences (for terminal title updates). function removeOSCTitle(text) { const oscRegex = /\x1b\]0;.*?(?:\x07|\x1b\\)/g; return text.replace(oscRegex, ""); } // Process backspace characters in a string (for PTY output). function processBackspaces(text) { let result = ""; for (let i = 0; i < text.length; i++) { const ch = text[i]; if (ch === "\x7f" || ch === "\b") { result = result.slice(0, -1); } else { result += ch; } } return result; } });