diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..5689a71
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+build/
+./gommand
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..b6a1859
--- /dev/null
+++ b/Makefile
@@ -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}'
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..dbdbdbc
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,3 @@
+module gommand
+
+go 1.23.5
diff --git a/gommand.go b/main.go
similarity index 59%
rename from gommand.go
rename to main.go
index 906f2e8..e122ad0 100644
--- a/gommand.go
+++ b/main.go
@@ -4,6 +4,7 @@ import (
// "bytes"
// "bufio"
"bytes"
+ "embed"
"fmt"
"html/template"
"io"
@@ -36,9 +37,16 @@ type CommandNode struct {
Command string
}
+//go:embed templates/*
+var templateFiles embed.FS
+//go:embed static/*
+var staticFiles embed.FS
+
var commandLog []CommandOutput
var variables = make(map[string]string)
+var tmpl = template.Must(template.ParseFS(templateFiles, "templates/index.html"))
+
func expandCommandSubstitution(command string, currentDir string) (string, error) {
// Do not expand single quoted strings
reSingleQuotes := regexp.MustCompile(`'([^']*)'`)
@@ -458,269 +466,6 @@ func handler(w http.ResponseWriter, r *http.Request) {
}
}
- tmpl := template.Must(template.New("index").Parse(`
-
-
-
-
-
- Go Web Shell
-
-
-
-
-
-
-
Current Directory: {{.CurrentDir}}
- {{range .CommandLog}}
-
gommand:$ {{.Command}}
- {{if .Output}}
-
{{.Output}}
- {{end}}
- {{if .Error}}
-
{{.Error}}
- {{end}}
- {{end}}
-
-
-
-
-
- `))
-
data := PageData{
CurrentDir: currentDir,
CurrentUsername: currentUsername,
@@ -733,6 +478,7 @@ func main() {
http.HandleFunc("/", handler)
http.HandleFunc("/upload", fileUploadHandler)
http.HandleFunc("/download", fileDownloadHandler)
+ http.Handle("/static/", http.FileServer(http.FS(staticFiles)))
fmt.Println("Starting server on :8080")
http.ListenAndServe(":8080", nil)
}
diff --git a/static/download-command.js b/static/download-command.js
new file mode 100644
index 0000000..25ad655
--- /dev/null
+++ b/static/download-command.js
@@ -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));
+}
diff --git a/static/keyboard-shortcuts.js b/static/keyboard-shortcuts.js
new file mode 100644
index 0000000..d6e126c
--- /dev/null
+++ b/static/keyboard-shortcuts.js
@@ -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);
+});
diff --git a/static/stylesheet.css b/static/stylesheet.css
new file mode 100644
index 0000000..2d70cfc
--- /dev/null
+++ b/static/stylesheet.css
@@ -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;
+}
diff --git a/templates/index.html b/templates/index.html
new file mode 100644
index 0000000..9707fba
--- /dev/null
+++ b/templates/index.html
@@ -0,0 +1,32 @@
+
+
+
+
+
+ Go Web Shell
+
+
+
+
+
+
+
Current Directory: {{.CurrentDir}}
+ {{range .CommandLog}}
+
gommand:$ {{.Command}}
+ {{if .Output}}
+
{{.Output}}
+ {{end}}
+ {{if .Error}}
+
{{.Error}}
+ {{end}}
+ {{end}}
+
+
+
+
+