feat: basic terminal printing
This commit is contained in:
14
index.html
14
index.html
@@ -7,16 +7,20 @@
|
||||
<!-- google fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link 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>
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<div id="app">
|
||||
<div id="terminal"></div>
|
||||
</div>
|
||||
|
||||
<!-- run main script -->
|
||||
<script type="module" src="/src/app.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
18
src/app.ts
18
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()
|
||||
|
||||
5
src/input/keyboard.ts
Normal file
5
src/input/keyboard.ts
Normal 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
9
src/styles/app.scss
Normal file
@@ -0,0 +1,9 @@
|
||||
@forward "terminal.scss";
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
2
src/styles/colors.scss
Normal file
2
src/styles/colors.scss
Normal file
@@ -0,0 +1,2 @@
|
||||
$terminal-background: #141414;
|
||||
$terminal-white: #fff;
|
||||
21
src/styles/terminal.scss
Normal file
21
src/styles/terminal.scss
Normal 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
56
src/terminal/Terminal.ts
Normal 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
|
||||
}
|
||||
}
|
||||
43
src/utils/EventBroadcaster.ts
Normal file
43
src/utils/EventBroadcaster.ts
Normal 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
13
src/utils/sqs.ts
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user