import { useState, useEffect, useRef } from "react"; import { fileSystem } from "../data/filesystem"; export default function Terminal() { const [currentPath, setCurrentPath] = useState("/home/guest"); const [commandHistory, setCommandHistory] = useState([]); const [inputValue, setInputValue] = useState(""); const inputRef = useRef(null); const terminalRef = useRef(null); const [hasBeenCleared, setHasBeenCleared] = useState(false); // Focus the input when the component mounts and on click useEffect(() => { inputRef.current.focus(); }, []); useEffect(() => { // Make sure input is focused after render if (inputRef.current) { inputRef.current.focus(); } }, [commandHistory]); // Re-focus after command history changes const focusInput = () => { inputRef.current.focus(); }; const handleKeyDown = (e) => { console.log("Key pressed:", e.key, "Input value:", inputValue); // Add debugging if (e.key === "Enter") { e.preventDefault(); e.stopPropagation(); // Stop event propagation console.log("Enter pressed, executing command:", inputValue); executeCommand(); return false; // Ensure the event is fully handled } else if (e.key === "Tab") { e.preventDefault(); handleTabCompletion(); } else if (e.key === "l" && e.ctrlKey) { e.preventDefault(); handleClear(); setInputValue(""); return; } }; const handleContainerKeyDown = (e) => { if (e.key === "Enter" && document.activeElement === inputRef.current) { e.preventDefault(); executeCommand(); } }; const handleClear = () => { setCommandHistory([]); setHasBeenCleared(true); // Return focus to input setTimeout(() => { if (inputRef.current) { inputRef.current.focus(); } }, 10); return ""; }; // Path resolution functions const resolvePath = (path) => { // If it's an absolute path, start from root if (path.startsWith("/")) { return path; } // If it's ".", return current path if (path === ".") { return currentPath; } // If it's "..", go up one level if (path === "..") { const pathParts = currentPath.split("/").filter(Boolean); if (pathParts.length === 0) { return "/"; } pathParts.pop(); return "/" + pathParts.join("/"); } // Handle relative paths - append to current path if (currentPath.endsWith("/")) { return currentPath + path; } return currentPath + "/" + path; }; const getItemAtPath = (path) => { const normalizedPath = path.endsWith("/") && path !== "/" ? path.slice(0, -1) : path; if (normalizedPath === "/") { return fileSystem["/"]; } const pathParts = normalizedPath.split("/").filter(Boolean); let current = fileSystem["/"]; for (const part of pathParts) { if (!current || current.type !== "directory" || !current.children[part]) { return null; } current = current.children[part]; } return current; }; // Handle tab completion const handleTabCompletion = () => { if (!inputValue.trim()) return; const parts = inputValue.trim().split(" "); const command = parts[0].toLowerCase(); // If completing a command if (parts.length === 1) { const commands = [ "ls", "cd", "cat", "pwd", "date", "help", "clear", "view", ]; const matchingCommands = commands.filter((cmd) => cmd.startsWith(command) ); if (matchingCommands.length === 1) { setInputValue(matchingCommands[0]); } return; } // If completing a path/file/directory if (["cd", "cat", "ls", "view"].includes(command)) { const partialPath = parts[parts.length - 1]; const suggestions = getPathSuggestions(partialPath); if (suggestions.length === 1) { const completedParts = [...parts]; completedParts[completedParts.length - 1] = suggestions[0]; setInputValue(completedParts.join(" ")); } } }; // Get path suggestions based on current directory const getPathSuggestions = (partialPath) => { const dirPath = partialPath.includes("/") ? partialPath.substring(0, partialPath.lastIndexOf("/") + 1) : ""; const prefix = partialPath.includes("/") ? partialPath.substring(partialPath.lastIndexOf("/") + 1) : partialPath; const resolvedDirPath = resolvePath(dirPath || currentPath); const dir = getItemAtPath(resolvedDirPath); if (!dir || dir.type !== "directory") return []; return Object.keys(dir.children) .filter((name) => name.startsWith(prefix)) .map((name) => { const fullPath = dirPath + name; if (dir.children[name].type === "directory") { return fullPath + "/"; } return fullPath; }); }; // Process and execute commands const executeCommand = () => { if (!inputValue.trim()) return; // Create a new history entry with the CURRENT PATH at execution time const newHistoryEntry = { command: inputValue, output: "", path: currentPath, // Store the path at execution time }; const parts = inputValue.trim().split(/\s+/); const command = parts[0].toLowerCase(); const args = parts.slice(1); let output = ""; switch (command) { case "ls": output = handleLs(args); break; case "cd": output = handleCd(args); break; case "cat": output = handleCat(args); break; case "view": output = handleView(args); break; case "pwd": output = currentPath; break; case "date": output = new Date().toLocaleString(); break; case "help": output = "Available commands:\n" + " ls - List directory contents\n" + " cd [directory] - Change directory\n" + " cat [file] - Display file contents\n" + " view [image] - Display image files\n" + " pwd - Print working directory\n" + " clear - Clear the terminal\n" + " help - Display this help message\n\n" + "Shortcuts:\n" + " Tab - Autocomplete commands and paths\n" + " Ctrl+L - Clear the terminal"; break; case "clear": clearTerminal(); setInputValue(""); return; default: output = `Command not found: ${command}`; } newHistoryEntry.output = output; setCommandHistory([...commandHistory, newHistoryEntry]); setInputValue(""); // Auto-scroll to bottom and re-focus input after rendering setTimeout(() => { if (terminalRef.current) { terminalRef.current.scrollTop = terminalRef.current.scrollHeight; } if (inputRef.current) { inputRef.current.focus(); } }, 50); }; // Handle ls command const handleLs = (args) => { let targetPath = currentPath; if (args.length > 0) { // Handle path argument targetPath = resolvePath(args[0]); } const item = getItemAtPath(targetPath); if (!item) { return `ls: ${args[0]}: No such file or directory`; } if (item.type !== "directory") { return `ls: ${args[0]}: Not a directory`; } const children = Object.keys(item.children).sort(); if (children.length === 0) { return "Empty directory"; } return children .map((name) => { const child = item.children[name]; const itemClass = child.type === "directory" ? "directory" : child.type === "image" ? "image" : "file"; return `
${name}
`; }) .join(""); }; // Handle cd command const handleCd = (args) => { if (!args.length || args[0] === "~" || args[0] === "/home/guest") { setCurrentPath("/home/guest"); return ""; } // Handle special case for tilde expansion let targetPath = args[0]; if (targetPath.startsWith("~/")) { targetPath = "/home/guest/" + targetPath.substring(2); } else if (targetPath === "~") { targetPath = "/home/guest"; } let newPath = resolvePath(targetPath); // Ensure path has leading slash if (!newPath.startsWith("/")) { newPath = "/" + newPath; } // Normalize path (remove trailing slash except for root) if (newPath !== "/" && newPath.endsWith("/")) { newPath = newPath.slice(0, -1); } const targetDir = getItemAtPath(newPath); if (!targetDir) { return `cd: ${targetPath}: No such directory`; } if (targetDir.type !== "directory") { return `cd: ${targetPath}: Not a directory`; } setCurrentPath(newPath); return ""; }; // Handle cat command const handleCat = (args) => { if (!args.length) { return "Usage: cat [filename]"; } const fileName = args[0]; const currentDir = getItemAtPath(currentPath); if (!currentDir || currentDir.type !== "directory") { return "Current location is not a directory"; } if ( fileName in currentDir.children && currentDir.children[fileName].type === "file" ) { return currentDir.children[fileName].content; } return `cat: ${fileName}: No such file`; }; // Handle view command for images const handleView = (args) => { if (!args.length) { return "Usage: view [image filename]"; } const fileName = args[0]; let file; // Handle paths with slashes if (fileName.includes("/")) { const path = resolvePath(fileName); file = getItemAtPath(path); } else { // Look in current directory const currentDir = getItemAtPath(currentPath); if (!currentDir || currentDir.type !== "directory") { return "Current location is not a directory"; } file = currentDir.children[fileName]; } if (!file) { return `view: ${fileName}: No such file`; } if (file.type !== "image") { return `view: ${fileName}: Not an image file`; } // Return HTML with the image setTimeout(() => { // Re-focus the input after image is displayed if (inputRef.current) { inputRef.current.focus(); } }, 50); return `
${file.description ||
${file.description || ""}
`; }; return (
{!hasBeenCleared && (
              {`
 _   _ ____   ____      _    ____   ___  _   _ _____      __  __ _____ 
| \\ | |  _ \\ / ___|    / \\  | __ ) / _ \\| | | |_   _|    |  \\/  | ____|
|  \\| | |_) | |       / _ \\ |  _ \\| | | | | | | | |      | |\\/| |  _|  
| |\\  |  __/| |___   / ___ \\| |_) | |_| | |_| | | |      | |  | | |___ 
|_| \\_|_|    \\____| /_/   \\_\\____/ \\___/ \\___/  |_|      |_|  |_|_____|
                                                                        
`}
            
Welcome to my interactive terminal!
Type{" "} help{" "} to see available commands.
)} {commandHistory.map((entry, index) => (
guest@npc-shell: {entry.path === "/home/guest" ? "~" : entry.path.replace("/home/guest", "~")} $ {entry.command}
{entry.output && (
)}
))}
{ e.preventDefault(); executeCommand(); }} className="w-full" >
guest @ npc-shell : {currentPath === "/home/guest" ? "~" : currentPath.replace("/home/guest", "~")} $ setInputValue(e.target.value)} onKeyDown={handleKeyDown} autoComplete="off" spellCheck="false" />
); }