From 6cc4837a8e9df52ca9e072987c94516f31bf0e2e Mon Sep 17 00:00:00 2001 From: binekrasik Date: Mon, 23 Mar 2026 23:42:24 +0100 Subject: [PATCH] feat: basic terminal printing --- index.html | 14 +++++---- src/app.ts | 18 +++++++++++ src/input/keyboard.ts | 5 ++++ src/styles/app.scss | 9 ++++++ src/styles/colors.scss | 2 ++ src/styles/terminal.scss | 21 +++++++++++++ src/terminal/Terminal.ts | 56 +++++++++++++++++++++++++++++++++++ src/utils/EventBroadcaster.ts | 43 +++++++++++++++++++++++++++ src/utils/sqs.ts | 13 ++++++++ 9 files changed, 176 insertions(+), 5 deletions(-) create mode 100644 src/input/keyboard.ts create mode 100644 src/styles/app.scss create mode 100644 src/styles/colors.scss create mode 100644 src/styles/terminal.scss create mode 100644 src/terminal/Terminal.ts create mode 100644 src/utils/EventBroadcaster.ts create mode 100644 src/utils/sqs.ts diff --git a/index.html b/index.html index b095e35..c3072e6 100644 --- a/index.html +++ b/index.html @@ -7,16 +7,20 @@ - + + + + webshell -
+
+
+
+ + diff --git a/src/app.ts b/src/app.ts index e69de29..0c08052 100644 --- a/src/app.ts +++ b/src/app.ts @@ -0,0 +1,18 @@ +import { CreateKeyboardListeners } from './input/keyboard' +import { Terminal } from './terminal/Terminal' +import { EventBroadcaster } from './utils/EventBroadcaster' + +// Initializes the app +const init = () => { + const localBroadcaster = new EventBroadcaster() + + // creates keyboard listeners for the local event broadcaster + CreateKeyboardListeners( + (key: string) => localBroadcaster.emit('keydown', key), + (key: string) => localBroadcaster.emit('keyup', key), + ) + + const terminal = new Terminal() +} + +init() diff --git a/src/input/keyboard.ts b/src/input/keyboard.ts new file mode 100644 index 0000000..a87369e --- /dev/null +++ b/src/input/keyboard.ts @@ -0,0 +1,5 @@ +// creates keyboard listeners +export const CreateKeyboardListeners = (onKeyDown: (key: string) => void, onKeyUp: (key: string) => void) => { + document.addEventListener('keydown', event => onKeyDown(event.key)) + document.addEventListener('keyup', event => onKeyUp(event.key)) +} diff --git a/src/styles/app.scss b/src/styles/app.scss new file mode 100644 index 0000000..09ca3a3 --- /dev/null +++ b/src/styles/app.scss @@ -0,0 +1,9 @@ +@forward "terminal.scss"; + +body { + margin: 0; +} + +* { + box-sizing: border-box; +} diff --git a/src/styles/colors.scss b/src/styles/colors.scss new file mode 100644 index 0000000..0448b9e --- /dev/null +++ b/src/styles/colors.scss @@ -0,0 +1,2 @@ +$terminal-background: #141414; +$terminal-white: #fff; diff --git a/src/styles/terminal.scss b/src/styles/terminal.scss new file mode 100644 index 0000000..80cef7a --- /dev/null +++ b/src/styles/terminal.scss @@ -0,0 +1,21 @@ +@use "colors.scss" as colors; + +#terminal { + background: colors.$terminal-background; + color: colors.$terminal-white; + font-family: "JetBrains Mono", "Cascadia Mono", "Consolas", monospace; + + ::-moz-selection { + background: colors.$terminal-white; + color: colors.$terminal-background; + } + + p { + margin: 0; + height: fit-content; + + span { + display: inline-block; + } + } +} diff --git a/src/terminal/Terminal.ts b/src/terminal/Terminal.ts new file mode 100644 index 0000000..2dd98ef --- /dev/null +++ b/src/terminal/Terminal.ts @@ -0,0 +1,56 @@ +import sqs from '../utils/sqs' + +export class Terminal { + private terminal: HTMLElement + private cellHeight = 16 + private cellWidth = 8 + + constructor() { + this.terminal = sqs('#terminal') + + this.ResetCellSize() + + this.NewPage() + this.AppendLine('idk./home/idk -> ') + } + + NewPage() { + this.terminal.innerHTML = '' + } + + AppendLine(line: string) { + const paragraph = document.createElement('p') + paragraph.style.height = `${this.cellHeight}px` + paragraph.style.lineHeight = `${this.cellHeight}px` + + line.split('').forEach((char) => { + const span = document.createElement('span') + span.textContent = char.replace(' ', '\u00A0') + span.style.width = `${this.cellWidth}px` + span.style.height = `${this.cellHeight}px` + span.style.lineHeight = `${this.cellHeight}px` + paragraph.appendChild(span) + }) + + this.terminal.appendChild(paragraph) + } + + ResetCellSize() { + // dynamically determine cell size using dom + const cell = document.createElement('span') + cell.textContent = '\\' + + this.terminal.appendChild(cell) + + this.cellWidth = cell.scrollWidth + this.cellHeight = cell.scrollHeight + } + + GetHeightCells(): number { + return this.terminal.clientHeight / this.cellHeight + } + + GetWidthCells(): number { + return this.terminal.clientWidth / this.cellWidth + } +} diff --git a/src/utils/EventBroadcaster.ts b/src/utils/EventBroadcaster.ts new file mode 100644 index 0000000..018179f --- /dev/null +++ b/src/utils/EventBroadcaster.ts @@ -0,0 +1,43 @@ +export class EventBroadcaster { + private listeners: Map> = new Map() + + /** + * Assigns a listener for the specified event + * @param event event name + * @param listener function to call when the event is emitted + */ + on(event: string, listener: Function) { + if (!this.listeners.has(event)) + this.listeners.set(event, new Set()) + this.listeners.get(event)!.add(listener) + } + + /** + * Removes a listener from the specified event + * @param event event name + * @param listener function to remove + */ + off(event: string, listener: Function) { + if (this.listeners.has(event)) + this.listeners.get(event)!.delete(listener) + } + + /** + * Removes all listeners for the specified event + * @param event event name + */ + clear(event: string) { + if (this.listeners.has(event)) + this.listeners.get(event)!.clear() + } + + /** + * Emits an event with the given arguments + * @param event event name + * @param args arguments to pass to the listeners + */ + emit(event: string, ...args: any[]) { + if (this.listeners.has(event)) + this.listeners.get(event)!.forEach((listener) => listener(...args)) + } +} diff --git a/src/utils/sqs.ts b/src/utils/sqs.ts new file mode 100644 index 0000000..16d3e6e --- /dev/null +++ b/src/utils/sqs.ts @@ -0,0 +1,13 @@ +/** + * a "safe" wrapper for querySelector. ugly as hell but whatever + * @param query css selector query + * @returns first element matching the query + */ +export default function sqs(query: string): T { + const element = document.querySelector(query) + + if (!(element instanceof HTMLElement)) + throw new Error(`Failed to process a short element query: ${query}`) + + return element as T +}