implement shell command interface and add basic commands (echo, ls, pwd, cd, touch, cat, rm) with syscall integration
All checks were successful
Master Build / Build and Push Docker Image (amd64) (push) Successful in 53s
Master Build / Update running container (push) Successful in 52s

This commit is contained in:
2025-01-23 15:22:42 -03:00
parent 2817165386
commit f39333cae6
18 changed files with 519 additions and 46 deletions

View File

@@ -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>

View 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;

View File

@@ -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
View 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
View 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}`);
}
}

View File

@@ -0,0 +1,4 @@
export interface IShellCommand {
getName(): string;
execute(input: string | string[]): string;
}

15
src/shell/commands/cat.ts Normal file
View 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
View 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';
}
}

View 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
View 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
View 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
View 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}`;
}
}

View 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
View 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 };