Bash and IO working, basic error setup. Changelog:
Commands:
- ls (only with -l)
- cd (basic, probably unfinished)
This commit is contained in:
@@ -1,44 +1,43 @@
|
|||||||
import { command } from '$app/server';
|
import { COMMANDS, GROUP, PASSWD, type CommandArgs, type ICommand, type Result } from './static';
|
||||||
import { COMMANDS, GROUP, HELP_ARGS, PASSWD, type CommandArg, type ICommand } from './static';
|
|
||||||
import { VirtualFS } from './fs';
|
import { VirtualFS } from './fs';
|
||||||
import { Terminal, type PrintData } from '../terminal';
|
import { Terminal, type PrintData } from '../terminal/terminal';
|
||||||
import { Stack } from '../stack';
|
import { Stack } from '../stack';
|
||||||
import path from 'path';
|
|
||||||
|
|
||||||
export interface Permission {
|
export type Permission = {
|
||||||
r: boolean;
|
r: boolean;
|
||||||
w: boolean;
|
w: boolean;
|
||||||
x: boolean;
|
x: boolean;
|
||||||
}
|
};
|
||||||
|
|
||||||
export interface BashInitArgs {
|
export type BashInitArgs = {
|
||||||
stdio?: Terminal;
|
stdio?: Terminal;
|
||||||
user: User;
|
user: User;
|
||||||
fs: any;
|
fs: any;
|
||||||
}
|
};
|
||||||
|
|
||||||
// TODO: Finish this
|
// TODO: Finish this
|
||||||
|
// TODO: Change into a type instead of an enum for performance (low priority)
|
||||||
export enum ExitCode {
|
export enum ExitCode {
|
||||||
SUCCESS = 0,
|
SUCCESS = 0,
|
||||||
ERROR = 1
|
ERROR = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface User {
|
export type User = {
|
||||||
username: string;
|
username: string;
|
||||||
passwd: string; //HASHED PASSWORD
|
passwd: string; //HASHED PASSWORD //TODO: Make a formated type
|
||||||
uid: number; // Normal user 1000+ System user 1-999 root - 0
|
readonly uid: number; // Normal user 1000+ System user 1-999 root - 0 //TODO: Make a formated type
|
||||||
gid: number; // Primary group | 'Users' 1000 - Others - 1000+ root - 0
|
readonly gid: number; // Primary group | 'Users' 1000 - Others - 1000+ root - 0 //TODO: Make a formated type
|
||||||
home: string;
|
home: string; //TODO: Make a formated type
|
||||||
history: string[];
|
history: string[];
|
||||||
cwd?: string[];
|
cwd?: string[]; //TODO: Make a formated type
|
||||||
pwd?: string[];
|
pwd?: string[]; //TODO: Make a formated type
|
||||||
}
|
};
|
||||||
|
|
||||||
export interface Group {
|
export type Group = {
|
||||||
groupname: string;
|
groupname: string;
|
||||||
gid: number; // Primary group 'Users' 1000 - Others - 1000+ root - 0
|
gid: number; // Primary group 'Users' 1000 - Others - 1000+ root - 0
|
||||||
members: string[];
|
members: number[]; //TODO: Make a formated type UID
|
||||||
}
|
};
|
||||||
|
|
||||||
export class Bash {
|
export class Bash {
|
||||||
private vfs: VirtualFS;
|
private vfs: VirtualFS;
|
||||||
@@ -47,12 +46,10 @@ export class Bash {
|
|||||||
private _group: Group[];
|
private _group: Group[];
|
||||||
private _terminal!: Terminal;
|
private _terminal!: Terminal;
|
||||||
private user: User;
|
private user: User;
|
||||||
private _helpArgs: CommandArg[];
|
private readonly _commands: Record<string, ICommand>;
|
||||||
private _commands: Record<string, ICommand>;
|
|
||||||
|
|
||||||
constructor(args: BashInitArgs) {
|
constructor(args: BashInitArgs) {
|
||||||
this.user = args.user;
|
this.user = args.user;
|
||||||
this._helpArgs = HELP_ARGS;
|
|
||||||
this._commands = COMMANDS;
|
this._commands = COMMANDS;
|
||||||
this._passwd = PASSWD;
|
this._passwd = PASSWD;
|
||||||
this._group = GROUP;
|
this._group = GROUP;
|
||||||
@@ -60,8 +57,6 @@ export class Bash {
|
|||||||
this._instances = new Stack<User>();
|
this._instances = new Stack<User>();
|
||||||
|
|
||||||
this.vfs = new VirtualFS({ fs: args.fs, user: args.user });
|
this.vfs = new VirtualFS({ fs: args.fs, user: args.user });
|
||||||
|
|
||||||
console.log(this._commands);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateHistory(input: string): void {
|
updateHistory(input: string): void {
|
||||||
@@ -90,6 +85,10 @@ export class Bash {
|
|||||||
return this.vfs;
|
return this.vfs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hasSudoPerms(uid: number): boolean {
|
||||||
|
return this._group[1].members.includes(uid);
|
||||||
|
}
|
||||||
|
|
||||||
changeUser(user: User) {
|
changeUser(user: User) {
|
||||||
this.user = user;
|
this.user = user;
|
||||||
this.vfs.home = this.vfs._splitPathString(user.home);
|
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);
|
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];
|
const command = this._commands[commandName];
|
||||||
if (!command) this.throwError(ExitCode.ERROR);
|
if (!command) this.throwError(result);
|
||||||
|
|
||||||
if (command.root) {
|
if (command.root) {
|
||||||
if (this._group[1].members.includes(this.user.username)) {
|
if (this.hasSudoPerms(this.user.uid)) {
|
||||||
let out: ExitCode = command.method.call(this, ...args);
|
let out: Result = command.method.call(this, args);
|
||||||
this.throwError(out);
|
this.appendNewResult(this.getPwd(), out, this.user.history[0]);
|
||||||
}
|
}
|
||||||
this.throwError(ExitCode.ERROR);
|
this.throwError(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
let out: ExitCode = command.method.call(this, ...args);
|
let out: Result = command.method.call(this, args);
|
||||||
this.throwError(out);
|
this.appendNewResult(this.getPwd(), out.data?.data, this.user.history[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
throwError(code: ExitCode, data?: any): void {
|
throwError(result: Result): void {
|
||||||
//TODO: Make data some interface format or smh.
|
switch (result.exitCode) {
|
||||||
switch (code) {
|
default: {
|
||||||
default:
|
throw new Error(`Error, dont know where, just look for it;`);
|
||||||
this.appendNewResult(this.vfs.pwd, 'Success!');
|
}
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,12 +141,24 @@ export class Bash {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
appendNewResult(path: string[], output: any) {
|
private appendNewResult(path: string[], output: any, cmd: string) {
|
||||||
const data: PrintData = {
|
const data: PrintData = {
|
||||||
path: this.vfs.formatPath(this.vfs.pathArrayToString(path)),
|
path: this.vfs.formatPath(this.vfs.pathArrayToString(path)),
|
||||||
output: output
|
output: output,
|
||||||
|
cmd: cmd
|
||||||
};
|
};
|
||||||
console.log('NEW RESULT - ', data);
|
|
||||||
this._terminal.PrintOutput(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]}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
252
src/lib/stores/bash/commands/ls.ts
Normal file
252
src/lib/stores/bash/commands/ls.ts
Normal file
@@ -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
|
||||||
|
};
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { readonly } from 'svelte/store';
|
||||||
import type { Permission, User } from './bash';
|
import type { Permission, User } from './bash';
|
||||||
|
|
||||||
export enum Type {
|
export enum Type {
|
||||||
@@ -5,18 +6,18 @@ export enum Type {
|
|||||||
File = 32768
|
File = 32768
|
||||||
}
|
}
|
||||||
|
|
||||||
type NodePerms = {
|
export type NodePerms = {
|
||||||
user: Permission;
|
user: Permission;
|
||||||
group: Permission;
|
group: Permission;
|
||||||
other: Permission;
|
other: Permission;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface FsInitArgs {
|
export type FsInitArgs = {
|
||||||
fs: any;
|
fs: any;
|
||||||
user: User;
|
user: User;
|
||||||
}
|
};
|
||||||
|
|
||||||
export interface TreeNode {
|
export type TreeNode = {
|
||||||
name: string;
|
name: string;
|
||||||
type: Type;
|
type: Type;
|
||||||
readonly: boolean;
|
readonly: boolean;
|
||||||
@@ -29,7 +30,8 @@ export interface TreeNode {
|
|||||||
owner: string;
|
owner: string;
|
||||||
group: string;
|
group: string;
|
||||||
modtime: Date;
|
modtime: Date;
|
||||||
}
|
parent?: TreeNode;
|
||||||
|
};
|
||||||
|
|
||||||
export class VirtualFS {
|
export class VirtualFS {
|
||||||
private root: TreeNode; // TODO make this the correct type
|
private root: TreeNode; // TODO make this the correct type
|
||||||
@@ -73,7 +75,6 @@ export class VirtualFS {
|
|||||||
}
|
}
|
||||||
|
|
||||||
formatPath(path: string): string {
|
formatPath(path: string): string {
|
||||||
console.log('FORMAT PATH ', path);
|
|
||||||
const prefix = this.pathArrayToString(this.home);
|
const prefix = this.pathArrayToString(this.home);
|
||||||
if (path.startsWith(prefix)) {
|
if (path.startsWith(prefix)) {
|
||||||
return path.replace(prefix, '~');
|
return path.replace(prefix, '~');
|
||||||
@@ -91,9 +92,7 @@ export class VirtualFS {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const start = this._isAbsolutePath(path) ? [] : this.cwd.slice();
|
const start = this._isAbsolutePath(path) ? [] : this.cwd.slice();
|
||||||
console.log('START', start);
|
|
||||||
const parts = this._splitPathString(path);
|
const parts = this._splitPathString(path);
|
||||||
console.log('PARTS', parts);
|
|
||||||
|
|
||||||
for (let i = 0; i < parts.length; i++) {
|
for (let i = 0; i < parts.length; i++) {
|
||||||
const seg = parts[i];
|
const seg = parts[i];
|
||||||
@@ -111,7 +110,7 @@ export class VirtualFS {
|
|||||||
return start;
|
return start;
|
||||||
}
|
}
|
||||||
|
|
||||||
_getNodeByPathArray(path: string[]): TreeNode {
|
_getNodeByPathArray(path: string[]): TreeNode | null {
|
||||||
if (path.length === 1 && path[0] === '/') return this.root;
|
if (path.length === 1 && path[0] === '/') return this.root;
|
||||||
|
|
||||||
let node: TreeNode = this.root;
|
let node: TreeNode = this.root;
|
||||||
@@ -122,9 +121,24 @@ export class VirtualFS {
|
|||||||
|
|
||||||
if (node.type === Type.File) return node;
|
if (node.type === Type.File) return node;
|
||||||
const newNode = node.children.find((child) => child.name === seg);
|
const newNode = node.children.find((child) => child.name === seg);
|
||||||
|
console.log(newNode);
|
||||||
if (newNode !== undefined) node = newNode;
|
if (newNode !== undefined) node = newNode;
|
||||||
|
else return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return node;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
47
src/lib/stores/bash/sort.ts
Normal file
47
src/lib/stores/bash/sort.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
@@ -1,27 +1,43 @@
|
|||||||
import { Bash, ExitCode, type Group, type User } from './bash';
|
import { Bash, ExitCode, type Group, type User } from './bash';
|
||||||
import { Type, type TreeNode } from './fs';
|
import { Type, type TreeNode } from './fs';
|
||||||
|
import type { Char } from '../char';
|
||||||
|
import { ls } from './commands/ls';
|
||||||
|
|
||||||
export type CommandArg = `-${string}`;
|
export type ICommand = {
|
||||||
|
method: (this: Bash, args: CommandArgs) => Result;
|
||||||
export interface ICommand {
|
flags: string[];
|
||||||
method: (this: Bash, ...args: any[]) => ExitCode;
|
|
||||||
args: CommandArg[] | string[] | null;
|
|
||||||
help: string;
|
help: string;
|
||||||
root: boolean;
|
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[] = [
|
export const GROUP: Group[] = [
|
||||||
{
|
{
|
||||||
groupname: 'sudo',
|
groupname: 'sudo',
|
||||||
gid: 69,
|
gid: 69,
|
||||||
members: ['root', 'admin']
|
members: [0, 1001]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
groupname: 'users',
|
groupname: 'users',
|
||||||
gid: 1000,
|
gid: 1000,
|
||||||
members: ['admin', 'user']
|
members: [1001, 1002]
|
||||||
}
|
}
|
||||||
];
|
] as const;
|
||||||
|
|
||||||
export const PASSWD: User[] = [
|
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: CommandArgs): Result {
|
||||||
|
let result: Result = { exitCode: ExitCode.ERROR };
|
||||||
export const cmd_return = function (this: Bash, ...args: string[]): ExitCode {
|
return result;
|
||||||
return 0;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const cmd_cd = function (this: Bash, ...args: string[]): ExitCode {
|
export const cmd_cd = function (this: Bash, args: CommandArgs): Result {
|
||||||
const path = args[0];
|
let result: Result = { exitCode: ExitCode.ERROR };
|
||||||
let targetNode: TreeNode;
|
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 no args cd into home dir
|
||||||
|
|
||||||
if (args.length === 0) {
|
if (args.args.length === 0) {
|
||||||
this.getFs().cwd = this.getFs().home;
|
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 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];
|
[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
|
// 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;
|
this.getFs().pwd = this.getFs().cwd;
|
||||||
targetNode = this.getFs()._getNodeByPathArray(this.getFs().resolvePath(resolvedPath)); // Conversion from STRING path to ARRAY
|
targetNode = this.getFs()._getNodeByPathArray(this.getFs().resolvePath(resolvedPath)); // Conversion from STRING path to ARRAY
|
||||||
|
|
||||||
if (!targetNode) return ExitCode.ERROR;
|
if (targetNode === null) return result;
|
||||||
if (targetNode.type !== Type.Directory) return ExitCode.ERROR;
|
if (targetNode.type !== Type.Directory) return result;
|
||||||
//if () return ExitCode.ERROR; // Check for read permissions on node and user
|
//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
|
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 = {
|
export const COMMANDS = {
|
||||||
return: {
|
return: {
|
||||||
method: cmd_return,
|
method: cmd_return,
|
||||||
args: [] as CommandArg[],
|
flags: [] as string[],
|
||||||
help: 'PATH TO HELP.MD',
|
help: 'PATH TO HELP.MD',
|
||||||
root: false
|
root: false
|
||||||
},
|
},
|
||||||
cd: {
|
cd: {
|
||||||
method: cmd_cd,
|
method: cmd_cd,
|
||||||
args: [] as string[],
|
flags: [] as string[],
|
||||||
help: 'PATH TO HELP.MD',
|
help: 'PATH TO HELP.MD',
|
||||||
root: false
|
root: false
|
||||||
}
|
},
|
||||||
} satisfies Record<string, ICommand>;
|
ls
|
||||||
|
} as const satisfies Record<string, ICommand>;
|
||||||
|
|
||||||
/* //export const commands {
|
/* //export const commands {
|
||||||
return: {
|
return: {
|
||||||
|
|||||||
76
src/lib/stores/char.ts
Normal file
76
src/lib/stores/char.ts
Normal file
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
82
src/lib/stores/terminal/init.svelte.ts
Normal file
82
src/lib/stores/terminal/init.svelte.ts
Normal file
@@ -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<any> {
|
||||||
|
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<Terminal> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
41
src/lib/stores/terminal/stdio.ts
Normal file
41
src/lib/stores/terminal/stdio.ts
Normal file
@@ -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<Output>();
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,28 +1,28 @@
|
|||||||
import { FileOutput } from '@lucide/svelte';
|
import { Bash, ExitCode, type BashInitArgs, type User } from '../bash/bash';
|
||||||
import Cursor from '../../modules/terminal/Cursor.svelte';
|
import type { VirtualFS } from '../bash/fs';
|
||||||
import { Bash, ExitCode, type BashInitArgs, type User } from './bash/bash';
|
import type { CommandArgs } from '../bash/static';
|
||||||
import { Stack } from './stack';
|
import { Char } from '../char';
|
||||||
import type { VirtualFS } from './bash/fs';
|
|
||||||
|
|
||||||
export interface TerminalMode {}
|
export type TerminalMode = {};
|
||||||
|
|
||||||
export interface TermInitArgs {
|
export type TermInitArgs = {
|
||||||
bash: BashInitArgs;
|
bash: BashInitArgs;
|
||||||
}
|
};
|
||||||
|
|
||||||
export interface ParsedInput {
|
export type ParsedInput = {
|
||||||
command: string;
|
command: string;
|
||||||
args: string[];
|
args: CommandArgs;
|
||||||
}
|
};
|
||||||
|
|
||||||
export interface PrintData {
|
export type PrintData = {
|
||||||
path: string;
|
path: string;
|
||||||
output: any; // TODO: Make this be any predefined format of outputs like ls, ls w/ flags and so on;
|
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;
|
print: (data: PrintData) => void;
|
||||||
}
|
};
|
||||||
|
|
||||||
export class Terminal {
|
export class Terminal {
|
||||||
private bash: Bash;
|
private bash: Bash;
|
||||||
@@ -34,41 +34,59 @@ export class Terminal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _parseInput(input: string): ParsedInput {
|
private _parseInput(input: string): ParsedInput {
|
||||||
const result: ParsedInput = { command: '', args: [] };
|
let args: string[] = [];
|
||||||
|
const result: ParsedInput = { command: '', args: { flags: [], args: [] } };
|
||||||
let current: string = '';
|
let current: string = '';
|
||||||
let inQuotes: boolean = false;
|
let inQuotes: boolean = false;
|
||||||
let quoteChar: Stack<string> = new Stack<string>();
|
let quoteChar: string = '';
|
||||||
|
|
||||||
for (let i = 0; i < input.length; i++) {
|
for (let i = 0; i < input.length; i++) {
|
||||||
const char = input[i];
|
const char = input[i];
|
||||||
|
|
||||||
if ((char === '"' || char === "'") && !inQuotes) {
|
if ((char === '"' || char === "'") && !inQuotes) {
|
||||||
inQuotes = true;
|
inQuotes = true;
|
||||||
quoteChar.push(char);
|
quoteChar = char;
|
||||||
continue;
|
continue;
|
||||||
} else if (char === quoteChar.peek() && inQuotes) {
|
} else if (char === quoteChar && inQuotes) {
|
||||||
inQuotes = false;
|
inQuotes = false;
|
||||||
quoteChar.pop();
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (char === ' ' && !inQuotes) {
|
if (char === ' ' && !inQuotes) {
|
||||||
if (current !== '') {
|
if (current === '') continue;
|
||||||
result.command = current;
|
|
||||||
|
result.command === '' ? (result.command = current) : args.push(current);
|
||||||
current = '';
|
current = '';
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
current += char;
|
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;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
executeCommand(input: string): void {
|
executeCommand(input: string): void {
|
||||||
this.bash.updateHistory(input);
|
this.bash.updateHistory(input);
|
||||||
const parsed: ParsedInput = this._parseInput(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 {
|
registerCallbacks(callbacks: PageCallbacks): void {
|
||||||
@@ -1,65 +1,46 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { User } from '$lib/stores/bash/bash';
|
import type { User } from '$lib/stores/bash/bash';
|
||||||
import type { TreeNode } from '$lib/stores/bash/fs';
|
import { Terminal, type PrintData } from '$lib/stores/terminal/terminal';
|
||||||
import { Terminal, type TermInitArgs } from '$lib/stores/terminal';
|
import { onMount } from 'svelte';
|
||||||
import {
|
import Input from './terminal/Input.svelte';
|
||||||
onDestroy,
|
import { initTerminal, isInitializing } from '$lib/stores/terminal/init.svelte';
|
||||||
onMount,
|
import { clear, print } from '$lib/stores/terminal/stdio';
|
||||||
type Snippet,
|
|
||||||
type Component,
|
|
||||||
type ComponentProps,
|
|
||||||
mount
|
|
||||||
} from 'svelte';
|
|
||||||
|
|
||||||
let { children, username, cwd }: { children: Snippet; username: string; cwd: string } = $props();
|
const clearTerminal = (): void => clear();
|
||||||
|
|
||||||
/* function jsonToTreeNode(data: any): TreeNode {
|
const printOutput = (e: HTMLElement, d: PrintData): void => print(e, d);
|
||||||
return {
|
|
||||||
name: data.Name,
|
function updateTerminal() {
|
||||||
type: data.Type,
|
username = terminal!.getUser().username;
|
||||||
readonly: data.ReadOnly,
|
cwd = terminal!.getCwd();
|
||||||
interactible: data.Interactible,
|
|
||||||
func: data.Func,
|
|
||||||
children: data.Children ? data.Children.map((child: any) => jsonToTreeNode(child)) : [],
|
|
||||||
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)
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchFileSystem(path: string): Promise<any> {
|
function handleInput(e: KeyboardEvent) {
|
||||||
const response = await fetch(path);
|
switch (e.key) {
|
||||||
if (!response.ok) throw new Error('Failed to fetch the file system json');
|
case 'Enter': {
|
||||||
|
terminal.executeCommand(inputValue);
|
||||||
const data = await response.json();
|
break;
|
||||||
|
}
|
||||||
const node: TreeNode = jsonToTreeNode(data);
|
case 'ArrowRight': {
|
||||||
return node;
|
//TODO: Move cursor visually
|
||||||
|
}
|
||||||
|
case 'ArrowLeft': {
|
||||||
|
//TODO: Move cursor visually
|
||||||
|
}
|
||||||
|
case 'ArrowUp': {
|
||||||
|
//TODO: Make a traverse history function with up/down args
|
||||||
|
}
|
||||||
|
case 'ArrowDown': {
|
||||||
|
//TODO: Make a traverse history function with up/down args
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let callbackInit = {
|
let callbackInit = {
|
||||||
print: (data: any) => {
|
print: (data: any) => {
|
||||||
console.log('print callback executed');
|
const e = document.getElementById('outputWrapper');
|
||||||
//print(data);
|
if (!e) return;
|
||||||
|
printOutput(e, data);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -76,87 +57,20 @@
|
|||||||
let username: string = $state(testUser.username);
|
let username: string = $state(testUser.username);
|
||||||
let cwd: string = $state(testUser.home);
|
let cwd: string = $state(testUser.home);
|
||||||
|
|
||||||
let isInitializing = $state(true);
|
let inputValue = $state<string>('');
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
try {
|
try {
|
||||||
let fsJson = await fetchFileSystem('/src/lib/assets/fs/fs.json');
|
terminal = await initTerminal(testUser, callbackInit);
|
||||||
|
|
||||||
let args: TermInitArgs = {
|
|
||||||
bash: {
|
|
||||||
user: testUser,
|
|
||||||
fs: fsJson
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
terminal = new Terminal(args);
|
|
||||||
terminal.registerCallbacks(callbackInit);
|
|
||||||
|
|
||||||
username = terminal.getUser().username;
|
|
||||||
cwd = terminal.getCwd();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to initialize terminal:', error);
|
|
||||||
} finally {
|
|
||||||
updateTerminal();
|
updateTerminal();
|
||||||
isInitializing = false;
|
} catch (error) {
|
||||||
}
|
console.error('onMount trycatch failed');
|
||||||
});
|
|
||||||
|
|
||||||
function updateTerminal() {
|
|
||||||
username = terminal!.getUser().username;
|
|
||||||
cwd = terminal!.getCwd();
|
|
||||||
} */
|
|
||||||
|
|
||||||
let outputContainer = $state<HTMLElement>();
|
|
||||||
let instances = $state<Set<ReturnType<typeof mount>>>(new Set());
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
const scrollable = document.getElementById('cout');
|
|
||||||
|
|
||||||
const config = { childList: true };
|
|
||||||
|
|
||||||
const callback = function (mutationList: any, observer: any) {
|
|
||||||
for (let mutation of mutationList) {
|
|
||||||
if (mutation.type === 'childList') {
|
|
||||||
scrollable?.scrollTo(0, scrollable.scrollHeight);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const observer = new MutationObserver(callback);
|
|
||||||
observer.observe(scrollable!, config);
|
|
||||||
});
|
|
||||||
|
|
||||||
export function addComponent<T extends Component>(
|
|
||||||
component: T,
|
|
||||||
props: ComponentProps<T> = {} as ComponentProps<T>
|
|
||||||
): ReturnType<typeof mount> | undefined {
|
|
||||||
if (!outputContainer) return;
|
|
||||||
|
|
||||||
const instance = mount(component, {
|
|
||||||
target: outputContainer,
|
|
||||||
props
|
|
||||||
});
|
|
||||||
|
|
||||||
instances.add(instance);
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function clearComponents(): void {
|
|
||||||
for (const instance of instances) {
|
|
||||||
instance.$destroy();
|
|
||||||
}
|
|
||||||
instances.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
onDestroy(() => {
|
|
||||||
for (const instance of instances) {
|
|
||||||
instance.$destroy();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div id="terminal" class="terminal-window shadow-() size-full rounded-md shadow-bg">
|
<label for="input" onkeydowncapture={(e) => handleInput(e)}>
|
||||||
|
<div id="terminal" class="terminal-window shadow-() size-full resize rounded-md shadow-bg">
|
||||||
<div
|
<div
|
||||||
class="terminal-bar flex h-9 w-full flex-row items-center rounded-t-md bg-bg-dark text-center font-terminal text-sm font-bold text-primary-dark light:bg-bg-dark-light light:text-primary-light"
|
class="terminal-bar flex h-9 w-full flex-row items-center rounded-t-md bg-bg-dark text-center font-terminal text-sm font-bold text-primary-dark light:bg-bg-dark-light light:text-primary-light"
|
||||||
>
|
>
|
||||||
@@ -176,10 +90,11 @@
|
|||||||
class="inner-content scroll-hidden h-[860px] origin-top overflow-y-auto rounded-b-md bg-bg-light-dark p-4 text-text-dark shadow-subtle light:bg-bg-lighter-light light:text-text-light"
|
class="inner-content scroll-hidden h-[860px] origin-top overflow-y-auto rounded-b-md bg-bg-light-dark p-4 text-text-dark shadow-subtle light:bg-bg-lighter-light light:text-text-light"
|
||||||
id="cout"
|
id="cout"
|
||||||
>
|
>
|
||||||
<div bind:this={outputContainer}></div>
|
<div id="outputWrapper"></div>
|
||||||
{@render children()}
|
<Input {cwd} bind:inputValue />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</label>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
* {
|
* {
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
let { path, output }: { path: string; output: any } = $props(); //TODO: change any to matching
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<p class="cwd" id="cwd">{path}</p>
|
|
||||||
<div class="pointer-wrapper mr-4 mb-2.5 flex flex-row items-center font-terminal">
|
|
||||||
<span class="pointer pr-2">$</span>
|
|
||||||
<!-- prettier-ignore -->
|
|
||||||
<div style="white-space: preserve;" class=" relative wrap-break-word">{output}</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,47 +1,34 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
let inputElement = $state<HTMLInputElement>();
|
let {
|
||||||
|
inputValue = $bindable(),
|
||||||
let { value, isFocused, cwd }: { value?: string; isFocused?: boolean; cwd: string } = $props();
|
isFocused,
|
||||||
|
cwd
|
||||||
export function focus() {
|
}: { inputValue: string; isFocused?: boolean; cwd: string } = $props();
|
||||||
console.log('WOOOOW');
|
|
||||||
inputElement?.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function blur() {
|
|
||||||
console.log('beeeeeeeee');
|
|
||||||
inputElement?.blur();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setValue(newValue: string) {
|
|
||||||
if (inputElement) {
|
|
||||||
inputElement.value = newValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function clear() {
|
|
||||||
setValue('');
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleInput(event: Event) {
|
function handleInput(event: Event) {
|
||||||
value = (event.target as HTMLInputElement).value;
|
inputValue = (event.target as HTMLInputElement).value;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class=" relative">
|
<div class=" relative">
|
||||||
<input
|
<input
|
||||||
bind:this={inputElement}
|
bind:value={inputValue}
|
||||||
|
onkeydown={(e) => e.key === 'Enter' && (inputValue = '')}
|
||||||
oninput={handleInput}
|
oninput={handleInput}
|
||||||
onfocus={() => (isFocused = true)}
|
onfocus={() => (isFocused = true)}
|
||||||
onblur={() => (isFocused = false)}
|
onblur={() => (isFocused = false)}
|
||||||
type="text"
|
type="text"
|
||||||
|
autocapitalize="off"
|
||||||
|
autocomplete="off"
|
||||||
|
autocorrect="off"
|
||||||
class=" pointer-events-none absolute left-0 m-0 w-0 border-none p-0 opacity-0"
|
class=" pointer-events-none absolute left-0 m-0 w-0 border-none p-0 opacity-0"
|
||||||
|
id="input"
|
||||||
/>
|
/>
|
||||||
<p class="cwd" id="cwd">{cwd}</p>
|
<p class="cwd" id="cwd">{cwd}</p>
|
||||||
<div class="w flex-column flex flex-row flex-wrap font-terminal">
|
<div class="w flex-column flex flex-row flex-wrap font-terminal">
|
||||||
<span class="pointer pr-2">$</span>
|
<span class="pointer pr-2">$</span>
|
||||||
<!-- prettier-ignore -->
|
<!-- prettier-ignore -->
|
||||||
<div style="white-space: preserve;" class=" relative wrap-break-word">{value}</div>
|
<div style="white-space: preserve;" class=" relative wrap-break-word">{inputValue}</div>
|
||||||
<span id="cursor" class={isFocused ? 'animate-cursor-blink' : ''}>_</span>
|
<span id="cursor" class={isFocused ? 'animate-cursor-blink' : ''}>_</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
20
src/modules/terminal/Output.svelte
Normal file
20
src/modules/terminal/Output.svelte
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
let { path, output, cmd }: { path: string; output: any; cmd: string } = $props(); //TODO: change any to matching
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<p class="cwd" id="cwd">{path}</p>
|
||||||
|
<div class="pointer-wrapper mr-4 mb-2.5 flex flex-col justify-center font-terminal">
|
||||||
|
<div class=" flex flex-row items-center">
|
||||||
|
<span class=" pointer self-start pr-2">$</span>
|
||||||
|
<p>{cmd}</p>
|
||||||
|
</div>
|
||||||
|
<div style="white-space: preserve;" class=" relative wrap-break-word">
|
||||||
|
{#if typeof output === 'string'}
|
||||||
|
{output}
|
||||||
|
{:else if output instanceof Element}
|
||||||
|
{@html output.outerHTML}
|
||||||
|
{:else}
|
||||||
|
{output}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -1,166 +1,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import TerminalModule from '../modules/Terminal.svelte';
|
import TerminalModule from '../modules/Terminal.svelte';
|
||||||
|
|
||||||
import { Terminal, type PrintData, type TermInitArgs } from '$lib/stores/terminal';
|
|
||||||
import type { User } from '$lib/stores/bash/bash';
|
|
||||||
import { onMount } from 'svelte';
|
|
||||||
import Settings from '../modules/Settings.svelte';
|
import Settings from '../modules/Settings.svelte';
|
||||||
import Loading from '../modules/Loading.svelte';
|
import Loading from '../modules/Loading.svelte';
|
||||||
import type { TreeNode } from '$lib/stores/bash/fs';
|
import { isInitializing } from '$lib/stores/terminal/init.svelte';
|
||||||
import Input from '../modules/terminal/Input.svelte';
|
|
||||||
import Cursor from '../modules/terminal/Cursor.svelte';
|
|
||||||
|
|
||||||
//let terminalMode =
|
|
||||||
|
|
||||||
function jsonToTreeNode(data: any): TreeNode {
|
|
||||||
return {
|
|
||||||
name: data.Name,
|
|
||||||
type: data.Type,
|
|
||||||
readonly: data.ReadOnly,
|
|
||||||
interactible: data.Interactible,
|
|
||||||
func: data.Func,
|
|
||||||
children: data.Children ? data.Children.map((child: any) => jsonToTreeNode(child)) : [],
|
|
||||||
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)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchFileSystem(path: string): Promise<any> {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
let callbackInit = {
|
|
||||||
print: (data: any) => {
|
|
||||||
console.log('print callback executed');
|
|
||||||
print(data);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let testUser: User = {
|
|
||||||
username: 'kamil',
|
|
||||||
passwd: '123',
|
|
||||||
uid: 0,
|
|
||||||
gid: 0,
|
|
||||||
home: '/home/kamil',
|
|
||||||
history: []
|
|
||||||
};
|
|
||||||
|
|
||||||
let terminal: Terminal;
|
|
||||||
let username: string = $state(testUser.username);
|
|
||||||
let cwd: string = $state(testUser.home);
|
|
||||||
|
|
||||||
let isInitializing = $state(true);
|
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
try {
|
|
||||||
let fsJson = await fetchFileSystem('/src/lib/assets/fs/fs.json');
|
|
||||||
|
|
||||||
let args: TermInitArgs = {
|
|
||||||
bash: {
|
|
||||||
user: testUser,
|
|
||||||
fs: fsJson
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
terminal = new Terminal(args);
|
|
||||||
terminal.registerCallbacks(callbackInit);
|
|
||||||
|
|
||||||
username = terminal.getUser().username;
|
|
||||||
cwd = terminal.getCwd();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to initialize terminal:', error);
|
|
||||||
} finally {
|
|
||||||
updateTerminal();
|
|
||||||
isInitializing = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function updateTerminal() {
|
|
||||||
username = terminal!.getUser().username;
|
|
||||||
cwd = terminal!.getCwd();
|
|
||||||
}
|
|
||||||
|
|
||||||
let terminalComponent = $state<any>();
|
|
||||||
|
|
||||||
let inputComponent = $state<any>();
|
|
||||||
|
|
||||||
function focusInput() {
|
|
||||||
console.log('focus');
|
|
||||||
inputComponent.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
function blurInput() {
|
|
||||||
console.log('blur');
|
|
||||||
inputComponent.blur();
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearInput() {
|
|
||||||
inputComponent.value = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function print(data: PrintData): void {
|
|
||||||
if (isInitializing) {
|
|
||||||
console.error('Terminal is initializing! Skipping Print');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
terminalComponent.addComponent(Cursor, {
|
|
||||||
path: data.path,
|
|
||||||
output: data.output
|
|
||||||
});
|
|
||||||
updateTerminal();
|
|
||||||
}
|
|
||||||
|
|
||||||
function testAction() {
|
|
||||||
if (!terminal || isInitializing) {
|
|
||||||
console.error('Terminal is initializing!');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
terminal.executeCommand('cd ~/.config');
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Settings></Settings>
|
<Settings></Settings>
|
||||||
{#if !isInitializing}
|
<div class="h-dvh w-full p-24">
|
||||||
<div class="h-dvh w-full p-24">
|
<TerminalModule />
|
||||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
</div>
|
||||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
||||||
<div onclick={() => focusInput()}>
|
|
||||||
<TerminalModule bind:this={terminalComponent} {username} {cwd}>
|
|
||||||
<Input {cwd} bind:this={inputComponent} />
|
|
||||||
</TerminalModule>
|
|
||||||
<button title="" onclick={() => testAction()}>Test Action</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<Loading></Loading>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
0
src/routes/test/+page.svelte
Normal file
0
src/routes/test/+page.svelte
Normal file
Reference in New Issue
Block a user