From 97b999459412cdeebb9e90727357fa227e036721 Mon Sep 17 00:00:00 2001 From: binekrasik Date: Tue, 12 May 2026 10:38:30 +0200 Subject: [PATCH] sync: sync wip stuff --- Writing Programs.md | 46 ++++++++++ src/app.ts | 33 ++++++- src/fs/Directory.ts | 9 -- src/fs/File.ts | 42 --------- src/fs/Item.ts | 175 +++++++++++++++++++++++++++++++++++++ src/gui/Alert.ts | 23 +++++ src/gui/alertTemplate.html | 24 +++++ src/program/Clear.ts | 3 +- src/program/Eval.ts | 4 +- src/program/Info.ts | 3 +- src/program/Loadprg.ts | 20 ++--- src/program/Ls.ts | 16 +++- src/program/Lsprg.ts | 3 +- src/program/Program.ts | 8 +- src/program/Rm.ts | 0 src/program/Sl.ts | 119 +++++++++++++++++++++++++ src/program/Touch.ts | 25 ++++++ src/shell/Shell.ts | 3 + src/shell/Wush.ts | 55 ++++++++---- src/styles/app.scss | 5 ++ src/utils/SimpleStream.ts | 16 ++-- 21 files changed, 532 insertions(+), 100 deletions(-) create mode 100644 Writing Programs.md delete mode 100644 src/fs/Directory.ts delete mode 100644 src/fs/File.ts create mode 100644 src/fs/Item.ts create mode 100644 src/gui/Alert.ts create mode 100644 src/gui/alertTemplate.html create mode 100644 src/program/Rm.ts create mode 100644 src/program/Sl.ts create mode 100644 src/program/Touch.ts diff --git a/Writing Programs.md b/Writing Programs.md new file mode 100644 index 0000000..839fe09 --- /dev/null +++ b/Writing Programs.md @@ -0,0 +1,46 @@ +Implement the `sl` ("steam locomotive") command from Linux in TypeScript. I want an __exact replica__ of the original Linux command. Strictly follow the following APIs and concepts: +1. class SimpleStream + - on(listener: (data: T) => any): void + - Attaches a listener to the stream + - param listener -> the function to call when data is streamed + - once(listener: (data: T) => any): void + - Attaches a one-time listener to the stream + - param listener -> the function to call when data is streamed + - wait(): Promise + - Patiently waits until new data is streamed, then returns it + - return a promise for a single chunk of streamed data + - off(listener: Function): void + - Removes a listener from the stream + - param listener -> the function to remove + - emit(data: T): void + - Streams data + - param data -> the data to stream +2. Escape codes + - Escape codes can be formatted using one of the following syntaxes: + - `\{code letter}` -> for short codes like \n or \f + - `\0{code id};{first parameter};{second parameter};{...parameters}\0` -> for longer codes with complex parameters like cursor movement codes + - A semicolon right after or before a null terminator (`\0`) inside of an escape code is a syntax error + - Supported escape codes: + - Short codes ( -> ) + 1. n -> line feed (new line) + 2. f -> form feed (new page) + - Longer codes (,,<...parameters> -> ) + 1. cmr,{delta x},{delta y} -> moves the cursor relative to its current position by the specified amounts + 2. cma,{absolute x},{absolute y} -> moves the cursor absolute in the terminal coordinate space + - Example usage: + - `stdout.emit("\\cma;3;2\\")` - sets the cursor position to the specified XY coordinates - x=3, y=2 +2. stdout/stdin - SimpleStream instances + - they function as input/output interfaces to send text between the shell and other programs + - stdout allows for formatting using the above mentioned Escape codes + - Example usage: + - `stdout.emit("Hello world!\n")` - streams the text to the stdout stream, works like printf in C + - ```stdin.once(data => stdout.emit(`Received data: ${data}`))``` - creates a single use listener in the stdin stream and prints out the received data +3. abstract class Program + - abstract Exec(stdin: SimpleStream, stdout: SimpleStream, workdir: Item, args: string[]): Promise + - Entry point for a program + - param stdin -> input stream + - param stdout -> output stream + - param workdir -> Working directory filesystem item + - param args -> arguments array, including the executed program name from the shell + - for example for echo command: `[ "echo", "test", "one", "two", "three" ]` + - returns a posix-like exit code diff --git a/src/app.ts b/src/app.ts index cbf98a6..8b990b6 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,3 +1,4 @@ +import { Alert } from './gui/Alert' import { CreateKeyboardListeners } from './input/keyboard' import { Wush } from './shell/Wush' import { Terminal } from './terminal/Terminal' @@ -5,6 +6,36 @@ import { EventBroadcaster } from './utils/EventBroadcaster' export const WEBSHELL_VERSION = "0.1.0" +// initialize object store for webfs +let WebfsDatabase: IDBDatabase | null = null + +const request = indexedDB.open("webshell") + +request.onupgradeneeded = event => { + console.log("creating database") + WebfsDatabase = (event.target as any).result as IDBDatabase + WebfsDatabase.createObjectStore("webfs") +} + +request.onerror = _ => { + new Alert( + "webfs error", + "Failed to initialize the webfs database using IndexedDB. Make sure webshell does not have any IndexedDB related permissions disabled and try again.", + 'critical', + ).Show(-1) +} + +request.onsuccess = event => { + WebfsDatabase = (event.target as any).result as IDBDatabase + console.log("database initialized") + + // initialize the app after the database + init() +} + +export const GetWebfsDatabase = (): IDBDatabase | null => WebfsDatabase + +// terminal management let CurrentTerminal: Terminal export const SetCurrentTerminal = (terminal: Terminal) => CurrentTerminal = terminal export const GetCurrentTerminal = (): Terminal => CurrentTerminal @@ -22,5 +53,3 @@ const init = () => { const terminal = new Terminal() terminal.LoadShell(new Wush(localBroadcaster, terminal)) } - -init() diff --git a/src/fs/Directory.ts b/src/fs/Directory.ts deleted file mode 100644 index 36277b8..0000000 --- a/src/fs/Directory.ts +++ /dev/null @@ -1,9 +0,0 @@ -export class Directory { - readonly path: string - - constructor(path: string) { - this.path = path - - console.log(this.path.split('/')) - } -} diff --git a/src/fs/File.ts b/src/fs/File.ts deleted file mode 100644 index b7a0bcb..0000000 --- a/src/fs/File.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Directory } from "./Directory" - -export class File { - readonly location: Directory - readonly name: string - readonly path: string - private data: any - - constructor(path: string) { - this.path = path - const match = path.match(/^(\/[^\/]*)+\/?$/gm) - - if (!match) - throw new SyntaxError("Bad filepath") - - this.location = new Directory(match[0]) - this.name = match[1] - - new Directory(path) - - console.log(this.location) - console.log(this.name) - } - - Exists(): boolean { - return window.localStorage.getItem(this.path) !== null && window.localStorage.getItem(this.path) !== undefined - } - - Remove() { - window.localStorage.removeItem(this.path) - } - - Write(data: any) { - this.data = data - - window.localStorage.setItem(this.path, JSON.stringify(this)) - } - - Read(): any { - return this.data - } -} diff --git a/src/fs/Item.ts b/src/fs/Item.ts new file mode 100644 index 0000000..d7079d5 --- /dev/null +++ b/src/fs/Item.ts @@ -0,0 +1,175 @@ +import { GetWebfsDatabase } from "../app" + +export class Item { + private path: string + private isDirectory: boolean = false + private executable: boolean = false + private data: string | null = null + + private children: string[] = [] + + constructor(path: string, data?: string) { + this.path = this.normalizePath(path) + + if (this.Exists()) { + this.load() + } else { + this.data = data !== undefined ? data : null + this.isDirectory = data === undefined + } + } + + static get Root(): Item { + const root = new Item('/') + if (!root.Exists()) { + root.Create() + } + return root + } + + Create(): void { + if (this.Exists()) throw new Error("File already exists") + + this.save() + + if (this.path !== '/') { + const parentPath = this.getParentPath() + const parent = new Item(parentPath) + + // recursively creates parent directories + if (!parent.Exists()) { + parent.Create() + } + + // adds this item to the parent's children list and saves the parent + if (!parent.children.includes(this.path)) { + parent.children.push(this.path) + parent.save() + } + } + } + + Exists(): boolean { + // localStorage.getItem(this.storageKey) !== null + const store = GetWebfsDatabase()! + .transaction("webfs", "readonly") + .objectStore("webfs") + + return Boolean(store.getKey(this.storageKey)) + } + + IsDirectory(): boolean { + return this.isDirectory + } + + Append(data: string): void { + if (this.isDirectory) { + throw new Error("Cannot append data to a directory") + } + this.data = (this.data || '') + data + this.save() + } + + Write(data: string): void { + if (this.isDirectory) + throw new Error("Cannot write data to a directory") + + this.data = data + this.save() + } + + SetExecutable(executable: boolean): void { + this.executable = executable + + // update the item if it already exists in the filesystem + if (this.Exists()) + this.save() + } + + GetPath(): string { + return this.path + } + + GetName(): string { + return this.path + } + + ReadData(): string | null { + return this.data + } + + List(): Item[] { + if (!this.isDirectory) + throw new Error(`Not a directory: ${this.path}`) + + // grab all the children + return this.children.map(childPath => new Item(childPath)) + } + + Delete(): void { + if (!this.Exists()) return + + // delete children recursively if this item is a directory + if (this.isDirectory) { + this.List().forEach(child => child.Delete()) + } + + // clear all traces + localStorage.removeItem(this.storageKey) + + if (this.path !== '/') { + const parent = new Item(this.getParentPath()) + if (parent.Exists()) { + parent.children = parent.children.filter(path => path !== this.path) + parent.save() + } + } + } + + private get storageKey(): string { + return this.path + } + + private normalizePath(path: string): string { + if (!path.startsWith('/')) path = '/' + path + if (path.length > 1 && path.endsWith('/')) path = path.slice(0, -1) + return path + } + + private getParentPath(): string { + if (this.path === '/') return '/' + const lastSlashIndex = this.path.lastIndexOf('/') + return lastSlashIndex === 0 + ? '/' + : this.path.substring(0, lastSlashIndex) + } + + /** + * Saves the item to the filesystem + */ + private save(): void { + const payload = { + isDirectory: this.isDirectory, + executable: this.executable, + data: this.data, + children: this.children, + } + + window.localStorage.setItem(this.storageKey, JSON.stringify(payload)) + } + + /** + * Loads data associated with the item from the filesystem + */ + private load(): void { + const raw = localStorage.getItem(this.storageKey) + + if (raw) { + const parsed = JSON.parse(raw) + this.isDirectory = parsed.isDirectory + this.executable = parsed.executable + this.data = parsed.data + this.children = parsed.children + } + } +} diff --git a/src/gui/Alert.ts b/src/gui/Alert.ts new file mode 100644 index 0000000..14f2d71 --- /dev/null +++ b/src/gui/Alert.ts @@ -0,0 +1,23 @@ +import HtmlAlertTemplate from "./alertTemplate.html?raw" + +export type AlertSeverity = 'debug' | 'info' | 'warning' | 'error' | 'critical' + +export class Alert { + readonly title: string + readonly description: string + readonly severity: AlertSeverity + + // a html5 template + readonly template: string + + constructor(title: string, description: string, severity: AlertSeverity, template: string = HtmlAlertTemplate) { + this.title = title + this.description = description + this.severity = severity + this.template = template + } + + Show(time: number = -1): void { + alert(`${this.title} [${this.severity}]: ${this.description}`) + } +} diff --git a/src/gui/alertTemplate.html b/src/gui/alertTemplate.html new file mode 100644 index 0000000..4efe2a5 --- /dev/null +++ b/src/gui/alertTemplate.html @@ -0,0 +1,24 @@ +
+ + +

