mirror of
https://github.com/coding-horror/basic-computer-games.git
synced 2025-12-22 07:10:42 -08:00
show terminal header with link to sources
This commit is contained in:
69
00_Common/javascript/WebTerminal/HtmlTerminal.css
Normal file
69
00_Common/javascript/WebTerminal/HtmlTerminal.css
Normal file
@@ -0,0 +1,69 @@
|
||||
:root {
|
||||
--terminal-font: 1em "Lucida Console", "Courier New", monospace;
|
||||
--background-color: transparent;
|
||||
--text-color: var(--text);
|
||||
--prompt-char: '$ ';
|
||||
--cursor-char: '_';
|
||||
}
|
||||
|
||||
/* Basic terminal style.
|
||||
* If you wan t to overwrite them use custom properties (variables).
|
||||
*/
|
||||
.terminal {
|
||||
font: var(--terminal-font);
|
||||
background-color: var(--background-color);
|
||||
color: var(--text-color);
|
||||
|
||||
overflow-y: scroll;
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
/* The terminal consits of multiple "line" elements
|
||||
* Because sometimes we want to add a simulates "prompt" at the end of a line
|
||||
* we need to make it an "inline" element and handle line-breaks
|
||||
* by adding <br> elements */
|
||||
.terminal pre.line {
|
||||
display: inline-block;
|
||||
font: var(--terminal-font);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* The "terminal" has one "prompt" element.
|
||||
* This prompt is not any kind of input, but just a simple <span>
|
||||
* with an id "prompt" and a
|
||||
*/
|
||||
@keyframes prompt-blink {
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
.terminal #prompt {
|
||||
display: inline-block;
|
||||
}
|
||||
.terminal #prompt:before {
|
||||
display: inline-block;
|
||||
content: var(--prompt-char);
|
||||
font: var(--terminal-font);
|
||||
}
|
||||
.terminal #prompt:after {
|
||||
display: inline-block;
|
||||
content: var(--cursor-char);
|
||||
background: var(--text);
|
||||
animation: prompt-blink 1s steps(2) infinite;
|
||||
width: 0.75rem;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
|
||||
/* Terminal scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 3px;
|
||||
height: 3px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--background-color);
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--text-color);
|
||||
}
|
||||
190
00_Common/javascript/WebTerminal/HtmlTerminal.js
Normal file
190
00_Common/javascript/WebTerminal/HtmlTerminal.js
Normal file
@@ -0,0 +1,190 @@
|
||||
/**
|
||||
* @class HtmlTerminal
|
||||
*
|
||||
* This class is a very basic implementation of a "terminal" in the browser.
|
||||
* It provides simple functions like "write" and an "input" Callback.
|
||||
*
|
||||
* @license AGPL-2.0
|
||||
* @author Alexaner Wunschik <https://github.com/mojoaxel>
|
||||
*/
|
||||
class HtmlTerminal {
|
||||
|
||||
/**
|
||||
* Input callback.
|
||||
* If the prompt is activated by calling the input function
|
||||
* a callback is defined. If this member is not set this means
|
||||
* the prompt is not active.
|
||||
*
|
||||
* @private
|
||||
* @type {function}
|
||||
*/
|
||||
#inputCallback = undefined;
|
||||
|
||||
/**
|
||||
* A html element to show a "prompt".
|
||||
*
|
||||
* @private
|
||||
* @type {HTMLElement}
|
||||
*/
|
||||
#$prompt = undefined;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* Creates a basic terminal simulation on the provided HTMLElement.
|
||||
*
|
||||
* @param {HTMLElement} $output - a dom element
|
||||
*/
|
||||
constructor($output) {
|
||||
// Store the output DOM element in a local variable.
|
||||
this.$output = $output;
|
||||
|
||||
// Clear terminal.
|
||||
this.clear();
|
||||
|
||||
// Add the call "terminal" to the $output element.
|
||||
this.$output.classList.add('terminal');
|
||||
|
||||
// Create a prompt element.
|
||||
// This element gets added if input is needed
|
||||
this.#$prompt = document.createElement("span");
|
||||
this.#$prompt.setAttribute("id", "prompt");
|
||||
this.#$prompt.innerText = "";
|
||||
|
||||
//TODO: this handler shouls be only on the propt element and only active if cursor is visible
|
||||
document.addEventListener("keyup", this.#handleKey.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new HTMLElement with the given text content.
|
||||
* This element than gets added to the $output as a new "line".
|
||||
*
|
||||
* @private
|
||||
* @memberof MinimalTerminal
|
||||
* @param {String} text - text that should be displayed in the new "line".
|
||||
* @returns {HTMLElement} return a new DOM Element <pre class="line"></pre>
|
||||
*/
|
||||
#newLine(text) {
|
||||
const $lineNode = document.createElement("pre");
|
||||
$lineNode.classList.add("line");
|
||||
$lineNode.innerText = text;
|
||||
return $lineNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*
|
||||
* @private
|
||||
* @param {*} e
|
||||
*/
|
||||
#handleKey(e) {
|
||||
// if no input-callback is defined
|
||||
if (!this.#inputCallback) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.keyCode === 13 /* ENTER */) {
|
||||
// create a new line with the text input and remove the prompt
|
||||
const text = this.#$prompt.innerText;
|
||||
this.write(text + "\n");
|
||||
this.#$prompt.innerText = "";
|
||||
this.#$prompt.remove();
|
||||
|
||||
// return the inputed text
|
||||
this.#inputCallback(text);
|
||||
|
||||
// remove the callback and the key handler
|
||||
this.#inputCallback = undefined;
|
||||
} else if (e.keyCode === 8 /* BACKSPACE */) {
|
||||
this.#$prompt.innerText = this.#$prompt.innerText.slice(0, -1);
|
||||
} else {
|
||||
this.#$prompt.innerHtml = '';
|
||||
this.#$prompt.innerText = this.#$prompt.innerText + e.key;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the terminal.
|
||||
* Remove all lines.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
clear() {
|
||||
this.$output.innerText = "";
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO:
|
||||
*
|
||||
* @public
|
||||
* @param {*} htmlContent
|
||||
*/
|
||||
inserHtml(htmlContent) {
|
||||
const $htmlNode = document.createElement("div");
|
||||
$htmlNode.innerHTML = htmlContent;
|
||||
this.$output.appendChild($htmlNode);
|
||||
document.body.scrollTo(0, document.body.scrollHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a text to the terminal.
|
||||
* By default there is no linebreak at the end of a new line
|
||||
* except the line ensd with a "\n".
|
||||
* If the given text has multible linebreaks, multibe lines are inserted.
|
||||
*
|
||||
* @public
|
||||
* @param {string} text
|
||||
*/
|
||||
write(text) {
|
||||
if (text.match(/^\n*$/)) {
|
||||
// empty new line
|
||||
text.match(/\n/g).forEach(() => {
|
||||
const $br = document.createElement("br");
|
||||
this.$output.appendChild($br);
|
||||
});
|
||||
} else if (text && text.length && text.includes("\n")) {
|
||||
const lines = text.split("\n");
|
||||
lines.forEach((line) => {
|
||||
if (line.length === 0 || line.match(/^\s*$/)) {
|
||||
this.$output.appendChild(document.createElement("br"));
|
||||
} else {
|
||||
const $lineNode = this.#newLine(line);
|
||||
this.$output.appendChild($lineNode);
|
||||
//this.$node.appendChild(document.createElement("br"));
|
||||
}
|
||||
});
|
||||
} else if (text && text.length) {
|
||||
// simple line
|
||||
const $lineNode = this.#newLine(text);
|
||||
this.$output.appendChild($lineNode);
|
||||
}
|
||||
|
||||
// scroll to the buttom of the page
|
||||
document.body.scrollTo(0, document.body.scrollHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
* Like "write" but with a newline at the end.
|
||||
*
|
||||
* @public
|
||||
* @param {*} text
|
||||
*/
|
||||
writeln(text) {
|
||||
this.write(text + "\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Query from user input.
|
||||
* This is done by adding a input-element at the end of the terminal,
|
||||
* that showes a prompt and a blinking cursor.
|
||||
* If a key is pressed the input is added to the prompt element.
|
||||
* The input ends with a linebreak.
|
||||
*
|
||||
* @public
|
||||
* @param {*} callback
|
||||
*/
|
||||
input(callback) {
|
||||
// show prompt with a blinking prompt
|
||||
this.$output.appendChild(this.#$prompt);
|
||||
this.#inputCallback = callback;
|
||||
}
|
||||
}
|
||||
116
00_Common/javascript/WebTerminal/terminal.html
Normal file
116
00_Common/javascript/WebTerminal/terminal.html
Normal file
@@ -0,0 +1,116 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>Minimal node.js terminal</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="../../../00_Utilities/javascript/style_terminal.css"
|
||||
/>
|
||||
<link rel="stylesheet" href="HtmlTerminal.css" />
|
||||
<style>
|
||||
header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
border-bottom: 1px solid var(--text);
|
||||
padding: 0.25rem 0.5rem;
|
||||
margin: 0;
|
||||
margin-bottom: 1rem;
|
||||
background: black;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
header h1 {
|
||||
font-size: small;
|
||||
color: var(--text),
|
||||
}
|
||||
header div {
|
||||
font-size: small;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1><a href="../../../../">BASIC Computer Games</a></h1>
|
||||
</header>
|
||||
<main id="output"></main>
|
||||
<script src="HtmlTerminal.js" type="text/javascript"></script>
|
||||
<script>
|
||||
const term = new HtmlTerminal(document.getElementById("output"));
|
||||
|
||||
function getGameScriptFromHash() {
|
||||
const hash = window.location.hash;
|
||||
|
||||
// if no game-script was provided redirect to the overview.
|
||||
if (!hash) {
|
||||
// show error message and link back to the index.html
|
||||
console.debug("[HtmlTerminal] No game script found!");
|
||||
term.writeln(`no game script found :-(\n`);
|
||||
term.inserHtml(`<a href="/">>> Back to game overview!</a>`);
|
||||
return;
|
||||
}
|
||||
|
||||
// remove the hash
|
||||
const gameFile = hash.replace("#", "");
|
||||
return gameFile;
|
||||
}
|
||||
|
||||
function addGitHubLink(gameFile) {
|
||||
const gameFolder = gameFile.split("/")[0];
|
||||
|
||||
$gitHubLink = document.createElement("a");
|
||||
$gitHubLink.href = `https://github.com/coding-horror/basic-computer-games/tree/main/${gameFolder}`;
|
||||
$gitHubLink.innerText = `show source-code`;
|
||||
|
||||
var $gitHubBanner = document.createElement("div");
|
||||
$gitHubBanner.classList.add("githublink");
|
||||
$gitHubBanner.appendChild($gitHubLink);
|
||||
|
||||
const $header = document.getElementsByTagName('header')[0];
|
||||
$header.append($gitHubBanner);
|
||||
}
|
||||
|
||||
function loadGameScript(gameFile) {
|
||||
// clear terminal
|
||||
term.clear();
|
||||
|
||||
// load game-script
|
||||
console.debug("[HtmlTerminal] Game script found: ", gameFile);
|
||||
const gameScript = `../../../${gameFile}`;
|
||||
var $scriptTag = document.createElement("script");
|
||||
$scriptTag.async = "async";
|
||||
$scriptTag.type = "module";
|
||||
$scriptTag.src = gameScript;
|
||||
$scriptTag.onerror = () => {
|
||||
term.clear();
|
||||
term.writeln(`Error loading game-script "${gameFile}" :-(\n`);
|
||||
term.inserHtml(`<a href="/">>> Back to game overview!</a>`);
|
||||
};
|
||||
$scriptTag.addEventListener("load", function () {
|
||||
console.log("[HtmlTerminal] Game script loaded!");
|
||||
});
|
||||
document.body.append($scriptTag);
|
||||
}
|
||||
|
||||
/* Redirect stdin/stdout to the HtmlTerminal.
|
||||
* This is VERY hacky and should never be done in a serious project!
|
||||
* We can use this here because we know what we are doing and...
|
||||
* ...it's just simple games ;-) */
|
||||
window.process = {
|
||||
stdout: {
|
||||
write: (t) => term.write(t),
|
||||
},
|
||||
stdin: {
|
||||
on: (event, callback) => term.input(callback),
|
||||
},
|
||||
exit: (code) => {},
|
||||
};
|
||||
|
||||
// let's play 🚀
|
||||
const gameFile = getGameScriptFromHash();
|
||||
addGitHubLink(gameFile);
|
||||
loadGameScript(gameFile);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
30
00_Common/javascript/WebTerminal/terminal_tests.mjs
Normal file
30
00_Common/javascript/WebTerminal/terminal_tests.mjs
Normal file
@@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { print, println, tab, input } from '../common.mjs';
|
||||
|
||||
async function main() {
|
||||
println(tab(20), "Minimal node.js terminal 2");
|
||||
println("");
|
||||
println(tab(0), "tab 0");
|
||||
println(tab(5), "tab 5");
|
||||
println(tab(10), "tab 10");
|
||||
println(tab(15), "tab 15");
|
||||
println(tab(20), "tab 20");
|
||||
println(tab(25), "tab 25");
|
||||
println("");
|
||||
println("1234567890", " ", "ABCDEFGHIJKLMNOPRSTUVWXYZ");
|
||||
println("");
|
||||
print("\nHallo"); print(" "); print("Welt!\n");
|
||||
println("");
|
||||
print("Line 1\nLine 2\nLine 3\nLine 4");
|
||||
println("");
|
||||
|
||||
const value = await input("input");
|
||||
println(`input value was "${value}"`);
|
||||
|
||||
println("End of script");
|
||||
|
||||
// 320 END
|
||||
process.exit(0);
|
||||
}
|
||||
main();
|
||||
21
00_Common/javascript/common.mjs
Normal file
21
00_Common/javascript/common.mjs
Normal file
@@ -0,0 +1,21 @@
|
||||
|
||||
export function print(...messages) {
|
||||
process.stdout.write(messages.join(""));
|
||||
}
|
||||
|
||||
export function println(...messages) {
|
||||
process.stdout.write(messages.join("") + "\n");
|
||||
}
|
||||
|
||||
export function tab(count) {
|
||||
return " ".repeat(count);
|
||||
}
|
||||
|
||||
export async function input(message = "") {
|
||||
process.stdout.write(message + ' ');
|
||||
return new Promise(resolve => {
|
||||
process.stdin.on('data', (input) => {
|
||||
resolve(input.toString().replace('\n', ''));
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user