// 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 { Printf } from '../../program/Printf' import { Pwd } from '../../program/Pwd' // 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.programs['printf'] = new Printf() 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] } async ExecuteProgram(name: string, args: string[]): Promise { return new Promise(resolve => { this.programs[name].Exec(this.stdin, this.stdout, this.workingDirectory, args) .then(code => { this.execExitCode = code != -1 ? code : -2 resolve() }) .catch((e) => { this.WriteEscapedString(`wush: command ${name} exited with the following exception\n`) this.WriteEscapedString(` | ${String(e)}\n`) resolve() }) .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}.`) resolve() }) }) } Prompt() { this.terminal.Write(`user 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 } async ExecuteBuffer() { type OperatorToken = '&&' | '||' | '|' | ';' type Token = { type: 'word'; value: string } | { type: 'operator'; value: OperatorToken } type ParsedCommand = { args: string[] stdin: null | SimpleStream stdout: null | SimpleStream } type ParsedPipeline = { commands: ParsedCommand[] } type ParsedListItem = { pipeline: ParsedPipeline; operator: '&&' | '||' | ';' | null } const tokenize = (text: string): { tokens: Token[]; error: string | null } => { const tokens: Token[] = [] let current = '' let wordStarted = false let inSingleQuote = false let inDoubleQuote = false let escapeNext = false const pushWord = () => { if (!wordStarted) return tokens.push({ type: 'word', value: current }) current = '' wordStarted = false } for (let i = 0; i < text.length; i++) { const char = text[i] if (escapeNext) { current += char escapeNext = false wordStarted = true continue } if (inSingleQuote) { if (char === "'") { inSingleQuote = false wordStarted = true } else { current += char wordStarted = true } continue } if (inDoubleQuote) { if (char === '"') { inDoubleQuote = false wordStarted = true } else if (char === '\\') { if (i + 1 >= text.length) return { tokens: [], error: 'unterminated escape sequence' } current += text[i + 1] i++ wordStarted = true } else { current += char wordStarted = true } continue } if (char === '\\') { escapeNext = true wordStarted = true continue } if (char === "'") { inSingleQuote = true wordStarted = true continue } if (char === '"') { inDoubleQuote = true wordStarted = true continue } if (char === ' ' || char === '\t') { pushWord() continue } if (char === '#') { if (!wordStarted) break current += char wordStarted = true continue } if (char === ';') { pushWord() tokens.push({ type: 'operator', value: ';' }) continue } if (char === '|') { pushWord() if (text[i + 1] === '|') { tokens.push({ type: 'operator', value: '||' }) i++ } else { tokens.push({ type: 'operator', value: '|' }) } continue } if (char === '&') { if (text[i + 1] === '&') { pushWord() tokens.push({ type: 'operator', value: '&&' }) i++ } else { current += char wordStarted = true } continue } current += char wordStarted = true } if (escapeNext) return { tokens: [], error: 'unterminated escape sequence' } if (inSingleQuote) return { tokens: [], error: 'unterminated single quote' } if (inDoubleQuote) return { tokens: [], error: 'unterminated double quote' } pushWord() return { tokens, error: null } } const parseTokens = (tokens: Token[]): { list: ParsedListItem[]; error: string | null } => { const list: ParsedListItem[] = [] let currentArgs: string[] = [] let currentPipeline: ParsedCommand[] = [] let expectCommand = false const pushCommand = () => { if (currentArgs.length === 0) return false currentPipeline.push({ args: currentArgs, stdin: null, stdout: null, }) currentArgs = [] return true } const pushPipeline = (operator: '&&' | '||' | ';' | null) => { if (currentPipeline.length === 0) return false list.push({ pipeline: { commands: currentPipeline }, operator }) currentPipeline = [] return true } for (const token of tokens) { if (token.type === 'word') { currentArgs.push(token.value) expectCommand = false continue } if (token.value === '|') { if (!pushCommand()) return { list: [], error: 'unexpected "|" operator' } expectCommand = true continue } if (!pushCommand() || !pushPipeline(token.value)) return { list: [], error: `unexpected "${token.value}" operator` } expectCommand = true } if (expectCommand) return { list: [], error: 'unexpected end of input' } if (currentArgs.length > 0) pushCommand() if (currentPipeline.length > 0) pushPipeline(null) return { list, error: null } } const line = this.buffer.join('') this.FlushBuffer() const { tokens, error } = tokenize(line) if (error) { this.terminal.Write(`wush: error: ${error}`) this.terminal.MoveCursor(0, 1, { x: true, y: false }) this.Prompt() return } const { list, error: parseError } = parseTokens(tokens) if (parseError) { this.terminal.Write(`wush: error: ${parseError}`) this.terminal.MoveCursor(0, 1, { x: true, y: false }) this.Prompt() return } if (list.length === 0) { this.Prompt() return } if (list.some(item => item.pipeline.commands.length > 1)) { this.terminal.Write(`wush: error: pipes are not supported yet`) this.terminal.MoveCursor(0, 1, { x: true, y: false }) this.Prompt() return } for (const item of list) { const command = item.pipeline.commands[0] this.execExitCode = -1 if (this.programs[command.args[0]] instanceof Program) { await this.ExecuteProgram(command.args[0], command.args) } else { this.execExitCode = 0 // don't print anything if there are no arguments if (command.args[0].length !== 0) { this.terminal.Write(`wush: error: unknown command: ${command.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) } } }