sync: sync wip stuff

This commit is contained in:
2026-05-12 10:38:30 +02:00
parent dc706d6262
commit 97b9994594
21 changed files with 532 additions and 100 deletions

46
Writing Programs.md Normal file
View File

@@ -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<T>
- 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<T>
- 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 (<code letter> -> <description>)
1. n -> line feed (new line)
2. f -> form feed (new page)
- Longer codes (<code id>,<first parameter>,<...parameters> -> <description>)
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<string>, stdout: SimpleStream<string>, workdir: Item, args: string[]): Promise<number>
- 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

View File

@@ -1,3 +1,4 @@
import { Alert } from './gui/Alert'
import { CreateKeyboardListeners } from './input/keyboard' import { CreateKeyboardListeners } from './input/keyboard'
import { Wush } from './shell/Wush' import { Wush } from './shell/Wush'
import { Terminal } from './terminal/Terminal' import { Terminal } from './terminal/Terminal'
@@ -5,6 +6,36 @@ import { EventBroadcaster } from './utils/EventBroadcaster'
export const WEBSHELL_VERSION = "0.1.0" 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 let CurrentTerminal: Terminal
export const SetCurrentTerminal = (terminal: Terminal) => CurrentTerminal = terminal export const SetCurrentTerminal = (terminal: Terminal) => CurrentTerminal = terminal
export const GetCurrentTerminal = (): Terminal => CurrentTerminal export const GetCurrentTerminal = (): Terminal => CurrentTerminal
@@ -22,5 +53,3 @@ const init = () => {
const terminal = new Terminal() const terminal = new Terminal()
terminal.LoadShell(new Wush(localBroadcaster, terminal)) terminal.LoadShell(new Wush(localBroadcaster, terminal))
} }
init()

View File

@@ -1,9 +0,0 @@
export class Directory {
readonly path: string
constructor(path: string) {
this.path = path
console.log(this.path.split('/'))
}
}

View File

@@ -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
}
}

175
src/fs/Item.ts Normal file
View File

@@ -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
}
}
}

23
src/gui/Alert.ts Normal file
View File

