This commit is contained in:
250
src/utils/terminal/commands.ts
Normal file
250
src/utils/terminal/commands.ts
Normal file
@@ -0,0 +1,250 @@
|
||||
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 "";
|
||||
}
|
||||
Reference in New Issue
Block a user