sync: sync wip stuff
This commit is contained in:
46
Writing Programs.md
Normal file
46
Writing Programs.md
Normal 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
|
||||||
33
src/app.ts
33
src/app.ts
@@ -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()
|
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
export class Directory {
|
|
||||||
readonly path: string
|
|
||||||
|
|
||||||
constructor(path: string) {
|
|
||||||
this.path = path
|
|
||||||
|
|
||||||
console.log(this.path.split('/'))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
175
src/fs/Item.ts
Normal 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
23
src/gui/Alert.ts
Normal 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}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/gui/alertTemplate.html
Normal file
24
src/gui/alertTemplate.html
Normal 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>
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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`)
|
||||||
|
|||||||
@@ -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`)
|
||||||
|
|||||||
@@ -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`)
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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`)
|
||||||
|
|
||||||
|
|||||||
@@ -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
0
src/program/Rm.ts
Normal file
119
src/program/Sl.ts
Normal file
119
src/program/Sl.ts
Normal 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
25
src/program/Touch.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -9,3 +9,8 @@ body {
|
|||||||
* {
|
* {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::selection {
|
||||||
|
background: colors.$terminal-white;
|
||||||
|
color: colors.$terminal-background;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user