170 lines
5.7 KiB
JavaScript
170 lines
5.7 KiB
JavaScript
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', '<span class="cursor">█</span>');
|
||
}
|
||
|
||
// 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;
|
||
}
|
||
});
|