From 064984382100e4429ed30b597ccd7de7413592ae Mon Sep 17 00:00:00 2001 From: binekrasik Date: Wed, 20 May 2026 22:46:59 +0200 Subject: [PATCH] chore: offload user input to InputManager --- src/program/Mkdir.ts | 2 +- src/program/Touch.ts | 2 +- src/shell/Shell.ts | 3 +- .../{ShellEnvironment.ts => Environment.ts} | 2 +- src/shell/wush/InputManager.ts | 230 ++++++++++++++++++ src/shell/wush/ShellKeyboardManager.ts | 129 ---------- src/shell/wush/Wush.ts | 129 ++++------ 7 files changed, 285 insertions(+), 212 deletions(-) rename src/shell/wush/{ShellEnvironment.ts => Environment.ts} (97%) create mode 100644 src/shell/wush/InputManager.ts delete mode 100644 src/shell/wush/ShellKeyboardManager.ts diff --git a/src/program/Mkdir.ts b/src/program/Mkdir.ts index 82cc2bb..aaac737 100644 --- a/src/program/Mkdir.ts +++ b/src/program/Mkdir.ts @@ -21,7 +21,7 @@ export class Mkdir extends Program { return 2 } - item.Create() + await item.Create() return 0 } diff --git a/src/program/Touch.ts b/src/program/Touch.ts index abe8a12..0a0119b 100644 --- a/src/program/Touch.ts +++ b/src/program/Touch.ts @@ -20,7 +20,7 @@ export class Touch extends Program { return 1 } - item.Create() + await item.Create() return 0 } diff --git a/src/shell/Shell.ts b/src/shell/Shell.ts index c89a34e..6a29b81 100644 --- a/src/shell/Shell.ts +++ b/src/shell/Shell.ts @@ -7,7 +7,7 @@ export abstract class Shell { readonly abstract Version: string readonly abstract Name: string broadcaster: EventBroadcaster - terminal: Terminal + readonly terminal: Terminal constructor(broadcaster: EventBroadcaster, terminal: Terminal) { this.broadcaster = broadcaster @@ -17,6 +17,5 @@ export abstract class Shell { abstract LoadProgram(program: Program, name: string): void abstract ExecuteProgram(name: string, args: string[]): void abstract Init(): Promise - abstract HandleKeyInput(key: string, isCharacter: boolean): void abstract SetWorkingDirectory(directory: Item): Promise } diff --git a/src/shell/wush/ShellEnvironment.ts b/src/shell/wush/Environment.ts similarity index 97% rename from src/shell/wush/ShellEnvironment.ts rename to src/shell/wush/Environment.ts index 0b9ec03..8f1175d 100644 --- a/src/shell/wush/ShellEnvironment.ts +++ b/src/shell/wush/Environment.ts @@ -2,7 +2,7 @@ * Wush environment manager * Manages environment variables, aliases etc. */ -export class ShellEnvironment { +export class Environment { private variables: Record = {} private aliases: Record = {} diff --git a/src/shell/wush/InputManager.ts b/src/shell/wush/InputManager.ts new file mode 100644 index 0000000..c301a74 --- /dev/null +++ b/src/shell/wush/InputManager.ts @@ -0,0 +1,230 @@ +import type { EventBroadcaster } from "../../utils/EventBroadcaster" +import type { Wush } from "./Wush" + +export type ModifierKey = 'shift' | 'ctrl' | 'alt' | 'rightAlt' + +/** + * Wush input manager + * Manages input events and holds command history + */ +export class InputManager { + private modifierKeys: ModifierKey[] = [] + private shell: Wush + private keyEventBroadcaster: EventBroadcaster + + // history + history: string[] = [] + historyIndex: number | null = null + historyDraft: string = '' + + constructor(shell: Wush, keyEventBroadcaster: EventBroadcaster) { + this.shell = shell + this.keyEventBroadcaster = keyEventBroadcaster + } + + // + // -> history management + // + + private NavigateHistory(direction: -1 | 1) { + // don't continue if there's no history + if (this.history.length === 0) + return + + // I honestly don't know how but it works. + // __**do not touch this under any circumstances**__ + + if (direction === -1) { + if (this.historyIndex === null) { + this.historyDraft = this.shell._buffer.join('') + this.historyIndex = this.history.length - 1 + } else if (this.historyIndex > 0) { + this.historyIndex -= 1 + } else return + + this.shell._SetBuffer(this.history[this.historyIndex]) + return + } + + if (this.historyIndex === null) + return + + if (this.historyIndex < this.history.length - 1) { + this.historyIndex += 1 + this.shell._SetBuffer(this.history[this.historyIndex]) + return + } + + this.historyIndex = null + this.shell._SetBuffer(this.historyDraft) + } + + private ExitHistoryNavigation() { + this.historyIndex = null + this.historyDraft = '' + } + + // + // -> keyboard management + // + + /** + * Checks if the modifier includes every key of the combo + * @param combo modifier combo to check for + * @returns boolean according to the check truthfullness + */ + static HasModifierCombo(modifiers: ModifierKey[], combo: ModifierKey[]) { + let result = true + + // check if the modifier buffer has every key in the combo + combo.forEach(key => { + if (!modifiers.includes(key)) { + result = false + return + } + }) + + return result + } + + /** + * Gets a ModifierKey using a key id from KeyboardEvent + * @param key key from KeyboardEvent + * @returns a ModifierKey string if the provided key is valid, null otherwise + */ + static GetModifierKeyFromKey(key: string): ModifierKey | null { + switch (key) { + case 'Control': + return 'ctrl' + case 'Shift': + return 'shift' + case 'Alt': + return 'alt' + case 'AltGraph': + return 'rightAlt' + default: + return null + } + } + + /** + * Registers keyboard listeners + */ + RegisterEvents() { + // also check for unfocus events as the modifier keys may be pressed when the window loses focus + window.addEventListener('blur', () => { + this.modifierKeys = [] + }) + + this.keyEventBroadcaster.on('keyup', (key: string, isCharacter: boolean) => { + // deregister a modifier key as pressed + if (!isCharacter) { + const mod = InputManager.GetModifierKeyFromKey(key) + + if (mod && this.modifierKeys.includes(mod)) + this.modifierKeys = this.modifierKeys.filter(k => k !== mod) + } + }) + + this.keyEventBroadcaster.on('keydown', (key: string, isCharacter: boolean) => { + // register a modifier key as pressed + if (!isCharacter && !this.modifierKeys.includes(InputManager.GetModifierKeyFromKey(key)!)) { + const mod = InputManager.GetModifierKeyFromKey(key) + if (mod) this.modifierKeys.push(mod) + } + + this.HandleKeyInput(key, isCharacter) + }) + } + + HandleKeyInput(key: string, isCharacter: boolean) { + // handle special input + switch (key.toUpperCase()) { + case 'R': + if (InputManager.HasModifierCombo(this.modifierKeys, ['ctrl'])) { + location.reload() + return + } + + break + + case 'F5': + location.reload() + return + } + + // redirect input to a running program instead + if (this.shell.HasRunningProgram()) { + this.shell.WriteStdin(key) + return + } + + // handle standard keys + if (!isCharacter) { + switch (key) { + case 'ArrowLeft': + if (this.shell._bufferPos > 0) { + this.shell._bufferPos -= 1 + this.shell._SyncCursorToBuffer() + } + + break + case 'ArrowRight': + if (this.shell._bufferPos < this.shell._buffer.length) { + this.shell._bufferPos += 1 + this.shell._SyncCursorToBuffer() + } + break + case 'ArrowUp': + this.NavigateHistory(-1) + break + case 'ArrowDown': + this.NavigateHistory(1) + break + case 'Backspace': + // don't erase anything if there's nothing left in the buffer + if (this.shell._bufferPos === 0) + break + + this.ExitHistoryNavigation() + this.shell.terminal.RemoveCell() + this.shell.RemoveCharFromBuffer(1, this.shell._bufferPos) + this.shell._SyncCursorToBuffer() + break + case 'Delete': + if (this.shell._bufferPos >= this.shell._buffer.length || this.shell._buffer.length <= 0) + break + + this.ExitHistoryNavigation() + this.shell._bufferPos += 1 + this.shell._SyncCursorToBuffer() + this.shell.terminal.RemoveCell() + this.shell.RemoveCharFromBuffer(1, this.shell._bufferPos) + this.shell._SyncCursorToBuffer() + break + case 'Enter': + // send the buffer to stdin if an exec is running + if (this.shell.GetExecExitCode() === -1) { + this.shell.WriteStdin(`${this.shell._buffer.join('')}\n`) + this.shell.FlushBuffer() + } else { + // "execute" the buffer + this.shell.terminal.MoveCursor(0, 1, { x: true, y: false }) + const command = this.shell._buffer.join('') + if (command.length > 0) + this.history.push(command) + + this.historyIndex = null + this.historyDraft = '' + this.shell.ExecuteLineBuffer() + } + + break + } + } else { + this.ExitHistoryNavigation() + // push the character into the buffer + this.shell._InsertText(key) + } + } +} diff --git a/src/shell/wush/ShellKeyboardManager.ts b/src/shell/wush/ShellKeyboardManager.ts deleted file mode 100644 index 177b937..0000000 --- a/src/shell/wush/ShellKeyboardManager.ts +++ /dev/null @@ -1,129 +0,0 @@ -import type { Wush } from "./Wush" - -export type ModifierKey = 'shift' | 'rightShift' | 'ctrl' | 'rightCtrl' | 'alt' | 'rightAlt' - -export class ShellInputManager { - private modifierKeys: ModifierKey[] = [] - private shell: Wush - - constructor(shell: Wush) { - this.shell = shell - } - - /** - * Checks if the modifier includes every key of the combo - * @param combo modifier combo to check for - * @returns boolean according to the check truthfullness - */ - HasModifierCombo(combo: ModifierKey[]) { - let result = true - - // check if the modifier buffer has every key in the combo - combo.forEach(key => { - if (!modifiers.includes(key)) { - result = false - return - } - }) - - return result - } - - /** - * - * @param event keypress event to process - */ - private processKeypressEvent(event: KeyboardEvent) { - - } - - HandleKeyInput(key: string, modifiers: ModifierKey[], isCharacter: boolean) { - // handle special input - switch (key.toUpperCase()) { - case 'R': - if (hasModifierCombo(['ctrl'])) - location.reload() - break - - case 'F5': - location.reload() - break - } - - if (this.execExitCode === -1) { - console.log('redirecting input to a running program') - - this.WriteStdin(key) - return - } - - // handle standard keys - if (!isCharacter) { - switch (key) { - case 'ArrowLeft': - if (this.bufferPos > 0) { - this.bufferPos -= 1 - this.SyncCursorToBuffer() - } - - break - case 'ArrowRight': - if (this.bufferPos < this.buffer.length) { - this.bufferPos += 1 - this.SyncCursorToBuffer() - } - break - case 'ArrowUp': - this.NavigateHistory(-1) - break - case 'ArrowDown': - this.NavigateHistory(1) - break - case 'Backspace': - // don't erase anything if there's nothing left in the buffer - if (this.bufferPos === 0) - break - - this.ExitHistoryNavigation() - this.terminal.RemoveCell() - this.RemoveCharFromBuffer(1, this.bufferPos) - this.SyncCursorToBuffer() - break - case 'Delete': - if (this.bufferPos >= this.buffer.length || this.buffer.length <= 0) - break - - this.ExitHistoryNavigation() - this.bufferPos += 1 - this.SyncCursorToBuffer() - this.terminal.RemoveCell() - this.RemoveCharFromBuffer(1, this.bufferPos) - this.SyncCursorToBuffer() - 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 }) - const command = this.buffer.join('') - if (command.length > 0) - this.history.push(command) - - this.historyIndex = null - this.historyDraft = '' - this.ExecuteLineBuffer() - } - - break - } - } else { - this.ExitHistoryNavigation() - // push the character into the buffer - this.InsertText(key) - } - } - -} diff --git a/src/shell/wush/Wush.ts b/src/shell/wush/Wush.ts index 99c9585..8fab5c8 100644 --- a/src/shell/wush/Wush.ts +++ b/src/shell/wush/Wush.ts @@ -25,20 +25,21 @@ import { Mkdir } from '../../program/Mkdir' import { Cd } from '../../program/Cd' import { Printf } from '../../program/Printf' import { Pwd } from '../../program/Pwd' -import { ShellEnvironment } from './ShellEnvironment' +import { Environment } from './Environment' +import { InputManager } from './InputManager' export class Wush extends Shell { public readonly Version = "0.3.1" public readonly Name = "wush" // buffer - private buffer: string[] = [] - private bufferPos: number = 0 - private promptStart: CursorPosition = { col: 0, row: 0 } + _buffer: string[] = [] + _bufferPos: number = 0 + _promptStart: CursorPosition = { col: 0, row: 0 } - private history: string[] = [] - private historyIndex: number | null = null - private historyDraft: string = '' + private inputManager: InputManager + // @ts-ignore unused for now + private environment: Environment // exec stuff /** @@ -46,8 +47,6 @@ export class Wush extends Shell { */ private execExitCode: number = 0 - readonly environment: ShellEnvironment = new ShellEnvironment() - // streams readonly stdin: SimpleStream readonly stdout: SimpleStream @@ -61,13 +60,14 @@ export class Wush extends Shell { // create streams this.stdin = new SimpleStream() this.stdout = new SimpleStream() + + this.inputManager = new InputManager(this, broadcaster) + this.environment = new Environment() } async Init() { // initialize keyboard listener - this.broadcaster.on('keydown', (key: string, isCharacter: boolean) => - this.HandleKeyInput(key, isCharacter), - ) + this.inputManager.RegisterEvents() // load workdir this.workingDirectory = await Item.Root() @@ -98,6 +98,10 @@ export class Wush extends Shell { this.Prompt() } + HasRunningProgram(): boolean { + return this.execExitCode === -1 + } + GetPrograms(): { [name: string]: Program } { return this.programs } @@ -110,6 +114,10 @@ export class Wush extends Shell { delete this.programs[name] } + GetExecExitCode(): number { + return this.execExitCode + } + async ExecuteProgram(name: string, args: string[]): Promise { return new Promise(resolve => { this.programs[name].Exec(this.stdin, this.stdout, this.workingDirectory, args) @@ -136,11 +144,12 @@ export class Wush extends Shell { Prompt() { this.terminal.Write(`user in ${this.workingDirectory.GetPath()} -> `) - this.promptStart = this.terminal.GetCursorPosition() - this.buffer = [] - this.bufferPos = 0 - this.historyIndex = null - this.historyDraft = '' + this._promptStart = this.terminal.GetCursorPosition() + this._buffer = [] + this._bufferPos = 0 + + this.inputManager.historyIndex = null + this.inputManager.historyDraft = '' } /** @@ -206,23 +215,23 @@ export class Wush extends Shell { PushToBuffer(text: string) { text.split('').forEach(char => { - this.buffer.splice(this.bufferPos, 0, char) - this.bufferPos++ + this._buffer.splice(this._bufferPos, 0, char) + this._bufferPos++ }) } RemoveCharFromBuffer(amount: number, index: number) { - this.buffer.splice(index - amount, amount) - this.bufferPos -= amount + this._buffer.splice(index - amount, amount) + this._bufferPos -= amount } MoveBufferPos(index: number, absolute: boolean = false) { - this.bufferPos = Math.max(absolute ? index : this.bufferPos + index, 0) + this._bufferPos = Math.max(absolute ? index : this._bufferPos + index, 0) } FlushBuffer() { - this.buffer = [] - this.bufferPos = 0 + this._buffer = [] + this._bufferPos = 0 } // takes the prompt buffer, parses it and executes the contents @@ -453,12 +462,12 @@ export class Wush extends Shell { let line: string if (data) { // data overrides the buffer - this.buffer = data.split('') - this.bufferPos = 0 + this._buffer = data.split('') + this._bufferPos = 0 line = data } else { // use shell buffer if no arbitrary string was provided - line = this.buffer.join('') + line = this._buffer.join('') this.FlushBuffer() } @@ -585,83 +594,47 @@ export class Wush extends Shell { private GetCursorPositionForBufferPos(pos: number): CursorPosition { const width = this.GetWrapWidth() - const absoluteIndex = this.promptStart.col + Math.max(pos, 0) + const absoluteIndex = this._promptStart.col + Math.max(pos, 0) const rowOffset = Math.floor(absoluteIndex / width) const col = absoluteIndex % width - return { row: this.promptStart.row + rowOffset, col } + return { row: this._promptStart.row + rowOffset, col } } - private SyncCursorToBuffer() { - const position = this.GetCursorPositionForBufferPos(this.bufferPos) + _SyncCursorToBuffer() { + const position = this.GetCursorPositionForBufferPos(this._bufferPos) this.terminal.SetCursorPosition(position.col, position.row) } private ClearBuffer() { - if (this.buffer.length === 0) + if (this._buffer.length === 0) return - if (this.bufferPos < this.buffer.length) { - this.bufferPos = this.buffer.length - this.SyncCursorToBuffer() + if (this._bufferPos < this._buffer.length) { + this._bufferPos = this._buffer.length + this._SyncCursorToBuffer() } - while (this.bufferPos > 0) { + while (this._bufferPos > 0) { this.terminal.RemoveCell() - this.RemoveCharFromBuffer(1, this.bufferPos) - this.SyncCursorToBuffer() + this.RemoveCharFromBuffer(1, this._bufferPos) + this._SyncCursorToBuffer() } } - private InsertText(text: string) { + _InsertText(text: string) { if (text.length === 0) return for (const char of text) { this.terminal.InsertCell(char) this.PushToBuffer(char) - this.SyncCursorToBuffer() + this._SyncCursorToBuffer() } } - private SetBuffer(text: string) { + _SetBuffer(text: string) { this.ClearBuffer() - this.InsertText(text) - } - - private ExitHistoryNavigation() { - this.historyIndex = null - this.historyDraft = '' - } - - private NavigateHistory(direction: -1 | 1) { - if (this.history.length === 0) - return - - if (direction === -1) { - if (this.historyIndex === null) { - this.historyDraft = this.buffer.join('') - this.historyIndex = this.history.length - 1 - } else if (this.historyIndex > 0) { - this.historyIndex -= 1 - } else { - return - } - - this.SetBuffer(this.history[this.historyIndex]) - return - } - - if (this.historyIndex === null) - return - - if (this.historyIndex < this.history.length - 1) { - this.historyIndex += 1 - this.SetBuffer(this.history[this.historyIndex]) - return - } - - this.historyIndex = null - this.SetBuffer(this.historyDraft) + this._InsertText(text) } }