Compare commits
2 Commits
ed15d94255
...
21a1a34309
| Author | SHA1 | Date | |
|---|---|---|---|
| 21a1a34309 | |||
| 686abeafb4 |
@@ -9,13 +9,13 @@ Aplikace je zdarma hostovaná přes Vercel a je veřejně dostupná na adrese ht
|
||||
- Univerzálně:
|
||||
1. pomocí npm příkazem `npm i -g bun`
|
||||
2. z oficiální stránky https://bun.sh/
|
||||
- Win NT:
|
||||
- M$ Win:
|
||||
1. z powershellu příkazem `powershell -c "irm bun.sh/install.ps1 | iex"`
|
||||
2. přes winget powershellovým příkazem `winget install --id Oven-sh.Bun`
|
||||
2. (NT 10+) přes winget powershellovým příkazem `winget install --id Oven-sh.Bun`
|
||||
3. pomocí chocolatey příkazem `choco install bun`
|
||||
- Arch Linux:
|
||||
- Linux:
|
||||
1. z terminálu příkazem `curl -fsSL https://bun.sh/install | bash`
|
||||
2. z `extra` repozitáře příkazem `pacman -S bun`
|
||||
2. (Arch) z `extra` repozitáře příkazem `pacman -S bun`
|
||||
- MacOS:
|
||||
1. z terminálu příkazem `curl -fsSL https://bun.sh/install | bash`
|
||||
2. pomocí homebrew příkazem `brew tap oven-sh/bun && brew install bun`
|
||||
@@ -34,7 +34,6 @@ Aplikace je zdarma hostovaná přes Vercel a je veřejně dostupná na adrese ht
|
||||
## testování
|
||||
|
||||
Aplikace je testována v následujících prohlížečích:
|
||||
- `Chromium 146.0.7680.153 (Official Build) Arch Linux (64-bit)`
|
||||
- `Zen Browser 1.19.3b (Firefox 148.0.2) (64-bit)`
|
||||
|
||||
## licence
|
||||
|
||||
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
|
||||
// the best name I could come up with lmao
|
||||
|
||||
import { ClearExec } from '../program/clear'
|
||||
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'
|
||||
|
||||
export class Wush extends Shell {
|
||||
// buffer
|
||||
private buffer: string[] = []
|
||||
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) {
|
||||
super(broadcaster, terminal)
|
||||
|
||||
// create streams
|
||||
this.stdin = new SimpleStream<string>()
|
||||
this.stdout = new SimpleStream<string>()
|
||||
}
|
||||
|
||||
Init() {
|
||||
this.broadcaster.on('keydown', (key: string, isCharacter: boolean) =>
|
||||
this.HandleKeyInput(key, isCharacter),
|
||||
)
|
||||
|
||||
this.stdout.on(data => this.WriteEscapedString(data))
|
||||
this.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) {
|
||||
@@ -54,31 +87,90 @@ export class Wush extends Shell {
|
||||
this.FlushBuffer()
|
||||
|
||||
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) {
|
||||
if (this.execExitCode === -1) console.log('program running')
|
||||
if (!isCharacter) {
|
||||
switch (key) {
|
||||
case 'ArrowLeft':
|
||||
this.terminal.MoveCursor(-1, 0)
|
||||
this.MoveBufferPos(-1)
|
||||
if (this.bufferPos > 0) {
|
||||
this.terminal.MoveCursor(-1, 0)
|
||||
this.MoveBufferPos(-1)
|
||||
}
|
||||
|
||||
break
|
||||
case 'ArrowRight':
|
||||
this.terminal.MoveCursor(1, 0)
|
||||
this.MoveBufferPos(1)
|
||||
if (this.bufferPos < this.buffer.length) {
|
||||
this.terminal.MoveCursor(1, 0)
|
||||
this.MoveBufferPos(1)
|
||||
}
|
||||
break
|
||||
case 'Backspace':
|
||||
// don't erase anything if there's nothing left in the buffer
|
||||
if (this.bufferPos === 0)
|
||||
break
|
||||
|
||||
this.terminal.RemoveCell()
|
||||
this.RemoveLastCharsFromBuffer(1)
|
||||
this.terminal.MoveCursor(-1, 0)
|
||||
break
|
||||
case 'Enter':
|
||||
this.terminal.MoveCursor(0, 1, { x: true, y: false })
|
||||
this.ExecuteBuffer()
|
||||
this.Prompt()
|
||||
// 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.ExecuteBuffer()
|
||||
}
|
||||
|
||||
break
|
||||
case 'F5':
|
||||
location.reload()
|
||||
break
|
||||
}
|
||||
} else {
|
||||
// push the character into the buffer
|
||||
this.terminal.InsertCell(key)
|
||||
this.PushToBuffer(key)
|
||||
this.terminal.MoveCursor(1, 0)
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
@forward "terminal.scss";
|
||||
@use "./colors.scss" as colors;
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background: colors.$terminal-background;
|
||||
}
|
||||
|
||||
* {
|
||||
|
||||
@@ -43,6 +43,7 @@ export class Terminal {
|
||||
paragraph.className = 'line'
|
||||
|
||||
this.terminal.appendChild(paragraph)
|
||||
this.UpdateLines()
|
||||
paragraph.scrollIntoView({behavior: 'smooth'})
|
||||
}
|
||||
|
||||
@@ -70,10 +71,8 @@ export class Terminal {
|
||||
const selector = `#line-${this.cursorPosition.row} .cell-${this.cursorPosition.col}`
|
||||
|
||||
// adjust for the cursor
|
||||
console.log(`going from ${this.terminal.children.length - 1} to ${this.cursorPosition.row}`)
|
||||
if (!document.querySelector(`#line-${this.cursorPosition.row}`)) {
|
||||
for (let i = this.terminal.children.length - 1; i < this.cursorPosition.row; i++) {
|
||||
console.log(i)
|
||||
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