changed interactive mode to xterm.js

This commit is contained in:
Stefan Friese 2025-02-12 16:22:59 +00:00
parent 761a3a122b
commit 82b6670838
3 changed files with 62 additions and 128 deletions

View File

@ -515,7 +515,8 @@ func terminalHandler (w http.ResponseWriter, r *http.Request) {
return return
} }
defer func() { _ = ptmx.Close() }() defer func() { _ = ptmx.Close() }()
ptmx.Write([]byte("stty raw -echo\n")) // ptmx.Write([]byte("stty raw -echo\n"))
// ptmx.Write([]byte("stty -echo\n"))
// ptmx.Write([]byte("export SHELL=bash 1>&2 2>/dev/null; export TERM=xterm-256color 1>&2 2>/dev/null")) // ptmx.Write([]byte("export SHELL=bash 1>&2 2>/dev/null; export TERM=xterm-256color 1>&2 2>/dev/null"))
go func() { go func() {

View File

@ -1,16 +1,12 @@
document.addEventListener("DOMContentLoaded", function () { document.addEventListener("DOMContentLoaded", function () {
const input = document.getElementById("command-input"); const input = document.getElementById("command-input");
const terminal = document.getElementById("terminal"); // We assume your normal terminal UI is in the element with id "terminal".
const normalTerminal = document.getElementById("terminal");
let interactiveWS = null; let interactiveWS = null;
let interactiveMode = false; let interactiveMode = false;
let lastKeySentTime = 0;
const throttleInterval = 50; // milliseconds
// Holds the users input on the current line.
let currentLineBuffer = "";
let currentLineElem = null;
const ansi_up = new AnsiUp; const ansi_up = new AnsiUp;
// Listen for the "start-interactive" command in normal mode.
input.addEventListener("keydown", function (event) { input.addEventListener("keydown", function (event) {
if (!interactiveMode && event.key === "Enter") { if (!interactiveMode && event.key === "Enter") {
const command = input.value.trim(); const command = input.value.trim();
@ -20,150 +16,82 @@ document.addEventListener("DOMContentLoaded", function () {
event.preventDefault(); event.preventDefault();
return; return;
} }
// Otherwise, normal HTTP submission… // Otherwise, your normal HTTP submission…
} }
}); });
function startInteractiveSession() { function startInteractiveSession() {
interactiveWS = new WebSocket("ws://" + location.host + "/terminal"); // Hide the normal terminal and input.
interactiveMode = true; normalTerminal.style.display = "none";
terminal.insertAdjacentHTML('beforeend', "\n--- Interactive session started ---\n"); input.style.display = "none";
input.style.display = "none"; // Hide normal input.
currentLineBuffer = "";
createCurrentLineElem();
// Create a new container for xterm.js.
const xtermContainer = document.createElement("div");
xtermContainer.id = "xterm-container";
xtermContainer.style.width = "100%";
xtermContainer.style.height = "100vh";
// Optionally set additional styling (e.g. background color).
document.body.appendChild(xtermContainer);
// Initialize xterm.js Terminal.
const term = new Terminal({
cursorBlink: true,
scrollback: 1000,
theme: {
background: "#222",
foreground: "#eee"
}
});
const fitAddon = new FitAddon.FitAddon();
term.loadAddon(fitAddon);
term.open(xtermContainer);
fitAddon.fit();
term.focus();
// Establish the WebSocket connection.
interactiveWS = new WebSocket("ws://" + location.host + "/terminal");
interactiveWS.binaryType = "arraybuffer"; interactiveWS.binaryType = "arraybuffer";
interactiveWS.onmessage = function (event) { interactiveWS.onmessage = function (event) {
// When the shell returns output (for example, after Enter), process it. const text = new TextDecoder("utf-8").decode(event.data);
let text = new TextDecoder("utf-8").decode(event.data); term.write(text);
text = removeOSCTitle(text);
processPTYOutput(text);
}; };
interactiveWS.onclose = function () { interactiveWS.onclose = function () {
interactiveMode = false; interactiveMode = false;
terminal.insertAdjacentHTML('beforeend', "\n--- Interactive session ended ---\n"); term.write("\r\n--- Interactive session ended ---\r\n");
// Remove the xterm container.
if (xtermContainer.parentNode) {
xtermContainer.parentNode.removeChild(xtermContainer);
}
// Restore the normal terminal UI.
normalTerminal.style.display = "block";
input.style.display = "block"; input.style.display = "block";
input.focus(); input.focus();
document.removeEventListener("keydown", handleInteractiveKey, true);
}; };
interactiveWS.onerror = function (err) { interactiveWS.onerror = function (err) {
terminal.insertAdjacentHTML('beforeend', "\n--- Error in interactive session ---\n"); term.write("\r\n--- Error in interactive session ---\r\n");
console.error("Interactive WS error:", err); console.error("Interactive WS error:", err);
interactiveMode = false; interactiveMode = false;
if (xtermContainer.parentNode) {
xtermContainer.parentNode.removeChild(xtermContainer);
}
normalTerminal.style.display = "block";
input.style.display = "block"; input.style.display = "block";
document.removeEventListener("keydown", handleInteractiveKey, true);
}; };
// Listen for all key events while in interactive mode. // When the user types in xterm, send the data to the server.
document.addEventListener("keydown", handleInteractiveKey, true); term.onData(function (data) {
} // If the user presses Ctrl+D (ASCII 4), exit the session.
if (data === "\x04") {
// Process output received from the PTY. term.write("\r\n--- Exiting interactive session ---\r\n");
// We expect output to include completed lines (with newline) as command results. interactiveWS.close();
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 { } 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); interactiveWS.send(data);
event.preventDefault(); }
} });
// Remove OSC sequences (for terminal title updates). interactiveMode = true;
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;
} }
}); });

View File

@ -9,6 +9,11 @@
<script type="text/javascript" src="static/switch-themes.js"></script> <script type="text/javascript" src="static/switch-themes.js"></script>
<script type="text/javascript" src="static/start-interactive.js"></script> <script type="text/javascript" src="static/start-interactive.js"></script>
<script src="https://cdn.jsdelivr.net/npm/ansi_up@5.0.0/ansi_up.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/ansi_up@5.0.0/ansi_up.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm/css/xterm.css" />
<script src="https://cdn.jsdelivr.net/npm/xterm/lib/xterm.js"></script>
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit/lib/xterm-addon-fit.js"></script>
<link rel="stylesheet" type="text/css" href="static/stylesheet.css"> <link rel="stylesheet" type="text/css" href="static/stylesheet.css">
</head> </head>
<body> <body>