added interactive-mode
This commit is contained in:
parent
ac1179a9b5
commit
761a3a122b
5
go.mod
5
go.mod
|
@ -1,3 +1,8 @@
|
|||
module gommand
|
||||
|
||||
go 1.23.5
|
||||
|
||||
require (
|
||||
github.com/creack/pty v1.1.24
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
)
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
||||
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
56
main.go
56
main.go
|
@ -8,6 +8,7 @@ import (
|
|||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
|
@ -16,6 +17,9 @@ import (
|
|||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/creack/pty"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
type PageData struct {
|
||||
|
@ -176,6 +180,7 @@ func executeCommandTree(node *CommandNode, currentDir string) (string, error) {
|
|||
}
|
||||
return executeCommandTree(node.Right, currentDir)
|
||||
|
||||
// TODO: or operator does not work correctly, fix it
|
||||
case "||":
|
||||
leftOutput, err := executeCommandTree(node.Left, currentDir)
|
||||
if err == nil && leftOutput != "" {
|
||||
|
@ -442,8 +447,6 @@ func parseCommandWithQuotes(command string) []string {
|
|||
func handler(w http.ResponseWriter, r *http.Request) {
|
||||
currentDir, _ := os.Getwd()
|
||||
displayDir := filepath.Base(currentDir)
|
||||
// displayDirList := strings.Split(currentDir, "/")
|
||||
// displayDir := displayDirList[len(displayDirList)-1] // get the last element of the path
|
||||
currentUser, _ := user.Current()
|
||||
currentUsername := currentUser.Username
|
||||
hostname, _ := os.Hostname()
|
||||
|
@ -485,7 +488,56 @@ func main() {
|
|||
http.HandleFunc("/", handler)
|
||||
http.HandleFunc("/upload", fileUploadHandler)
|
||||
http.HandleFunc("/download", fileDownloadHandler)
|
||||
http.HandleFunc("/terminal", terminalHandler)
|
||||
http.Handle("/static/", http.FileServer(http.FS(staticFiles)))
|
||||
fmt.Println("Starting server on :8080")
|
||||
http.ListenAndServe(":8080", nil)
|
||||
}
|
||||
|
||||
var upgrader = websocket.Upgrader {
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
CheckOrigin: func(r *http.Request) bool { return true },
|
||||
}
|
||||
|
||||
func terminalHandler (w http.ResponseWriter, r *http.Request) {
|
||||
ws, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
log.Printf("Websocket upgrade error: %v", err)
|
||||
return
|
||||
}
|
||||
defer ws.Close()
|
||||
|
||||
cmd := exec.Command("bash")
|
||||
ptmx, err := pty.Start(cmd)
|
||||
if err != nil {
|
||||
log.Printf("Error starting PTY: %v", err)
|
||||
return
|
||||
}
|
||||
defer func() { _ = ptmx.Close() }()
|
||||
ptmx.Write([]byte("stty raw -echo\n"))
|
||||
// ptmx.Write([]byte("export SHELL=bash 1>&2 2>/dev/null; export TERM=xterm-256color 1>&2 2>/dev/null"))
|
||||
|
||||
go func() {
|
||||
buf := make([]byte, 1024)
|
||||
for {
|
||||
n, err := ptmx.Read(buf)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if err := ws.WriteMessage(websocket.BinaryMessage, buf[:n]); err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
_, message, err := ws.ReadMessage()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if _, err := ptmx.Write(message); err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,169 @@
|
|||
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;
|
||||
}
|
||||
});
|
|
@ -41,6 +41,10 @@ body {
|
|||
/* overflow: hidden; */
|
||||
/* text-overflow: ellipsis; */
|
||||
}
|
||||
/* current-line is for interactive mode */
|
||||
#current-line {
|
||||
display: inline;
|
||||
}
|
||||
input {
|
||||
flex-grow: 1;
|
||||
font-size: 14pt;
|
||||
|
@ -54,6 +58,15 @@ input {
|
|||
input:focus {
|
||||
outline: none;
|
||||
}
|
||||
span.cursor {
|
||||
display: inline-block;
|
||||
width: 10px;
|
||||
animation: blink 1s step-end infinite;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
@keyframes blink {
|
||||
50% { opacity: 0; }
|
||||
}
|
||||
span.command {
|
||||
color: var(--command-color);
|
||||
}
|
||||
|
|
|
@ -7,6 +7,8 @@
|
|||
<script type="text/javascript" src="static/keyboard-shortcuts.js"></script>
|
||||
<script type="text/javascript" src="static/download-command.js"></script>
|
||||
<script type="text/javascript" src="static/switch-themes.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>
|
||||
<link rel="stylesheet" type="text/css" href="static/stylesheet.css">
|
||||
</head>
|
||||
<body>
|
||||
|
|
Loading…
Reference in New Issue