From 21a1a3430978f9edc09e4eb6f15ec9fe995031a3 Mon Sep 17 00:00:00 2001 From: binekrasik Date: Thu, 26 Mar 2026 21:05:35 +0100 Subject: [PATCH] feat: streams, 'clear' command --- src/program/clear.ts | 7 +++ src/program/stdlibwsh.ts | 8 +++ src/shell/ControlCode.ts | 4 ++ src/shell/Wush.ts | 110 ++++++++++++++++++++++++++++++++++---- src/styles/app.scss | 2 + src/terminal/Terminal.ts | 3 +- src/utils/SimpleStream.ts | 50 +++++++++++++++++ 7 files changed, 173 insertions(+), 11 deletions(-) create mode 100644 src/program/clear.ts create mode 100644 src/program/stdlibwsh.ts create mode 100644 src/shell/ControlCode.ts create mode 100644 src/utils/SimpleStream.ts diff --git a/src/program/clear.ts b/src/program/clear.ts new file mode 100644 index 0000000..f3b3ac7 --- /dev/null +++ b/src/program/clear.ts @@ -0,0 +1,7 @@ +import type { SimpleStream } from '../utils/SimpleStream' + +export const ClearExec = async (stdin: SimpleStream, stdout: SimpleStream): Promise => { + stdout.emit("\f") + + return 0 +} diff --git a/src/program/stdlibwsh.ts b/src/program/stdlibwsh.ts new file mode 100644 index 0000000..87809e4 --- /dev/null +++ b/src/program/stdlibwsh.ts @@ -0,0 +1,8 @@ +// stdlibwsh - (st)andard (lib)rary (w)eb (sh)ell +// - céčkoismy at their peak + +import type { SimpleStream } from '../utils/SimpleStream' + +export const io = { + +} diff --git a/src/shell/ControlCode.ts b/src/shell/ControlCode.ts new file mode 100644 index 0000000..9fdad98 --- /dev/null +++ b/src/shell/ControlCode.ts @@ -0,0 +1,4 @@ +// a representation of ascii control codes +export const ControlCode = { + FormFeed: 12, +} diff --git a/src/shell/Wush.ts b/src/shell/Wush.ts index 1282850..e153bc9 100644 --- a/src/shell/Wush.ts +++ b/src/shell/Wush.ts @@ -1,27 +1,60 @@ // Web-Uno Shell // the best name I could come up with lmao +import { ClearExec } from '../program/clear' import { Terminal } from '../terminal/Terminal' -import type { EventBroadcaster } from '../utils/EventBroadcaster' +import { EventBroadcaster } from '../utils/EventBroadcaster' +import { SimpleStream } from '../utils/SimpleStream' +import { ControlCode } from './ControlCode' import { Shell } from './Shell' export class Wush extends Shell { + // buffer private buffer: string[] = [] private bufferPos: number = 0 + // exec stuff + /** + * -1 if the exec is currently running and anything else when it's not + */ + private execExitCode: number = 0 + + // streams + readonly stdin: SimpleStream + readonly stdout: SimpleStream + constructor(broadcaster: EventBroadcaster, terminal: Terminal) { super(broadcaster, terminal) + + // create streams + this.stdin = new SimpleStream() + this.stdout = new SimpleStream() } Init() { this.broadcaster.on('keydown', (key: string, isCharacter: boolean) => this.HandleKeyInput(key, isCharacter), ) + + this.stdout.on(data => this.WriteEscapedString(data)) this.Prompt() } Prompt() { - this.terminal.Write('hi -> ') + this.terminal.Write(`hi [${this.execExitCode}] -> `) + } + + WriteStdin(data: string) { + this.stdin.emit(data) + } + + WriteEscapedString(data: string) { + let buf = data.split('') + buf.forEach((char, i) => { + if (this.ProcessControlCode(char)) { + buf.splice(i, 1) + } + }) } PushToBuffer(text: string) { @@ -54,31 +87,90 @@ export class Wush extends Shell { this.FlushBuffer() console.log(`Executing ${args[0]} with args '${args}'`) + + this.execExitCode = -1 + + if (args[0] === 'clear') { + ClearExec(this.stdin, this.stdout) + .then(code => { + this.execExitCode = code != -1 ? code : -2 + }) + .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() + }) + } else { + this.execExitCode = 0 + + // don't print anything if there are no arguments + if (args[0].length !== 0) { + this.terminal.Write(`Program ${args[0]} was not found.`) + this.terminal.MoveCursor(0, 1, { x: true, y: false }) + } + + this.Prompt() + } + } + + ProcessControlCode(code: string): boolean { + switch (code) { + case '\f': + this.terminal.NewPage() + return true + default: + return false + } } HandleKeyInput(key: string, isCharacter: boolean) { + if (this.execExitCode === -1) console.log('program running') if (!isCharacter) { switch (key) { case 'ArrowLeft': - this.terminal.MoveCursor(-1, 0) - this.MoveBufferPos(-1) + if (this.bufferPos > 0) { + this.terminal.MoveCursor(-1, 0) + this.MoveBufferPos(-1) + } + break case 'ArrowRight': - this.terminal.MoveCursor(1, 0) - this.MoveBufferPos(1) + if (this.bufferPos < this.buffer.length) { + this.terminal.MoveCursor(1, 0) + this.MoveBufferPos(1) + } break case 'Backspace': + // don't erase anything if there's nothing left in the buffer + if (this.bufferPos === 0) + break + this.terminal.RemoveCell() this.RemoveLastCharsFromBuffer(1) this.terminal.MoveCursor(-1, 0) break case 'Enter': - this.terminal.MoveCursor(0, 1, { x: true, y: false }) - this.ExecuteBuffer() - this.Prompt() + // send the buffer to stdin if an exec is running + if (this.execExitCode === -1) { + this.WriteStdin(`${this.buffer.join('')}\n`) + this.FlushBuffer() + } else { + // "execute" the buffer + this.terminal.MoveCursor(0, 1, { x: true, y: false }) + this.ExecuteBuffer() + } + + break + case 'F5': + location.reload() break } } else { + // push the character into the buffer this.terminal.InsertCell(key) this.PushToBuffer(key) this.terminal.MoveCursor(1, 0) diff --git a/src/styles/app.scss b/src/styles/app.scss index 09ca3a3..c6abf5e 100644 --- a/src/styles/app.scss +++ b/src/styles/app.scss @@ -1,7 +1,9 @@ @forward "terminal.scss"; +@use "./colors.scss" as colors; body { margin: 0; + background: colors.$terminal-background; } * { diff --git a/src/terminal/Terminal.ts b/src/terminal/Terminal.ts index 43935b0..5c94a4f 100644 --- a/src/terminal/Terminal.ts +++ b/src/terminal/Terminal.ts @@ -43,6 +43,7 @@ export class Terminal { paragraph.className = 'line' this.terminal.appendChild(paragraph) + this.UpdateLines() paragraph.scrollIntoView({behavior: 'smooth'}) } @@ -70,10 +71,8 @@ export class Terminal { const selector = `#line-${this.cursorPosition.row} .cell-${this.cursorPosition.col}` // adjust for the cursor - console.log(`going from ${this.terminal.children.length - 1} to ${this.cursorPosition.row}`) if (!document.querySelector(`#line-${this.cursorPosition.row}`)) { for (let i = this.terminal.children.length - 1; i < this.cursorPosition.row; i++) { - console.log(i) this.AppendLine() } } diff --git a/src/utils/SimpleStream.ts b/src/utils/SimpleStream.ts new file mode 100644 index 0000000..9ca0eff --- /dev/null +++ b/src/utils/SimpleStream.ts @@ -0,0 +1,50 @@ +export class SimpleStream { + private listeners: Set = new Set() + + /** + * Attaches a listener to the stream + * @param listener the function to call when new data is broadcasted + */ + on(listener: (data: T) => any) { + this.listeners.add(listener) + } + + /** + * Attaches a one-time listener to the stream + * @param listener the function to call when new data is broadcasted + */ + once(listener: (data: T) => any) { + const func = (data: T) => { + listener(data) + this.off(func) + } + + this.on(func) + } + + /** + * Patiently waits until data is streamed, then returns it + * @returns a promise of a single chunk of streamed data + */ + wait(): Promise { + return new Promise(resolve => { + this.once(data => resolve(data)) + }) + } + + /** + * Removes a listener from the stream + * @param listener the function to remove + */ + off(listener: Function) { + this.listeners.delete(listener) + } + + /** + * Streams data + * @param data the data to stream + */ + emit(data: T) { + this.listeners.forEach((listener) => listener(data)) + } +}