Skip to content

Commit 13483fb

Browse files
committed
javascript: target node.js instead of the browser
implemented minimal html terminal emulator
1 parent b0ebcc7 commit 13483fb

File tree

13 files changed

+540
-144
lines changed

13 files changed

+540
-144
lines changed
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
:root {
2+
--terminal-font: 1em "Lucida Console", "Courier New", monospace;
3+
--background-color: transparent;
4+
--text-color: var(--text);
5+
--prompt-char: '$ ';
6+
--cursor-char: '_';
7+
}
8+
9+
/* Basic terminal style.
10+
* If you wan t to overwrite them use custom properties (variables).
11+
*/
12+
.terminal {
13+
font: var(--terminal-font);
14+
background-color: var(--background-color);
15+
color: var(--text-color);
16+
17+
overflow-y: scroll;
18+
width: max-content;
19+
}
20+
21+
/* The terminal consits of multiple "line" elements
22+
* Because sometimes we want to add a simulates "prompt" at the end of a line
23+
* we need to make it an "inline" element and handle line-breaks
24+
* by adding <br> elements */
25+
.terminal pre.line {
26+
display: inline-block;
27+
font: var(--terminal-font);
28+
margin: 0;
29+
padding: 0;
30+
}
31+
32+
/* The "terminal" has one "prompt" element.
33+
* This prompt is not any kind of input, but just a simple <span>
34+
* with an id "prompt" and a
35+
*/
36+
@keyframes prompt-blink {
37+
100% {
38+
opacity: 0;
39+
}
40+
}
41+
.terminal #prompt {
42+
display: inline-block;
43+
}
44+
.terminal #prompt:before {
45+
display: inline-block;
46+
content: var(--prompt-char);
47+
font: var(--terminal-font);
48+
}
49+
.terminal #prompt:after {
50+
display: inline-block;
51+
content: var(--cursor-char);
52+
background: var(--text);
53+
animation: prompt-blink 1s steps(2) infinite;
54+
width: 0.75rem;
55+
opacity: 1;
56+
}
57+
58+
59+
/* Terminal scrollbar */
60+
::-webkit-scrollbar {
61+
width: 3px;
62+
height: 3px;
63+
}
64+
::-webkit-scrollbar-track {
65+
background: var(--background-color);
66+
}
67+
::-webkit-scrollbar-thumb {
68+
background: var(--text-color);
69+
}
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
/**
2+
* @class HtmlTerminal
3+
*
4+
* This class is a very basic implementation of a "terminal" in the browser.
5+
* It provides simple functions like "write" and an "input" Callback.
6+
*
7+
* @license AGPL-2.0
8+
* @author Alexaner Wunschik <https://github.com/mojoaxel>
9+
*/
10+
class HtmlTerminal {
11+
12+
/**
13+
* Input callback.
14+
* If the prompt is activated by calling the input function
15+
* a callback is defined. If this member is not set this means
16+
* the prompt is not active.
17+
*
18+
* @private
19+
* @type {function}
20+
*/
21+
#inputCallback = undefined;
22+
23+
/**
24+
* A html element to show a "prompt".
25+
*
26+
* @private
27+
* @type {HTMLElement}
28+
*/
29+
#$prompt = undefined;
30+
31+
/**
32+
* Constructor
33+
* Creates a basic terminal simulation on the provided HTMLElement.
34+
*
35+
* @param {HTMLElement} $output - a dom element
36+
*/
37+
constructor($output) {
38+
// Store the output DOM element in a local variable.
39+
this.$output = $output;
40+
41+
// Clear terminal.
42+
this.clear();
43+
44+
// Add the call "terminal" to the $output element.
45+
this.$output.classList.add('terminal');
46+
47+
// Create a prompt element.
48+
// This element gets added if input is needed
49+
this.#$prompt = document.createElement("span");
50+
this.#$prompt.setAttribute("id", "prompt");
51+
this.#$prompt.innerText = "";
52+
53+
//TODO: this handler shouls be only on the propt element and only active if cursor is visible
54+
document.addEventListener("keyup", this.#handleKey.bind(this));
55+
}
56+
57+
/**
58+
* Creates a new HTMLElement with the given text content.
59+
* This element than gets added to the $output as a new "line".
60+
*
61+
* @private
62+
* @memberof MinimalTerminal
63+
* @param {String} text - text that should be displayed in the new "line".
64+
* @returns {HTMLElement} return a new DOM Element <pre class="line"></pre>
65+
*/
66+
#newLine(text) {
67+
const $lineNode = document.createElement("pre");
68+
$lineNode.classList.add("line");
69+
$lineNode.innerText = text;
70+
return $lineNode;
71+
}
72+
73+
/**
74+
* TODO
75+
*
76+
* @private
77+
* @param {*} e
78+
*/
79+
#handleKey(e) {
80+
// if no input-callback is defined
81+
if (!this.#inputCallback) {
82+
return;
83+
}
84+
85+
if (e.keyCode === 13 /* ENTER */) {
86+
// create a new line with the text input and remove the prompt
87+
const text = this.#$prompt.innerText;
88+
this.write(text + "\n");
89+
this.#$prompt.innerText = "";
90+
this.#$prompt.remove();
91+
92+
// return the inputed text
93+
this.#inputCallback(text);
94+
95+
// remove the callback and the key handler
96+
this.#inputCallback = undefined;
97+
} else if (e.keyCode === 8 /* BACKSPACE */) {
98+
this.#$prompt.innerText = this.#$prompt.innerText.slice(0, -1);
99+
} else {
100+
this.#$prompt.innerHtml = '';
101+
this.#$prompt.innerText = this.#$prompt.innerText + e.key;
102+
}
103+
}
104+
105+
/**
106+
* Clear the terminal.
107+
* Remove all lines.
108+
*
109+
* @public
110+
*/
111+
clear() {
112+
this.$output.innerText = "";
113+
}
114+
115+
/**
116+
* TODO:
117+
*
118+
* @public
119+
* @param {*} htmlContent
120+
*/
121+
inserHtml(htmlContent) {
122+
const $htmlNode = document.createElement("div");
123+
$htmlNode.innerHTML = htmlContent;
124+
this.$output.appendChild($htmlNode);
125+
document.body.scrollTo(0, document.body.scrollHeight);
126+
}
127+
128+
/**
129+
* Write a text to the terminal.
130+
* By default there is no linebreak at the end of a new line
131+
* except the line ensd with a "\n".
132+
* If the given text has multible linebreaks, multibe lines are inserted.
133+
*
134+
* @public
135+
* @param {string} text
136+
*/
137+
write(text) {
138+
if (text.match(/^\n*$/)) {
139+
// empty new line
140+
text.match(/\n/g).forEach(() => {
141+
const $br = document.createElement("br");
142+
this.$output.appendChild($br);
143+
});
144+
} else if (text && text.length && text.includes("\n")) {
145+
const lines = text.split("\n");
146+
lines.forEach((line) => {
147+
if (line.length === 0 || line.match(/^\s*$/)) {
148+
this.$output.appendChild(document.createElement("br"));
149+
} else {
150+
const $lineNode = this.#newLine(line);
151+
this.$output.appendChild($lineNode);
152+
//this.$node.appendChild(document.createElement("br"));
153+
}
154+
});
155+
} else if (text && text.length) {
156+
// simple line
157+
const $lineNode = this.#newLine(text);
158+
this.$output.appendChild($lineNode);
159+
}
160+
161+
// scroll to the buttom of the page
162+
document.body.scrollTo(0, document.body.scrollHeight);
163+
}
164+
165+
/**
166+
* Like "write" but with a newline at the end.
167+
*
168+
* @public
169+
* @param {*} text
170+
*/
171+
writeln(text) {
172+
this.write(text + "\n");
173+
}
174+
175+
/**
176+
* Query from user input.
177+
* This is done by adding a input-element at the end of the terminal,
178+
* that showes a prompt and a blinking cursor.
179+
* If a key is pressed the input is added to the prompt element.
180+
* The input ends with a linebreak.
181+
*
182+
* @public
183+
* @param {*} callback
184+
*/
185+
input(callback) {
186+
// show prompt with a blinking prompt
187+
this.$output.appendChild(this.#$prompt);
188+
this.#inputCallback = callback;
189+
}
190+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<html>
2+
<head>
3+
<title>Minimal node.js terminal</title>
4+
<link
5+
rel="stylesheet"
6+
href="../../../00_Utilities/javascript/style_terminal.css"
7+
/>
8+
<link
9+
rel="stylesheet"
10+
href="HtmlTerminal.css"
11+
/>
12+
</head>
13+
<body>
14+
<div id="output"></div>
15+
<script src="HtmlTerminal.js" type="text/javascript"></script>
16+
<script>
17+
const term = new HtmlTerminal(document.getElementById("output"));
18+
19+
function loadGameScript() {
20+
const hash = window.location.hash;
21+
22+
// if no game-script was provided redirect to the overview.
23+
if (!hash) {
24+
// show error message and link back to the index.html
25+
console.debug('[HtmlTerminal] No game script found!');
26+
term.writeln(`no game script found :-(\n`);
27+
term.inserHtml(`<a href="/">>> Back to game overview!</a>`);
28+
return;
29+
}
30+
31+
// remove the hash
32+
const gameFile = hash.replace('#','');
33+
34+
// clear terminal
35+
term.clear();
36+
term.writeln('loading...');
37+
38+
// load game-script
39+
console.debug('[HtmlTerminal] Game script found: ', gameFile);
40+
const gameScript = `../../../${gameFile}`;
41+
var $scriptTag = document.createElement("script");
42+
$scriptTag.async = 'async'
43+
$scriptTag.type = "module";
44+
$scriptTag.src = gameScript;
45+
$scriptTag.onerror = () => {
46+
term.clear();
47+
term.writeln(`Error loading game-script "${gameFile}" :-(\n`);
48+
term.inserHtml(`<a href="/">>> Back to game overview!</a>`);
49+
};
50+
$scriptTag.addEventListener('load', function() {
51+
term.clear();
52+
});
53+
document.body.append($scriptTag);
54+
}
55+
56+
/* Redirect stdin/stdout to the HtmlTerminal.
57+
* This is VERY hacky and should never be done in a serious project!
58+
* We can use this here because we know what we are doing and...
59+
* ...it's just simple games ;-) */
60+
window.process = {
61+
stdout: {
62+
write: (t) => term.write(t),
63+
},
64+
stdin: {
65+
on: (event, callback) => term.input(callback)
66+
},
67+
exit: (code) => {},
68+
};
69+
70+
// let the games begin 🚀
71+
loadGameScript();
72+
</script>
73+
</body>
74+
</html>
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
#!/usr/bin/env node
2+
3+
import { print, println, tab, input } from '../common.mjs';
4+
5+
async function main() {
6+
println(tab(20), "Minimal node.js terminal 2");
7+
println("");
8+
println(tab(0), "tab 0");
9+
println(tab(5), "tab 5");
10+
println(tab(10), "tab 10");
11+
println(tab(15), "tab 15");
12+
println(tab(20), "tab 20");
13+
println(tab(25), "tab 25");
14+
println("");
15+
println("1234567890", " ", "ABCDEFGHIJKLMNOPRSTUVWXYZ");
16+
println("");
17+
print("\nHallo"); print(" "); print("Welt!\n");
18+
println("");
19+
print("Line 1\nLine 2\nLine 3\nLine 4");
20+
println("");
21+
22+
const value = await input("input");
23+
println(`input value was "${value}"`);
24+
25+
println("End of script");
26+
27+
// 320 END
28+
process.exit(0);
29+
}
30+
main();

00_Common/javascript/common.mjs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
2+
export function print(...messages) {
3+
process.stdout.write(messages.join(""));
4+
}
5+
6+
export function println(...messages) {
7+
process.stdout.write(messages.join("") + "\n");
8+
}
9+
10+
export function tab(count) {
11+
return " ".repeat(count);
12+
}
13+
14+
export async function input(message = "") {
15+
process.stdout.write(message + ' ');
16+
return new Promise(resolve => {
17+
process.stdin.on('data', (input) => {
18+
resolve(input.toString().replace('\n', ''));
19+
});
20+
});
21+
}

0 commit comments

Comments
 (0)