2.0.0 - Overhaul
All checks were successful
Docker Deploy / build-and-push (push) Successful in 4m13s
All checks were successful
Docker Deploy / build-and-push (push) Successful in 4m13s
This commit is contained in:
@@ -1,250 +0,0 @@
|
||||
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 "";
|
||||
}
|
||||
@@ -1,384 +0,0 @@
|
||||
import type { FileSystemNode, ResumeData } from "./types";
|
||||
import { talks, projects, socialLinks, techLinks } from "../../config/data";
|
||||
|
||||
export async function buildFileSystem(): Promise<{
|
||||
[key: string]: FileSystemNode;
|
||||
}> {
|
||||
try {
|
||||
const response = await fetch("/api/resume.json");
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch resume data: ${response.status} ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
|
||||
const resumeData: any = await response.json();
|
||||
|
||||
// Fetch blog posts
|
||||
const postsResponse = await fetch("/api/posts.json");
|
||||
let postsData = [];
|
||||
try {
|
||||
postsData = await postsResponse.json();
|
||||
} catch (error) {
|
||||
console.log("Could not fetch posts data:", error);
|
||||
}
|
||||
|
||||
// Build resume files from rxresume json
|
||||
const resumeFiles = buildResumeFiles(resumeData);
|
||||
const postsFiles = buildPostsFiles(postsData);
|
||||
const talksFiles = buildTalksFiles();
|
||||
const projectsFiles = buildProjectsFiles();
|
||||
const socialFiles = buildSocialFiles();
|
||||
const techFiles = buildTechFiles();
|
||||
const contactContent = buildContactContent(resumeData);
|
||||
|
||||
const fs: { [key: string]: FileSystemNode } = {
|
||||
"/": {
|
||||
type: "directory",
|
||||
name: "/",
|
||||
children: {
|
||||
"about.txt": {
|
||||
type: "file",
|
||||
name: "about.txt",
|
||||
content: `${resumeData.basics.name}\nResearcher, Full-Stack Developer, and IT Professional.\n\nExplore the directories:\n- /resume - Professional experience and skills\n- /posts - Blog posts and articles\n- /talks - Conference presentations\n- /projects - Personal and professional projects\n- /social - Social media and contact links\n- /tech - Technologies and tools I use\n\nType "ls" to see all available files and directories.`,
|
||||
},
|
||||
resume: {
|
||||
type: "directory",
|
||||
name: "resume",
|
||||
children: resumeFiles,
|
||||
},
|
||||
posts: {
|
||||
type: "directory",
|
||||
name: "posts",
|
||||
children: postsFiles,
|
||||
},
|
||||
talks: {
|
||||
type: "directory",
|
||||
name: "talks",
|
||||
children: talksFiles,
|
||||
},
|
||||
projects: {
|
||||
type: "directory",
|
||||
name: "projects",
|
||||
children: projectsFiles,
|
||||
},
|
||||
social: {
|
||||
type: "directory",
|
||||
name: "social",
|
||||
children: socialFiles,
|
||||
},
|
||||
tech: {
|
||||
type: "directory",
|
||||
name: "tech",
|
||||
children: techFiles,
|
||||
},
|
||||
"contact.txt": {
|
||||
type: "file",
|
||||
name: "contact.txt",
|
||||
content: contactContent,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return fs;
|
||||
} catch (error) {
|
||||
console.error("Error loading resume data:", error);
|
||||
return buildFallbackFileSystem();
|
||||
}
|
||||
}
|
||||
|
||||
function buildResumeFiles(resumeData: any): { [key: string]: FileSystemNode } {
|
||||
const resumeFiles: { [key: string]: FileSystemNode } = {};
|
||||
|
||||
try {
|
||||
if (resumeData.summary) {
|
||||
resumeFiles["summary.txt"] = {
|
||||
type: "file",
|
||||
name: "summary.txt",
|
||||
content: resumeData.summary.content,
|
||||
};
|
||||
}
|
||||
|
||||
if (resumeData.skills && Array.isArray(resumeData.skills)) {
|
||||
const skillsContent = resumeData.skills
|
||||
.map((skill: any) => `${skill.name} (Level: ${skill.level}/5)`)
|
||||
.join("\n");
|
||||
resumeFiles["skills.txt"] = {
|
||||
type: "file",
|
||||
name: "skills.txt",
|
||||
content: skillsContent,
|
||||
};
|
||||
}
|
||||
|
||||
if (resumeData.experience && Array.isArray(resumeData.experience)) {
|
||||
const experienceContent = resumeData.experience
|
||||
.map((exp: any) => {
|
||||
const description = Array.isArray(exp.description)
|
||||
? exp.description.join("\n• ")
|
||||
: "";
|
||||
return `${exp.position} at ${exp.company}\n${exp.date} | ${exp.location}\n• ${description}\n${exp.url ? `URL: ${exp.url}` : ""}\n`;
|
||||
})
|
||||
.join("\n---\n\n");
|
||||
resumeFiles["experience.txt"] = {
|
||||
type: "file",
|
||||
name: "experience.txt",
|
||||
content: experienceContent,
|
||||
};
|
||||
}
|
||||
|
||||
if (resumeData.education && Array.isArray(resumeData.education)) {
|
||||
const educationContent = resumeData.education
|
||||
.map(
|
||||
(edu: any) =>
|
||||
`${edu.institution}\n${edu.degree} - ${edu.field}\n${edu.date}\n${edu.details && Array.isArray(edu.details) ? edu.details.join("\n• ") : ""}`,
|
||||
)
|
||||
.join("\n\n---\n\n");
|
||||
resumeFiles["education.txt"] = {
|
||||
type: "file",
|
||||
name: "education.txt",
|
||||
content: educationContent,
|
||||
};
|
||||
}
|
||||
|
||||
if (resumeData.volunteer && Array.isArray(resumeData.volunteer)) {
|
||||
const volunteerContent = resumeData.volunteer
|
||||
.map((vol: any) => `${vol.organization}\n${vol.position}\n${vol.date}`)
|
||||
.join("\n\n---\n\n");
|
||||
resumeFiles["volunteer.txt"] = {
|
||||
type: "file",
|
||||
name: "volunteer.txt",
|
||||
content: volunteerContent,
|
||||
};
|
||||
}
|
||||
|
||||
if (resumeData.awards && Array.isArray(resumeData.awards)) {
|
||||
const awardsContent = resumeData.awards
|
||||
.map(
|
||||
(award: any) =>
|
||||
`${award.title}\n${award.organization}\n${award.date}\n${award.description || ""}`,
|
||||
)
|
||||
.join("\n\n---\n\n");
|
||||
resumeFiles["awards.txt"] = {
|
||||
type: "file",
|
||||
name: "awards.txt",
|
||||
content: awardsContent,
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error building resume files:", error);
|
||||
}
|
||||
|
||||
return resumeFiles;
|
||||
}
|
||||
|
||||
function buildPostsFiles(postsData: any[]): { [key: string]: FileSystemNode } {
|
||||
const postsFiles: { [key: string]: FileSystemNode } = {};
|
||||
|
||||
postsData.forEach((post: any) => {
|
||||
const fileName = `${post.slug}.md`;
|
||||
let content = `---
|
||||
title: "${post.title}"
|
||||
description: "${post.description}"
|
||||
pubDate: "${post.pubDate}"
|
||||
tags: [${post.tags.map((tag: string) => `"${tag}"`).join(", ")}]
|
||||
---
|
||||
|
||||
${post.content}`;
|
||||
|
||||
postsFiles[fileName] = {
|
||||
type: "file",
|
||||
name: fileName,
|
||||
content,
|
||||
};
|
||||
});
|
||||
|
||||
return postsFiles;
|
||||
}
|
||||
|
||||
function buildTalksFiles(): { [key: string]: FileSystemNode } {
|
||||
const talksFiles: { [key: string]: FileSystemNode } = {};
|
||||
|
||||
talks.forEach((talk) => {
|
||||
const fileName = `${talk.id}.txt`;
|
||||
let content = `${talk.name}
|
||||
${talk.description}
|
||||
${talk.date || ""}
|
||||
${talk.link}`;
|
||||
|
||||
talksFiles[fileName] = {
|
||||
type: "file",
|
||||
name: fileName,
|
||||
content,
|
||||
};
|
||||
});
|
||||
|
||||
return talksFiles;
|
||||
}
|
||||
|
||||
function buildProjectsFiles(): { [key: string]: FileSystemNode } {
|
||||
const projectsFiles: { [key: string]: FileSystemNode } = {};
|
||||
|
||||
projects.forEach((project) => {
|
||||
const fileName = `${project.id}.txt`;
|
||||
let content = `${project.name}
|
||||
${project.description}
|
||||
${project.status || ""}
|
||||
${project.technologies ? project.technologies.join(", ") : ""}
|
||||
${project.link}`;
|
||||
|
||||
projectsFiles[fileName] = {
|
||||
type: "file",
|
||||
name: fileName,
|
||||
content,
|
||||
};
|
||||
});
|
||||
|
||||
return projectsFiles;
|
||||
}
|
||||
|
||||
function buildSocialFiles(): { [key: string]: FileSystemNode } {
|
||||
const socialFiles: { [key: string]: FileSystemNode } = {};
|
||||
|
||||
socialLinks.forEach((link) => {
|
||||
const fileName = `${link.id}.txt`;
|
||||
let content = `${link.name}
|
||||
${link.url}`;
|
||||
|
||||
socialFiles[fileName] = {
|
||||
type: "file",
|
||||
name: fileName,
|
||||
content,
|
||||
};
|
||||
});
|
||||
|
||||
return socialFiles;
|
||||
}
|
||||
|
||||
function buildTechFiles(): { [key: string]: FileSystemNode } {
|
||||
const techFiles: { [key: string]: FileSystemNode } = {};
|
||||
|
||||
techLinks.forEach((link) => {
|
||||
const fileName = `${link.id}.txt`;
|
||||
let content = `${link.name}
|
||||
${link.url}`;
|
||||
|
||||
techFiles[fileName] = {
|
||||
type: "file",
|
||||
name: fileName,
|
||||
content,
|
||||
};
|
||||
});
|
||||
|
||||
return techFiles;
|
||||
}
|
||||
|
||||
function buildContactContent(resumeData: any): string {
|
||||
try {
|
||||
const basics = resumeData.basics || {};
|
||||
const email = basics.email || "Not provided";
|
||||
const profiles = basics.profiles || [];
|
||||
|
||||
return [
|
||||
`Email: ${email}`,
|
||||
"",
|
||||
"Social Profiles:",
|
||||
...profiles.map((profile: any) => `${profile.network}: ${profile.url}`),
|
||||
].join("\n");
|
||||
} catch (error) {
|
||||
console.error("Error building contact content:", error);
|
||||
return "Contact information unavailable";
|
||||
}
|
||||
}
|
||||
|
||||
function buildFallbackFileSystem(): { [key: string]: FileSystemNode } {
|
||||
const talksFiles = buildTalksFiles();
|
||||
const projectsFiles = buildProjectsFiles();
|
||||
const socialFiles = buildSocialFiles();
|
||||
const techFiles = buildTechFiles();
|
||||
|
||||
return {
|
||||
"/": {
|
||||
type: "directory",
|
||||
name: "/",
|
||||
children: {
|
||||
"about.txt": {
|
||||
type: "file",
|
||||
name: "about.txt",
|
||||
content:
|
||||
"Atridad Lahiji\nResearcher, Full-Stack Developer, and IT Professional.\n\nError loading resume data. Basic navigation still available.\n\nExplore the directories:\n- /talks - Conference presentations\n- /projects - Personal and professional projects\n- /social - Social media and contact links\n- /tech - Technologies and tools I use\n\nType 'ls' to see all available files and directories.",
|
||||
},
|
||||
talks: {
|
||||
type: "directory",
|
||||
name: "talks",
|
||||
children: talksFiles,
|
||||
},
|
||||
projects: {
|
||||
type: "directory",
|
||||
name: "projects",
|
||||
children: projectsFiles,
|
||||
},
|
||||
social: {
|
||||
type: "directory",
|
||||
name: "social",
|
||||
children: socialFiles,
|
||||
},
|
||||
tech: {
|
||||
type: "directory",
|
||||
name: "tech",
|
||||
children: techFiles,
|
||||
},
|
||||
"help.txt": {
|
||||
type: "file",
|
||||
name: "help.txt",
|
||||
content:
|
||||
"Available commands:\n- ls - list files\n- cd <directory> - change directory\n- cat <file> - view file contents\n- pwd - show current directory\n- clear - clear terminal\n- help - show this help\n- train - run the train animation",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function getCurrentDirectory(
|
||||
fileSystem: { [key: string]: FileSystemNode },
|
||||
currentPath: string,
|
||||
): FileSystemNode {
|
||||
const pathParts = currentPath
|
||||
.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];
|
||||
}
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
export function resolvePath(currentPath: string, path: string): string {
|
||||
if (path.startsWith("/")) {
|
||||
return path;
|
||||
}
|
||||
|
||||
const currentParts = currentPath
|
||||
.split("/")
|
||||
.filter((part: string) => part !== "");
|
||||
const pathParts = path.split("/");
|
||||
|
||||
for (const part of pathParts) {
|
||||
if (part === "..") {
|
||||
currentParts.pop();
|
||||
} else if (part !== "." && part !== "") {
|
||||
currentParts.push(part);
|
||||
}
|
||||
}
|
||||
|
||||
return "/" + currentParts.join("/");
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
export interface FileSystemNode {
|
||||
type: 'directory' | 'file';
|
||||
name: string;
|
||||
content?: string;
|
||||
children?: { [key: string]: FileSystemNode };
|
||||
}
|
||||
|
||||
export interface ResumeData {
|
||||
basics: {
|
||||
name: string;
|
||||
email: string;
|
||||
url?: { href: string };
|
||||
};
|
||||
sections: {
|
||||
summary: { name: string; content: string };
|
||||
profiles: { name: string; items: { network: string; username: string; url: { href: string } }[] };
|
||||
skills: { name: string; items: { id: string; name: string; level: number }[] };
|
||||
experience: { name: string; items: { id: string; company: string; position: string; date: string; location: string; summary: string; url?: { href: string } }[] };
|
||||
education: { name: string; items: { id: string; institution: string; studyType: string; area: string; date: string; summary: string }[] };
|
||||
volunteer: { name: string; items: { id: string; organization: string; position: string; date: string }[] };
|
||||
};
|
||||
}
|
||||
|
||||
export interface Command {
|
||||
input: string;
|
||||
output: string;
|
||||
timestamp: Date;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface TerminalState {
|
||||
currentPath: string;
|
||||
commandHistory: Command[];
|
||||
currentInput: string;
|
||||
historyIndex: number;
|
||||
fileSystem: { [key: string]: FileSystemNode };
|
||||
isTrainRunning: boolean;
|
||||
trainPosition: number;
|
||||
persistentHistory: string[];
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
import type { FileSystemNode } from "./types";
|
||||
import { resolvePath } from "./fs";
|
||||
|
||||
export function getCompletions(
|
||||
input: string,
|
||||
currentPath: string,
|
||||
fileSystem: { [key: string]: FileSystemNode },
|
||||
): { completion: string | null; replaceFrom: number } {
|
||||
const parts = input.trim().split(" ");
|
||||
const command = parts[0];
|
||||
const partialPath = parts[parts.length - 1] || "";
|
||||
|
||||
// Only complete paths for these commands
|
||||
if (parts.length > 1 && ["ls", "cd", "cat", "open"].includes(command)) {
|
||||
// Path completion
|
||||
const isAbsolute = partialPath.startsWith("/");
|
||||
const pathToComplete = isAbsolute
|
||||
? partialPath
|
||||
: resolvePath(currentPath, partialPath);
|
||||
|
||||
// Find the directory to search in and the prefix to match
|
||||
let dirPath: string;
|
||||
let searchPrefix: string;
|
||||
|
||||
if (pathToComplete.endsWith("/")) {
|
||||
// Path ends with slash - complete inside this directory
|
||||
dirPath = pathToComplete;
|
||||
searchPrefix = "";
|
||||
} else {
|
||||
// Base case - find directory and prefix
|
||||
const lastSlash = pathToComplete.lastIndexOf("/");
|
||||
if (lastSlash >= 0) {
|
||||
dirPath = pathToComplete.substring(0, lastSlash + 1);
|
||||
searchPrefix = pathToComplete.substring(lastSlash + 1);
|
||||
} else {
|
||||
dirPath = currentPath.endsWith("/") ? currentPath : currentPath + "/";
|
||||
searchPrefix = pathToComplete;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate where to start replacement in the original input
|
||||
const spaceBeforeArg = input.lastIndexOf(" ");
|
||||
const replaceFrom = spaceBeforeArg >= 0 ? spaceBeforeArg + 1 : 0;
|
||||
|
||||
// Navigate to the directory
|
||||
const dirParts = dirPath.split("/").filter((part: string) => part !== "");
|
||||
let current = fileSystem["/"];
|
||||
|
||||
for (const part of dirParts) {
|
||||
if (
|
||||
current?.children &&
|
||||
current.children[part] &&
|
||||
current.children[part].type === "directory"
|
||||
) {
|
||||
current = current.children[part];
|
||||
} else {
|
||||
return { completion: null, replaceFrom };
|
||||
}
|
||||
}
|
||||
|
||||
if (!current?.children) {
|
||||
return { completion: null, replaceFrom };
|
||||
}
|
||||
|
||||
// Get first matching item
|
||||
const match = Object.keys(current.children).find((name) =>
|
||||
name.startsWith(searchPrefix),
|
||||
);
|
||||
|
||||
if (match) {
|
||||
const item = current.children[match];
|
||||
const completion = item.type === "directory" ? `${match}/` : match;
|
||||
return { completion, replaceFrom };
|
||||
}
|
||||
}
|
||||
|
||||
return { completion: null, replaceFrom: input.length };
|
||||
}
|
||||
|
||||
export function formatOutput(text: string): string {
|
||||
return text
|
||||
.replace(/\x1b\[34m/g, '<span class="text-primary">')
|
||||
.replace(/\x1b\[0m/g, "</span>");
|
||||
}
|
||||
|
||||
export function saveCommandToHistory(
|
||||
command: string,
|
||||
persistentHistory: string[],
|
||||
): string[] {
|
||||
if (command.trim()) {
|
||||
const updatedHistory = [...persistentHistory, command].slice(-100); // Keep last 100 commands
|
||||
localStorage.setItem("terminal-history", JSON.stringify(updatedHistory));
|
||||
return updatedHistory;
|
||||
}
|
||||
return persistentHistory;
|
||||
}
|
||||
|
||||
export function loadCommandHistory(): string[] {
|
||||
const savedHistory = localStorage.getItem("terminal-history");
|
||||
if (savedHistory) {
|
||||
try {
|
||||
return JSON.parse(savedHistory);
|
||||
} catch (error) {
|
||||
console.error("Error loading command history:", error);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
Reference in New Issue
Block a user