/* 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