diff --git a/index.html b/index.html index c3072e6..d0a9fcd 100644 --- a/index.html +++ b/index.html @@ -17,7 +17,11 @@
-
+
+ +
+
+
diff --git a/src/app.ts b/src/app.ts index 0c08052..b0722b7 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,4 +1,5 @@ import { CreateKeyboardListeners } from './input/keyboard' +import { Wush } from './shell/Wush' import { Terminal } from './terminal/Terminal' import { EventBroadcaster } from './utils/EventBroadcaster' @@ -8,11 +9,12 @@ const init = () => { // creates keyboard listeners for the local event broadcaster CreateKeyboardListeners( - (key: string) => localBroadcaster.emit('keydown', key), - (key: string) => localBroadcaster.emit('keyup', key), + (key: string, isCharacter: boolean) => localBroadcaster.emit('keydown', key, isCharacter), + (key: string, isCharacter: boolean) => localBroadcaster.emit('keyup', key, isCharacter), ) - const terminal = new Terminal() + const terminal = new Terminal(localBroadcaster) + terminal.LoadShell(new Wush(localBroadcaster, terminal)) } init() diff --git a/src/init.ts b/src/init.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/input/keyboard.ts b/src/input/keyboard.ts index a87369e..68c116b 100644 --- a/src/input/keyboard.ts +++ b/src/input/keyboard.ts @@ -1,5 +1,15 @@ // 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)) +export const CreateKeyboardListeners = ( + onKeyDown: (key: string, isCharacter: boolean) => void, + onKeyUp: (key: string, isCharacter: boolean) => void, +) => { + document.addEventListener('keydown', event => { + onKeyDown(event.key, event.key.length === 1) + event.preventDefault() + }) + + document.addEventListener('keyup', event => { + onKeyUp(event.key, event.key.length === 1) + event.preventDefault() + }) } diff --git a/src/shell/Shell.ts b/src/shell/Shell.ts new file mode 100644 index 0000000..ebbd99d --- /dev/null +++ b/src/shell/Shell.ts @@ -0,0 +1,15 @@ +import type { Terminal } from '../terminal/Terminal' +import type { EventBroadcaster } from '../utils/EventBroadcaster' + +export abstract class Shell { + broadcaster: EventBroadcaster + terminal: Terminal + + constructor(broadcaster: EventBroadcaster, terminal: Terminal) { + this.broadcaster = broadcaster + this.terminal = terminal + } + + abstract Init(): void + abstract HandleKeyInput(key: string, isCharacter: boolean): void +} diff --git a/src/shell/Wush.ts b/src/shell/Wush.ts new file mode 100644 index 0000000..ba35f1f --- /dev/null +++ b/src/shell/Wush.ts @@ -0,0 +1,42 @@ +// Web-Uno Shell +// the best name I could come up with lmao + +import { Terminal } from '../terminal/Terminal' +import type { EventBroadcaster } from '../utils/EventBroadcaster' +import { Shell } from './Shell' + +export class Wush extends Shell { + constructor(broadcaster: EventBroadcaster, terminal: Terminal) { + super(broadcaster, terminal) + } + + Init() { + this.broadcaster.on('keydown', (key: string, isCharacter: boolean) => + this.HandleKeyInput(key, isCharacter), + ) + this.Prompt() + } + + Prompt() { + this.terminal.Write('hi -> ') + } + + HandleKeyInput(key: string, isCharacter: boolean) { + console.log(key) + console.log(isCharacter) + + if (!isCharacter) + switch (key) { + case 'Backspace': + this.terminal.RemoveCells(1) + break + case 'Enter': + this.Prompt() + break + } + else { + this.terminal.Write(key) + this.terminal.AdjustCursorPosition(1, 0) + } + } +} diff --git a/src/styles/cursor.scss b/src/styles/cursor.scss new file mode 100644 index 0000000..db44c03 --- /dev/null +++ b/src/styles/cursor.scss @@ -0,0 +1,10 @@ +@use "colors.scss" as colors; + +#cursor { + top: 0; + left: 0; + width: 1px; + height: 20px; + position: absolute; + background: colors.$terminal-white; +} diff --git a/src/styles/terminal.scss b/src/styles/terminal.scss index 80cef7a..7e53580 100644 --- a/src/styles/terminal.scss +++ b/src/styles/terminal.scss @@ -1,5 +1,7 @@ @use "colors.scss" as colors; +@forward "cursor.scss"; + #terminal { background: colors.$terminal-background; color: colors.$terminal-white; @@ -13,9 +15,16 @@ p { margin: 0; height: fit-content; + word-wrap: break-word; + text-wrap: nowrap; span { display: inline-block; } } + + .line { + background: red; + width: fit-content + } } diff --git a/src/terminal/CursorProperties.ts b/src/terminal/CursorProperties.ts new file mode 100644 index 0000000..f1de570 --- /dev/null +++ b/src/terminal/CursorProperties.ts @@ -0,0 +1,6 @@ +export type CursorPosition = { + col: number, + row: number, +} + +export type CursorStyle = 'bar' | 'underline' | 'cell' | 'cell_hollow' diff --git a/src/terminal/Terminal.ts b/src/terminal/Terminal.ts index 2dd98ef..f7b2874 100644 --- a/src/terminal/Terminal.ts +++ b/src/terminal/Terminal.ts @@ -1,49 +1,122 @@ +import type { Shell } from '../shell/Shell' +import type { EventBroadcaster } from '../utils/EventBroadcaster' import sqs from '../utils/sqs' +import type { CursorPosition, CursorStyle } from './CursorProperties' export class Terminal { private terminal: HTMLElement + private cursor: HTMLElement private cellHeight = 16 private cellWidth = 8 - constructor() { + private cursorPosition: CursorPosition = { + col: 0, + row: 0, + } + + private shell?: Shell + + constructor(broadcaster: EventBroadcaster) { this.terminal = sqs('#terminal') + this.cursor = sqs('#cursor') + + this.SetCursorStyle('bar') this.ResetCellSize() - this.NewPage() - this.AppendLine('idk./home/idk -> ') + } + + LoadShell(shell: Shell) { + this.shell = shell + this.shell.Init() } NewPage() { this.terminal.innerHTML = '' + this.SetCursorPosition(0, 0) } - AppendLine(line: string) { + AppendLine(content: string) { + this.AdjustCursorPosition(0, 1) + 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) - }) + paragraph.id = `line-${this.cursorPosition.row}` + paragraph.className = 'line' this.terminal.appendChild(paragraph) + this.AppendCells(content) + } + + Write(text: string) { + let adjustment = 0 + + text.split('').forEach(char => { + console.log(this.cursorPosition) + + this.AdjustCursorPosition(adjustment, 0) + adjustment = 1 + + this.SetCell(char) + }) + } + + SetCell(char: string) { + const id = `#cell-${this.cursorPosition.row}_${this.cursorPosition.col}` + + try { + sqs(id) + } catch (_) { + const cell = document.createElement('span') + cell.id = id + cell.style.width = `${this.cellWidth}px` + cell.style.height = `${this.cellHeight}px` + cell.style.lineHeight = `${this.cellHeight}px` + + const lineId = `#line-${this.cursorPosition.row}` + + try { + sqs(lineId) + } catch (_) { + // todo: create as many lines as we need + for (let i = sqs('#terminal'); ;) {} + } + } finally { + sqs(id).innerText = char[0] + } + } + + AppendCells(content: string) { + const paragraph = sqs(`#line-${this.cursorPosition.row}`) + + content.split('').forEach(char => { + paragraph.textContent += char.replace(' ', '\u00A0') + this.AdjustCursorPosition(1, 0) + }) + } + + RemoveCells(amount: number) { + const paragraph = sqs(`#line-${this.cursorPosition.row}`) + paragraph.innerText = paragraph.innerText.slice( + 0, + Math.max(paragraph.innerText.length - amount, 0), + ) + + this.AdjustCursorPosition(-amount, 0) } ResetCellSize() { // dynamically determine cell size using dom const cell = document.createElement('span') - cell.textContent = '\\' + cell.textContent = 'A' this.terminal.appendChild(cell) this.cellWidth = cell.scrollWidth this.cellHeight = cell.scrollHeight + + cell.remove() } GetHeightCells(): number { @@ -53,4 +126,39 @@ export class Terminal { GetWidthCells(): number { return this.terminal.clientWidth / this.cellWidth } + + UpdateCursor() { + this.cursor.style.left = `${this.cursorPosition.col * this.cellWidth}px` + this.cursor.style.top = `${this.cursorPosition.row * this.cellHeight}px` + } + + GetCursorPosition(): CursorPosition { + return this.cursorPosition + } + + SetCursorPosition(col: number, row: number) { + this.cursorPosition.col = Math.min( + this.GetWidthCells(), + Math.max(col, 0), + ) + + this.cursorPosition.row = Math.min( + this.GetHeightCells(), + Math.max(row, 0), + ) + + this.UpdateCursor() + } + + AdjustCursorPosition(col: number, row: number) { + this.SetCursorPosition(this.cursorPosition.col + col, this.cursorPosition.row + row) + } + + SetCursorStyle(style: CursorStyle) { + switch (style) { + default: + this.cursor.style.width = '1px' + this.cursor.style.height = `${this.cellHeight}px` + } + } }