separated templates and made the binary standalone, which means there are no external files needed for the template after compilation
This commit is contained in:
parent
9fe1f78c7f
commit
1144be1c1c
|
@ -0,0 +1,2 @@
|
||||||
|
build/
|
||||||
|
./gommand
|
|
@ -0,0 +1,30 @@
|
||||||
|
# Define variables
|
||||||
|
APP_NAME = gommand
|
||||||
|
GO_CMD = go
|
||||||
|
GO_BUILD = $(GO_CMD) build
|
||||||
|
GO_INSTALL = $(GO_CMD) install
|
||||||
|
GO_CLEAN = $(GO_CMD) clean
|
||||||
|
BUILD_DIR = build
|
||||||
|
BINARY = $(BUILD_DIR)/$(APP_NAME)
|
||||||
|
|
||||||
|
.PHONY: all build help clean install
|
||||||
|
|
||||||
|
all: build
|
||||||
|
|
||||||
|
build: ## Build the application
|
||||||
|
@echo "Building $(APP_NAME)..."
|
||||||
|
$(GO_BUILD) -o $(BINARY)
|
||||||
|
|
||||||
|
install: build ## Install the application
|
||||||
|
@echo "Installing $(APP_NAME)..."
|
||||||
|
$(GO_INSTALL)
|
||||||
|
|
||||||
|
clean: ## Remove build artifacts
|
||||||
|
@echo "Cleaning up build artifacts..."
|
||||||
|
$(GO_CLEAN)
|
||||||
|
rm -f $(BINARY)
|
||||||
|
|
||||||
|
help: ## Print the make targets
|
||||||
|
@echo "Makefile for $(APP_NAME)"
|
||||||
|
@echo "Targets:"
|
||||||
|
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
|
|
@ -4,6 +4,7 @@ import (
|
||||||
// "bytes"
|
// "bytes"
|
||||||
// "bufio"
|
// "bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"embed"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"io"
|
"io"
|
||||||
|
@ -36,9 +37,16 @@ type CommandNode struct {
|
||||||
Command string
|
Command string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//go:embed templates/*
|
||||||
|
var templateFiles embed.FS
|
||||||
|
//go:embed static/*
|
||||||
|
var staticFiles embed.FS
|
||||||
|
|
||||||
var commandLog []CommandOutput
|
var commandLog []CommandOutput
|
||||||
var variables = make(map[string]string)
|
var variables = make(map[string]string)
|
||||||
|
|
||||||
|
var tmpl = template.Must(template.ParseFS(templateFiles, "templates/index.html"))
|
||||||
|
|
||||||
func expandCommandSubstitution(command string, currentDir string) (string, error) {
|
func expandCommandSubstitution(command string, currentDir string) (string, error) {
|
||||||
// Do not expand single quoted strings
|
// Do not expand single quoted strings
|
||||||
reSingleQuotes := regexp.MustCompile(`'([^']*)'`)
|
reSingleQuotes := regexp.MustCompile(`'([^']*)'`)
|
||||||
|
@ -458,269 +466,6 @@ func handler(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tmpl := template.Must(template.New("index").Parse(`
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Go Web Shell</title>
|
|
||||||
<script>
|
|
||||||
document.addEventListener("DOMContentLoaded", function() {
|
|
||||||
const input = document.getElementById("command-input");
|
|
||||||
if (!input) return;
|
|
||||||
let commandHistory = JSON.parse(sessionStorage.getItem("commandHistory")) || [];
|
|
||||||
let historyIndex = commandHistory.length;
|
|
||||||
let tabIndex = -1;
|
|
||||||
let tabMatches = [];
|
|
||||||
|
|
||||||
document.querySelector("form").addEventListener("submit", function(event) {
|
|
||||||
const command = input.value.trim();
|
|
||||||
if (command) {
|
|
||||||
commandHistory.push(command);
|
|
||||||
sessionStorage.setItem("commandHistory", JSON.stringify(commandHistory));
|
|
||||||
historyIndex = commandHistory.length;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
window.addEventListener("keydown", function(event) {
|
|
||||||
if (document.activeElement !== input) return;
|
|
||||||
|
|
||||||
const cursorPos = input.selectionStart;
|
|
||||||
const textLength = input.value.length;
|
|
||||||
|
|
||||||
// Prevent default behavior for specific Ctrl+Key shortcuts
|
|
||||||
if (event.ctrlKey && ["w", "n", "p", "h", "e", "a", "k", "u", "d", "r", "t"].includes(event.key.toLowerCase())) {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ctrl+A: Move cursor to the beginning
|
|
||||||
if (event.ctrlKey && event.key === "a") {
|
|
||||||
input.setSelectionRange(0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ctrl+E: Move cursor to the end
|
|
||||||
else if (event.ctrlKey && event.key === "e") {
|
|
||||||
input.setSelectionRange(textLength, textLength);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ctrl+U: Clear the input field
|
|
||||||
else if (event.ctrlKey && event.key === "u") {
|
|
||||||
input.value = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ctrl+K: Delete everything after the cursor
|
|
||||||
else if (event.ctrlKey && event.key === "k") {
|
|
||||||
input.value = input.value.substring(0, cursorPos);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ctrl+W: Delete the previous word
|
|
||||||
else if (event.ctrlKey && event.key === "w") {
|
|
||||||
const beforeCursor = input.value.substring(0, cursorPos);
|
|
||||||
const afterCursor = input.value.substring(cursorPos);
|
|
||||||
const newBeforeCursor = beforeCursor.replace(/\S+\s*$/, ""); // Delete last word
|
|
||||||
input.value = newBeforeCursor + afterCursor;
|
|
||||||
input.setSelectionRange(newBeforeCursor.length, newBeforeCursor.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ctrl+H: Delete the previous character (Backspace)
|
|
||||||
else if (event.ctrlKey && event.key === "h") {
|
|
||||||
if (cursorPos > 0) {
|
|
||||||
input.value = input.value.substring(0, cursorPos - 1) + input.value.substring(cursorPos);
|
|
||||||
input.setSelectionRange(cursorPos - 1, cursorPos - 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ctrl+D: Delete character under cursor (or clear input if empty)
|
|
||||||
else if (event.ctrlKey && event.key === "d") {
|
|
||||||
if (textLength === 0) {
|
|
||||||
console.log("Ctrl+D: No input, simulating EOF");
|
|
||||||
} else if (cursorPos < textLength) {
|
|
||||||
input.value = input.value.substring(0, cursorPos) + input.value.substring(cursorPos + 1);
|
|
||||||
input.setSelectionRange(cursorPos, cursorPos);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ctrl+P: Previous command (up)
|
|
||||||
else if (event.ctrlKey && event.key === "p") {
|
|
||||||
if (historyIndex > 0) {
|
|
||||||
historyIndex--;
|
|
||||||
input.value = commandHistory[historyIndex];
|
|
||||||
} else if (historyIndex === 0) {
|
|
||||||
input.value = commandHistory[0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ctrl+N: Next command (down)
|
|
||||||
else if (event.ctrlKey && event.key === "n") {
|
|
||||||
if (historyIndex < commandHistory.length - 1) {
|
|
||||||
historyIndex++;
|
|
||||||
input.value = commandHistory[historyIndex];
|
|
||||||
} else {
|
|
||||||
historyIndex = commandHistory.length;
|
|
||||||
input.value = "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ctrl+R: Prevent page reload (for future reverse search)
|
|
||||||
else if (event.ctrlKey && event.key === "r") {
|
|
||||||
console.log("Reverse search triggered (not yet implemented)");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tab Completion
|
|
||||||
else if (event.key === "Tab") {
|
|
||||||
event.preventDefault();
|
|
||||||
const currentText = input.value.trim();
|
|
||||||
|
|
||||||
if (currentText === "") return;
|
|
||||||
|
|
||||||
// Find all matching commands from history
|
|
||||||
if (tabIndex === -1) {
|
|
||||||
tabMatches = commandHistory.filter(cmd => cmd.startsWith(currentText));
|
|
||||||
if (tabMatches.length === 0) return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cycle through matches
|
|
||||||
if (event.shiftKey) {
|
|
||||||
tabIndex = tabIndex > 0 ? tabIndex - 1 : tabMatches.length - 1; // Shift+Tab goes backward
|
|
||||||
} else {
|
|
||||||
tabIndex = (tabIndex + 1) % tabMatches.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
input.value = tabMatches[tabIndex];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allow Enter key to submit form
|
|
||||||
if (event.key === "Enter") {
|
|
||||||
tabIndex = -1; // Reset tab completion cycle
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}, true);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
<script>
|
|
||||||
document.addEventListener("DOMContentLoaded", function() {
|
|
||||||
const input = document.getElementById("command-input");
|
|
||||||
const fileInput = document.getElementById("fileInput");
|
|
||||||
let isUploadTriggered = false;
|
|
||||||
|
|
||||||
document.querySelector("form").addEventListener("submit", function(event) {
|
|
||||||
const command = input.value.trim();
|
|
||||||
const parts = command.split(" ");
|
|
||||||
|
|
||||||
if (parts[0] === "upload") {
|
|
||||||
event.preventDefault();
|
|
||||||
if (parts.length === 1) {
|
|
||||||
fileInput.click();
|
|
||||||
isUploadTriggered = true;
|
|
||||||
} else {
|
|
||||||
const filePath = parts[1];
|
|
||||||
const targetPath = parts.length > 2 ? parts[2] : ".";
|
|
||||||
uploadFileFromBrowser(filePath, targetPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
fileInput.addEventListener("change", function () {
|
|
||||||
if (fileInput.files.length > 0 && isUploadTriggered) {
|
|
||||||
const file = fileInput.files[0];
|
|
||||||
input.value = 'upload "' + file.name + '"';
|
|
||||||
isUploadTriggered = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function uploadFileFromBrowser(filePath, targetPath) {
|
|
||||||
const fileInput = document.getElementById("fileInput");
|
|
||||||
if (fileInput.files.length === 0) {
|
|
||||||
console.error("No file selected");
|
|
||||||
alert("No file selected");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const file = fileInput.files[0];
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append("file", file);
|
|
||||||
fetch("/upload", {
|
|
||||||
method: "POST",
|
|
||||||
body: formData,
|
|
||||||
})
|
|
||||||
.then((response) => response.text())
|
|
||||||
.then((data) => {
|
|
||||||
console.log("Upload successful:", data);
|
|
||||||
document.getElementById("command-input").value = "";
|
|
||||||
})
|
|
||||||
.catch((error) => console.error("Upload failed:", error));
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
background-color: #222;
|
|
||||||
color: #eee;
|
|
||||||
font-family: monospace;
|
|
||||||
font-size: 14pt;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
#terminal {
|
|
||||||
flex: 1;
|
|
||||||
padding: 12px;
|
|
||||||
overflow-y: auto;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
border: 1px solid white;
|
|
||||||
margin: 10px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
input {
|
|
||||||
font-size: 14pt;
|
|
||||||
background: #222;
|
|
||||||
color: #eee;
|
|
||||||
border: none;
|
|
||||||
width: 80%;
|
|
||||||
font-family: monospace;
|
|
||||||
}
|
|
||||||
input:focus {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
span.command {
|
|
||||||
color: #75df0b;
|
|
||||||
}
|
|
||||||
span.error {
|
|
||||||
color: #ff5555;
|
|
||||||
}
|
|
||||||
span.directory {
|
|
||||||
color: #1bc9e7;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="terminal">
|
|
||||||
<span>Current Directory: {{.CurrentDir}}</span>
|
|
||||||
{{range .CommandLog}}
|
|
||||||
<div><span class="command">gommand:$ {{.Command}}</span></div>
|
|
||||||
{{if .Output}}
|
|
||||||
<div>{{.Output}}</div>
|
|
||||||
{{end}}
|
|
||||||
{{if .Error}}
|
|
||||||
<div class="error">{{.Error}}</div>
|
|
||||||
{{end}}
|
|
||||||
{{end}}
|
|
||||||
<form method="POST" autocomplete="off">
|
|
||||||
<div style="display: flex; align-items: center;">
|
|
||||||
<span class="command">{{.CurrentUsername}}:<span class="directory">{{.CurrentDir}}</span>$ </span>
|
|
||||||
<input id="command-input" type="text" name="command" placeholder="Type a command here..." autofocus required>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<input type="file" id="fileInput" style="display: none;" onchange="setFilePath()">
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
`))
|
|
||||||
|
|
||||||
data := PageData{
|
data := PageData{
|
||||||
CurrentDir: currentDir,
|
CurrentDir: currentDir,
|
||||||
CurrentUsername: currentUsername,
|
CurrentUsername: currentUsername,
|
||||||
|
@ -733,6 +478,7 @@ func main() {
|
||||||
http.HandleFunc("/", handler)
|
http.HandleFunc("/", handler)
|
||||||
http.HandleFunc("/upload", fileUploadHandler)
|
http.HandleFunc("/upload", fileUploadHandler)
|
||||||
http.HandleFunc("/download", fileDownloadHandler)
|
http.HandleFunc("/download", fileDownloadHandler)
|
||||||
|
http.Handle("/static/", http.FileServer(http.FS(staticFiles)))
|
||||||
fmt.Println("Starting server on :8080")
|
fmt.Println("Starting server on :8080")
|
||||||
http.ListenAndServe(":8080", nil)
|
http.ListenAndServe(":8080", nil)
|
||||||
}
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
|
const input = document.getElementById("command-input");
|
||||||
|
const fileInput = document.getElementById("fileInput");
|
||||||
|
let isUploadTriggered = false;
|
||||||
|
|
||||||
|
document.querySelector("form").addEventListener("submit", function(event) {
|
||||||
|
const command = input.value.trim();
|
||||||
|
const parts = command.split(" ");
|
||||||
|
|
||||||
|
if (parts[0] === "upload") {
|
||||||
|
event.preventDefault();
|
||||||
|
if (parts.length === 1) {
|
||||||
|
fileInput.click();
|
||||||
|
isUploadTriggered = true;
|
||||||
|
} else {
|
||||||
|
const filePath = parts[1];
|
||||||
|
const targetPath = parts.length > 2 ? parts[2] : ".";
|
||||||
|
uploadFileFromBrowser(filePath, targetPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
fileInput.addEventListener("change", function () {
|
||||||
|
if (fileInput.files.length > 0 && isUploadTriggered) {
|
||||||
|
const file = fileInput.files[0];
|
||||||
|
input.value = 'upload "' + file.name + '"';
|
||||||
|
isUploadTriggered = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function uploadFileFromBrowser(filePath, targetPath) {
|
||||||
|
const fileInput = document.getElementById("fileInput");
|
||||||
|
if (fileInput.files.length === 0) {
|
||||||
|
console.error("No file selected");
|
||||||
|
alert("No file selected");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const file = fileInput.files[0];
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("file", file);
|
||||||
|
fetch("/upload", {
|
||||||
|
method: "POST",
|
||||||
|
body: formData,
|
||||||
|
})
|
||||||
|
.then((response) => response.text())
|
||||||
|
.then((data) => {
|
||||||
|
console.log("Upload successful:", data);
|
||||||
|
document.getElementById("command-input").value = "";
|
||||||
|
})
|
||||||
|
.catch((error) => console.error("Upload failed:", error));
|
||||||
|
}
|
|
@ -0,0 +1,132 @@
|
||||||
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
|
const input = document.getElementById("command-input");
|
||||||
|
if (!input) return;
|
||||||
|
let commandHistory = JSON.parse(sessionStorage.getItem("commandHistory")) || [];
|
||||||
|
let historyIndex = commandHistory.length;
|
||||||
|
let tabIndex = -1;
|
||||||
|
let tabMatches = [];
|
||||||
|
|
||||||
|
document.querySelector("form").addEventListener("submit", function(event) {
|
||||||
|
const command = input.value.trim();
|
||||||
|
if (command) {
|
||||||
|
commandHistory.push(command);
|
||||||
|
sessionStorage.setItem("commandHistory", JSON.stringify(commandHistory));
|
||||||
|
historyIndex = commandHistory.length;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener("keydown", function(event) {
|
||||||
|
if (document.activeElement !== input) return;
|
||||||
|
|
||||||
|
const cursorPos = input.selectionStart;
|
||||||
|
const textLength = input.value.length;
|
||||||
|
|
||||||
|
// Prevent default behavior for specific Ctrl+Key shortcuts
|
||||||
|
if (event.ctrlKey && ["w", "n", "p", "h", "e", "a", "k", "u", "d", "r", "t"].includes(event.key.toLowerCase())) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ctrl+A: Move cursor to the beginning
|
||||||
|
if (event.ctrlKey && event.key === "a") {
|
||||||
|
input.setSelectionRange(0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ctrl+E: Move cursor to the end
|
||||||
|
else if (event.ctrlKey && event.key === "e") {
|
||||||
|
input.setSelectionRange(textLength, textLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ctrl+U: Clear the input field
|
||||||
|
else if (event.ctrlKey && event.key === "u") {
|
||||||
|
input.value = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ctrl+K: Delete everything after the cursor
|
||||||
|
else if (event.ctrlKey && event.key === "k") {
|
||||||
|
input.value = input.value.substring(0, cursorPos);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ctrl+W: Delete the previous word
|
||||||
|
else if (event.ctrlKey && event.key === "w") {
|
||||||
|
const beforeCursor = input.value.substring(0, cursorPos);
|
||||||
|
const afterCursor = input.value.substring(cursorPos);
|
||||||
|
const newBeforeCursor = beforeCursor.replace(/\S+\s*$/, ""); // Delete last word
|
||||||
|
input.value = newBeforeCursor + afterCursor;
|
||||||
|
input.setSelectionRange(newBeforeCursor.length, newBeforeCursor.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ctrl+H: Delete the previous character (Backspace)
|
||||||
|
else if (event.ctrlKey && event.key === "h") {
|
||||||
|
if (cursorPos > 0) {
|
||||||
|
input.value = input.value.substring(0, cursorPos - 1) + input.value.substring(cursorPos);
|
||||||
|
input.setSelectionRange(cursorPos - 1, cursorPos - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ctrl+D: Delete character under cursor (or clear input if empty)
|
||||||
|
else if (event.ctrlKey && event.key === "d") {
|
||||||
|
if (textLength === 0) {
|
||||||
|
console.log("Ctrl+D: No input, simulating EOF");
|
||||||
|
} else if (cursorPos < textLength) {
|
||||||
|
input.value = input.value.substring(0, cursorPos) + input.value.substring(cursorPos + 1);
|
||||||
|
input.setSelectionRange(cursorPos, cursorPos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ctrl+P: Previous command (up)
|
||||||
|
else if (event.ctrlKey && event.key === "p") {
|
||||||
|
if (historyIndex > 0) {
|
||||||
|
historyIndex--;
|
||||||
|
input.value = commandHistory[historyIndex];
|
||||||
|
} else if (historyIndex === 0) {
|
||||||
|
input.value = commandHistory[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ctrl+N: Next command (down)
|
||||||
|
else if (event.ctrlKey && event.key === "n") {
|
||||||
|
if (historyIndex < commandHistory.length - 1) {
|
||||||
|
historyIndex++;
|
||||||
|
input.value = commandHistory[historyIndex];
|
||||||
|
} else {
|
||||||
|
historyIndex = commandHistory.length;
|
||||||
|
input.value = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ctrl+R: Prevent page reload (for future reverse search)
|
||||||
|
else if (event.ctrlKey && event.key === "r") {
|
||||||
|
console.log("Reverse search triggered (not yet implemented)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tab Completion
|
||||||
|
else if (event.key === "Tab") {
|
||||||
|
event.preventDefault();
|
||||||
|
const currentText = input.value.trim();
|
||||||
|
|
||||||
|
if (currentText === "") return;
|
||||||
|
|
||||||
|
// Find all matching commands from history
|
||||||
|
if (tabIndex === -1) {
|
||||||
|
tabMatches = commandHistory.filter(cmd => cmd.startsWith(currentText));
|
||||||
|
if (tabMatches.length === 0) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cycle through matches
|
||||||
|
if (event.shiftKey) {
|
||||||
|
tabIndex = tabIndex > 0 ? tabIndex - 1 : tabMatches.length - 1; // Shift+Tab goes backward
|
||||||
|
} else {
|
||||||
|
tabIndex = (tabIndex + 1) % tabMatches.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.value = tabMatches[tabIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow Enter key to submit form
|
||||||
|
if (event.key === "Enter") {
|
||||||
|
tabIndex = -1; // Reset tab completion cycle
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}, true);
|
||||||
|
});
|
|
@ -0,0 +1,41 @@
|
||||||
|
body {
|
||||||
|
background-color: #222;
|
||||||
|
color: #eee;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 14pt;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
#terminal {
|
||||||
|
flex: 1;
|
||||||
|
padding: 12px;
|
||||||
|
overflow-y: auto;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
border: none;
|
||||||
|
margin: 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
input {
|
||||||
|
font-size: 14pt;
|
||||||
|
background: #222;
|
||||||
|
color: #eee;
|
||||||
|
border: none;
|
||||||
|
width: 80%;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
input:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
span.command {
|
||||||
|
color: #75df0b;
|
||||||
|
}
|
||||||
|
span.error {
|
||||||
|
color: #ff5555;
|
||||||
|
}
|
||||||
|
span.directory {
|
||||||
|
color: #1bc9e7;
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Go Web Shell</title>
|
||||||
|
<script type="text/javascript" src="static/keyboard-shortcuts.js"></script>
|
||||||
|
<script type="text/javascript" src="static/download-command.js"></script>
|
||||||
|
<link rel="stylesheet" type="text/css" href="static/stylesheet.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="terminal">
|
||||||
|
<span>Current Directory: {{.CurrentDir}}</span>
|
||||||
|
{{range .CommandLog}}
|
||||||
|
<div><span class="command">gommand:$ {{.Command}}</span></div>
|
||||||
|
{{if .Output}}
|
||||||
|
<div>{{.Output}}</div>
|
||||||
|
{{end}}
|
||||||
|
{{if .Error}}
|
||||||
|
<div class="error">{{.Error}}</div>
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
<form method="POST" autocomplete="off">
|
||||||
|
<div style="display: flex; align-items: center;">
|
||||||
|
<span class="command">{{.CurrentUsername}}:<span class="directory">{{.CurrentDir}}</span>$ </span>
|
||||||
|
<input id="command-input" type="text" name="command" placeholder="Type a command here..." autofocus required>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<input type="file" id="fileInput" style="display: none;" onchange="setFilePath()">
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Reference in New Issue