@@ -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}`)
}
}

View File

@@ -0,0 +1,24 @@
<div class="webshellAlert">
<style>
#webshellAlert {
border-radius: 0px;
border: 2px solid {{severity_color}};
display: flex;
flex-direction: column;
width: fit-content;
height: fit-content;
}
button {
border-radius: 0px;
border: 2px solid #86e9d5;
}
</style>
<h1>{{title}}</h1>
<p>{{description}}</p>
<button>ok</button>
<button>ok</button>
</div>

View File

@@ -1,8 +1,9 @@
import type { Item } from '../fs/Item'
import type { SimpleStream } from '../utils/SimpleStream' import type { SimpleStream } from '../utils/SimpleStream'
import { Program } from './Program' import { Program } from './Program'
export class Clear extends Program { export class Clear extends Program {
async Exec(_: SimpleStream<string>, stdout: SimpleStream<string>, __: string[]): Promise<number> { async Exec(_: SimpleStream<string>, stdout: SimpleStream<string>, __: Item, ___: string[]): Promise<number> {
stdout.emit('\f') stdout.emit('\f')
return 0 return 0

View File

@@ -1,11 +1,13 @@
import type { Item } from '../fs/Item'
import type { SimpleStream } from '../utils/SimpleStream' import type { SimpleStream } from '../utils/SimpleStream'
import { Program } from './Program' import { Program } from './Program'
export class Eval extends Program { export class Eval extends Program {
async Exec(_: SimpleStream<string>, stdout: SimpleStream<string>, args: string[]): Promise<number> { async Exec(_: SimpleStream<string>, stdout: SimpleStream<string>, _workdir: Item, args: string[]): Promise<number> {
const javascript = args.slice(1).join(' ') const javascript = args.slice(1).join(' ')
try { try {
// todo: pass workdir to the program
eval(javascript) eval(javascript)
} catch (e) { } catch (e) {
stdout.emit(`${String(e)}\n`) stdout.emit(`${String(e)}\n`)

View File

@@ -1,10 +1,11 @@
import { GetCurrentTerminal, WEBSHELL_VERSION } from '../app' import { GetCurrentTerminal, WEBSHELL_VERSION } from '../app'
import type { Item } from '../fs/Item'
import { Terminal } from '../terminal/Terminal' import { Terminal } from '../terminal/Terminal'
import type { SimpleStream } from '../utils/SimpleStream' import type { SimpleStream } from '../utils/SimpleStream'
import { Program } from './Program' import { Program } from './Program'
export class Info extends Program { export class Info extends Program {
async Exec(_: SimpleStream<string>, stdout: SimpleStream<string>, __: string[]): Promise<number> { async Exec(_: SimpleStream<string>, stdout: SimpleStream<string>, __: Item, ___: string[]): Promise<number> {
stdout.emit(`Webshell v${WEBSHELL_VERSION}\n`) stdout.emit(`Webshell v${WEBSHELL_VERSION}\n`)
stdout.emit(`Terminal v${Terminal.Version}\n`) stdout.emit(`Terminal v${Terminal.Version}\n`)
stdout.emit(`Running ${GetCurrentTerminal().GetShell()?.Name} v${GetCurrentTerminal().GetShell()?.Version}\n`) stdout.emit(`Running ${GetCurrentTerminal().GetShell()?.Name} v${GetCurrentTerminal().GetShell()?.Version}\n`)

View File

@@ -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 type { SimpleStream } from '../utils/SimpleStream'
import { Program } from './Program' import { Program } from './Program'
export class Loadprg extends Program { export class Loadprg extends Program {
private wush: Wush private shell: Shell
constructor(wush: Wush) { constructor(shell: Shell) {
super() super()
this.wush = wush this.shell = shell
} }
async Exec(_: SimpleStream<string>, stdout: SimpleStream<string>, args: string[]): Promise<number> { async Exec(_: SimpleStream<string>, stdout: SimpleStream<string>, __: Item, args: string[]): Promise<number> {
// stdout.emit('Not implemented yet.\n')
// return 0
const javascript = args.slice(2).join(' ') const javascript = args.slice(2).join(' ')
try { try {
const exec: Function = eval(javascript) const exec: Function = eval(javascript)
const program = class extends Program { const program = class extends Program {
async Exec(stdin: SimpleStream<string>, stdout: SimpleStream<string>, args: string[]): Promise<number> { async Exec(stdin: SimpleStream<string>, stdout: SimpleStream<string>, workdir: Item, args: string[]): Promise<number> {
exec(stdin, stdout, args) exec(stdin, stdout, workdir, args)
return 0 return 0
} }
} }
this.wush.LoadProgram(new program(), args[1]) this.shell.LoadProgram(new program(), args[1])
} catch (e) { } catch (e) {
stdout.emit(`${String(e)}\n`) stdout.emit(`${String(e)}\n`)
stdout.emit(`${String((e as Error).stack)}\n`) stdout.emit(`${String((e as Error).stack)}\n`)

View File

@@ -1,15 +1,23 @@
import { Item } from '../fs/Item'
import type { SimpleStream } from '../utils/SimpleStream' import type { SimpleStream } from '../utils/SimpleStream'
import { Program } from './Program' import { Program } from './Program'
import { File } from '../fs/File'
import { Directory } from '../fs/Directory'
export class Ls extends Program { export class Ls extends Program {
constructor() { constructor() {
super() super()
} }
async Exec(_: SimpleStream<string>, stdout: SimpleStream<string>, workdir: Directory, __: string[]): Promise<number> { async Exec(_: SimpleStream<string>, stdout: SimpleStream<string>, workdir: Item, args: string[]): Promise<number> {
new Directory('/etc/system/idk') 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 return 0
} }

View File

@@ -1,3 +1,4 @@
import type { Item } from '../fs/Item'
import type { Wush } from '../shell/Wush' import type { Wush } from '../shell/Wush'
import type { SimpleStream } from '../utils/SimpleStream' import type { SimpleStream } from '../utils/SimpleStream'
import { Program } from './Program' import { Program } from './Program'
@@ -10,7 +11,7 @@ export class Lsprg extends Program {
this.wush = wush this.wush = wush
} }
async Exec(_: SimpleStream<string>, stdout: SimpleStream<string>, __: string[]): Promise<number> { async Exec(_: SimpleStream<string>, stdout: SimpleStream<string>, __: Item, ___: string[]): Promise<number> {
for (const program in this.wush.GetPrograms()) for (const program in this.wush.GetPrograms())
stdout.emit(`${program}\n`) stdout.emit(`${program}\n`)

View File

@@ -1,5 +1,11 @@
import type { Item } from "../fs/Item"
import type { SimpleStream } from "../utils/SimpleStream" import type { SimpleStream } from "../utils/SimpleStream"
export abstract class Program { export abstract class Program {
abstract Exec(stdin: SimpleStream<string>, stdout: SimpleStream<string>, args: string[]): Promise<number> abstract Exec(
stdin: SimpleStream<string>,
stdout: SimpleStream<string>,
workdir: Item,
args: string[]
): Promise<number>
} }

0
src/program/Rm.ts Normal file
View File

119
src/program/Sl.ts Normal file
View File

@@ -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<string>,
stdout: SimpleStream<string>,
__: Item,
args: string[],
): Promise<number> {
// 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<void> {
return new Promise(resolve => setTimeout(resolve, ms))
}
}

25
src/program/Touch.ts Normal file
View File

@@ -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<string>, stdout: SimpleStream<string>, workdir: Item, args: string[]): Promise<number> {
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
}
}

View File

@@ -1,3 +1,4 @@
import type { Program } from '../program/Program'
import type { Terminal } from '../terminal/Terminal' import type { Terminal } from '../terminal/Terminal'
import type { EventBroadcaster } from '../utils/EventBroadcaster' import type { EventBroadcaster } from '../utils/EventBroadcaster'
@@ -12,6 +13,8 @@ export abstract class Shell {
this.terminal = terminal this.terminal = terminal
} }
abstract LoadProgram(program: Program, name: string): void
abstract ExecuteProgram(name: string, args: string[]): void
abstract Init(): void abstract Init(): void
abstract HandleKeyInput(key: string, isCharacter: boolean): void abstract HandleKeyInput(key: string, isCharacter: boolean): void
} }

View File

@@ -12,6 +12,9 @@ import { EventBroadcaster } from '../utils/EventBroadcaster'
import { SimpleStream } from '../utils/SimpleStream' import { SimpleStream } from '../utils/SimpleStream'
import { Shell } from './Shell' import { Shell } from './Shell'
import { Ls } from '../program/Ls' 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 { export class Wush extends Shell {
public readonly Version = "0.1.0" public readonly Version = "0.1.0"
@@ -35,6 +38,7 @@ export class Wush extends Shell {
readonly stdout: SimpleStream<string> readonly stdout: SimpleStream<string>
private programs: { [name: string]: Program } = {} private programs: { [name: string]: Program } = {}
private workingDirectory: Item = Item.Root
constructor(broadcaster: EventBroadcaster, terminal: Terminal) { constructor(broadcaster: EventBroadcaster, terminal: Terminal) {
super(broadcaster, terminal) super(broadcaster, terminal)
@@ -56,6 +60,8 @@ export class Wush extends Shell {
this.programs['lsprg'] = new Lsprg(this) this.programs['lsprg'] = new Lsprg(this)
this.programs['info'] = new Info() this.programs['info'] = new Info()
this.programs['ls'] = new Ls() this.programs['ls'] = new Ls()
this.programs['touch'] = new Touch()
this.programs['sl'] = new Sl()
this.stdout.on(data => this.WriteEscapedString(data)) this.stdout.on(data => this.WriteEscapedString(data))
this.Prompt() this.Prompt()
@@ -73,6 +79,26 @@ export class Wush extends Shell {
delete this.programs[name] 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() { Prompt() {
this.terminal.Write(`hi [${this.execExitCode}] -> `) this.terminal.Write(`hi [${this.execExitCode}] -> `)
} }
@@ -84,7 +110,7 @@ export class Wush extends Shell {
WriteEscapedString(data: string) { WriteEscapedString(data: string) {
let buf = data.split('') let buf = data.split('')
buf.forEach((char) => { buf.forEach((char) => {
if (!this.ProcessControlCode(char)) if (!this.ProcessSimpleControlCode(char))
this.terminal.Write(char) this.terminal.Write(char)
}) })
} }
@@ -123,23 +149,7 @@ export class Wush extends Shell {
this.execExitCode = -1 this.execExitCode = -1
if (this.programs[args[0]] instanceof Program) { if (this.programs[args[0]] instanceof Program) {
this.programs[args[0]].Exec(this.stdin, this.stdout, args) this.ExecuteProgram(args[0], 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()
})
} else { } else {
this.execExitCode = 0 this.execExitCode = 0
@@ -153,7 +163,7 @@ export class Wush extends Shell {
} }
} }
ProcessControlCode(code: string): boolean { ProcessSimpleControlCode(code: string): boolean {
switch (code) { switch (code) {
case '\f': case '\f':
this.terminal.NewPage() 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) { HandleKeyInput(key: string, isCharacter: boolean) {
if (this.execExitCode === -1) console.log('program running') if (this.execExitCode === -1) console.log('program running')
if (!isCharacter) { if (!isCharacter) {

View File

@@ -9,3 +9,8 @@ body {
* { * {
box-sizing: border-box; box-sizing: border-box;
} }
::selection {
background: colors.$terminal-white;
color: colors.$terminal-background;
}

View File

@@ -3,17 +3,17 @@ export class SimpleStream<T> {
/** /**
* Attaches a listener to the stream * 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) this.listeners.add(listener)
} }
/** /**
* Attaches a one-time listener to the stream * 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) => { const func = (data: T) => {
listener(data) listener(data)
this.off(func) this.off(func)
@@ -23,8 +23,8 @@ export class SimpleStream<T> {
} }
/** /**
* Patiently waits until data is streamed, then returns it * Patiently waits until new data is streamed, then returns it
* @returns a promise of a single chunk of streamed data * @returns a promise for a single chunk of streamed data
*/ */
wait(): Promise<T> { wait(): Promise<T> {
return new Promise<T>(resolve => { return new Promise<T>(resolve => {
@@ -36,7 +36,7 @@ export class SimpleStream<T> {
* Removes a listener from the stream * Removes a listener from the stream
* @param listener the function to remove * @param listener the function to remove
*/ */
off(listener: Function) { off(listener: Function): void {
this.listeners.delete(listener) this.listeners.delete(listener)
} }
@@ -44,7 +44,7 @@ export class SimpleStream<T> {
* Streams data * Streams data
* @param data the data to stream * @param data the data to stream
*/ */
emit(data: T) { emit(data: T): void {
this.listeners.forEach((listener) => listener(data)) this.listeners.forEach((listener) => listener(data))
} }
} }