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