Skip to content

javascript node.js scripts #687

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Apr 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions 00_Common/javascript/WebTerminal/HtmlTerminal.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
:root {
--terminal-font: 1rem "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 {
display: block;
font: var(--terminal-font);
background-color: var(--background-color);
color: var(--text-color);

overflow-y: scroll;
width: 100%;
max-width: 60rem;
margin: 0 auto;
}

/* 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);
}
195 changes: 195 additions & 0 deletions 00_Common/javascript/WebTerminal/HtmlTerminal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
/**
* @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 if (
e.keyCode == 16 // "Shift"
|| e.keyCode == 17 // "Control"
|| e.keyCode == 20 // "CapsLock"
|| !e.key.match(/^[a-z0-9!"§#$%&'()*+,.\/:;<=>?@\[\] ^_`{|}~-]$/i)
) {
// ignore non-visible characters
return e;
} else {
this.#$prompt.innerHtml = '';
const key = e.shiftKey ? e.key.toUpperCase() : e.key;
this.#$prompt.innerText = this.#$prompt.innerText + 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 || text.length <= 0) {
// empty line
this.$output.appendChild(document.createElement("br"));
} else if (text.endsWith("\n")) {
// single line with linebrank
const $lineNode = this.#newLine(text);
this.$output.appendChild(this.#newLine(text));
this.$output.appendChild(document.createElement("br"));
} else if (text.includes("\n")) {
// multible lines
const lines = text.split("\n");
lines.forEach((line) => {
this.write(line);
});
} else {
// single line
this.$output.appendChild(this.#newLine(text));
}

// 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;
}
}
129 changes: 129 additions & 0 deletions 00_Common/javascript/WebTerminal/terminal.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
<html>
<head>
<title>Minimal node.js terminal</title>
<meta name="viewport" content="width=device-width, initial-scale=.75">
<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 $output = document.getElementById("output");
const term = new HtmlTerminal($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);
}

/**
* Determine how much chars will fit in each terminal line.
*/
function getOutputColumns($element) {

const fontWidth = 10; //TODO: this width could be measured but it may be complicated!
const columnWidth = Math.trunc($element.clientWidth / fontWidth);
console.warn(`[terminal] document.body.clientWidth:${$element.clientWidth} fontsize:${fontWidth} columnWidth:${columnWidth}`);
return columnWidth;
}

/* 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),
columns: getOutputColumns($output)
},
stdin: {
on: (event, callback) => term.input(callback),
},
exit: (code) => {},
};

// let's play 🚀
const gameFile = getGameScriptFromHash();
addGitHubLink(gameFile);
loadGameScript(gameFile);
</script>
</body>
</html>
Loading