implement shell command interface and add basic commands (echo, ls, pwd, cd, touch, cat, rm) with syscall integration
This commit is contained in:
16
src/App.tsx
16
src/App.tsx
@@ -3,6 +3,7 @@ import { Terminal } from 'lucide-react';
|
||||
import TerminalButton from './components/TerminalButton';
|
||||
import ProfileContent from './components/ProfileContent';
|
||||
import ProjectsContent from './components/ProjectsContent';
|
||||
import TerminalShell from './components/TerminalShell';
|
||||
|
||||
function App() {
|
||||
const [activeSection, setActiveSection] = React.useState('about');
|
||||
@@ -10,7 +11,7 @@ function App() {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 flex items-center justify-center overflow-hidden">
|
||||
{/* TV Container */}
|
||||
<div className="w-[95%] max-w-[1400px] relative h-[80vh]">
|
||||
<div className="w-[95%] max-w-[1400px] relative h-[90vh]">
|
||||
{/* TV Frame */}
|
||||
<div className="absolute inset-0 tv-frame">
|
||||
{/* Screen Container with Convex Effect */}
|
||||
@@ -47,18 +48,18 @@ function App() {
|
||||
>
|
||||
SKILLS
|
||||
</TerminalButton>
|
||||
<TerminalButton
|
||||
onClick={() => setActiveSection('contact')}
|
||||
isSelected={activeSection === 'contact'}
|
||||
>
|
||||
CONTACT
|
||||
</TerminalButton>
|
||||
<TerminalButton
|
||||
onClick={() => setActiveSection('resume')}
|
||||
isSelected={activeSection === 'resume'}
|
||||
>
|
||||
RESUME
|
||||
</TerminalButton>
|
||||
<TerminalButton
|
||||
onClick={() => setActiveSection('shell')}
|
||||
isSelected={activeSection === 'shell'}
|
||||
>
|
||||
SHELL
|
||||
</TerminalButton>
|
||||
</div>
|
||||
|
||||
{/* Vertical Separator */}
|
||||
@@ -70,6 +71,7 @@ function App() {
|
||||
{activeSection === 'projects' && (
|
||||
<ProjectsContent markdownPath="/content/projects.md" />
|
||||
)}
|
||||
{activeSection === 'shell' && <TerminalShell />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
59
src/components/TerminalShell.tsx
Normal file
59
src/components/TerminalShell.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { Terminal } from 'lucide-react';
|
||||
import { System } from '../shell/system';
|
||||
|
||||
const TerminalShell: React.FC = () => {
|
||||
const [history, setHistory] = useState<string[]>(['Welcome to the terminal! Type "help" for commands.']);
|
||||
const [currentCommand, setCurrentCommand] = useState('');
|
||||
const bottomRef = useRef<HTMLDivElement>(null);
|
||||
const system = System.getInstance(); // Use singleton instead of creating new instance
|
||||
|
||||
useEffect(() => {
|
||||
bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
}, [history]);
|
||||
|
||||
const handleCommand = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter') {
|
||||
const command = currentCommand.trim();
|
||||
const [cmdName, ...args] = command.split(' ');
|
||||
|
||||
if (system.knowsCommand(cmdName)) {
|
||||
const output = system.executeCommand(cmdName, args);
|
||||
|
||||
const newLines = output.split('\\n').filter(l => l);
|
||||
|
||||
setHistory(prev => [...prev, `$ ${command}`, ...newLines]);
|
||||
} else {
|
||||
setHistory(prev => [...prev, `$ ${command}`, `Command not found: ${cmdName}`]);
|
||||
}
|
||||
setCurrentCommand('');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
<div className="flex-1 overflow-y-auto mb-4">
|
||||
{history.map((line, index) => (
|
||||
<div key={index} className="mb-2">
|
||||
{line}
|
||||
</div>
|
||||
))}
|
||||
<div ref={bottomRef} />
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Terminal className="mr-2" size={18} />
|
||||
<span className="mr-2">$</span>
|
||||
<input
|
||||
type="text"
|
||||
value={currentCommand}
|
||||
onChange={(e) => setCurrentCommand(e.target.value)}
|
||||
onKeyDown={handleCommand}
|
||||
className="flex-1 bg-transparent border-none outline-none text-[#00FF00]"
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TerminalShell;
|
@@ -173,7 +173,7 @@
|
||||
transparent 100%
|
||||
);
|
||||
animation: glare 3s ease-in-out infinite;
|
||||
opacity: 0.5;
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
.screen-glare {
|
||||
@@ -187,6 +187,7 @@
|
||||
transparent 100%
|
||||
);
|
||||
animation: horizontal-glare 4s linear infinite;
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
|
||||
|
109
src/shell/FileSystem.ts
Normal file
109
src/shell/FileSystem.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
interface INode {
|
||||
name: string;
|
||||
isDirectory: boolean;
|
||||
content?: string;
|
||||
children?: Map<string, INode>;
|
||||
}
|
||||
|
||||
export class FileSystem {
|
||||
private root: INode;
|
||||
|
||||
constructor() {
|
||||
console.log('[FileSystem] Initializing new filesystem');
|
||||
this.root = {
|
||||
name: '/',
|
||||
isDirectory: true,
|
||||
children: new Map()
|
||||
};
|
||||
}
|
||||
|
||||
createDirectory(path: string): void {
|
||||
console.log(`[FileSystem] Creating directory: ${path}`);
|
||||
const parts = path.split('/').filter(p => p);
|
||||
let current = this.root;
|
||||
|
||||
for (const part of parts) {
|
||||
if (!current.children!.has(part)) {
|
||||
console.log(`[FileSystem] Creating new directory node: ${part}`);
|
||||
current.children!.set(part, {
|
||||
name: part,
|
||||
isDirectory: true,
|
||||
children: new Map()
|
||||
});
|
||||
}
|
||||
current = current.children!.get(part)!;
|
||||
}
|
||||
}
|
||||
|
||||
writeFile(path: string, content: string): void {
|
||||
console.log(`[FileSystem] Writing file: ${path}`);
|
||||
const parts = path.split('/').filter(p => p);
|
||||
const fileName = parts.pop()!;
|
||||
let current = this.root;
|
||||
|
||||
for (const part of parts) {
|
||||
if (!current.children!.has(part)) {
|
||||
console.log(`[FileSystem] Creating parent directory: ${part}`);
|
||||
this.createDirectory(part);
|
||||
}
|
||||
current = current.children!.get(part)!;
|
||||
}
|
||||
|
||||
current.children!.set(fileName, {
|
||||
name: fileName,
|
||||
isDirectory: false,
|
||||
content
|
||||
});
|
||||
console.log(`[FileSystem] File written: ${fileName}`);
|
||||
}
|
||||
|
||||
listDirectory(path: string): string[] {
|
||||
console.log(`[FileSystem] Listing directory: ${path}`);
|
||||
const parts = path.split('/').filter(p => p);
|
||||
let current = this.root;
|
||||
|
||||
for (const part of parts) {
|
||||
if (!current.children!.has(part)) return [];
|
||||
current = current.children!.get(part)!;
|
||||
}
|
||||
|
||||
const result = Array.from(current.children!.keys());
|
||||
console.log(`[FileSystem] Found ${result.length} entries`);
|
||||
return result;
|
||||
}
|
||||
|
||||
readFile(path: string): string | null {
|
||||
console.log(`[FileSystem] Reading file: ${path}`);
|
||||
const parts = path.split('/').filter(p => p);
|
||||
const fileName = parts.pop()!;
|
||||
let current = this.root;
|
||||
|
||||
for (const part of parts) {
|
||||
if (!current.children!.has(part)) return null;
|
||||
current = current.children!.get(part)!;
|
||||
}
|
||||
|
||||
const file = current.children!.get(fileName);
|
||||
if (!file || file.isDirectory) {
|
||||
console.log(`[FileSystem] File not found or is directory: ${path}`);
|
||||
return null;
|
||||
}
|
||||
console.log(`[FileSystem] File read successfully: ${path}`);
|
||||
return file.content!;
|
||||
}
|
||||
|
||||
deleteFile(path: string): void {
|
||||
console.log(`[FileSystem] Deleting file: ${path}`);
|
||||
const parts = path.split('/').filter(p => p);
|
||||
const fileName = parts.pop()!;
|
||||
let current = this.root;
|
||||
|
||||
for (const part of parts) {
|
||||
if (!current.children!.has(part)) return;
|
||||
current = current.children!.get(part)!;
|
||||
}
|
||||
|
||||
current.children!.delete(fileName);
|
||||
console.log(`[FileSystem] File deleted: ${path}`);
|
||||
}
|
||||
}
|
43
src/shell/ShellSyscall.ts
Normal file
43
src/shell/ShellSyscall.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { FileSystem } from "./FileSystem";
|
||||
import axios from 'axios';
|
||||
|
||||
export class ShellSyscall {
|
||||
constructor(
|
||||
private fs: FileSystem,
|
||||
private cwd: string
|
||||
) {}
|
||||
|
||||
getCurrentDirectory(): string {
|
||||
return this.cwd;
|
||||
}
|
||||
|
||||
listCurrentDirectory(): string[] {
|
||||
return this.fs.listDirectory(this.cwd);
|
||||
}
|
||||
|
||||
changeDirectory(path: string): boolean {
|
||||
// For now, just accept any path
|
||||
this.cwd = path;
|
||||
return true;
|
||||
}
|
||||
|
||||
createFile(filename: string): void {
|
||||
this.fs.writeFile(`${this.cwd}/${filename}`, '');
|
||||
}
|
||||
|
||||
readFile(filename: string): string {
|
||||
return this.fs.readFile(`${this.cwd}/${filename}`) || '';
|
||||
}
|
||||
|
||||
writeFile(filename: string, content: string) {
|
||||
this.fs.writeFile(`${this.cwd}/${filename}`, content);
|
||||
}
|
||||
|
||||
async fetchUrl(url: string) {
|
||||
return await axios.get(url);
|
||||
}
|
||||
|
||||
deleteFile(path: string): void {
|
||||
this.fs.deleteFile(`${this.cwd}/${path}`);
|
||||
}
|
||||
}
|
4
src/shell/commands/IShellCommand.ts
Normal file
4
src/shell/commands/IShellCommand.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface IShellCommand {
|
||||
getName(): string;
|
||||
execute(input: string | string[]): string;
|
||||
}
|
15
src/shell/commands/cat.ts
Normal file
15
src/shell/commands/cat.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { IShellCommand } from "./IShellCommand";
|
||||
import { ShellSyscall } from "../ShellSyscall";
|
||||
|
||||
export class cat implements IShellCommand {
|
||||
constructor(private syscall: ShellSyscall) {}
|
||||
|
||||
getName(): string {
|
||||
return "cat";
|
||||
}
|
||||
|
||||
execute(args: string[]): string {
|
||||
if (args.length === 0) return "cat: missing file operand";
|
||||
return this.syscall.readFile(args[0]);
|
||||
}
|
||||
}
|
16
src/shell/commands/cd.ts
Normal file
16
src/shell/commands/cd.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { IShellCommand } from "./IShellCommand";
|
||||
import { ShellSyscall } from "../ShellSyscall";
|
||||
|
||||
export class cd implements IShellCommand {
|
||||
constructor(private syscall: ShellSyscall) {}
|
||||
|
||||
getName(): string {
|
||||
return "cd";
|
||||
}
|
||||
|
||||
execute(args: string[]): string {
|
||||
const path = args[0] || '/';
|
||||
const success = this.syscall.changeDirectory(path);
|
||||
return success ? '' : 'Invalid directory';
|
||||
}
|
||||
}
|
14
src/shell/commands/echo.ts
Normal file
14
src/shell/commands/echo.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { IShellCommand } from "./IShellCommand";
|
||||
|
||||
export class echo implements IShellCommand {
|
||||
getName(): string {
|
||||
return 'echo';
|
||||
}
|
||||
|
||||
execute(input: string | string[]): string {
|
||||
if (Array.isArray(input)) {
|
||||
return input.join(' ').trim();
|
||||
}
|
||||
return input.trim();
|
||||
}
|
||||
}
|
15
src/shell/commands/ls.ts
Normal file
15
src/shell/commands/ls.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { IShellCommand } from "./IShellCommand";
|
||||
import { ShellSyscall } from "../ShellSyscall";
|
||||
|
||||
export class ls implements IShellCommand {
|
||||
constructor(private syscall: ShellSyscall) {}
|
||||
|
||||
getName(): string {
|
||||
return 'ls';
|
||||
}
|
||||
|
||||
execute(dir: string[]): string {
|
||||
const files = this.syscall.listCurrentDirectory();
|
||||
return files.join('\n');
|
||||
}
|
||||
}
|
14
src/shell/commands/pwd.ts
Normal file
14
src/shell/commands/pwd.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { IShellCommand } from "./IShellCommand";
|
||||
import { ShellSyscall } from "../ShellSyscall";
|
||||
|
||||
export class pwd implements IShellCommand {
|
||||
constructor(private syscall: ShellSyscall) {}
|
||||
|
||||
getName(): string {
|
||||
return "pwd";
|
||||
}
|
||||
|
||||
execute(_: string[]): string {
|
||||
return this.syscall.getCurrentDirectory();
|
||||
}
|
||||
}
|
24
src/shell/commands/rm.ts
Normal file
24
src/shell/commands/rm.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { IShellCommand } from "./IShellCommand";
|
||||
import { ShellSyscall } from "../ShellSyscall";
|
||||
|
||||
export class rm implements IShellCommand {
|
||||
private syscall: ShellSyscall;
|
||||
|
||||
constructor(syscall: ShellSyscall) {
|
||||
this.syscall = syscall;
|
||||
}
|
||||
|
||||
getName(): string {
|
||||
return "rm";
|
||||
}
|
||||
|
||||
execute(args: string[]): string {
|
||||
if (args.length < 1) {
|
||||
return "Usage: rm <filename>";
|
||||
}
|
||||
|
||||
const filename = args[0];
|
||||
this.syscall.deleteFile(filename);
|
||||
return `Removed ${filename}`;
|
||||
}
|
||||
}
|
16
src/shell/commands/touch.ts
Normal file
16
src/shell/commands/touch.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { IShellCommand } from "./IShellCommand";
|
||||
import { ShellSyscall } from "../ShellSyscall";
|
||||
|
||||
export class touch implements IShellCommand {
|
||||
constructor(private syscall: ShellSyscall) {}
|
||||
|
||||
getName(): string {
|
||||
return "touch";
|
||||
}
|
||||
|
||||
execute(args: string[]): string {
|
||||
if (args.length === 0) return "touch: missing file operand";
|
||||
this.syscall.createFile(args[0]);
|
||||
return "";
|
||||
}
|
||||
}
|
58
src/shell/system.ts
Normal file
58
src/shell/system.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { echo } from "./commands/echo";
|
||||
import { ls } from "./commands/ls";
|
||||
import { pwd } from "./commands/pwd";
|
||||
import { cd } from "./commands/cd";
|
||||
import { touch } from "./commands/touch";
|
||||
import { cat } from "./commands/cat";
|
||||
import { rm } from "./commands/rm";
|
||||
import { IShellCommand } from "./commands/IShellCommand";
|
||||
import { FileSystem } from "./FileSystem";
|
||||
import { ShellSyscall } from "./ShellSyscall";
|
||||
|
||||
// System class to manage commands
|
||||
class System {
|
||||
private static instance: System | null = null;
|
||||
private commands: Array<IShellCommand>;
|
||||
private fs: FileSystem;
|
||||
private cwd: string;
|
||||
|
||||
constructor() {
|
||||
this.commands = new Array<IShellCommand>();
|
||||
this.fs = new FileSystem();
|
||||
this.cwd = '/';
|
||||
this.initializeCommands();
|
||||
}
|
||||
|
||||
private initializeCommands(): void {
|
||||
const syscall = new ShellSyscall(this.fs, this.cwd);
|
||||
this.commands.push(new echo());
|
||||
this.commands.push(new ls(syscall));
|
||||
this.commands.push(new pwd(syscall));
|
||||
this.commands.push(new cd(syscall));
|
||||
this.commands.push(new touch(syscall));
|
||||
this.commands.push(new cat(syscall));
|
||||
this.commands.push(new rm(syscall));
|
||||
}
|
||||
|
||||
public static getInstance(): System {
|
||||
if (!System.instance) {
|
||||
System.instance = new System();
|
||||
}
|
||||
return System.instance;
|
||||
}
|
||||
|
||||
knowsCommand(commandName: string): boolean {
|
||||
return this.commands.some(c => c.getName() === commandName);
|
||||
}
|
||||
|
||||
executeCommand(commandName: string, args: string[]): string {
|
||||
const command = this.commands.find(c => c.getName() === commandName);
|
||||
if (command) {
|
||||
return command.execute(args);
|
||||
}
|
||||
return `Command not found: ${commandName}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Export the System class and getInstance for use in other files
|
||||
export { System };
|
Reference in New Issue
Block a user