feat: basic terminal printing

This commit is contained in:
2026-03-23 23:42:24 +01:00
parent 64717cc652
commit 6cc4837a8e
9 changed files with 176 additions and 5 deletions

View File

@@ -7,16 +7,20 @@
<!-- google fonts --> <!-- google fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&display=swap" rel="stylesheet" />
href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&display=swap"
rel="stylesheet" <!-- import main stylesheet -->
/> <link rel="stylesheet" href="/src/styles/app.scss" />
<title>webshell</title> <title>webshell</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
</head> </head>
<body> <body>
<div id="app"></div> <div id="app">
<div id="terminal"></div>
</div>
<!-- run main script -->
<script type="module" src="/src/app.ts"></script> <script type="module" src="/src/app.ts"></script>
</body> </body>
</html> </html>

View File

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

5
src/input/keyboard.ts Normal file
View File

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

9
src/styles/app.scss Normal file
View File

@@ -0,0 +1,9 @@
@forward "terminal.scss";
body {
margin: 0;
}
* {
box-sizing: border-box;
}

2
src/styles/colors.scss Normal file
View File

@@ -0,0 +1,2 @@
$terminal-background: #141414;
$terminal-white: #fff;

21
src/styles/terminal.scss Normal file
View File

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

56
src/terminal/Terminal.ts Normal file
View File

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

View File

@@ -0,0 +1,43 @@
export class EventBroadcaster {
private listeners: Map<string, Set<Function>> = 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))
}
}

13
src/utils/sqs.ts Normal file
View File

@@ -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<T extends HTMLElement>(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
}