/* Author: Ward Truyen * Version: 1.3.0 * About: This started as a library of functions for output/printing to a 'terminal' * But then the terminal got bigger and more fun! */ class WTerminal { //TERMINAL const static get VERSION() { return "1.3.0"; }; // terminal version, duh. static get CSS_LINK_URL() { return "term/wterminal.css"; }; // the link to auto insert when terminal initializes and WTerminal.AUTO_INSERT_CSS = true static get CSS_LINK_ID() { return "wterminal-css"; }; // just in case we need to remove it later //css & html element relations: static get BACKGROUND_CLASS() { return "wterminal-background"; }; // blurred background div class, contains it all, hides it all. static get CONTAINER_CLASS() { return "wterminal-container"; }; // container div class for all the terminal elements. static get OUTPUT_CLASS() { return "wterminal-output"; }; // the class from the
tag where we will to print to.
static get INPUT_CLASS() { return "wterminal-input"; }; // class.
static get VISIBLE_CLASS() { return "wterminal-visible"; }; // the class that (by default) hides the terminal when set to the terminal-background div.
//globals (for fun experiments)
static get GLOBAL_LAST_RESULT() { return true; }; // when true: creates global terminal variable lastResult when a command reurns something
static get GLOBAL_LAST_ERROR() { return true; }; // when true: creates global terminal variable lastError when a command throws an error
static get GLOBAL_HISTORY() { return false; }; // when true: creates global terminal variable history and stores entered commands
//start up values: auto insert & logo print
static get AUTO_INSERT_GLOBAL_TEST_VARIABLES() { return false; }; // when true: adds extra global terminal variables for testing printVar (commands: gg, terminal)
static get AUTO_INSERT_DROPDOWN() { return false; }; // when true: automaticly inserts a hidden div containing the terminal in html.body
static get AUTO_INSERT_CSS() { return true; }; // when true: automaticly inserts a stylesheet-link in the html.head
static get PRINT_LOGO() { return true; }; // when true: prints the WTerminal logo after terminal construction
//Options: open/close
static get KEY_OPEN() { return 'Backquote'; }; // 'Backquote' is the event.code to look for on keyDown to open the terminal.
static get KEY_OPEN_CTRL() { return false; }; // When true: ctrl-key must be pressed together with WTerminal.KEY_OPEN.
static get KEY_CLOSE() { return 'Escape'; }; // 'Escape' is the event.code to look for on keyDown to close the terminal.
static get KEY_HISTORY() { return 'ArrowUp'; }; // 'ArrowUp' is the event.code to look for on keyDown of the input-field to get previous command'
//Options: output
static get PRINT_TO_CONSOLE_LOG() { return false; }; // when true: printing logs to console too.
//Options: input
static get SLASH_COMMANDS() { return false; }; // when true: all the commands start with a forward slash.
static get INPUT_STRICT() { return true; }; // when true: input commands must strictly match.
static get PRINT_ALIAS_CHANGE() { return false; }; // when true: prints the change when an alias is used
static get PRINT_INNER_COMMANDS() { return false; }; // when true: prints the multiple-commands after && split
static get PRINT_COMMAND_RETURN() { return false; }; // when true: prints returned value of executed command, if anny
static get MAX_HISTORY() { return 32; }; // the maximum length of the history we keep
//Options: extensions
static get PRINT_ALIAS_ADD() { return false; }; // when true: prints anny added alias
static get PRINT_EXTENSION_ADD() { return false; }; // when true: prints anny extension command names that are added
//Options; }; TPO aka terminalPrintObject static get
static get TPO_UNKNOWN_OBJECT_PRINT() { return false; }; // when true and printVar detects an empty unkown object, then it prints prototype stuff
static get TPO_OBJECT_PREFIX() { return "| "; }; // when printVar is printing keys of an object, this is added in front.
static get TPO_SPECIAL_PREFIX() { return " *" + WTerminal.TPO_OBJECT_PREFIX; }; // when printVar is printing special (keyless or HTMLElement) objects
static get TPO_MAX_DEPTH() { return 8; }; // when printVar is gooing this deep in objects it stops
static get TPO_INNER_MAX_LENGTH() { return 64; }; // when objects in objects are bigger than this it prints an empty code block { length=100 (too long) }
static terminals = {};
static createElement(tagName, tagAttributes, ...tagContents) {
const el = document.createElement(tagName);
if (typeof tagAttributes === 'object' && tagAttributes != null) {
for (let ta of Object.keys(tagAttributes)) {
el.setAttribute(ta, tagAttributes[ta]);
}
}
for (let tc of tagContents) {
if (typeof tc === "string") {
el.appendChild(document.createTextNode(tc))
continue;
} else if (typeof tc === 'object') {
if (tc instanceof HTMLElement) {
el.appendChild(tc)
continue;
}
}
el.appendChild(document.createTextNode(tc.toString()))
}
return el;
};
static splitToArguments(str) {
function _countChar(str, char) {
let index = str.indexOf(char);
let count = 0;
while (index != -1) {
count++;
index = str.indexOf(char, index + 1);
}
return count;
}
let words = str.split(" ");
let quoteCounts = [];
for (let i = 0; i < words.length; i++) {
quoteCounts[i] = _countChar(words[i], '"');
}
for (let i = 0; i < words.length; i++) {
quoteCounts[i] = quoteCounts[i] % 2;
}
let quotes = [];
let quotesIndex = 0;
let harvesting = false;
for (let i = 0; i < quoteCounts.length; i++) {
if (harvesting) {
quotes[quotesIndex] += " " + words[i];
if (quoteCounts[i] == 1) {
harvesting = false;
quotesIndex++;
}
} else {
if (quoteCounts[i] == 1) {
harvesting = true;
quotes[quotesIndex] = words[i];
}
}
}
for (let i = 0; i < quotes.length; i++) {
if (quotes[i].startsWith('"')) quotes[i] = quotes[i].replaceAll('"', '');
}
quotesIndex = 0;
let removing = false;
for (let i = quoteCounts.length - 1; i >= 0; i--) {
if (removing) {
words.splice(i, 1);
if (quoteCounts[i] == 1) {
removing = false;
words.splice(i, 0, quotes[quotesIndex]);//insert quote
}
} else {
if (quoteCounts[i] == 1) {
removing = true;
words.splice(i, 1);
}
}
}
return words;
};
static stringToValue(str) {
if (typeof str !== "string") {
throw new Error("StringToValue error: str must be a string!");
}
if (str === "true") return true;
else if (str === "false") return false;
else if (str.startsWith("(global)") || str.startsWith("(Global)")) {
return WTerminal.getGlobalVariable(str.substring(8));
} else if (str.startsWith("(number)") || str.startsWith("(Number)")) {
str = str.substring(8);
return str.includes(".") ? parseFloat(str) : parseInt(str);
} else if (str.startsWith("{")) {
return JSON.parse(str);
} else if (str.startsWith("(function)") || str.startsWith("(Function)")) {
return new Function(str.substring(10));
} else if (str.startsWith("'") || str.startsWith('"')) {
return str.substring(1, str.length - 1);
} else if (!isNaN(parseFloat(str)) && isFinite(str)) {
return str.includes(".") ? parseFloat(str) : parseInt(str);
} else {
return str;
}
}
static getGlobalVariable(gName) {
if (globalThis === undefined) {
throw new Error("Missing globalThis");
// this.printError("GetGlobal error: Missing globalThis");
} else {
if (gName == '') {
throw new Error("Missing argument: VARIABLE_NAME");
// this.printError("GetGlobal error: Missing argument: VARIABLE_NAME");
} else {
const names = gName.split(".");
if (names.length == 1) {
return globalThis[gName];
} else {
let obj = globalThis;
for (let i = 0; i < names.length - 1; i++) {
const nobj = obj[names[i]]; // nobj is short for new object
if (typeof nobj === "object" && nobj !== null) obj = nobj;
else {
let rem = names.length - 1 - i;
names.splice(names.length - rem, rem);
let name = names.join('.');
// terminalPrint('Variable is ', nobj === null ? 'null' : 'not an object', ': ');
// this.printError(nobj === null ? 'null' : 'not an object');
// this.printVar(nobj, name);
throw new Error(name + " is " + nobj === null ? 'null' : 'not an object');
// return;
};
}
return obj[names[names.length - 1]];
}
}
}
};
constructor(name, locationId, options) {
// use name in static storage of terminals
this.name = name;
this.locationId = location;
WTerminal.terminals[name] = this;
//create terminal elements
const container = WTerminal.createElement('div', { class: WTerminal.CONTAINER_CLASS, title: "Terminal" });
const output = WTerminal.createElement('pre', { class: WTerminal.OUTPUT_CLASS, title: "Terminal output" });
const inputForm = WTerminal.createElement('form', { style: "display: inline;", onsubmit: "return false;" });
const inputLabel = WTerminal.createElement('label', null, "Input:");
const inputText = WTerminal.createElement('input', { class: WTerminal.INPUT_CLASS, title: "Terminal input", type: "text", name: WTerminal.INPUT_CLASS, placeholder: "help" });
const inputSubmit = WTerminal.createElement('input', { title: "Submit input", type: "submit", value: "Enter" });
const controls = WTerminal.createElement('span', { style: "float: right;" });
const btnScrollTop = WTerminal.createElement('button', { title: "Scroll to top" });//, "↑")
btnScrollTop.innerHTML = "↑";
const btnScrollBottom = WTerminal.createElement('button', { title: "Scroll to bottom" });//, "↓")
btnScrollBottom.innerHTML = "↓";
this.outputEl = output;
this.inputTextEl = inputText;
// insert submit function
this.onInputFormSubmit = function(event) {
event.stopPropagation();
this.submitTerminalInput();
return false;
};
inputForm.onsubmit = (e) => this.onInputFormSubmit(e);
// insert button functions
this.scrollToTop = function() {
this.outputEl.scrollTop = 0;
this.inputTextEl.focus();
}
btnScrollTop.onclick = () => this.scrollToTop();
this.scrollToBottom = function() {
this.outputEl.scrollTop = this.outputEl.scrollHeight;
this.inputTextEl.focus();
}
btnScrollBottom.onclick = () => this.scrollToBottom();
container.onclick = function(event) {
// clicking in the terminal should not close it
event.stopPropagation();
};
// install shortcuts: up-history && close
this.onInputTextKeyDown = function(event) {
if (event.repeat == false) {
if (event.code == this.options.keyHistory) {
if (this.history instanceof Array) {
this.inputTextEl.value = this.history[0];
event.preventDefault();
return false;
}
}
if (event.code == this.options.keyClose && this.isTerminalOpen()) {
if (typeof this.backgroundEl !== "undefined") {
this.terminalClose();
}
event.preventDefault();
return false;
}
}
};
inputText.addEventListener("keydown", (e) => this.onInputTextKeyDown(e));
inputForm.appendChild(inputLabel);
inputForm.appendChild(inputText);
inputForm.appendChild(inputSubmit);
controls.appendChild(btnScrollTop);
controls.appendChild(btnScrollBottom);
container.appendChild(output);
container.appendChild(inputForm);
if (locationId === null) { // use location to insert terminal in a div(string id) or dropdown (null)
const background = WTerminal.createElement('div', { class: WTerminal.BACKGROUND_CLASS, title: "Close terminal" });
this.backgroundEl = background;
const btnClose = WTerminal.createElement('button', { title: "Close terminal" });//, "✖")
btnClose.innerHTML = "✖";
btnClose.onclick = (e) => this.terminalClose();
this.onDocBodyKeyDown = function(event) {
//this.printLn('keydown.code ' + event.code);
//console.log('terminal keydown.code ' + event.code);
//this.printVar(event, "keydownEvent");
if (event.repeat == false) {
if (event.code == this.options.keyOpen && (this.options.keyOpenCtrl ? event.ctrlKey : !event.ctrlKey) && !this.isTerminalOpen()) {
this.terminalOpen();
event.preventDefault();
return false;
} else if (event.code == this.options.keyClose && this.isTerminalOpen()) {
if (typeof this.backgroundEl !== "undefined") {
this.terminalClose();
}
event.preventDefault();
return false;
}
}
};
document.body.addEventListener("keydown", (e) => this.onDocBodyKeyDown(e));
// close terminal events
this.onBackgroundClick = function(event) {
// clicking next to the terminal is closing it
if (this.isTerminalOpen()) {
this.terminalClose();
event.stopPropagation();
}
};
background.onclick = (e) => this.onBackgroundClick(e);
controls.appendChild(btnClose);
container.appendChild(controls);
background.appendChild(container);
document.body.appendChild(background);
} else {
let locEl = document.getElementById(locationId);
container.appendChild(controls);
locEl.appendChild(container);
}
// use function-options-var to overwrite default options todo: !!
this.options = {
//open/close
keyOpen: WTerminal.KEY_OPEN,
keyOpenCtrl: WTerminal.KEY_OPEN_CTRL,
keyClose: WTerminal.KEY_CLOSE,
keyHistory: WTerminal.KEY_HISTORY,
//output
printToConsoleLog: WTerminal.PRINT_TO_CONSOLE_LOG,
//input
slashCommands: WTerminal.SLASH_COMMANDS,
inputStrict: WTerminal.INPUT_STRICT,
printAliasChange: WTerminal.PRINT_ALIAS_CHANGE,
printInnerCommands: WTerminal.PRINT_INNER_COMMANDS,
printCommandReturn: WTerminal.PRINT_COMMAND_RETURN,
maxHistory: WTerminal.MAX_HISTORY,
//extensions
printExtensionAdd: WTerminal.PRINT_EXTENSION_ADD,
printAliasAdd: WTerminal.PRINT_ALIAS_ADD,
//TPO aka terminalPrintObject const
tpo_unknownObjectPrint: WTerminal.TPO_UNKNOWN_OBJECT_PRINT,
tpo_objectPrefix: WTerminal.TPO_OBJECT_PREFIX,
tpo_specialPrefix: WTerminal.TPO_SPECIAL_PREFIX,
tpo_maxDepth: WTerminal.TPO_MAX_DEPTH,
tpo_innerMaxLength: WTerminal.TPO_INNER_MAX_LENGTH,
};
// finish loading: Welcome prints
this.aliasExtensionList = {};
this.commandListExtension = {};
this.startupDate = new Date();
this.printLn(`WTerminal ${WTerminal.VERSION} initialized on `, this.startupDate);
if (WTerminal.PRINT_LOGO) {
this.printLn(" _ . _ _____ .----..----. .-. .-..-..-. .-. .--. .-. ");
this.printLn("| |/ \\| |[_ _]| {__ | {) }| .`-'. ||~|| .`| | / {} \\ | | ");
this.printLn("| ,-, | | | | {__ | .-. \\| |\\ /| || || |\\ |/ /\\ \\| `--.");
this.printLn("'-' `-' '-' `----'`-' `-'`-' ` `-'`-'`-' `-'`-' `-'`----'");
}
}
//#region output
print(...args) {
if (this.options.printToConsoleLog) {
console.log("terminalPrint: ", ...args);
}
for (let arg of args) {
if (arg instanceof HTMLElement) {
this.outputEl.appendChild(arg);
} else {
this.outputEl.appendChild(document.createTextNode(new String(arg)));
}
}
this.outputEl.scrollTop = this.outputEl.scrollHeight;
};
printLn(...args) {
if (this.options.printToConsoleLog) {
console.log("terminalPrintLn: ", ...args);
}
for (let arg of args) {
if (arg instanceof HTMLElement) {
this.outputEl.appendChild(arg);
} else {
this.outputEl.appendChild(document.createTextNode(new String(arg)));
}
}
this.outputEl.appendChild(document.createElement('br'));
this.outputEl.scrollTop = this.outputEl.scrollHeight;
};
clearOutput() {
if (this.options.printToConsoleLog) {
console.log("Terminal cleared");
}
this.outputEl.replaceChildren();
this.outputEl.scrollTop = this.outputEl.scrollHeight;
};
//#endregion
static print(...args) {
for (const tName in this.terminals) {
const t = this.terminals[tName];
t.print(...args)
}
}
static printLn(...args) {
for (const tName in this.terminals) {
const t = this.terminals[tName];
t.printLn(...args)
}
}
//#region extra-output
/* prints out bold */
printBold = function(text) {
this.printLn(WTerminal.createElement('b', null, text));
}
/* prints out underlined */
printTitle(title, useTags = true, char = "=") {
if (title && typeof title === "string" && title.length > 0) {
if (useTags == false) {
this.printLn(title);
let underline = "";
for (let i = 0; i < title.length; i++) {
underline += char;
}
this.printLn(underline);
} else {
this.printLn(WTerminal.createElement('u', null, WTerminal.createElement('b', null, title)));
}
}
};
/* prints out with red text */
printError(...args) {
this.printLn(WTerminal.createElement('span', { style: "color: red;" }, ...args));
};
printList(list, printKeys = true) {
if (typeof list !== "object") {
this.printError("printList error: Not a list");
return;
}
if (list === null) return;
const keys = Object.keys(list);
if (keys.length == 0) {
if (list.length !== undefined && list.length > 0) {
for (let i = 0; i < list.length; i++) {
const t = typeof (list[i]);
if (t === "undefined") {
this.printLn("undefined");
} else if (t === "string") {
this.printLn('`', list[i], '`');
} else if (t === "object" && list[i] === null) {
this.printLn("null");
} else {
this.printLn(list[i]);
}
}
}
} else if (printKeys) {
keys.forEach((key) => {
const t = typeof (list[key]);
if (t === "undefined") {
this.printLn(key, " = undefined");
} else if (t === "string") {
this.printLn(key, ' = `', list[key], '`');
} else if (t === "object" && list[key] === null) {
this.printLn(key, " = null");
} else {
this.printLn(key, " = ", list[key]);
}
});
} else {
keys.forEach((key) => {
const t = typeof (list[key]);
if (t === "undefined") {
this.printLn("undefined");
} else if (t === "string") {
this.printLn('`', list[key], '`');
} else if (t === "object" && obj === null) {
this.printLn("null");
} else {
this.printLn(list[key]);
}
});
}
};
/* prints out var/object content */
printVar(obj, name = "var", prefix = "") {
if (this.options.printToConsoleLog) {
console.log("terminalPrintVar: ", name, " = ", obj);
}
const t = typeof (obj);
if (t === "undefined") {
this.printLn(prefix, name, " = undefined");
return;
} else if (t === "string") {
this.printLn(prefix, name, ' = `', obj, '`');
return;
} else if (t === "object" && obj === null) {
this.printLn(prefix, name, " = null");
return;
} else if (t !== "object") {
if (t === "function") {
this.printLn(prefix, name, " = ", obj.toString());
} else if (t === "number" || t === "boolean") {
this.printLn(prefix, name, " = ", obj);
} else {
this.printLn(prefix, name, " = ", "(", typeof (obj), ") ", obj);
}
return;
}
this._printObject(obj, name, prefix);
};//-> function printVar
/* internal/private function: get Object Type */
_getObjType(obj) {
let objType = typeof obj;
if (obj !== null) {
if (obj instanceof Date) {
objType += " Date";
} else if (obj instanceof Float32Array) {
objType += " Float32Array";
} else if (obj instanceof Float64Array) {
objType += " Float64Array";
} else {
try {
let className = Object.getPrototypeOf(obj).toString().replace('[', '').replace(']', '');
if (className == 'object Object' && typeof obj.constructor != 'undefined') {
objType += ' ' + obj.constructor.name;
} else {
objType = className;
}
} catch (e) {
if (obj instanceof Element) {
objType += " Element";
} else {
// this.printError("Bad prototype toString");
try {
if (className == 'object Object' && typeof obj.constructor != 'undefined') {
objType += ' ' + obj.constructor.name;
}
} catch { }
}
}
}
}
return objType;
}
/* internal/private function: print Object*/
_printObject(obj, name, prefix = "", lvl = 0) {
if (obj === null) {
this.printLn(prefix, name, " = null");
} else if (lvl > this.options.tpo_maxDepth) {
this.printLn(prefix, name, " = {} max depth reached(" + lvl + ")");
} else {
const keys = Object.keys(obj);
if (keys.length === 0) {
// special objects: no keys
if (obj instanceof Date) {
this.printLn(prefix, name, " = (Date)", obj.toString());
} else if (obj instanceof Array) {
this.printLn(prefix, name, " = []");
} else if (obj instanceof HTMLElement) {
this.printLn(prefix, name, " = (", this._getObjType(obj), "){");
this.printLn(prefix + this.options.tpo_specialPrefix, "tagName = ", obj.tagName);
if (obj.attributes.length != 0) {
let attr = {};
for (let i = 0; i < obj.attributes.length; i++) {
let a = obj.attributes[i];
if (a.value !== '') {
attr[a.name] = a.value;
}
}
// this.printLn(prefix + this.options.tpo_specialPrefix, "attributes = { ", attr, " }");
this.printVar(attr, "attributes", prefix + this.options.tpo_specialPrefix);
}
if (obj.children && obj.children.length > 0) {
this.printLn(prefix + this.options.tpo_specialPrefix, "childrenCount = ", obj.children.length);
}
this.printLn(prefix, "}");
} else if (obj instanceof Error) {
this.printLn(prefix, name, " = (", this._getObjType(obj), "){}");
const pre = prefix + this.options.tpo_specialPrefix;
this.printLn(pre, "message = ", obj.message);
this.printLn(pre, "name = ", obj.name);
try { this.printLn(pre, "fileName = ", obj.fileName); } catch { }
try { this.printLn(pre, "lineNumber = ", obj.lineNumber); } catch { }
try { this.printLn(pre, "columnNumber = ", obj.columnNumber); } catch { }
try { this.printLn(pre, "stack = ", obj.columnNumber); } catch { }
} else if (Object.getPrototypeOf(obj) == "[object Object]") {
this.printLn(prefix, name, " = {}");
} else {
// all properties hidden
this.printLn(prefix, name, " = (", this._getObjType(obj), "){}");
if (this.options.tpo_unknownObjectPrint) {
const pre = prefix + this.options.tpo_specialPrefix;
this.printLn(pre, "isSealed = ", Object.isSealed(obj))
this.printLn(pre, "isFrozen = ", Object.isFrozen(obj));
this.printLn(pre, "isExtensible = ", Object.isExtensible(obj));
this.printLn(pre, "prototype = ", Object.getPrototypeOf(obj));
this.printLn(pre, "prototype.prototype = ", Object.getPrototypeOf(Object.getPrototypeOf(obj)));
this.printVar(Object.getOwnPropertyNames(obj), ".propertyNames", pre);
this.printVar(Object.getOwnPropertyDescriptors(obj), "propertyDescriptors", pre);
}
}
} else {
// print keys of object
const isArray = obj instanceof Array || obj instanceof HTMLCollection;
if (isArray) {
if (obj instanceof Array) {
this.printLn(prefix, name, " = [ length: ", keys.length);
} else {
let t = "unknown Object";
try {
t = this._getObjType(obj);
} catch (e) { }
this.printLn(prefix, name, " = (", t, ")[ length: ", keys.length);
}
} else {
let t = "unknown Object";
try {
t = this._getObjType(obj);
} catch (e) { }
this.printLn(prefix, name, " = (", t, "){ length: ", keys.length);
}
if (lvl !== 0 && keys.length > this.options.tpo_innerMaxLength) {
this.printLn(prefix + this.options.tpo_objectPrefix, "(too long)");
} else {
const prefixIn = prefix + this.options.tpo_objectPrefix;
keys.forEach((key, index) => {
const type = typeof (obj[key]);
const position = isArray ? key : (index + ": " + key);
if (obj[key] === obj) {
this.printLn(prefixIn, position, " = (parent redefinition) ");
} else if (type === "function" || type === "undefined") {
this.printLn(prefixIn, position, " = (", type, ") ");
} else if (type === "boolean" || type === "number") {
this.printLn(prefixIn, position, ' = ', obj[key]);
} else if (type === "string") {
this.printLn(prefixIn, position, ' = `', obj[key], '`');
} else if (type === "object") {
this._printObject(obj[key], position, prefixIn, lvl + 1);
} else {
this.printLn(prefixIn, position, " = (", type, ") ", obj[key]);
}
});//-> forEach key
}
this.printLn(prefix, isArray ? "]" : "}");
}//-> if keys
}//-> if obj, lvl
}//-> function _printObject
//#endregion
submitTerminalInput() {
const input = this.inputTextEl.value;
if (input.length > 0) {
if (this.options.slashCommands && !input.startsWith("/")) {
//# if not a command: print
this.printLn(input);
} else {
//# if it's a command: execute
this.terminalCommand(this.options.slashCommands ? input.substring(1) : input);
}
}
this.inputTextEl.value = ""; //# clear the input field
this.inputTextEl.focus();
return false; //# prevent