feat: streams, 'clear' command
This commit is contained in:
7
src/program/clear.ts
Normal file
7
src/program/clear.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import type { SimpleStream } from '../utils/SimpleStream'
|
||||||
|
|
||||||
|
export const ClearExec = async (stdin: SimpleStream<string>, stdout: SimpleStream<string>): Promise<number> => {
|
||||||
|
stdout.emit("\f")
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
8
src/program/stdlibwsh.ts
Normal file
8
src/program/stdlibwsh.ts
Normal file
@@ -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 = {
|
||||||
|
|
||||||
|
}
|
||||||
4
src/shell/ControlCode.ts
Normal file
4
src/shell/ControlCode.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
// a representation of ascii control codes
|
||||||
|
export const ControlCode = {
|
||||||
|
FormFeed: 12,
|
||||||
|
}
|
||||||
@@ -1,27 +1,60 @@
|
|||||||
// Web-Uno Shell
|
// Web-Uno Shell
|
||||||
// the best name I could come up with lmao
|
// the best name I could come up with lmao
|
||||||
|
|
||||||
|
import { ClearExec } from '../program/clear'
|
||||||
import { Terminal } from '../terminal/Terminal'
|
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'
|
import { Shell } from './Shell'
|
||||||
|
|
||||||
export class Wush extends Shell {
|
export class Wush extends Shell {
|
||||||
|
// buffer
|
||||||
private buffer: string[] = []
|
private buffer: string[] = []
|
||||||
private bufferPos: number = 0
|
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<string>
|
||||||
|
readonly stdout: SimpleStream<string>
|
||||||
|
|
||||||
constructor(broadcaster: EventBroadcaster, terminal: Terminal) {
|
constructor(broadcaster: EventBroadcaster, terminal: Terminal) {
|
||||||
super(broadcaster, terminal)
|
super(broadcaster, terminal)
|
||||||
|
|
||||||
|
// create streams
|
||||||
|
this.stdin = new SimpleStream<string>()
|
||||||
|
this.stdout = new SimpleStream<string>()
|
||||||
}
|
}
|
||||||
|
|
||||||
Init() {
|
Init() {
|
||||||
this.broadcaster.on('keydown', (key: string, isCharacter: boolean) =>
|
this.broadcaster.on('keydown', (key: string, isCharacter: boolean) =>
|
||||||
this.HandleKeyInput(key, isCharacter),
|
this.HandleKeyInput(key, isCharacter),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
this.stdout.on(data => this.WriteEscapedString(data))
|
||||||
this.Prompt()
|
this.Prompt()
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
PushToBuffer(text: string) {
|
||||||
@@ -54,31 +87,90 @@ export class Wush extends Shell {
|
|||||||
this.FlushBuffer()
|
this.FlushBuffer()
|
||||||
|
|
||||||
console.log(`Executing ${args[0]} with args '${args}'`)
|
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) {
|
HandleKeyInput(key: string, isCharacter: boolean) {
|
||||||
|
if (this.execExitCode === -1) console.log('program running')
|
||||||
if (!isCharacter) {
|
if (!isCharacter) {
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case 'ArrowLeft':
|
case 'ArrowLeft':
|
||||||
this.terminal.MoveCursor(-1, 0)
|
if (this.bufferPos > 0) {
|
||||||
this.MoveBufferPos(-1)
|
this.terminal.MoveCursor(-1, 0)
|
||||||
|
this.MoveBufferPos(-1)
|
||||||
|
}
|
||||||
|
|
||||||
break
|
break
|
||||||
case 'ArrowRight':
|
case 'ArrowRight':
|
||||||
this.terminal.MoveCursor(1, 0)
|
if (this.bufferPos < this.buffer.length) {
|
||||||
this.MoveBufferPos(1)
|
this.terminal.MoveCursor(1, 0)
|
||||||
|
this.MoveBufferPos(1)
|
||||||
|
}
|
||||||
break
|
break
|
||||||
case 'Backspace':
|
case 'Backspace':
|
||||||
|
// don't erase anything if there's nothing left in the buffer
|
||||||
|
if (this.bufferPos === 0)
|
||||||
|
break
|
||||||
|
|
||||||
this.terminal.RemoveCell()
|
this.terminal.RemoveCell()
|
||||||
this.RemoveLastCharsFromBuffer(1)
|
this.RemoveLastCharsFromBuffer(1)
|
||||||
this.terminal.MoveCursor(-1, 0)
|
this.terminal.MoveCursor(-1, 0)
|
||||||
break
|
break
|
||||||
case 'Enter':
|
case 'Enter':
|
||||||
this.terminal.MoveCursor(0, 1, { x: true, y: false })
|
// send the buffer to stdin if an exec is running
|
||||||
this.ExecuteBuffer()
|
if (this.execExitCode === -1) {
|
||||||
this.Prompt()
|
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
|
break
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// push the character into the buffer
|
||||||
this.terminal.InsertCell(key)
|
this.terminal.InsertCell(key)
|
||||||
this.PushToBuffer(key)
|
this.PushToBuffer(key)
|
||||||
this.terminal.MoveCursor(1, 0)
|
this.terminal.MoveCursor(1, 0)
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
@forward "terminal.scss";
|
@forward "terminal.scss";
|
||||||
|
@use "./colors.scss" as colors;
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
background: colors.$terminal-background;
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ export class Terminal {
|
|||||||
paragraph.className = 'line'
|
paragraph.className = 'line'
|
||||||
|
|
||||||
this.terminal.appendChild(paragraph)
|
this.terminal.appendChild(paragraph)
|
||||||
|
this.UpdateLines()
|
||||||
paragraph.scrollIntoView({behavior: 'smooth'})
|
paragraph.scrollIntoView({behavior: 'smooth'})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,10 +71,8 @@ export class Terminal {
|
|||||||
const selector = `#line-${this.cursorPosition.row} .cell-${this.cursorPosition.col}`
|
const selector = `#line-${this.cursorPosition.row} .cell-${this.cursorPosition.col}`
|
||||||
|
|
||||||
// adjust for the cursor
|
// adjust for the cursor
|
||||||
console.log(`going from ${this.terminal.children.length - 1} to ${this.cursorPosition.row}`)
|
|
||||||
if (!document.querySelector(`#line-${this.cursorPosition.row}`)) {
|
if (!document.querySelector(`#line-${this.cursorPosition.row}`)) {
|
||||||
for (let i = this.terminal.children.length - 1; i < this.cursorPosition.row; i++) {
|
for (let i = this.terminal.children.length - 1; i < this.cursorPosition.row; i++) {
|
||||||
console.log(i)
|
|
||||||
this.AppendLine()
|
this.AppendLine()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
50
src/utils/SimpleStream.ts
Normal file
50
src/utils/SimpleStream.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
export class SimpleStream<T> {
|
||||||
|
private listeners: Set<Function> = 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<T> {
|
||||||
|
return new Promise<T>(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))
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user