From d46f2ed40dc53fb7c5479611fd1af43ac3ff096a Mon Sep 17 00:00:00 2001 From: Nathan Woodburn Date: Mon, 27 Oct 2025 17:07:24 +1100 Subject: [PATCH] feat: Add tab completion and some long format ls --- blueprints/terminal.py | 60 +++++++++++++++++++++++---- templates/terminal.html | 91 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 144 insertions(+), 7 deletions(-) diff --git a/blueprints/terminal.py b/blueprints/terminal.py index a4ca2cc..ef2f42d 100644 --- a/blueprints/terminal.py +++ b/blueprints/terminal.py @@ -78,7 +78,7 @@ def setup_path_session(): binaries.append({"name": cmd.split()[0], "type": 2, "content": "", "permissions": 1}) boot_files = [] for bin_file in BOOT_FILES: - boot_files.append({"name": bin_file, "type": 2, "content": "", "permissions": 1}) + boot_files.append({"name": bin_file, "type": 1, "content": "", "permissions": 1}) session["paths"] = [ {"name": "home", "type": 0, "children": [ @@ -239,23 +239,69 @@ def ls(): all_files = True args.remove("--all") - path = session.get("pwd", f"/home/{getClientIP(request)}") + long_format = False + if "-l" in args: + long_format = True + args.remove("-l") + elif "--long" in args: + long_format = True + args.remove("--long") + + for arg in args: + if arg.startswith("-") and not arg.startswith("--"): + if "l" in arg: + long_format = True + if "a" in arg: + all_files = True + args.remove(arg) + + + ip = getClientIP(request) + path = session.get("pwd", f"/home/{ip}") if args: path = args[0] if not path.startswith("/"): # Relative path - path = sanitize_path(session.get("pwd", f"/home/{getClientIP(request)}") + "/" + path) + path = sanitize_path(session.get("pwd", f"/home/{ip}") + "/" + path) if not is_valid_path(path): + # If it is a file or binary, return it + if is_valid_file(path) or is_valid_binary(path): + if long_format: + node = get_node(path) + permissions = node.get("permissions", 0) + perm_str = "" + perm_str += "r" if permissions > 0 else "-" + perm_str += "w" if permissions > 1 else "-" + perm_str += "x" if (node.get("type", 0) == 2 and permissions > 1) else "-" + user = f"{ip}" if path.startswith(f"/home/{ip}") else "root" + output = f"{perm_str} {user} {user} {path.split('/')[-1]}" + return json_response(request, {"output": output}, 200) + return json_response(request, {"output": path.split("/")[-1]}, 200) + return json_response(request, {"output": f"ls: cannot access '{path}': No such file or directory"}, 200) files = get_nodes_in_directory(path) - output = [file["name"] for file in files] + output = [] + if long_format: + for f in files: + permissions = f.get("permissions", 0) + perm_str = "" + perm_str += "r" if permissions > 0 else "-" + perm_str += "w" if permissions > 1 else "-" + perm_str += "x" if (f.get("type", 0) == 2 and permissions >= 1) else "-" + user = f"{ip}" if path.startswith(f"/home/{ip}") else "root" + output.append(f"{perm_str} {user} {user} {f['name']}") + else: + output = [f["name"] for f in files] if all_files: - output.insert(0, ".") - output.insert(1, "..") + output.insert(0, ".") + output.insert(1, "..") else: output = [file for file in output if not file.startswith(".")] - output = " ".join(output) + if long_format: + output = "\n".join(output) + else: + output = " ".join(output) return json_response(request, {"output": output}, 200) diff --git a/templates/terminal.html b/templates/terminal.html index 799ca80..dfeae0e 100644 --- a/templates/terminal.html +++ b/templates/terminal.html @@ -123,6 +123,9 @@ let nanoCurrentFile = ''; let nanoOriginalContent = ''; let nanoSavePromptActive = false; + let tabCompletionIndex = 0; + let tabCompletionMatches = []; + let lastTabInput = ''; // Function to update the prompt with current pwd async function updatePrompt() { @@ -256,10 +259,98 @@ } }); + // Function to get available commands + function getAvailableCommands() { + return ['help', 'about', 'clear', 'echo', 'whoami', 'ls', 'pwd', 'date', 'cd', 'cat', 'rm', 'tree', 'touch', 'nano', 'reset', 'exit']; + } + + // Function to get files/directories for tab completion + async function getFilesForCompletion(path = '') { + try { + const response = await fetch('/terminal/execute/ls', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ args: path ? path : '' }) + }); + const data = await response.json(); + + if (data.output && !data.output.startsWith('ls:')) { + return data.output.split(' ').filter(f => f.length > 0); + } + } catch (err) { + console.error('Failed to get files:', err); + } + return []; + } + + // Function to handle tab completion + async function handleTabCompletion(currentInput) { + const parts = currentInput.split(' '); + const isFirstWord = parts.length === 1; + + if (isFirstWord) { + // Complete command names + const commands = getAvailableCommands(); + const matches = commands.filter(cmd => cmd.startsWith(parts[0])); + + if (matches.length === 1) { + return matches[0] + ' '; + } else if (matches.length > 1) { + // Cycle through matches + if (lastTabInput === currentInput) { + tabCompletionIndex = (tabCompletionIndex + 1) % matches.length; + } else { + tabCompletionIndex = 0; + tabCompletionMatches = matches; + } + lastTabInput = currentInput; + return matches[tabCompletionIndex] + ' '; + } + } else { + // Complete file/directory names + const lastPart = parts[parts.length - 1]; + const files = await getFilesForCompletion(); + const matches = files.filter(f => f.startsWith(lastPart)); + + if (matches.length === 1) { + parts[parts.length - 1] = matches[0]; + return parts.join(' ') + ' '; + } else if (matches.length > 1) { + // Cycle through matches + if (lastTabInput === currentInput) { + tabCompletionIndex = (tabCompletionIndex + 1) % matches.length; + } else { + tabCompletionIndex = 0; + tabCompletionMatches = matches; + } + lastTabInput = currentInput; + parts[parts.length - 1] = matches[tabCompletionIndex]; + return parts.join(' '); + } + } + + return currentInput; + } + // Update prompt on load updatePrompt(); input.addEventListener('keydown', async (e) => { + if (e.key === 'Tab') { + e.preventDefault(); + const currentInput = input.value; + const completed = await handleTabCompletion(currentInput); + input.value = completed; + return; + } + + // Reset tab completion on any other key + if (e.key !== 'Tab') { + tabCompletionIndex = 0; + tabCompletionMatches = []; + lastTabInput = ''; + } + if (e.key === 'Enter') { const command = input.value.trim();