{{title}}

+

{{description}}

+ + + +
diff --git a/src/program/Clear.ts b/src/program/Clear.ts index f33dfb8..ffcb325 100644 --- a/src/program/Clear.ts +++ b/src/program/Clear.ts @@ -1,8 +1,9 @@ +import type { Item } from '../fs/Item' import type { SimpleStream } from '../utils/SimpleStream' import { Program } from './Program' export class Clear extends Program { - async Exec(_: SimpleStream, stdout: SimpleStream, __: string[]): Promise { + async Exec(_: SimpleStream, stdout: SimpleStream, __: Item, ___: string[]): Promise { stdout.emit('\f') return 0 diff --git a/src/program/Eval.ts b/src/program/Eval.ts index ec3af0a..45882f1 100644 --- a/src/program/Eval.ts +++ b/src/program/Eval.ts @@ -1,11 +1,13 @@ +import type { Item } from '../fs/Item' import type { SimpleStream } from '../utils/SimpleStream' import { Program } from './Program' export class Eval extends Program { - async Exec(_: SimpleStream, stdout: SimpleStream, args: string[]): Promise { + async Exec(_: SimpleStream, stdout: SimpleStream, _workdir: Item, args: string[]): Promise { const javascript = args.slice(1).join(' ') try { + // todo: pass workdir to the program eval(javascript) } catch (e) { stdout.emit(`${String(e)}\n`) diff --git a/src/program/Info.ts b/src/program/Info.ts index ca221cc..7bc6af8 100644 --- a/src/program/Info.ts +++ b/src/program/Info.ts @@ -1,10 +1,11 @@ import { GetCurrentTerminal, WEBSHELL_VERSION } from '../app' +import type { Item } from '../fs/Item' import { Terminal } from '../terminal/Terminal' import type { SimpleStream } from '../utils/SimpleStream' import { Program } from './Program' export class Info extends Program { - async Exec(_: SimpleStream, stdout: SimpleStream, __: string[]): Promise { + async Exec(_: SimpleStream, stdout: SimpleStream, __: Item, ___: string[]): Promise { stdout.emit(`Webshell v${WEBSHELL_VERSION}\n`) stdout.emit(`Terminal v${Terminal.Version}\n`) stdout.emit(`Running ${GetCurrentTerminal().GetShell()?.Name} v${GetCurrentTerminal().GetShell()?.Version}\n`) diff --git a/src/program/Loadprg.ts b/src/program/Loadprg.ts index b4b9022..c9f733c 100644 --- a/src/program/Loadprg.ts +++ b/src/program/Loadprg.ts @@ -1,32 +1,30 @@ -import type { Wush } from '../shell/Wush' +import type { Item } from '../fs/Item' +import type { Shell } from '../shell/Shell' import type { SimpleStream } from '../utils/SimpleStream' import { Program } from './Program' export class Loadprg extends Program { - private wush: Wush + private shell: Shell - constructor(wush: Wush) { + constructor(shell: Shell) { super() - this.wush = wush + this.shell = shell } - async Exec(_: SimpleStream, stdout: SimpleStream, args: string[]): Promise { - // stdout.emit('Not implemented yet.\n') - // return 0 - + async Exec(_: SimpleStream, stdout: SimpleStream, __: Item, args: string[]): Promise { const javascript = args.slice(2).join(' ') try { const exec: Function = eval(javascript) const program = class extends Program { - async Exec(stdin: SimpleStream, stdout: SimpleStream, args: string[]): Promise { - exec(stdin, stdout, args) + async Exec(stdin: SimpleStream, stdout: SimpleStream, workdir: Item, args: string[]): Promise { + exec(stdin, stdout, workdir, args) return 0 } } - this.wush.LoadProgram(new program(), args[1]) + this.shell.LoadProgram(new program(), args[1]) } catch (e) { stdout.emit(`${String(e)}\n`) stdout.emit(`${String((e as Error).stack)}\n`) diff --git a/src/program/Ls.ts b/src/program/Ls.ts index 8a7fc65..e2d82fd 100644 --- a/src/program/Ls.ts +++ b/src/program/Ls.ts @@ -1,15 +1,23 @@ +import { Item } from '../fs/Item' import type { SimpleStream } from '../utils/SimpleStream' import { Program } from './Program' -import { File } from '../fs/File' -import { Directory } from '../fs/Directory' export class Ls extends Program { constructor() { super() } - async Exec(_: SimpleStream, stdout: SimpleStream, workdir: Directory, __: string[]): Promise { - new Directory('/etc/system/idk') + async Exec(_: SimpleStream, stdout: SimpleStream, workdir: Item, args: string[]): Promise { + if (!(new Item(args[1]).IsDirectory())) { + stdout.emit("ls: error: the provided path is not a directory") + return 1 + } + + stdout.emit(`item: '${workdir.GetPath()}'\n`) + stdout.emit(`index attr name\n`) + workdir.List().forEach((entry, i) => { + stdout.emit(`${i.toString().padEnd(8, ' ')} ${''.padEnd(4, '-').padEnd(8, ' ')} '${entry.GetName()}'\n`) + }) return 0 } diff --git a/src/program/Lsprg.ts b/src/program/Lsprg.ts index 308a1da..f9f16ad 100644 --- a/src/program/Lsprg.ts +++ b/src/program/Lsprg.ts @@ -1,3 +1,4 @@ +import type { Item } from '../fs/Item' import type { Wush } from '../shell/Wush' import type { SimpleStream } from '../utils/SimpleStream' import { Program } from './Program' @@ -10,7 +11,7 @@ export class Lsprg extends Program { this.wush = wush } - async Exec(_: SimpleStream, stdout: SimpleStream, __: string[]): Promise { + async Exec(_: SimpleStream, stdout: SimpleStream, __: Item, ___: string[]): Promise { for (const program in this.wush.GetPrograms()) stdout.emit(`${program}\n`) diff --git a/src/program/Program.ts b/src/program/Program.ts index 3c3f5e5..5eb6608 100644 --- a/src/program/Program.ts +++ b/src/program/Program.ts @@ -1,5 +1,11 @@ +import type { Item } from "../fs/Item" import type { SimpleStream } from "../utils/SimpleStream" export abstract class Program { - abstract Exec(stdin: SimpleStream, stdout: SimpleStream, args: string[]): Promise + abstract Exec( + stdin: SimpleStream, + stdout: SimpleStream, + workdir: Item, + args: string[] + ): Promise } diff --git a/src/program/Rm.ts b/src/program/Rm.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/program/Sl.ts b/src/program/Sl.ts new file mode 100644 index 0000000..b374270 --- /dev/null +++ b/src/program/Sl.ts @@ -0,0 +1,119 @@ +// --- `sl` Command Implementation --- + +import type { Item } from "../fs/Item" +import type { SimpleStream } from "../utils/SimpleStream" +import { Program } from "./Program" + +export class Sl extends Program { + // The classic D51 locomotive ASCII art with 3 frames of wheel animation. + // Notice the trailing space on each line: this acts as an automatic "eraser" + // for the previous frame as the train moves left! + private static readonly D51_FRAMES: string[][] = [ + [ + ' ==== ________ ___________ ', + ' _D _| |_______/ \\__I_I_____===__|_________| ', + ' |(_)--- | H\\________/ | | =|___ ___| ', + ' / | | H | | | | ||_| |_|| ', + ' | | | H |__--------------------| [___] | ', + ' | ________|___H__/__|_____/[][]~\\_______| | ', + ' |/ | |-----------I_____I [][] [] D |=======| ', + '__/ =| o |=-O=====O=====O=====O \\ ____Y___________| ', + ' |/-=|___|= || || || |_____/~\\___/ ', + ' \\_/ \\__/ \\__/ \\__/ \\__/ \\_/ ', + ], + [ + ' ==== ________ ___________ ', + ' _D _| |_______/ \\__I_I_____===__|_________| ', + ' |(_)--- | H\\________/ | | =|___ ___| ', + ' / | | H | | | | ||_| |_|| ', + ' | | | H |__--------------------| [___] | ', + ' | ________|___H__/__|_____/[][]~\\_______| | ', + ' |/ | |-----------I_____I [][] [] D |=======| ', + '__/ =| o |=-~O====O====O====O~ \\ ____Y___________| ', + ' |/-=|___|= || || || |_____/~\\___/ ', + ' \\_/ \\__/ \\__/ \\__/ \\__/ \\_/ ', + ], + [ + ' ==== ________ ___________ ', + ' _D _| |_______/ \\__I_I_____===__|_________| ', + ' |(_)--- | H\\________/ | | =|___ ___| ', + ' / | | H | | | | ||_| |_|| ', + ' | | | H |__--------------------| [___] | ', + ' | ________|___H__/__|_____/[][]~\\_______| | ', + ' |/ | |-----------I_____I [][] [] D |=======| ', + '__/ =| o |=-~~O===O===O===O~~ \\ ____Y___________| ', + ' |/-=|___|= || || || |_____/~\\___/ ', + ' \\_/ \\__/ \\__/ \\__/ \\__/ \\_/ ', + ], + ] + + public async Exec( + _: SimpleStream, + stdout: SimpleStream, + __: Item, + args: string[], + ): Promise { + // Original `sl` behavior flags + const isFly = args.includes('-F') + + // Terminal size assumptions since they aren't provided by the API + const termWidth = 100 + const trainWidth = Sl.D51_FRAMES[0][0].length + + // The train starts off-screen to the right and ends completely off-screen to the left + const startX = termWidth + const endX = -trainWidth + + // Clear screen (new page using form feed) + stdout.emit('\f') + + let frameIdx = 0 + for (let x = startX; x >= endX; x--) { + const frame = Sl.D51_FRAMES[frameIdx % Sl.D51_FRAMES.length] + + // If the `-F` flag is passed, the train "flies" upwards as it moves forward + let startY = isFly ? Math.floor(x / 4) + 2 : 5 + + for (let y = 0; y < frame.length; y++) { + const line = frame[y] + let outLine = line + let cursorX = x + + // When the train hits the left edge, we must truncate the string + // and lock the drawing cursor to X=0 to prevent terminal wrapping artifacts + if (x < 0) { + cursorX = 0 + outLine = line.substring(-x) + } + + const cursorY = startY + y + + // Only render if within vertical bounds and there's text left to draw + if (cursorY >= 0 && outLine.length > 0) { + // Send absolute cursor positioning sequence + stdout.emit(`\0cma;${cursorX};${cursorY}\0`) + // Render the frame line + stdout.emit(outLine) + } + } + + // Artificial delay to pace the animation + await this.sleep(40) + frameIdx++ + + console.log(frameIdx) + } + + // Return the cursor back to a safe position to give shell control back seamlessly + stdout.emit(`\0cma;0;20\0\n`) + + return 0 // POSIX successful exit code + } + + /** + * Utility method to pause execution to pace the animation frames. + */ + private sleep(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)) + } +} diff --git a/src/program/Touch.ts b/src/program/Touch.ts new file mode 100644 index 0000000..f8d14a2 --- /dev/null +++ b/src/program/Touch.ts @@ -0,0 +1,25 @@ +import { Item } from '../fs/Item' +import type { SimpleStream } from '../utils/SimpleStream' +import { Program } from './Program' + +export class Touch extends Program { + constructor() { + super() + } + + async Exec(_: SimpleStream, stdout: SimpleStream, workdir: Item, args: string[]): Promise { + stdout.emit(`touching children, please wait...\n`) + + const file = new Item(args[1]) + stdout.emit(`does ${args[1]} exist? ${file.Exists()}\n`) + + if (file.Exists()) { + stdout.emit("touch: the file already exists.\n") + return 1 + } + + file.Create() + + return 0 + } +} diff --git a/src/shell/Shell.ts b/src/shell/Shell.ts index f3422f5..dc2b085 100644 --- a/src/shell/Shell.ts +++ b/src/shell/Shell.ts @@ -1,3 +1,4 @@ +import type { Program } from '../program/Program' import type { Terminal } from '../terminal/Terminal' import type { EventBroadcaster } from '../utils/EventBroadcaster' @@ -12,6 +13,8 @@ export abstract class Shell { this.terminal = terminal } + abstract LoadProgram(program: Program, name: string): void + abstract ExecuteProgram(name: string, args: string[]): void abstract Init(): void abstract HandleKeyInput(key: string, isCharacter: boolean): void } diff --git a/src/shell/Wush.ts b/src/shell/Wush.ts index 542d373..2531d13 100644 --- a/src/shell/Wush.ts +++ b/src/shell/Wush.ts @@ -12,6 +12,9 @@ import { EventBroadcaster } from '../utils/EventBroadcaster' import { SimpleStream } from '../utils/SimpleStream' import { Shell } from './Shell' import { Ls } from '../program/Ls' +import { Item } from '../fs/Item' +import { Touch } from '../program/Touch' +import { Sl } from '../program/Sl' export class Wush extends Shell { public readonly Version = "0.1.0" @@ -35,6 +38,7 @@ export class Wush extends Shell { readonly stdout: SimpleStream private programs: { [name: string]: Program } = {} + private workingDirectory: Item = Item.Root constructor(broadcaster: EventBroadcaster, terminal: Terminal) { super(broadcaster, terminal) @@ -56,6 +60,8 @@ export class Wush extends Shell { this.programs['lsprg'] = new Lsprg(this) this.programs['info'] = new Info() this.programs['ls'] = new Ls() + this.programs['touch'] = new Touch() + this.programs['sl'] = new Sl() this.stdout.on(data => this.WriteEscapedString(data)) this.Prompt() @@ -73,6 +79,26 @@ export class Wush extends Shell { delete this.programs[name] } + ExecuteProgram(name: string, args: string[]) { + this.programs[name].Exec(this.stdin, this.stdout, this.workingDirectory, args) + .then(code => { + this.execExitCode = code != -1 ? code : -2 + }) + .catch((e) => { + this.WriteEscapedString("lol") + this.WriteEscapedString(`\n${String(e)}\n`) + }) + .finally(() => { + // check if the exec actually exited with an exit code + // and if not, set it to -2 to indicate that it didn't exit with a valid code + if (this.execExitCode === -1) + this.execExitCode = -2 + + // this.terminal.Write(`The program exited with exit code ${this.execExitCode}.`) + this.Prompt() + }) + } + Prompt() { this.terminal.Write(`hi [${this.execExitCode}] -> `) } @@ -84,7 +110,7 @@ export class Wush extends Shell { WriteEscapedString(data: string) { let buf = data.split('') buf.forEach((char) => { - if (!this.ProcessControlCode(char)) + if (!this.ProcessSimpleControlCode(char)) this.terminal.Write(char) }) } @@ -123,23 +149,7 @@ export class Wush extends Shell { this.execExitCode = -1 if (this.programs[args[0]] instanceof Program) { - this.programs[args[0]].Exec(this.stdin, this.stdout, args) - .then(code => { - this.execExitCode = code != -1 ? code : -2 - }) - .catch((e) => { - this.WriteEscapedString("lol") - this.WriteEscapedString(`\n${String(e)}\n`) - }) - .finally(() => { - // check if the exec actually exited with an exit code - // and if not, set it to -2 to indicate that it didn't exit with a valid code - if (this.execExitCode === -1) - this.execExitCode = -2 - - // this.terminal.Write(`The program exited with exit code ${this.execExitCode}.`) - this.Prompt() - }) + this.ExecuteProgram(args[0], args) } else { this.execExitCode = 0 @@ -153,7 +163,7 @@ export class Wush extends Shell { } } - ProcessControlCode(code: string): boolean { + ProcessSimpleControlCode(code: string): boolean { switch (code) { case '\f': this.terminal.NewPage() @@ -166,6 +176,13 @@ export class Wush extends Shell { } } + ProcessAdvancedControlCode(complex: string): boolean { + if (!complex.match(/\\(.*;)/gm)) + return false + + const code = matches[] + } + HandleKeyInput(key: string, isCharacter: boolean) { if (this.execExitCode === -1) console.log('program running') if (!isCharacter) { diff --git a/src/styles/app.scss b/src/styles/app.scss index c6abf5e..980872a 100644 --- a/src/styles/app.scss +++ b/src/styles/app.scss @@ -9,3 +9,8 @@ body { * { box-sizing: border-box; } + +::selection { + background: colors.$terminal-white; + color: colors.$terminal-background; +} diff --git a/src/utils/SimpleStream.ts b/src/utils/SimpleStream.ts index 9ca0eff..9fdd9db 100644 --- a/src/utils/SimpleStream.ts +++ b/src/utils/SimpleStream.ts @@ -3,17 +3,17 @@ export class SimpleStream { /** * Attaches a listener to the stream - * @param listener the function to call when new data is broadcasted + * @param listener the function to call when new data is streamed */ - on(listener: (data: T) => any) { + on(listener: (data: T) => any): void { this.listeners.add(listener) } /** * Attaches a one-time listener to the stream - * @param listener the function to call when new data is broadcasted + * @param listener the function to call when new data is streamed */ - once(listener: (data: T) => any) { + once(listener: (data: T) => any): void { const func = (data: T) => { listener(data) this.off(func) @@ -23,8 +23,8 @@ export class SimpleStream { } /** - * Patiently waits until data is streamed, then returns it - * @returns a promise of a single chunk of streamed data + * Patiently waits until new data is streamed, then returns it + * @returns a promise for a single chunk of streamed data */ wait(): Promise { return new Promise(resolve => { @@ -36,7 +36,7 @@ export class SimpleStream { * Removes a listener from the stream * @param listener the function to remove */ - off(listener: Function) { + off(listener: Function): void { this.listeners.delete(listener) } @@ -44,7 +44,7 @@ export class SimpleStream { * Streams data * @param data the data to stream */ - emit(data: T) { + emit(data: T): void { this.listeners.forEach((listener) => listener(data)) } }