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}} -
-
- {{.CurrentUsername}}:{{.CurrentDir}}$ - -
-
-
- - - - `)) - 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}} +
+
+ {{.CurrentUsername}}:{{.CurrentDir}}$ + +
+
+
+ + +