sync: wip changes

This commit is contained in:
2026-03-24 13:58:59 +01:00
parent 6cc4837a8e
commit 475ba9d8ab
10 changed files with 227 additions and 21 deletions

View File

@@ -17,7 +17,11 @@
</head> </head>
<body> <body>
<div id="app"> <div id="app">
<div id="terminal"></div> <div id="cursor"></div>
<div id="terminal">
<div id="lines"></div>
</div>
</div> </div>
<!-- run main script --> <!-- run main script -->

View File

@@ -1,4 +1,5 @@
import { CreateKeyboardListeners } from './input/keyboard' import { CreateKeyboardListeners } from './input/keyboard'
import { Wush } from './shell/Wush'
import { Terminal } from './terminal/Terminal' import { Terminal } from './terminal/Terminal'
import { EventBroadcaster } from './utils/EventBroadcaster' import { EventBroadcaster } from './utils/EventBroadcaster'
@@ -8,11 +9,12 @@ const init = () => {
// creates keyboard listeners for the local event broadcaster // creates keyboard listeners for the local event broadcaster
CreateKeyboardListeners( CreateKeyboardListeners(
(key: string) => localBroadcaster.emit('keydown', key), (key: string, isCharacter: boolean) => localBroadcaster.emit('keydown', key, isCharacter),
(key: string) => localBroadcaster.emit('keyup', key), (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() init()

0
src/init.ts Normal file
View File

View File

@@ -1,5 +1,15 @@
// creates keyboard listeners // creates keyboard listeners
export const CreateKeyboardListeners = (onKeyDown: (key: string) => void, onKeyUp: (key: string) => void) => { export const CreateKeyboardListeners = (
document.addEventListener('keydown', event => onKeyDown(event.key)) onKeyDown: (key: string, isCharacter: boolean) => void,
document.addEventListener('keyup', event => onKeyUp(event.key)) 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()
})
} }

15
src/shell/Shell.ts Normal file
View File

@@ -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
}

42
src/shell/Wush.ts Normal file
View File

@@ -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)
}
}
}

10
src/styles/cursor.scss Normal file
View File

@@ -0,0 +1,10 @@
@use "colors.scss" as colors;
#cursor {
top: 0;
left: 0;
width: 1px;
height: 20px;
position: absolute;
background: colors.$terminal-white;
}

View File

@@ -1,5 +1,7 @@
@use "colors.scss" as colors; @use "colors.scss" as colors;
@forward "cursor.scss";
#terminal { #terminal {
background: colors.$terminal-background; background: colors.$terminal-background;
color: colors.$terminal-white; color: colors.$terminal-white;
@@ -13,9 +15,16 @@
p { p {
margin: 0; margin: 0;
height: fit-content; height: fit-content;
word-wrap: break-word;
text-wrap: nowrap;
span { span {
display: inline-block; display: inline-block;
} }
} }
.line {
background: red;
width: fit-content
}
} }

View File

@@ -0,0 +1,6 @@
export type CursorPosition = {
col: number,
row: number,
}
export type CursorStyle = 'bar' | 'underline' | 'cell' | 'cell_hollow'

View File

@@ -1,49 +1,122 @@
import type { Shell } from '../shell/Shell'
import type { EventBroadcaster } from '../utils/EventBroadcaster'
import sqs from '../utils/sqs' import sqs from '../utils/sqs'
import type { CursorPosition, CursorStyle } from './CursorProperties'
export class Terminal { export class Terminal {
private terminal: HTMLElement private terminal: HTMLElement
private cursor: HTMLElement
private cellHeight = 16 private cellHeight = 16
private cellWidth = 8 private cellWidth = 8
constructor() { private cursorPosition: CursorPosition = {
col: 0,
row: 0,
}
private shell?: Shell
constructor(broadcaster: EventBroadcaster) {
this.terminal = sqs('#terminal') this.terminal = sqs('#terminal')
this.cursor = sqs('#cursor')
this.SetCursorStyle('bar')
this.ResetCellSize() this.ResetCellSize()
this.NewPage() this.NewPage()
this.AppendLine('idk./home/idk -> ') }
LoadShell(shell: Shell) {
this.shell = shell
this.shell.Init()
} }
NewPage() { NewPage() {
this.terminal.innerHTML = '' this.terminal.innerHTML = ''
this.SetCursorPosition(0, 0)
} }
AppendLine(line: string) { AppendLine(content: string) {
this.AdjustCursorPosition(0, 1)
const paragraph = document.createElement('p') const paragraph = document.createElement('p')
paragraph.style.height = `${this.cellHeight}px` paragraph.style.height = `${this.cellHeight}px`
paragraph.style.lineHeight = `${this.cellHeight}px` paragraph.style.lineHeight = `${this.cellHeight}px`
paragraph.id = `line-${this.cursorPosition.row}`
line.split('').forEach((char) => { paragraph.className = 'line'
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) 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() { ResetCellSize() {
// dynamically determine cell size using dom // dynamically determine cell size using dom
const cell = document.createElement('span') const cell = document.createElement('span')
cell.textContent = '\\' cell.textContent = 'A'
this.terminal.appendChild(cell) this.terminal.appendChild(cell)
this.cellWidth = cell.scrollWidth this.cellWidth = cell.scrollWidth
this.cellHeight = cell.scrollHeight this.cellHeight = cell.scrollHeight
cell.remove()
} }
GetHeightCells(): number { GetHeightCells(): number {
@@ -53,4 +126,39 @@ export class Terminal {
GetWidthCells(): number { GetWidthCells(): number {
return this.terminal.clientWidth / this.cellWidth 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`
}
}
} }