changed interactive mode to xterm.js
This commit is contained in:
parent
761a3a122b
commit
82b6670838
3
main.go
3
main.go
|
@ -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() {
|
||||||
|
|
|
@ -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 user’s 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.
|
interactiveWS.send(data);
|
||||||
currentLineBuffer += processedPart;
|
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
updateCurrentLine();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the current line element with the current buffer and a blinking cursor.
|
interactiveMode = true;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in New Issue