/* Author: Ward Truyen * Version: 1.3.1 * 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.2"; }; // terminal version, duh. //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 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; } function _reconnectWordsByQuote(words, quoteCharacter) { let quoteCounts = []; for (let i = 0; i < words.length; i++) { quoteCounts[i] = _countChar(words[i], quoteCharacter); } 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(quoteCharacter)) quotes[i] = quotes[i].replaceAll(quoteCharacter, ''); } 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; } let args = str.split(" "); args = _reconnectWordsByQuote(args, '"'); args = _reconnectWordsByQuote(args, "'"); args = _reconnectWordsByQuote(args, '`'); return args; }; 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("{")) { str = str.replaceAll("'", '"'); //must have double quotes // str = str.replaceAll(' ', ''); //should have 1 space-character behind each comma ... ? auto minify? try { return JSON.parse(str); } catch (e) { if (e instanceof SyntaxError) { let err = new Error(e); err.message = e.message + `\n\`${str}\``; // err.data = str; throw err; } throw new Error(e); } } 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"); } else { if (gName == '') { throw new 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('.'); throw new Error(name + " is " + nobj === null ? 'null' : 'not an object'); }; } 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; // this.inputTextEl.addEventListener("focus", (e)=>{ // console.log("focus input"); // }); // 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(); // return false; }; // 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 */ static _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, " = (", WTerminal._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, " = (", WTerminal._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, " = (", WTerminal._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 = WTerminal._getObjType(obj); } catch (e) { } this.printLn(prefix, name, " = (", t, ")[ length: ", keys.length); } } else { let t = "unknown Object"; try { t = WTerminal._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