From b5f53473ed1c2c32625363129dd7a1bca8e83fff Mon Sep 17 00:00:00 2001 From: binekrasik Date: Mon, 18 May 2026 19:25:03 +0200 Subject: [PATCH] sync: actually sync everything --- src/shell/wush/Wush.ts | 359 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 359 insertions(+) create mode 100644 src/shell/wush/Wush.ts diff --git a/src/shell/wush/Wush.ts b/src/shell/wush/Wush.ts new file mode 100644 index 0000000..87f608f --- /dev/null +++ b/src/shell/wush/Wush.ts @@ -0,0 +1,359 @@ +// Web-Uno Shell +// the best name I could come up with lol + +import { Clear } from '../../program/Clear' +import { Eval } from '../../program/Eval' +import { Loadprg } from '../../program/Loadprg' +import { Lsprg } from '../../program/Lsprg' +import { Info } from '../../program/Info' +import { Program } from '../../program/Program' +import { Terminal } from '../../terminal/Terminal' +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' +import { Rm } from '../../program/Rm' +import { Rl } from '../../program/Rl' +import { ResetIndexedDb } from '../../program/ResetIndexedDb' +import { Cat } from '../../program/Cat' +import { Echo } from '../../program/Echo' +import { Mkdir } from '../../program/Mkdir' +import { Cd } from '../../program/Cd' +// import { Pwd } from '../program/Pwd' +// import { Cd } from '../program/Cd' + +export class Wush extends Shell { + public readonly Version = "0.1.0" + public readonly Name = "wush" + + // buffer + private buffer: string[] = [] + private bufferPos: number = 0 + + private history: string[] = [] + private historyPos: number = 0 + + // exec stuff + /** + * -1 if the exec is currently running and anything else when it's not + */ + private execExitCode: number = 0 + + // todo: workers + // private workersAllowed: boolean = false + + // streams + readonly stdin: SimpleStream + readonly stdout: SimpleStream + + private programs: { [name: string]: Program } = {} + private workingDirectory: Item = null as unknown as Item // workdir is initialized in Init so this should be safe + + constructor(broadcaster: EventBroadcaster, terminal: Terminal) { + super(broadcaster, terminal) + + // create streams + this.stdin = new SimpleStream() + this.stdout = new SimpleStream() + } + + async Init() { + this.broadcaster.on('keydown', (key: string, isCharacter: boolean) => + this.HandleKeyInput(key, isCharacter), + ) + + // load workdir + this.workingDirectory = await Item.Root() + + // load core programs + this.programs['clear'] = new Clear() + this.programs['eval'] = new Eval() + this.programs['loadprg'] = new Loadprg(this) + 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.programs['rm'] = new Rm() + this.programs['rl'] = new Rl() + this.programs['rsindb'] = new ResetIndexedDb() + this.programs['cat'] = new Cat() + this.programs['echo'] = new Echo() + this.programs['mkdir'] = new Mkdir() + // this.programs['pwd'] = new Pwd() + this.programs['cd'] = new Cd(this) + + this.stdout.on(data => this.WriteEscapedString(data)) + this.Prompt() + } + + GetPrograms(): { [name: string]: Program } { + return this.programs + } + + LoadProgram(program: Program, name: string) { + this.programs[name] = program + } + + UnloadProgram(name: string) { + 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(`wush: command ${name} exited with the following exception\n`) + this.WriteEscapedString(` | ${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(`boykisser in ${this.workingDirectory.GetPath()} -> `) + } + + /** + * Changes the working directory + * @param directory the directory to enter into + * @throws an error if the directory cannot be opened + */ + async SetWorkingDirectory(directory: Item) { + if (!(await directory.Exists()) || !directory.IsDirectory()) + throw new Error(`Directory ${directory.GetPath()} doesn't exist`) + + this.workingDirectory = directory + } + + WriteStdin(data: string) { + this.stdin.emit(data) + } + + WriteEscapedString(data: string) { + let buf = data.split('') + buf.forEach((char) => { + if (!this.ProcessSimpleControlCode(char)) + this.terminal.Write(char) + }) + } + + PushToBuffer(text: string) { + text.split('').forEach(char => { + this.buffer.splice(this.bufferPos, 0, char) + this.bufferPos++ + }) + + // console.log(this.buffer) + } + + RemoveCharFromBuffer(amount: number, index: number) { + this.buffer.splice(index - amount, amount) + this.bufferPos -= amount + + // console.log(this.buffer) + } + + MoveBufferPos(index: number, absolute: boolean = false) { + this.bufferPos = Math.max(absolute ? index : this.bufferPos + index, 0) + } + + FlushBuffer() { + this.buffer = [] + this.bufferPos = 0 + } + + ExecuteBuffer() { + // parse the buffer + const state: { + inString: boolean + inHardString: boolean + inAndOperator: boolean + } = { + inString: false, + inHardString: false, + inAndOperator: false, + } + + const commands: { + args: string[] + stdin: null | SimpleStream + stdout: null | SimpleStream + }[] = [] + + const textBuf = + this.buffer + .join('') + .split('') + + textBuf.forEach((char, i) => { + // spacing + if (char === ' ' && !(state.inString || state.inHardString )) { + if (state.args[state.args.length - 1] !== '') + state.args.push("") + // string ("") handling + } else if (char === '"' && !state.inHardString) { + state.inString = !state.inString + // hard string ('') handling + } else if (char === "'" && !state.inString) { + state.inHardString = !state.inHardString + // && operator + } else if (char === '&' || char === '|' && !(state.inString || state.inHardString )) { + if (state.inAndOperator) { + if (state.args[state.args.length - 1] === '') + state.args[state.args.length - 1] = `${char}${char}` + else state.args.push(`${char}${char}`) + + state.args.push("") + state.inAndOperator = false + } else if (this.buffer[i + 1] !== char) { + state.args[state.args.length - 1] += char + } else state.inAndOperator = true + // adding the char to the current argument + } else { + state.args[state.args.length - 1] += char + } + }) + + this.FlushBuffer() + + console.log(`Executing ${state.args[0]} with args '${state.args}'`) + + this.execExitCode = -1 + + if (this.programs[state.args[0]] instanceof Program) { + this.ExecuteProgram(state.args[0], state.args) + } else { + this.execExitCode = 0 + + // don't print anything if there are no arguments + if (state.args[0].length !== 0) { + this.terminal.Write(`wush: unknown command: ${state.args[0]}.`) + this.terminal.MoveCursor(0, 1, { x: true, y: false }) + + this.execExitCode = 127 + } + + this.Prompt() + } + } + + ProcessSimpleControlCode(code: string): boolean { + switch (code) { + case '\f': + this.terminal.NewPage() + return true + case '\n': + this.terminal.MoveCursor(0, 1, { x: true, y: false }) + return true + default: + return false + } + } + + // todo: actual processing + ProcessAdvancedControlCode(complex: string): boolean { + if (!complex.match(/\\(.*;)/gm)) + return false + + return true + // const code = matches[] + } + + HandleKeyInput(key: string, isCharacter: boolean) { + if (this.execExitCode === -1) console.log('program running') + if (!isCharacter) { + switch (key) { + case 'ArrowLeft': + if (this.bufferPos > 0) { + this.terminal.MoveCursor(-1, 0) + this.MoveBufferPos(-1) + } + + break + case 'ArrowRight': + if (this.bufferPos < this.buffer.length) { + this.terminal.MoveCursor(1, 0) + this.MoveBufferPos(1) + } + break + case 'ArrowUp': + if (this.historyPos < this.history.length) { + if (this.historyPos === 0) + this.history.splice(0, 0, this.buffer.join('')) + + this.historyPos++ + console.log(this.historyPos) + console.log(this.history[this.historyPos]) + } + break + case 'ArrowDown': + if (this.historyPos > 0) { + this.historyPos-- + + if (this.historyPos === 0) + this.history.splice(0, 1) + + console.log(this.historyPos) + console.log(this.history[this.historyPos]) + } + break + case 'Backspace': + // don't erase anything if there's nothing left in the buffer + if (this.bufferPos === 0) + break + + this.terminal.RemoveCell() + this.RemoveCharFromBuffer(1, this.bufferPos) + // this.buffer.splice(this.bufferPos, 1) + this.terminal.MoveCursor(-1, 0) + break + case 'Delete': + if (this.bufferPos >= this.buffer.length || this.buffer.length <= 0) + break + + this.bufferPos++ + this.terminal.MoveCursor(1, 0) + this.terminal.RemoveCell() + this.RemoveCharFromBuffer(1, this.bufferPos) + this.terminal.MoveCursor(-1, 0) + break + case 'Enter': + // 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.history.splice(0, 0, this.buffer.join('')) + this.ExecuteBuffer() + } + + break + case 'F5': + location.reload() + break + } + } else { + this.historyPos = 0 + + // push the character into the buffer + this.terminal.InsertCell(key) + this.PushToBuffer(key) + this.terminal.MoveCursor(1, 0) + } + } +}