All checks were successful
Docker Deploy / build-and-push (push) Successful in 3m26s
251 lines
6.9 KiB
TypeScript
251 lines
6.9 KiB
TypeScript
import type { FileSystemNode } from "./types";
|
|
import { getCurrentDirectory, resolvePath } from "./fs";
|
|
|
|
export interface CommandContext {
|
|
currentPath: string;
|
|
fileSystem: { [key: string]: FileSystemNode };
|
|
setCurrentPath: (path: string) => void;
|
|
setIsTrainRunning: (running: boolean) => void;
|
|
setTrainPosition: (position: number) => void;
|
|
}
|
|
|
|
export function executeCommand(input: string, context: CommandContext): string {
|
|
const trimmedInput = input.trim();
|
|
if (!trimmedInput) return "";
|
|
|
|
const [command, ...args] = trimmedInput.split(" ");
|
|
|
|
switch (command.toLowerCase()) {
|
|
case "help":
|
|
return handleHelp();
|
|
case "ls":
|
|
return handleLs(args, context);
|
|
case "cd":
|
|
return handleCd(args, context);
|
|
case "pwd":
|
|
return handlePwd(context);
|
|
case "cat":
|
|
return handleCat(args, context);
|
|
case "tree":
|
|
return handleTree(context);
|
|
case "clear":
|
|
return "";
|
|
case "whoami":
|
|
return handleWhoami();
|
|
case "open":
|
|
return handleOpen(args);
|
|
case "sl":
|
|
return handleSl(context);
|
|
default:
|
|
return `${command}: command not found. Type 'help' for available commands.`;
|
|
}
|
|
}
|
|
|
|
function handleHelp(): string {
|
|
return `Available commands:
|
|
ls [path] - List directory contents
|
|
cd <path> - Change directory
|
|
cat <file> - Display file contents
|
|
pwd - Show current directory
|
|
clear - Clear terminal
|
|
tree - Show directory structure
|
|
whoami - Display user info
|
|
open <path> - Open page in browser (simulated)
|
|
help - Show this help message
|
|
|
|
Navigation:
|
|
Use "cd .." to go up one directory
|
|
Use "cd /" to go to root directory
|
|
File paths can be relative or absolute
|
|
Use TAB for auto-completion
|
|
|
|
Examples:
|
|
ls
|
|
cd resume
|
|
cat about.txt
|
|
open /resume
|
|
open /talks
|
|
cat /talks/README.txt
|
|
cd social
|
|
cat /tech/README.txt`;
|
|
}
|
|
|
|
function handleLs(args: string[], context: CommandContext): string {
|
|
const { currentPath, fileSystem } = context;
|
|
const targetPath = args[0] ? resolvePath(currentPath, args[0]) : currentPath;
|
|
const pathParts = targetPath.split("/").filter((part: string) => part !== "");
|
|
let target = fileSystem["/"];
|
|
|
|
for (const part of pathParts) {
|
|
if (
|
|
target?.children &&
|
|
target.children[part] &&
|
|
target.children[part].type === "directory"
|
|
) {
|
|
target = target.children[part];
|
|
} else if (pathParts.length > 0) {
|
|
return `ls: cannot access '${targetPath}': No such file or directory`;
|
|
}
|
|
}
|
|
|
|
if (!target?.children) {
|
|
return `ls: cannot access '${targetPath}': Not a directory`;
|
|
}
|
|
|
|
const items = Object.values(target.children)
|
|
.map((item) => {
|
|
const color = item.type === "directory" ? "\x1b[34m" : "\x1b[0m";
|
|
const suffix = item.type === "directory" ? "/" : "";
|
|
return `${color}${item.name}${suffix}\x1b[0m`;
|
|
})
|
|
.join(" ");
|
|
|
|
return items || "Directory is empty";
|
|
}
|
|
|
|
function handleCd(args: string[], context: CommandContext): string {
|
|
const { currentPath, fileSystem, setCurrentPath } = context;
|
|
const targetPath = args[0] ? resolvePath(currentPath, args[0]) : "/";
|
|
const pathParts = targetPath.split("/").filter((part: string) => part !== "");
|
|
let current = fileSystem["/"];
|
|
|
|
for (const part of pathParts) {
|
|
if (
|
|
current?.children &&
|
|
current.children[part] &&
|
|
current.children[part].type === "directory"
|
|
) {
|
|
current = current.children[part];
|
|
} else {
|
|
return `cd: no such file or directory: ${targetPath}`;
|
|
}
|
|
}
|
|
|
|
setCurrentPath(targetPath || "/");
|
|
return "";
|
|
}
|
|
|
|
function handlePwd(context: CommandContext): string {
|
|
return context.currentPath || "/";
|
|
}
|
|
|
|
function handleCat(args: string[], context: CommandContext): string {
|
|
const { currentPath, fileSystem } = context;
|
|
|
|
if (!args[0]) {
|
|
return "cat: missing file argument";
|
|
}
|
|
|
|
const filePath = resolvePath(currentPath, args[0]);
|
|
const pathParts = filePath.split("/").filter((part: string) => part !== "");
|
|
const fileName = pathParts.pop();
|
|
|
|
let current = fileSystem["/"];
|
|
for (const part of pathParts) {
|
|
if (
|
|
current?.children &&
|
|
current.children[part] &&
|
|
current.children[part].type === "directory"
|
|
) {
|
|
current = current.children[part];
|
|
} else {
|
|
return `cat: ${filePath}: No such file or directory`;
|
|
}
|
|
}
|
|
|
|
if (!fileName || !current?.children || !current.children[fileName]) {
|
|
return `cat: ${filePath}: No such file or directory`;
|
|
}
|
|
|
|
const file = current.children[fileName];
|
|
if (file.type !== "file") {
|
|
return `cat: ${filePath}: Is a directory`;
|
|
}
|
|
|
|
return file.content || "";
|
|
}
|
|
|
|
function handleTree(context: CommandContext): string {
|
|
const { fileSystem } = context;
|
|
|
|
const buildTree = (
|
|
node: FileSystemNode,
|
|
prefix: string = "",
|
|
isLast: boolean = true,
|
|
): string => {
|
|
let result = "";
|
|
if (!node.children) return result;
|
|
|
|
const entries = Object.entries(node.children);
|
|
entries.forEach(([name, child], index) => {
|
|
const isLastChild = index === entries.length - 1;
|
|
const connector = isLastChild ? "└── " : "├── ";
|
|
const color = child.type === "directory" ? "\x1b[34m" : "\x1b[0m";
|
|
const suffix = child.type === "directory" ? "/" : "";
|
|
|
|
result += `${prefix}${connector}${color}${name}${suffix}\x1b[0m\n`;
|
|
|
|
if (child.type === "directory") {
|
|
const newPrefix = prefix + (isLastChild ? " " : "│ ");
|
|
result += buildTree(child, newPrefix, isLastChild);
|
|
}
|
|
});
|
|
|
|
return result;
|
|
};
|
|
|
|
return ".\n" + buildTree(fileSystem["/"]);
|
|
}
|
|
|
|
function handleWhoami(): string {
|
|
return "guest@atri.dad";
|
|
}
|
|
|
|
function handleOpen(args: string[]): string {
|
|
const path = args[0];
|
|
if (!path) {
|
|
return "open: missing path argument";
|
|
}
|
|
|
|
let url = "";
|
|
if (path === "/resume" || path.startsWith("/resume")) {
|
|
url = "/resume";
|
|
} else if (path === "/projects" || path.startsWith("/projects")) {
|
|
url = "/projects";
|
|
} else if (path === "/posts" || path.startsWith("/posts")) {
|
|
url = "/posts";
|
|
} else if (path === "/talks" || path.startsWith("/talks")) {
|
|
url = "/talks";
|
|
} else if (path === "/" || path === "/about.txt") {
|
|
url = "/";
|
|
} else {
|
|
return `open: cannot open '${path}': No associated page`;
|
|
}
|
|
|
|
window.open(url, "_blank");
|
|
return `Opening ${url} in new tab...`;
|
|
}
|
|
|
|
function handleSl(context: CommandContext): string {
|
|
const { setIsTrainRunning, setTrainPosition } = context;
|
|
|
|
setIsTrainRunning(true);
|
|
setTrainPosition(100);
|
|
|
|
const animateTrain = () => {
|
|
let position = 100;
|
|
const interval = setInterval(() => {
|
|
position -= 1.5;
|
|
setTrainPosition(position);
|
|
|
|
if (position < -50) {
|
|
clearInterval(interval);
|
|
setIsTrainRunning(false);
|
|
}
|
|
}, 60);
|
|
};
|
|
|
|
setTimeout(animateTrain, 100);
|
|
return "";
|
|
}
|