From e853268e527c61f394271ac316f784734a05c72d Mon Sep 17 00:00:00 2001 From: ChaosMakerMLG Date: Thu, 20 Nov 2025 14:10:20 +0100 Subject: [PATCH] Bash and IO working, basic error setup. Changelog: Commands: - ls (only with -l) - cd (basic, probably unfinished) --- src/lib/stores/bash/bash.ts | 94 ++++---- src/lib/stores/bash/commands/ls.ts | 252 ++++++++++++++++++++++ src/lib/stores/bash/fs.ts | 32 ++- src/lib/stores/bash/sort.ts | 47 ++++ src/lib/stores/bash/static.ts | 88 +++++--- src/lib/stores/char.ts | 76 +++++++ src/lib/stores/terminal/init.svelte.ts | 82 +++++++ src/lib/stores/terminal/stdio.ts | 41 ++++ src/lib/stores/{ => terminal}/terminal.ts | 70 +++--- src/modules/Terminal.svelte | 205 ++++++------------ src/modules/terminal/Cursor.svelte | 10 - src/modules/terminal/Input.svelte | 39 ++-- src/modules/terminal/Output.svelte | 20 ++ src/routes/+page.svelte | 160 +------------- src/routes/test/+page.svelte | 0 15 files changed, 775 insertions(+), 441 deletions(-) create mode 100644 src/lib/stores/bash/commands/ls.ts create mode 100644 src/lib/stores/bash/sort.ts create mode 100644 src/lib/stores/char.ts create mode 100644 src/lib/stores/terminal/init.svelte.ts create mode 100644 src/lib/stores/terminal/stdio.ts rename src/lib/stores/{ => terminal}/terminal.ts (54%) delete mode 100644 src/modules/terminal/Cursor.svelte create mode 100644 src/modules/terminal/Output.svelte create mode 100644 src/routes/test/+page.svelte diff --git a/src/lib/stores/bash/bash.ts b/src/lib/stores/bash/bash.ts index 191f894..2297f57 100644 --- a/src/lib/stores/bash/bash.ts +++ b/src/lib/stores/bash/bash.ts @@ -1,44 +1,43 @@ -import { command } from '$app/server'; -import { COMMANDS, GROUP, HELP_ARGS, PASSWD, type CommandArg, type ICommand } from './static'; +import { COMMANDS, GROUP, PASSWD, type CommandArgs, type ICommand, type Result } from './static'; import { VirtualFS } from './fs'; -import { Terminal, type PrintData } from '../terminal'; +import { Terminal, type PrintData } from '../terminal/terminal'; import { Stack } from '../stack'; -import path from 'path'; -export interface Permission { +export type Permission = { r: boolean; w: boolean; x: boolean; -} +}; -export interface BashInitArgs { +export type BashInitArgs = { stdio?: Terminal; user: User; fs: any; -} +}; // TODO: Finish this +// TODO: Change into a type instead of an enum for performance (low priority) export enum ExitCode { SUCCESS = 0, ERROR = 1 } -export interface User { +export type User = { username: string; - passwd: string; //HASHED PASSWORD - uid: number; // Normal user 1000+ System user 1-999 root - 0 - gid: number; // Primary group | 'Users' 1000 - Others - 1000+ root - 0 - home: string; + passwd: string; //HASHED PASSWORD //TODO: Make a formated type + readonly uid: number; // Normal user 1000+ System user 1-999 root - 0 //TODO: Make a formated type + readonly gid: number; // Primary group | 'Users' 1000 - Others - 1000+ root - 0 //TODO: Make a formated type + home: string; //TODO: Make a formated type history: string[]; - cwd?: string[]; - pwd?: string[]; -} + cwd?: string[]; //TODO: Make a formated type + pwd?: string[]; //TODO: Make a formated type +}; -export interface Group { +export type Group = { groupname: string; gid: number; // Primary group 'Users' 1000 - Others - 1000+ root - 0 - members: string[]; -} + members: number[]; //TODO: Make a formated type UID +}; export class Bash { private vfs: VirtualFS; @@ -47,12 +46,10 @@ export class Bash { private _group: Group[]; private _terminal!: Terminal; private user: User; - private _helpArgs: CommandArg[]; - private _commands: Record; + private readonly _commands: Record; constructor(args: BashInitArgs) { this.user = args.user; - this._helpArgs = HELP_ARGS; this._commands = COMMANDS; this._passwd = PASSWD; this._group = GROUP; @@ -60,8 +57,6 @@ export class Bash { this._instances = new Stack(); this.vfs = new VirtualFS({ fs: args.fs, user: args.user }); - - console.log(this._commands); } updateHistory(input: string): void { @@ -90,6 +85,10 @@ export class Bash { return this.vfs; } + hasSudoPerms(uid: number): boolean { + return this._group[1].members.includes(uid); + } + changeUser(user: User) { this.user = user; this.vfs.home = this.vfs._splitPathString(user.home); @@ -97,27 +96,28 @@ export class Bash { this.vfs.pwd = user.pwd ? user.pwd : this.vfs._splitPathString(user.home); } - executeCommand(commandName: string, ...args: string[]): void { + executeCommand(commandName: string, args: CommandArgs): void { + let result: Result = { exitCode: ExitCode.ERROR }; const command = this._commands[commandName]; - if (!command) this.throwError(ExitCode.ERROR); + if (!command) this.throwError(result); + if (command.root) { - if (this._group[1].members.includes(this.user.username)) { - let out: ExitCode = command.method.call(this, ...args); - this.throwError(out); + if (this.hasSudoPerms(this.user.uid)) { + let out: Result = command.method.call(this, args); + this.appendNewResult(this.getPwd(), out, this.user.history[0]); } - this.throwError(ExitCode.ERROR); + this.throwError(result); } - let out: ExitCode = command.method.call(this, ...args); - this.throwError(out); + let out: Result = command.method.call(this, args); + this.appendNewResult(this.getPwd(), out.data?.data, this.user.history[0]); } - throwError(code: ExitCode, data?: any): void { - //TODO: Make data some interface format or smh. - switch (code) { - default: - this.appendNewResult(this.vfs.pwd, 'Success!'); - break; + throwError(result: Result): void { + switch (result.exitCode) { + default: { + throw new Error(`Error, dont know where, just look for it;`); + } } } @@ -141,12 +141,24 @@ export class Bash { } } - appendNewResult(path: string[], output: any) { + private appendNewResult(path: string[], output: any, cmd: string) { const data: PrintData = { path: this.vfs.formatPath(this.vfs.pathArrayToString(path)), - output: output + output: output, + cmd: cmd }; - console.log('NEW RESULT - ', data); this._terminal.PrintOutput(data); } + + formatBytes(bytes: number, dPoint?: number): string { + if (!+bytes) return '0'; + + const k: number = 1024; + const dp: number = dPoint ? (dPoint < 0 ? 0 : dPoint) : 1; + const units: string[] = ['', 'K', 'M', 'G', 'T', 'P']; + + const i: number = Math.floor(Math.log(bytes) / Math.log(k)); + + return `${(bytes / Math.pow(k, i)).toFixed(dp)}${units[i]}`; + } } diff --git a/src/lib/stores/bash/commands/ls.ts b/src/lib/stores/bash/commands/ls.ts new file mode 100644 index 0000000..f729b51 --- /dev/null +++ b/src/lib/stores/bash/commands/ls.ts @@ -0,0 +1,252 @@ +import { Bash, ExitCode, type Permission } from '../bash'; +import { Type, type NodePerms, type TreeNode } from '../fs'; +import { asciiByteQSort } from '../sort'; +import type { CommandArgs, ICommand, Result, resultData } from '../static'; + +type LsEntry = { + perms: string; + children: string; + owners: string; + size: string; + modt: string; + name: string; +}; + +const LsEntryUtils = { + toString(entry: LsEntry): string { + return Object.entries(entry) + .filter(([_, value]) => value !== '') + .map(([_, value]) => value) + .join(' '); + } +}; + +const months: readonly string[] = [ + 'Jan', + 'Feb', + 'Mar', + 'Apr', + 'May', + 'Jun', + 'Jul', + 'Aug', + 'Sep', + 'Oct', + 'Nov', + 'Dec' +]; + +export const cmd_ls = function (this: Bash, args: CommandArgs): Result { + const resultData: resultData = { cmd: 'ls', data: null, args: args }; + const result: Result = { exitCode: ExitCode.ERROR, data: resultData }; + const nodes: TreeNode[] = []; + const paths: string[][] = []; + + //Check if args contain any nonexistent flags, if so add it to an array and check its length. if 0 no bad flags + const invalidItems = args.flags.filter((flag) => !ls.flags.includes(flag)); + console.log(invalidItems); + if (invalidItems.length > 0) { + this.throwError(result); //No such flag + } + + if (args.args.length === 0) { + const node = this.getFs()._getNodeByPathArray(this.getFs().cwd); + if (node === null) this.throwError(result); //no such path + + nodes.push(node!); + } + + for (let i = 0; i < args.args.length; i++) { + if (args.args.length !== 0) paths.push(this.getFs().resolvePath(args.args[i])); + + const node = this.getFs()._getNodeByPathArray(paths[i]); + if (node === null) this.throwError(result); //no such path + + nodes.push(node!); + } + + result.exitCode = ExitCode.SUCCESS; + resultData.data = result_ls.call(this, nodes, args); + return result; +}; + +function result_ls(this: Bash, data: any, args: CommandArgs): HTMLElement { + const dummysonoerror: HTMLElement = document.createElement('div'); + + const flagInfo = checkFlags(args.flags, ls.flags); + const nodes: TreeNode[] = data; + + const f_a: boolean = flagInfo.has('a') || flagInfo.has('f'); + const f_h: boolean = flagInfo.has('h'); + + if (flagInfo.has('l')) { + const w: HTMLElement = document.createElement('div'); + + for (const node of nodes) { + if (!flagInfo.has('U') && !flagInfo.has('f')) asciiByteQSort(node.children); + + const elem: HTMLElement = document.createElement('div'); + const rows: string[] = []; + + //TODO: Actually calculate sizes here instead of defining numbers for types of nodes + const sizes = node.children.map((child) => (child.type === Type.Directory ? '4096' : '1')); + const maxSizeWidth = Math.max(...sizes.map((size) => size.length)); + + if (f_a && !flagInfo.has('A')) { + const current: LsEntry = { + perms: formatPermission(node), + children: formatChildren(node), + owners: formatOwners(node, flagInfo), + size: formatSize.call(this, f_h, node, maxSizeWidth), + modt: formatModtime(node), + name: '.' + }; + const parent: LsEntry = node.parent + ? { + perms: formatPermission(node.parent), + children: formatChildren(node.parent), + owners: formatOwners(node.parent, flagInfo), + size: formatSize.call(this, f_h, node.parent, maxSizeWidth), + modt: formatModtime(node.parent), + name: '..' + } + : { + ...current, + name: '..' + }; + + rows.push(LsEntryUtils.toString(current), LsEntryUtils.toString(parent)); + } + + for (const child of node.children) { + if (child.name.startsWith('.') && !(f_a || flagInfo.has('A'))) continue; + + const cols: LsEntry = { + perms: formatPermission(child), + children: formatChildren(child), + owners: formatOwners(child, flagInfo), + size: formatSize.call(this, f_h, child, maxSizeWidth), + modt: formatModtime(child), + name: /\s/.test(child.name) ? `'${child.name}'` : `${child.name}` + }; + + if (flagInfo.has('g')) cols.owners = ''; + + rows.push(LsEntryUtils.toString(cols)); + } + + //TODO: Calculate the total size of contents in the node + rows.unshift('total ' + node.children.length.toString()); + + if (nodes.length > 1) { + const nodePath: string = + node.name === '/' ? '/:' : `${this.getFs()._getPathToNode(node).join('/').slice(1)}:`; + rows.unshift(nodePath); + rows.push('\n'); + } + + for (let i = 0; i < rows.length; i++) { + const p: HTMLElement = document.createElement('p'); + p.innerText = rows[i]; + + elem.appendChild(p); + } + + w.appendChild(elem); + } + return w; + } + + return dummysonoerror; //TEMP SO NO ERROR CUZ RETURNS HTMLElement EVERY TIME, DELETE LATER +} + +function parsePerms(perms: NodePerms): string { + const parts: string[] = []; + //for each key (key representing key name and p representing the key contents) of entries in perms as types keyof NodePerms and Permission + for (const [key, p] of Object.entries(perms) as [keyof NodePerms, Permission][]) { + const perms: string = `${p.r ? 'r' : '-'}${p.w ? 'w' : '-'}${p.x ? 'x' : '-'}`; + parts.push(perms); + } + return parts.join(''); +} + +function formatOwners(node: TreeNode, flag: any): string { + const owner: string = node.owner; + const group: string = node.group; + + if (flag.has('g')) { + return ''; + } + + return `${owner} ${group}`; +} + +function formatPermission(node: TreeNode): string { + return `${node.type === Type.Directory ? 'd' : '-'}${parsePerms(node.permission)}`; +} + +function formatChildren(node: TreeNode): string { + const c = node.children.length.toString(); + return c.length > 1 ? c : ` ${c}`; +} + +function formatSize(this: Bash, human: boolean, node: TreeNode, max: number): string { + const byteSize: number = node.type === Type.Directory ? 4096 : 1; //TEMP, later calculate the size. + let size: string; + if (human) { + size = this.formatBytes(byteSize); + } else size = byteSize.toString(); + + return size.padStart(max, ' '); +} + +function formatModtime(node: TreeNode): string { + const now = new Date(); + const hours: string = node.modtime.getHours().toString().padStart(2, '0'); + const minutes: string = node.modtime.getMinutes().toString().padStart(2, '0'); + const time: string = + now.getFullYear() === node.modtime.getFullYear() + ? `${hours}:${minutes}` + : node.modtime.getFullYear().toString(); + + return [ + months[node.modtime.getMonth()], + node.modtime.getDate().toString().padStart(2, ' '), + `${time}` + ].join(' '); +} + +const checkFlags = (pFlags: string[], dFlags: string[]) => { + const flagSet = new Set(pFlags); + + return { has: (flag: string) => flagSet.has(flag) }; +}; + +export const ls: ICommand = { + method: cmd_ls, + flags: [ + 'l', + 'a', + 'A', + 'c', + 'U', + 'g', + 'G', + 'h', + 'f', + 'x', + 'X', + 'u', + 't', + 'S', + 'r', + 'Q', + 'p', + 'o', + 'n', + 'N', + 'L' + ] as string[], + help: 'PATH TO HELP.MD', + root: false +}; diff --git a/src/lib/stores/bash/fs.ts b/src/lib/stores/bash/fs.ts index 2abdbf3..2084454 100644 --- a/src/lib/stores/bash/fs.ts +++ b/src/lib/stores/bash/fs.ts @@ -1,3 +1,4 @@ +import type { readonly } from 'svelte/store'; import type { Permission, User } from './bash'; export enum Type { @@ -5,18 +6,18 @@ export enum Type { File = 32768 } -type NodePerms = { +export type NodePerms = { user: Permission; group: Permission; other: Permission; }; -export interface FsInitArgs { +export type FsInitArgs = { fs: any; user: User; -} +}; -export interface TreeNode { +export type TreeNode = { name: string; type: Type; readonly: boolean; @@ -29,7 +30,8 @@ export interface TreeNode { owner: string; group: string; modtime: Date; -} + parent?: TreeNode; +}; export class VirtualFS { private root: TreeNode; // TODO make this the correct type @@ -73,7 +75,6 @@ export class VirtualFS { } formatPath(path: string): string { - console.log('FORMAT PATH ', path); const prefix = this.pathArrayToString(this.home); if (path.startsWith(prefix)) { return path.replace(prefix, '~'); @@ -91,9 +92,7 @@ export class VirtualFS { } const start = this._isAbsolutePath(path) ? [] : this.cwd.slice(); - console.log('START', start); const parts = this._splitPathString(path); - console.log('PARTS', parts); for (let i = 0; i < parts.length; i++) { const seg = parts[i]; @@ -111,7 +110,7 @@ export class VirtualFS { return start; } - _getNodeByPathArray(path: string[]): TreeNode { + _getNodeByPathArray(path: string[]): TreeNode | null { if (path.length === 1 && path[0] === '/') return this.root; let node: TreeNode = this.root; @@ -122,9 +121,24 @@ export class VirtualFS { if (node.type === Type.File) return node; const newNode = node.children.find((child) => child.name === seg); + console.log(newNode); if (newNode !== undefined) node = newNode; + else return null; } return node; } + + _getPathToNode(node: TreeNode): string[] { + const path: string[] = []; + let current = node; + path.push(node.name); + + while (current.parent) { + current = current.parent; + path.unshift(current.name); + } + + return path; + } } diff --git a/src/lib/stores/bash/sort.ts b/src/lib/stores/bash/sort.ts new file mode 100644 index 0000000..9693aeb --- /dev/null +++ b/src/lib/stores/bash/sort.ts @@ -0,0 +1,47 @@ +import type { TreeNode } from './fs'; + +export function asciiByteQSort(array: TreeNode[]) { + qSort(array, 0, array.length - 1); +} + +function qSort(array: TreeNode[], start: number, end: number) { + if (end <= start) return; + + let pivot: number = partition(array, start, end); + qSort(array, start, pivot - 1); + qSort(array, pivot + 1, end); +} + +function partition(part: TreeNode[], start: number, end: number): number { + let pivot: TreeNode = part[end]; + let i: number = start - 1; + + for (let j = start; j <= end; j++) { + if (compareStrings(part[j].name, pivot.name) < 0) { + i++; + let temp = part[i]; + part[i] = part[j]; + part[j] = temp; + } + } + i++; + let temp = part[i]; + part[i] = part[end]; + part[end] = temp; + + return i; +} + +function compareStrings(a: string, b: string): number { + const minLength = Math.min(a.length, b.length); + + for (let i = 0; i < minLength; i++) { + const charCodeA = a.charCodeAt(i); + const charCodeB = b.charCodeAt(i); + + if (charCodeA !== charCodeB) { + return charCodeA - charCodeB; + } + } + return a.length - b.length; +} diff --git a/src/lib/stores/bash/static.ts b/src/lib/stores/bash/static.ts index edae687..3948c5c 100644 --- a/src/lib/stores/bash/static.ts +++ b/src/lib/stores/bash/static.ts @@ -1,27 +1,43 @@ import { Bash, ExitCode, type Group, type User } from './bash'; import { Type, type TreeNode } from './fs'; +import type { Char } from '../char'; +import { ls } from './commands/ls'; -export type CommandArg = `-${string}`; - -export interface ICommand { - method: (this: Bash, ...args: any[]) => ExitCode; - args: CommandArg[] | string[] | null; +export type ICommand = { + method: (this: Bash, args: CommandArgs) => Result; + flags: string[]; help: string; root: boolean; -} +}; + +export type CommandArgs = { + flags: string[]; + args: string[]; +}; + +export type resultData = { + cmd: string; //the string that contains the shorthand for the command that was executed - used in a switch statement in parseResult + data: any; //the data that the commmand may have returned like TreeNodes[] from ls + args?: CommandArgs; +}; + +export type Result = { + exitCode: ExitCode; + data?: resultData; +}; export const GROUP: Group[] = [ { groupname: 'sudo', gid: 69, - members: ['root', 'admin'] + members: [0, 1001] }, { groupname: 'users', gid: 1000, - members: ['admin', 'user'] + members: [1001, 1002] } -]; +] as const; export const PASSWD: User[] = [ { @@ -50,30 +66,32 @@ export const PASSWD: User[] = [ } ]; -export const HELP_ARGS: CommandArg[] = ['-h', '--help']; - -export const cmd_return = function (this: Bash, ...args: string[]): ExitCode { - return 0; +export const cmd_return = function (this: Bash, args: CommandArgs): Result { + let result: Result = { exitCode: ExitCode.ERROR }; + return result; }; -export const cmd_cd = function (this: Bash, ...args: string[]): ExitCode { - const path = args[0]; - let targetNode: TreeNode; +export const cmd_cd = function (this: Bash, args: CommandArgs): Result { + let result: Result = { exitCode: ExitCode.ERROR }; + const path = args.args[0]; + let targetNode: TreeNode | null; - if (args.length > 1) return ExitCode.ERROR; // Too many args + if (args.args.length > 1) return result; // Too many args // if no args cd into home dir - if (args.length === 0) { + if (args.args.length === 0) { this.getFs().cwd = this.getFs().home; - return ExitCode.SUCCESS; + result.exitCode = ExitCode.SUCCESS; + return result; } // if the arg is - cd make your current dir the prev dir and vice versa - if (args[0] === '-') { + if (args.args[0] === '-') { [this.getFs().cwd, this.getFs().pwd] = [this.getFs().pwd, this.getFs().cwd]; - return ExitCode.SUCCESS; + result.exitCode = ExitCode.SUCCESS; + return result; } // Change the input STRING path from relative to absolute by replacing ~ with the home directory path @@ -87,28 +105,42 @@ export const cmd_cd = function (this: Bash, ...args: string[]): ExitCode { this.getFs().pwd = this.getFs().cwd; targetNode = this.getFs()._getNodeByPathArray(this.getFs().resolvePath(resolvedPath)); // Conversion from STRING path to ARRAY - if (!targetNode) return ExitCode.ERROR; - if (targetNode.type !== Type.Directory) return ExitCode.ERROR; + if (targetNode === null) return result; + if (targetNode.type !== Type.Directory) return result; //if () return ExitCode.ERROR; // Check for read permissions on node and user this.getFs().cwd = this.getFs().resolvePath(resolvedPath); // CD was successfull, change current dir to the verified target dir - return ExitCode.SUCCESS; + result.exitCode = ExitCode.SUCCESS; + return result; }; +/* const compareArrays = (A: string[], B: string[]): { value: string; isInB: boolean }[] => { + const result = A.map((item) => ({ value: item, isInB: B.includes(item) })); + + // Validate all B items are in A + const invalidItems = B.filter((item) => !A.includes(item)); + if (invalidItems.length > 0) { + throw new Error(`Items '${invalidItems.join("', '")}' from B not found in A`); + } + + return result; +}; */ + export const COMMANDS = { return: { method: cmd_return, - args: [] as CommandArg[], + flags: [] as string[], help: 'PATH TO HELP.MD', root: false }, cd: { method: cmd_cd, - args: [] as string[], + flags: [] as string[], help: 'PATH TO HELP.MD', root: false - } -} satisfies Record; + }, + ls +} as const satisfies Record; /* //export const commands { return: { diff --git a/src/lib/stores/char.ts b/src/lib/stores/char.ts new file mode 100644 index 0000000..801ea24 --- /dev/null +++ b/src/lib/stores/char.ts @@ -0,0 +1,76 @@ +type BrandedChar = string & { readonly __brand: unique symbol }; + +export class Char { + private readonly value: BrandedChar; + + private constructor(char: string) { + if (char.length !== 1) { + throw new Error('Char must be exactly one character'); + } + this.value = char as BrandedChar; + } + + // Public factory method + static from(char: string): Char { + return new Char(char); + } + + // Get the primitive value + valueOf(): string { + return this.value; + } + + toString(): string { + return this.value; + } + + // Character operations + charCode(): number { + return this.value.charCodeAt(0); + } + + isDigit(): boolean { + return /^\d$/.test(this.value); + } + + isLetter(): boolean { + return /^[a-zA-Z]$/.test(this.value); + } + + isAlphanumeric(): boolean { + return /^[a-zA-Z0-9]$/.test(this.value); + } + + isWhitespace(): boolean { + return /^\s$/.test(this.value); + } + + isUpperCase(): boolean { + return this.value === this.value.toUpperCase() && this.value !== this.value.toLowerCase(); + } + + isLowerCase(): boolean { + return this.value === this.value.toLowerCase() && this.value !== this.value.toUpperCase(); + } + + toUpperCase(): Char { + return Char.from(this.value.toUpperCase()); + } + + toLowerCase(): Char { + return Char.from(this.value.toLowerCase()); + } + + equals(other: Char): boolean { + return this.value === other.value; + } + + equalsIgnoreCase(other: Char): boolean { + return this.value.toLowerCase() === other.value.toLowerCase(); + } + + // Static constructors + static fromCharCode(charCode: number): Char { + return Char.from(String.fromCharCode(charCode)); + } +} diff --git a/src/lib/stores/terminal/init.svelte.ts b/src/lib/stores/terminal/init.svelte.ts new file mode 100644 index 0000000..b85af85 --- /dev/null +++ b/src/lib/stores/terminal/init.svelte.ts @@ -0,0 +1,82 @@ +import type { User } from '../bash/bash'; +import type { TreeNode } from '../bash/fs'; +import { Terminal, type TermInitArgs } from './terminal'; + +let initializing = $state(true); + +export function isInitializing(): boolean { + return initializing; +} + +function jsonToTreeNode(data: any, parent?: TreeNode): TreeNode { + const node: TreeNode = { + name: data.Name, + type: data.Type, + readonly: data.ReadOnly, + interactible: data.Interactible, + func: data.Func, + children: [], + content: data.Content, + link: data.Link || [], + permission: { + user: { + r: data.Permission[0]?.Read, + w: data.Permission[0]?.Write, + x: data.Permission[0]?.Exec + }, + group: { + r: data.Permission[1]?.Read, + w: data.Permission[1]?.Write, + x: data.Permission[1]?.Exec + }, + other: { + r: data.Permission[2]?.Read, + w: data.Permission[2]?.Write, + x: data.Permission[2]?.Exec + } + }, + owner: data.Owner, + group: data.Group, + modtime: new Date(data.Mtime), + parent: parent + }; + + node.children = data.Children + ? data.Children.map((child: any) => jsonToTreeNode(child, node)) + : []; + + return node; +} + +async function fetchFileSystem(path: string): Promise { + const response = await fetch(path); + if (!response.ok) throw new Error('Failed to fetch the file system json'); + + const data = await response.json(); + + const node: TreeNode = jsonToTreeNode(data); + return node; +} + +export async function initTerminal(user: User, callbackInit: any): Promise { + try { + let fsJson = await fetchFileSystem('/src/lib/assets/fs/fs.json'); + + let args: TermInitArgs = { + bash: { + user: user, + fs: fsJson + } + }; + + let terminal = new Terminal(args); + terminal.registerCallbacks(callbackInit); + + return terminal; + } catch (error) { + console.error('Failed to initialize terminal:', error); + throw error; + } finally { + initializing = false; + } +} diff --git a/src/lib/stores/terminal/stdio.ts b/src/lib/stores/terminal/stdio.ts new file mode 100644 index 0000000..ca8e6af --- /dev/null +++ b/src/lib/stores/terminal/stdio.ts @@ -0,0 +1,41 @@ +import { mount, unmount } from 'svelte'; +import Output from '../../../modules/terminal/Output.svelte'; +import { isInitializing } from './init.svelte'; +import type { PrintData } from './terminal'; + +interface OutputProps { + path: string; + output: any; + cmd: string; +} + +const outputInstances = new Set(); + +function appendOutput(container: HTMLElement, props: OutputProps): Output | undefined { + if (!container) return; + const instance = mount(Output, { + target: container, + props + }); + + outputInstances.add(instance); + return instance; +} + +export function print(e: HTMLElement, data: PrintData): void { + if (isInitializing()) { + console.error('Terminal is initializing! Skipping Print'); + return; + } + appendOutput(e, { + path: data.path, + output: data.output, + cmd: data.cmd + }); +} + +export function clear(): void { + for (const n of outputInstances) { + unmount(n); + } +} diff --git a/src/lib/stores/terminal.ts b/src/lib/stores/terminal/terminal.ts similarity index 54% rename from src/lib/stores/terminal.ts rename to src/lib/stores/terminal/terminal.ts index 203b81f..b59c835 100644 --- a/src/lib/stores/terminal.ts +++ b/src/lib/stores/terminal/terminal.ts @@ -1,28 +1,28 @@ -import { FileOutput } from '@lucide/svelte'; -import Cursor from '../../modules/terminal/Cursor.svelte'; -import { Bash, ExitCode, type BashInitArgs, type User } from './bash/bash'; -import { Stack } from './stack'; -import type { VirtualFS } from './bash/fs'; +import { Bash, ExitCode, type BashInitArgs, type User } from '../bash/bash'; +import type { VirtualFS } from '../bash/fs'; +import type { CommandArgs } from '../bash/static'; +import { Char } from '../char'; -export interface TerminalMode {} +export type TerminalMode = {}; -export interface TermInitArgs { +export type TermInitArgs = { bash: BashInitArgs; -} +}; -export interface ParsedInput { +export type ParsedInput = { command: string; - args: string[]; -} + args: CommandArgs; +}; -export interface PrintData { +export type PrintData = { path: string; output: any; // TODO: Make this be any predefined format of outputs like ls, ls w/ flags and so on; -} + cmd: string; +}; -export interface PageCallbacks { +export type PageCallbacks = { print: (data: PrintData) => void; -} +}; export class Terminal { private bash: Bash; @@ -34,41 +34,59 @@ export class Terminal { } private _parseInput(input: string): ParsedInput { - const result: ParsedInput = { command: '', args: [] }; + let args: string[] = []; + const result: ParsedInput = { command: '', args: { flags: [], args: [] } }; let current: string = ''; let inQuotes: boolean = false; - let quoteChar: Stack = new Stack(); + let quoteChar: string = ''; for (let i = 0; i < input.length; i++) { const char = input[i]; if ((char === '"' || char === "'") && !inQuotes) { inQuotes = true; - quoteChar.push(char); + quoteChar = char; continue; - } else if (char === quoteChar.peek() && inQuotes) { + } else if (char === quoteChar && inQuotes) { inQuotes = false; - quoteChar.pop(); continue; } if (char === ' ' && !inQuotes) { - if (current !== '') { - result.command = current; - current = ''; - } + if (current === '') continue; + + result.command === '' ? (result.command = current) : args.push(current); + current = ''; } else { current += char; } } - if (current !== '') result.args.push(current); + + if (current !== '') result.command === '' ? (result.command = current) : args.push(current); + + for (let i = 0; i < args.length; i++) { + let curr = args[i]; + + if (!curr.startsWith('-')) result.args.args.push(curr); + else { + curr = curr.replaceAll('-', ''); + + if (curr.length > 0) { + for (let n = 0; n < curr.length; n++) { + result.args.flags.push(curr[n]); + } + } + } + } + return result; } executeCommand(input: string): void { this.bash.updateHistory(input); const parsed: ParsedInput = this._parseInput(input); - this.bash.executeCommand(parsed.command, ...parsed.args); + console.log(parsed); + this.bash.executeCommand(parsed.command, parsed.args); } registerCallbacks(callbacks: PageCallbacks): void { diff --git a/src/modules/Terminal.svelte b/src/modules/Terminal.svelte index 58eca4e..ade926c 100644 --- a/src/modules/Terminal.svelte +++ b/src/modules/Terminal.svelte @@ -1,65 +1,46 @@ -
-
-
- - - + diff --git a/src/routes/test/+page.svelte b/src/routes/test/+page.svelte new file mode 100644 index 0000000..e69de29