From 27f0d4e1baee72b04588d93a618cdc101bd1908b Mon Sep 17 00:00:00 2001 From: Ward Truyen Date: Sat, 21 Sep 2024 16:19:56 +0200 Subject: [PATCH] Initial commit --- .gitignore | 0 Dockerfile | 8 + README.md | 1 + compose.yml | 9 + src/addon.html | 38 + src/css/game.css | 17 + src/css/main.css | 102 ++ src/css/wterminal.css | 166 ++++ src/dropdown.html | 57 ++ src/favicon.ico | Bin 0 -> 9662 bytes src/img/kitty.jpg | Bin 0 -> 32074 bytes src/index.html | 45 + src/js/game-pong-context2d.js | 471 +++++++++ src/js/static-terminal-buttons.js | 55 ++ src/js/termext/ext-cookies.js | 103 ++ src/js/termext/ext-eval.js | 32 + src/js/termext/ext-featuretest.js | 42 + src/js/termext/ext-passw.js | 96 ++ src/js/termext/ext-popvar.js | 164 ++++ src/js/termext/ext-stresstest.js | 40 + src/js/termext/ext-timerdebug.js | 45 + src/js/termext/ext-variables.js | 348 +++++++ src/js/theme.js | 38 + src/js/wterminal-autoextend.js | 33 + src/js/wterminal.js | 1353 ++++++++++++++++++++++++++ src/pong.html | 57 ++ src/snd/glass-knock.mp3 | Bin 0 -> 10752 bytes src/snd/short-success.mp3 | Bin 0 -> 50880 bytes src/static-terminal.html | 61 ++ src/test/asserts.html | 38 + src/test/css/main.css | 99 ++ src/test/index.html | 38 + src/test/js/asserts.js | 74 ++ src/test/js/test-asserts.js | 106 ++ src/test/js/test-splitToArguments.js | 20 + src/test/splitToArguments.html | 38 + src/test/test-all.html | 39 + 37 files changed, 3833 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 compose.yml create mode 100644 src/addon.html create mode 100644 src/css/game.css create mode 100644 src/css/main.css create mode 100644 src/css/wterminal.css create mode 100644 src/dropdown.html create mode 100644 src/favicon.ico create mode 100644 src/img/kitty.jpg create mode 100644 src/index.html create mode 100644 src/js/game-pong-context2d.js create mode 100644 src/js/static-terminal-buttons.js create mode 100644 src/js/termext/ext-cookies.js create mode 100644 src/js/termext/ext-eval.js create mode 100644 src/js/termext/ext-featuretest.js create mode 100644 src/js/termext/ext-passw.js create mode 100644 src/js/termext/ext-popvar.js create mode 100644 src/js/termext/ext-stresstest.js create mode 100644 src/js/termext/ext-timerdebug.js create mode 100644 src/js/termext/ext-variables.js create mode 100644 src/js/theme.js create mode 100644 src/js/wterminal-autoextend.js create mode 100644 src/js/wterminal.js create mode 100644 src/pong.html create mode 100644 src/snd/glass-knock.mp3 create mode 100644 src/snd/short-success.mp3 create mode 100644 src/static-terminal.html create mode 100644 src/test/asserts.html create mode 100644 src/test/css/main.css create mode 100644 src/test/index.html create mode 100644 src/test/js/asserts.js create mode 100644 src/test/js/test-asserts.js create mode 100644 src/test/js/test-splitToArguments.js create mode 100644 src/test/splitToArguments.html create mode 100644 src/test/test-all.html diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..447f0ff --- /dev/null +++ b/Dockerfile @@ -0,0 +1,8 @@ +FROM node:lts-alpine3.20 + +WORKDIR /app +COPY src/ . + +RUN npm install --global serve +EXPOSE 3000 +CMD [ "serve", "." ] diff --git a/README.md b/README.md new file mode 100644 index 0000000..d6e6781 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +#A site for WTerminal diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..a6ef32a --- /dev/null +++ b/compose.yml @@ -0,0 +1,9 @@ +services: + sitewterminal: + build: . + labels: + - "traefik.http.routers.sitewillem.rule=Host(`willem.truyen.network`) && PathPrefix(`/`)" + - "traefik.http.routers.sitewillem.entrypoints=websecure" + - "traefik.http.routers.sitewillem.tls=true" + - "traefik.http.routers.sitewillem.tls.certresolver=myresolver" + - "traefik.enable=true" diff --git a/src/addon.html b/src/addon.html new file mode 100644 index 0000000..7de2e41 --- /dev/null +++ b/src/addon.html @@ -0,0 +1,38 @@ + + + + + WTerminal as addon + + + + + + + +
+ + Home + Static + Dropdown + Pong + Addon +
+ +
+

WTerminal as addon:

+

WTerminal can be used as a browser addon/plugin.
+ By using the addon you can have a WTerminal on every site you visit!

+ +

Visit addons.mozilla.org to get WTerminal + as a firefox addon.

+ +

Or download the source code here.

+
+ + + + + diff --git a/src/css/game.css b/src/css/game.css new file mode 100644 index 0000000..ef7e55e --- /dev/null +++ b/src/css/game.css @@ -0,0 +1,17 @@ +/* Author: Ward Truyen +* Version: 1.0.0 +*/ +.game-area { + margin: 8px auto; + width: fit-content; +} + +.game-area p { + margin: 0 2px; +} + +canvas { + border: solid 2px #3b3b3b; + border-radius: 4px; + outline: none; +} diff --git a/src/css/main.css b/src/css/main.css new file mode 100644 index 0000000..cdf8f3c --- /dev/null +++ b/src/css/main.css @@ -0,0 +1,102 @@ +/* Author: Ward Truyen + * Version: 1.3.0 + */ + +html, +body { + margin: 0px; + padding: 0px; + background-image: linear-gradient(30deg, #73B2736F 70%, #73B2732F); + height: 100%; + display: flex; + flex-direction: column; +} + +main { + margin: auto; + width: 80%; + border: 2px solid darkgreen; + padding: 10px; + background-color: white; + box-shadow: 3px 3px 3px black; +} + +main > a, main > pre, main > p{ + margin-left: 1rem; +} + +h1 { + margin-top: 4px; + margin-bottom: 8px; + text-decoration: underline; +} + +header { + padding: 2px 16px 4px 16px; + /* width: 100%; */ + border-bottom: 1px solid black; + /* background-image: linear-gradient(#0000008f, #0000004f); */ + background-color: #80808080; + /* linear-gradient(#0000008f, #0000004f);*/ + /* margin-top: 16px; */ + text-shadow: 1px 1px 1px #0000004f; +} + +header a { + /* border: solid 1px black; */ + padding: 2px; + margin: 1px; +} + +footer { + padding: 8px 16px 4px 16px; + /* width: 100%; */ + border-top: 1px solid black; + /* background-color: #0000004f; */ + background-image: linear-gradient(#0000008f, #0000004f); + /* margin-top: 16px; */ + margin-top: auto; + text-shadow: 1px 1px 1px #0000004f; +} + +footer h1, +footer h2, +footer h3, +footer p { + display: inline; + margin: 0px; +} + +.float-right { + float: right; +} + +.text-red { + color: red; +} + +.button-red { + background-color: red; +} + +.button-black { + background-color: black; + color: white; +} + +html.theme-dark { + color-scheme: dark; + background-image: linear-gradient(-10deg, #1010206F 60%, #5050506F); + color: white; +} + +.theme-dark>body>main { + border: 1px solid darkgray; + background-color: black; + box-shadow: 3px 3px 5px #101010C0; +} + +.theme-dark>body>main>h1 { + text-shadow: 0px 0px 3px blue, 2px 2px 6px; +} + diff --git a/src/css/wterminal.css b/src/css/wterminal.css new file mode 100644 index 0000000..6d93b91 --- /dev/null +++ b/src/css/wterminal.css @@ -0,0 +1,166 @@ +/* Author: Ward Truyen +* Version: 1.1.0 +*/ + +.wterminal-background { + z-index: 9995; + position: fixed; + width: 100%; + height: 100%; + top: 0px; + left: 0px; + color: black; + line-height: normal; + visibility: hidden; + background: unset; + transition: all 0.2s ease-out 0s; +} + +.wterminal-background .wterminal-container{ + position: relative; + margin: 0px auto; + width: 70%; + border: 1px solid #888; + border-top: 0px solid grey; + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; + padding: 0px 4px 4px 4px; + background: linear-gradient(24deg, rgba(209,211,196,1) 20%, rgba(221,221,221,1) 50%, rgba(221,221,221,1) 70%, rgba(190,199,207,1) 90%); + box-shadow: 3px 3px 3px black; + text-align: left; + margin-top: -50%; +} + +.wterminal-container pre { + color: unset; + border: 2px solid #a0a0a0d0; + min-height: 20em; + max-height: 20em; + margin: 0px 0px 2px; + padding: 2px 4px 6px 4px; + background-color: #F0F0F0; + overflow-y: scroll; + font-family: Monospace, Incosolata, Courier; + font-size: 12px; + line-height: 1.08; + width: unset; +} + +.wterminal-output u{ + /* adds a nice fat blue underline to titles */ + text-decoration-color: #8cb4ff; + text-decoration-thickness: .15rem; + text-underline-offset: .1em; +} + +.wterminal-input { + padding: 1px 2px; + margin: 0px; + background-color: #F0F0F0; + color: black; + border: 1px solid #ccc; + border-radius: 2px; +} + +.wterminal-input:hover{ + background-color: #DDD; +} + +.wterminal-container input[type="submit"]:hover, +.wterminal-container button:hover{ + background-color: #DDD; + cursor: pointer; +} +.wterminal-container input, .wterminal-container label{ + line-height: unset; + display: unset; + /* width: unset; */ + height: unset; + margin: 0; + margin-left: 2px; +} +.wterminal-container input[type="submit"], +.wterminal-container button{ + width: unset; + height: unset; + margin: 0; + margin-left: 2px; + padding: 1px 4px; + color: black; + background-color: #eee; + border: 1px solid #aaa; + border-radius: 3px; + font-weight: normal; +} + +.wterminal-background.wterminal-visible{ + background-color: #0000008F; + backdrop-filter: blur(4px); + visibility: visible; + transition: background-color .1s ease-out 0s, backdrop-filter .1s ease-out 0s; +} + +.wterminal-visible .wterminal-container{ + margin-top: 0px; + transition: margin-top .2s ease-out 0s; +} + +.wterminal-container form{ + margin-left: 2px; +} + +/* Width */ +@media screen and (max-width: 480px) { + .wterminal-input{ + width: 40%; + } +} +@media screen and (min-width: 480px) and (max-width: 720px) { + .wterminal-input{ + width: 50%; + } +} +@media screen and (min-width: 720px) and (max-width: 1080px) { + .wterminal-input{ + width: 60%; + } +} +@media screen and (min-width: 1080px) { + .wterminal-input{ + width: 70%; + } +} + +/* Height */ +@media screen and (min-height: 512px) and (max-height: 1024px) { + .wterminal-background .wterminal-container .wterminal-output{ + min-height: 32em; + max-height: 32em; + } +} +@media screen and (min-height: 1024px) and (max-height: 1536px) { + .wterminal-background .wterminal-container .wterminal-output{ + min-height: 48em; + max-height: 48em; + } +} +@media screen and (min-height: 1536px) { + .wterminal-background .wterminal-container .wterminal-output{ + min-height: 64em; + max-height: 64em; + } +} + +.theme-dark .wterminal-background .wterminal-container{ + color: #F0F0F0; + background: #303030; +} + +.theme-dark .wterminal-container pre, +.theme-dark .wterminal-container button, +.theme-dark .wterminal-container input +{ + background-color: black; + /* color: #F0F0F0; */ + color: white; +} diff --git a/src/dropdown.html b/src/dropdown.html new file mode 100644 index 0000000..23fbfdf --- /dev/null +++ b/src/dropdown.html @@ -0,0 +1,57 @@ + + + + + WTerminal dropdown sample + + + + + + + + + + +
+ + Home + Static + Dropdown + Pong + Addon +
+ +
+

Dropdown terminal

+

WTerminal is installed on this site as a dropdown/quake style terminal.

+

How to use the terminal:

+ + +

How to add the terminal to a site:

+

Add the CSS and JavaScript files to your site header:
+ <link rel="stylesheet" href="css/wterminal.css" />
+ <script src="js/wterminal.js"></script>
+

+

Then add a script ellement with:
+ WTerminal.instalDropdownTerminal(); + Or + new WTerminal("dropdown", null, null); +

+ +
+ + + + + diff --git a/src/favicon.ico b/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..f9b5f37a9656df20aea1028955a73f9e09a2075d GIT binary patch literal 9662 zcmeHLX-HK;6u$kcA3+cy(Lz>87FJRq7$#Oq3I<6fWksflS%zk$WoBAhS+;1Y zxs+@AT%M(6wnGuLP+CMQlxj1vFc z-39T#vry?G2u^|^xQay-387*EIXtPNPJ1*D{7(;D`+J9sBq^yY0{pvpC8fYJ2^kbS@1!cmyoM_KDtesyv!M>(O?; z&jM__EOaIMp*~ftBl-KT_s6*pHsxbGIyX+0Ls8EewB2(n7boRzFaOx1@i=E(;cmm62O5nz zCH_RfvPw@A`e4R6MIg8SH=EU^f>lU4yOr7v^O59nE`{FY1v8=y`iFxilSZ zb{tDE@a+86OD4HYLHXGS75|mL^z5nWZ#K*aeBHd}A&>H>S7bcAf_zY4+*gWvI5%J) zK}jylK)KZCY~NP@=e8E&T(dmp5dCjs&WkJBgSym;EE66z1I?&|IB8|h3HChm+Yn%a zV>z>JTlt5lZ$TcAJJ>N#-@SmkrEC36bZm~@khs}I2k(mdA8N>N?6|zPE&L&Wu(6ug ze%xdp-M1bxe)0t5Qv3QU>Or87?d>67?|!yJULlJht%9tjWPSV6Q?hl8#%TFGd?!3+PMk zT|}8ru&)dQHtabt&)eN(UrFbHzAgM$$E-shO82eC4;^UZG<_oJ5YOU7=B|`dmdWMM z=3jV6RWBR4c62S)@`rmRj7y8!T|X!~811M%txF$Qb9r?R-NHKv*uwjuTzR!%1`HG2*K|4ZWi)Ec}wTaCZ^H%86>>i*H< zN8LYK{HXg!iyw9WXz`=&AA8~l?!dai9@WBrrx52NW#YZx9ESJJNa-B5hyT9v{dmU$ zZjLIJe!GG-Y5X1Mcl!Q@%l}F-Ih=BP;%7K{D$S+DALL4yks?>)uIV%nSnB~|c*KK9 YQV+sF;{;&<>d{(ls~@NzrT(S;e>^?>)c^nh literal 0 HcmV?d00001 diff --git a/src/img/kitty.jpg b/src/img/kitty.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2c084044cf44f07dd9f512968c31b01f5d44c4c8 GIT binary patch literal 32074 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 C0Ek@x literal 0 HcmV?d00001 diff --git a/src/index.html b/src/index.html new file mode 100644 index 0000000..b8b170d --- /dev/null +++ b/src/index.html @@ -0,0 +1,45 @@ + + + + + WTerminal home + + + + + + + +
+ + Home + Static + Dropdown + Pong + Addon +
+ +
+

WTerminal home:

+
*  _  .  _  _____ .----..----. .-.   .-..-..-. .-.  .--.  .-.   
+* | |/ \| |[_   _]| {__ | {)  }| .`-'. ||~|| .`| | / {} \ | |   
+* |  ,-,  |  | |  | {__ | .-. \| |\ /| || || |\  |/  /\  \| `--.
+* '-'   `-'  '-'  `----'`-' `-'`-' ` `-'`-'`-' `-'`-'  `-'`----'
+For terminal fun on the web.
+

The following pages show multiple ways that this terminal can be used.

+

Pages:

+ Static terminal
+ Dropdown terminal
+ Ping pong game + dropdown terminal
+ Browser addon
+ +

Testing:

+ terminal testing
+
+ +
+

Author: Ward Truyen

+
+ + + diff --git a/src/js/game-pong-context2d.js b/src/js/game-pong-context2d.js new file mode 100644 index 0000000..8ccc812 --- /dev/null +++ b/src/js/game-pong-context2d.js @@ -0,0 +1,471 @@ +/* About: Pingpong for HTML, you can insert it in anny Div tagged with an id + * This is a simple elegant sample for how to work with a Canvas + */ +class PingPong_CTX2D { + static get DEBUG_PARENT_OBJECT() { return true; }; + static get AUTO_CONTINUE_ON_FOCUS() { return false; }; + static get SHOW_FPS_INTERVAL() { return 1000 / 4; }; // four times per second + + static get STATE_COUNTDOWN() { return 0; }; + static get STATE_PLAYING() { return 1; }; + static get STATE_ENDED() { return 2; }; + static get BALL_SIZE() { return 8; }; + static get SPEED_HUMAN() { return 180; }; + static get SPEED_CPU() { return 210; }; + + static get PADDLE_WIDTH() { return 10; }; + static get PADDLE_HEIGHT() { return 60; }; + static get PADDLE_MARGIN() { return 10; }; + + static get KEY_ARROW_UP() { return 'ArrowUp'; }; + static get KEY_ARROW_DOWN() { return 'ArrowDown'; }; + static get KEY_W() { return 'w'; }; + static get KEY_S() { return 's'; }; + static get KEY_ENTER() { return 'Enter'; }; + static get KEY_SPACEBAR() { return ' '; }; + static get KEY_ESCAPE() { return 'Escape'; }; + + animationRequestId = 0; + running = false; + + constructor(divId, width, height, zoom = 1, showFps = false) { + this.createCanvas(divId, width, height, zoom); + this.canvasEl.title = "Playing: PingPong"; + this.ctx = this.canvasEl.getContext("2d"); + this.ctx.textAlign = "center"; + + this.audioCtx = new AudioContext(); + this.sounds = []; + this.sounds[0] = new Audio('./snd/glass-knock.mp3'); + this.audioCtx.createMediaElementSource(this.sounds[0]).connect(this.audioCtx.destination); + this.sounds[1] = new Audio('./snd/short-success.mp3'); + this.audioCtx.createMediaElementSource(this.sounds[1]).connect(this.audioCtx.destination); + + this.showFps = showFps; + if (showFps) { + // add framecounter and fps variables and html + this.frameCounter = 0; + this.initTime = performance.now(); + const el = document.createElement('p'); + this.frameLabel = document.createElement('span'); + const floatRight = document.createElement('span'); + floatRight.style = "float: right;"; + this.fpsCounter = 0; + this.fpsLabel = document.createElement('span'); + el.appendChild(document.createTextNode('fps: ')); + el.appendChild(this.fpsLabel); + floatRight.appendChild(document.createTextNode('frame: ')); + floatRight.appendChild(this.frameLabel); + el.appendChild(floatRight); + this.divEl.appendChild(el); + } + + this.restartGame(); + this.canvasEl.addEventListener("keydown", (e) => this.onKeyDown(e)); + this.canvasEl.addEventListener("keyup", (e) => this.onKeyUp(e)); + this.canvasEl.addEventListener("blur", (e) => this.onBlur(e)); + this.canvasEl.addEventListener("focus", (e) => this.onFocus(e)); + + if (PingPong_CTX2D.DEBUG_PARENT_OBJECT) window.game = this; + if (typeof WTerminal === "function") { + WTerminal.terminalAddCommand("restartgame", (t) => this.terminalRestartGame(t)); + WTerminal.terminalAddCommand("printgame", (t) => t.printVar(this, "pong")); + WTerminal.printLn("new PingPong: @", divId, ' ', width, 'x', height, ':', zoom, ' showFps=', showFps); + } + + this.drawCanvas(); + } + + playSound(index) { + try { + const snd = this.sounds[index]; + snd.currentTime = 0; + snd.play(); + } catch (e) { + console.log(`Failed to play sound '${index}': ${e}}`) + } + } + + restartGame() { + this.scoreCpu = 0; + this.scoreHuman = 0; + this.newRound(); + } + + newRound() { + this.countDown = 3; // seconds; + this.gameState = PingPong_CTX2D.STATE_COUNTDOWN; + // randomize ball speed + let vx = 200 + Math.random() * 100; + let vy = -20 + Math.random() * 20; + if (Math.random() > 0.5) vx = - vx; + if (Math.random() > 0.5) vy = - vy; + // generate objects + this.ball = new Ball(this.width / 2 - PingPong_CTX2D.BALL_SIZE / 2, this.height / 2 - PingPong_CTX2D.BALL_SIZE / 2, + PingPong_CTX2D.BALL_SIZE, PingPong_CTX2D.BALL_SIZE, vx, vy); + this.human = new Paddle(PingPong_CTX2D.PADDLE_MARGIN, this.height / 2 - PingPong_CTX2D.PADDLE_HEIGHT / 2, + PingPong_CTX2D.PADDLE_WIDTH, PingPong_CTX2D.PADDLE_HEIGHT); + this.cpu = new Paddle(this.width - PingPong_CTX2D.PADDLE_MARGIN - PingPong_CTX2D.PADDLE_WIDTH, this.height / 2 - PingPong_CTX2D.PADDLE_HEIGHT / 2, + PingPong_CTX2D.PADDLE_WIDTH, PingPong_CTX2D.PADDLE_HEIGHT); + + this.prevNow = performance.now(); + } + + createCanvas(divId, width = 0, height = 0, zoom = 1) { + this.divEl = document.getElementById(divId); + if (this.divEl === null) throw new Error("elementId not found: " + divId); + while (this.divEl.firstChild) { + this.divEl.removeChild(this.divEl.lastChild); + } + this.canvasEl = this.divEl.appendChild(document.createElement("canvas")); + const c = this.canvasEl; + this.width = width; + this.height = height; + if (width > 0 && height > 0) { + c.style.width = width * zoom + 'px'; + c.style.height = height * zoom + 'px'; + c.width = width; + c.height = height; + } + c.tabIndex = 0; // improtant for keyboard focus! + // c.style.imageRendering = 'pixelated'; + } + + drawCanvas() { + if (this.showFps) { + this.frameCounter++; + this.fpsCounter++; + + const now = performance.now(); + const diff = now - this.initTime; + if (PingPong_CTX2D.SHOW_FPS_INTERVAL < diff || !this.running) { + this.initTime = now; + const seconds = diff / 1000; + const fps = this.fpsCounter / seconds; + this.fpsCounter = 0; + if (this.frameLabel) this.frameLabel.innerHTML = this.frameCounter; + if (this.fpsLabel) { + this.fpsLabel.innerHTML = Math.round((fps + Number.EPSILON) * 100) / 100; + if (!this.running) this.fpsLabel.innerHTML += " (not running)"; + } + } + } + const ctx = this.ctx; + ctx.clearRect(0, 0, this.width, this.height); + //# print score + const FONT_SIZE = this.width > 440 ? 24 : 16; + ctx.font = FONT_SIZE + "px serif"; + ctx.fillStyle = 'red'; + ctx.fillText(this.scoreHuman + " - " + this.scoreCpu, this.width / 2, FONT_SIZE * 2); + //# print count down + if (this.gameState == PingPong_CTX2D.STATE_COUNTDOWN) { + ctx.font = 2 * FONT_SIZE + "px serif"; + ctx.fillText(Math.ceil(this.countDown), this.width / 2, this.height / 2); + } + //# shadow + ctx.save(); + ctx.shadowColor = '#000000bf'; + ctx.shadowBlur = 3; + ctx.shadowOffsetX = 2; + ctx.shadowOffsetY = 2; + //# draw ball + ctx.strokeStyle = 'black'; + ctx.fillStyle = 'orange'; + ctx.globalAlpha = 0.3; + ctx.beginPath(); + ctx.arc(this.ball.prevX + PingPong_CTX2D.BALL_SIZE / 2, this.ball.prevY + PingPong_CTX2D.BALL_SIZE / 2, PingPong_CTX2D.BALL_SIZE / 2, 0, Math.PI * 2, true); + ctx.fill(); + ctx.stroke(); + ctx.globalAlpha = 1; + ctx.beginPath(); + ctx.arc(this.ball.x + PingPong_CTX2D.BALL_SIZE / 2, this.ball.y + PingPong_CTX2D.BALL_SIZE / 2, PingPong_CTX2D.BALL_SIZE / 2, 0, Math.PI * 2, true); + ctx.fill(); + ctx.stroke(); + //# draw players paddle + ctx.fillStyle = 'green'; + ctx.fillRect(this.human.x, this.human.y, this.human.width, this.human.height); + ctx.strokeRect(this.human.x, this.human.y, this.human.width, this.human.height); + ctx.fillStyle = 'blue'; + ctx.fillRect(this.cpu.x, this.cpu.y, this.cpu.width, this.cpu.height); + ctx.strokeRect(this.cpu.x, this.cpu.y, this.cpu.width, this.cpu.height); + ctx.restore(); //end shadow + // unfocused & paused banner + if (!this.running) { + const x = this.width / 2; + const y = this.height / 2; + const y2 = y - FONT_SIZE / 2; + ctx.strokeStyle = 'black'; + const g = ctx.createLinearGradient(0, 0, this.width, 0); + g.addColorStop(0, '#404040a0'); + g.addColorStop(0.2, '#404040df'); + g.addColorStop(0.8, '#404040df'); + g.addColorStop(1, '#404040a0'); + ctx.fillStyle = g; + ctx.fillRect(0, y2 - 2, this.width + 1, FONT_SIZE + 5 * 2); + ctx.strokeRect(0, y2 - 2, this.width + 1, FONT_SIZE + 5 * 2); + ctx.fillStyle = (this.gameState === PingPong_CTX2D.STATE_ENDED) ? 'red' : 'gray'; + ctx.font = 4 * FONT_SIZE + "px serif"; + let text = (this.gameState === PingPong_CTX2D.STATE_ENDED) ? "Score!" : "Paused"; + ctx.fillText(text, x, y - 2 * FONT_SIZE); + ctx.strokeText(text, x, y - 2 * FONT_SIZE); + ctx.fillStyle = this.isFocused ? 'goldenrod' : 'lightgray'; + ctx.font = FONT_SIZE + "px serif"; + text = this.isFocused ? `Press space or enter to ${(this.gameState === PingPong_CTX2D.STATE_ENDED) ? "start next round" : "continue"}.` : "Click here to continue. (unfocused)"; + ctx.strokeText(text, x, y2 + FONT_SIZE); + ctx.fillText(text, x, y2 + FONT_SIZE); + } + } + + updateCanvas() { + const now = performance.now(); + const timeDelta = (now - this.prevNow) / 1000; //timeDelta = (milli - milli) / toSeconds + this.prevNow = now; + //#state switch + if (this.gameState == PingPong_CTX2D.STATE_COUNTDOWN) { + this.human.move(timeDelta); + this.human.borderTopAndBottom(this.height); + + this.cpu.move(timeDelta); + this.cpu.borderTopAndBottom(this.height); + + this.countDown -= timeDelta;//PONG_UPDATE_INTERVAL; + if (this.countDown < 0) { + this.gameState = PingPong_CTX2D.STATE_PLAYING; + } + } else if (this.gameState == PingPong_CTX2D.STATE_PLAYING) { + //cpu actions + if (this.ball.y + this.ball.height / 2 < this.cpu.y + this.cpu.height / 2) { + this.cpu.vy = -PingPong_CTX2D.SPEED_CPU; + } else if (this.ball.y > this.cpu.y + this.cpu.height / 2) { + this.cpu.vy = PingPong_CTX2D.SPEED_CPU; + } else { + this.cpu.vy = 0; + } + + //move ball and paddles + this.ball.move(timeDelta); + if (this.ball.borderTopAndBottom(this.height)) { + this.playSound(0); + } + //Horizontal + if (this.ball.x < 0) { + this.win(0); + this.playSound(1); + } else if (this.ball.x > this.width - this.ball.width) { + this.win(1); + this.playSound(1); + } + + this.human.move(timeDelta); + this.human.borderTopAndBottom(this.height); + + this.cpu.move(timeDelta); + this.cpu.borderTopAndBottom(this.height); + + // collide ball vs paddles + if (this.human.x + this.human.width < this.ball.prevX && this.human.x + this.human.width > this.ball.x) { + // console.log("pass 1.1"); + if (this.human.y < this.ball.y + this.ball.height && this.human.y + this.human.height > this.ball.y) { + // console.log("pass 1.2"); + this.ball.vx = this.ball.vx * -1.05; + this.ball.x = this.human.x + this.human.width; + this.ball.vy += ((this.ball.height / 2 + this.ball.y) - (this.human.height / 2 + this.human.y)) * 10; + this.playSound(0); + } + } + + if (this.cpu.x < this.ball.x + this.ball.width && this.cpu.x > this.ball.prevX + this.ball.width) { + // console.log("pass 2.1"); + if (this.cpu.y < this.ball.y + this.ball.height && this.cpu.y + this.cpu.height > this.ball.y) { + // console.log("pass 2.2"); + this.ball.vx = this.ball.vx * -1.05; + this.ball.x = this.cpu.x - this.ball.width; + let temp = ((this.ball.height / 2 + this.ball.y) - (this.cpu.height / 2 + this.cpu.y)) * 10; + this.ball.vy += temp; + this.playSound(0); + } + } + } + this.drawCanvas(); + if (this.running) { // loop + this.animationRequestId = requestAnimationFrame(() => this.updateCanvas()); + } else { // not looping + this.animationRequestId = 0; + } + } + + startRunning(fn) { + if (this.animationRequestId != 0) cancelAnimationFrame(this.animationRequestId); + this.running = true; + this.prevNow = performance.now(); + if (this.showFps) { + this.initTime = performance.now(); + } + this.animationRequestId = requestAnimationFrame(fn); + } + + close() { + if (this.animationRequestId != 0) { + cancelAnimationFrame(this.animationRequestId); + this.animationRequestId = 0; + } + this.running = false; + } + + pausePlayGame() { + if (this.gameState == PingPong_CTX2D.STATE_ENDED) return; + this.running = !this.running; + if (this.running) { + this.startRunning(() => this.updateCanvas()); + } + } + + terminalPrintGame(term) { + term.printVar(this, "pong"); + } + + terminalRestartGame(term) { + term.terminalClose(); + this.restartGame(); + // this.canvasEl.focus(); + setTimeout(() => { this.canvasEl.focus(); this.pausePlayGame(); }, 200); + } + + win(winner) { + this.running = false; //this.stopRunning(); + this.gameState = PingPong_CTX2D.STATE_ENDED; + if (winner == 0) { + this.scoreCpu++; + if (typeof WTerminal === "function") WTerminal.printLn("CPU scored?!"); + } else { + this.scoreHuman++; + if (typeof WTerminal === "function") WTerminal.printLn("Human scored!"); + } + if (typeof WTerminal === "function") WTerminal.printLn("Scores: Human " + this.scoreHuman + " - " + this.scoreCpu + " CPU"); + } + + onKeyDown(e) { + if (e.key == PingPong_CTX2D.KEY_ESCAPE) { + if (this.running) this.pausePlayGame() + e.preventDefault(); + return false; + } + if (e.key == PingPong_CTX2D.KEY_ARROW_UP || e.key == PingPong_CTX2D.KEY_W) { + this.human.vy = -PingPong_CTX2D.SPEED_HUMAN; + e.preventDefault(); + return false; + } else if (e.key == PingPong_CTX2D.KEY_ARROW_DOWN || e.key == PingPong_CTX2D.KEY_S) { + this.human.vy = PingPong_CTX2D.SPEED_HUMAN; + e.preventDefault(); + return false; + } else if (e.key == PingPong_CTX2D.KEY_ENTER || e.key == PingPong_CTX2D.KEY_SPACEBAR) { + //# next round/pause/play + if (this.gameState == PingPong_CTX2D.STATE_ENDED) { + this.newRound(); + this.startRunning(() => this.updateCanvas()); + } else { + this.pausePlayGame(); + } + e.preventDefault(); + return false; + } + return true; + } + + onKeyUp(e) { + if (e.key == PingPong_CTX2D.KEY_ARROW_UP || e.key == PingPong_CTX2D.KEY_W) { + this.human.vy = 0; + e.preventDefault(); + return false; + } else if (e.key == PingPong_CTX2D.KEY_ARROW_DOWN || e.key == PingPong_CTX2D.KEY_S) { + this.human.vy = 0; + e.preventDefault(); + return false; + } + return true; + } + + onBlur() { + this.isFocused = false; + this.canvasEl.style.borderColor = null; + if (this.running) { + this.pausePlayGame(); + } else { + this.drawCanvas(); + } + } + + onFocus() { + this.isFocused = true; + this.canvasEl.style.borderColor = "red"; + if (!this.running && PingPong_CTX2D.AUTO_CONTINUE_ON_FOCUS) { + this.pausePlayGame(); + } else { + this.drawCanvas(); + } + } +} + +class Rectangle { + constructor(x, y, width, height) { + this.x = x; + this.y = y; + this.width = width; + this.height = height; + } +} + +class Ball extends Rectangle { + constructor(x, y, width, height, vx, vy) { + super(x, y, width, height); + this.vx = vx; + this.vy = vy; + this.prevX = this.x; + this.prevY = this.y; + } + + move(timeDelta) { + this.prevX = this.x; + this.prevY = this.y; + this.x += this.vx * timeDelta; + this.y += this.vy * timeDelta; + } + + borderTopAndBottom(height) { + if (this.y < 0) { + this.y = 0; + this.vy = -this.vy; + return true; + } else if (this.y > height - this.height) { + this.y = height - this.height; + this.vy = - this.vy; + return true; + } + return false; + } +} + +class Paddle extends Rectangle { + constructor(x, y, width = 20, height = 20) { + super(x, y, width, height); + this.vy = 0; + } + + move(timeDelta) { + this.y += this.vy * timeDelta; + } + + borderTopAndBottom(height) { + if (this.y < 0) { + this.y = 0; + this.vy = 0; + } else if (this.y > height - this.height) { + this.y = height - this.height; + this.vy = 0; + } + } +} + +function startPingPong(divId, width = 480, height = 320, zoom = 1, showFps = true) { + return new PingPong_CTX2D(divId, width, height, zoom, showFps); +} diff --git a/src/js/static-terminal-buttons.js b/src/js/static-terminal-buttons.js new file mode 100644 index 0000000..3423943 --- /dev/null +++ b/src/js/static-terminal-buttons.js @@ -0,0 +1,55 @@ +const TERMINAL_NAME = "static"; + +window.addEventListener("load", + ()=>getWTerminal(TERMINAL_NAME).inputTextEl.focus()); + +function btn_terminalHelp() { + let term = getWTerminal(TERMINAL_NAME); + term.terminalCommand("help"); + term.inputTextEl.focus(); +} + +function btn_promptSample() { + let term = getWTerminal(TERMINAL_NAME); + term.printTitle("Prompt sample:"); + let age = prompt("Enter your age:", "404"); + term.printLn("Age-input: " + age); + let days = age * 365; + term.printLn(`Days alive: ${days}`); + let weeks = Math.round(days / 7); + term.printLn(`Weeks alive: ${weeks}`); + let adult = age >= 18; + term.printLn("Type: " + (adult ? "adult" : "minor")); + term.inputTextEl.focus(); +} + +function btn_kittySample() { + let term = getWTerminal(TERMINAL_NAME); + term.printTitle("Kitty sample:"); + term.printError("Error: Kitties are cute!"); + // term.printLn('Hello kitty!'); + let img = document.createElement("img"); + img.src = "img/kitty.jpg"; + term.printLn(img, "Hello kitty!"); + term.inputTextEl.focus(); +} + +function btn_printObjectSample() { + let term = getWTerminal(TERMINAL_NAME); + term.printTitle("print Object sample 1:"); + term.printVar("hello"); + term.printVar(3.1419); + term.printTitle("print Object sample 2:"); + let myObject = { hello: "World", number:100}; + myObject.InnerObject = { foo: "bar", myFunction: btn_printObjectSample} + myObject.array = [1,2,4,8]; + term.printVar(myObject, "sample"); + term.inputTextEl.focus(); +} + +function btn_terminalClear(){ + let term = getWTerminal(TERMINAL_NAME); + term.clearOutput(); + term.inputTextEl.focus(); +} + diff --git a/src/js/termext/ext-cookies.js b/src/js/termext/ext-cookies.js new file mode 100644 index 0000000..7fbf648 --- /dev/null +++ b/src/js/termext/ext-cookies.js @@ -0,0 +1,103 @@ +/* Author: Ward Truyen +* Version: 1.0.0 +* About: Cookie data managing +*/ + +if (WTerminal) { + const initTerminalCookieCommands = function() { + //getcookie + WTerminal.terminalAddCommand("getcookie", (term, argLine) => { + function getCookie(name, defaultValue = undefined) { + if (name === null || name === undefined || typeof name != "string" || name == '') { + console.log('error: cookie needs a name'); + return; + } + let pair = document.cookie.split(/; */).find((row) => row.startsWith(name + '=')) + //console.log("cookie pair: ", pair); + if (pair === undefined) + return defaultValue; + else + return pair.split('=')[1]; + } + return getCookie(...WTerminal.splitToArguments(argLine)); + }); + //setcookie + WTerminal.terminalAddCommand("setcookie", (term, argLine) => { + function setCookie(name, value = 1, days = 7) { + let date = new Date(); + date.setDate(date.getDate() + days); // add x days to date + document.cookie = name + "=" + value + "; expires=" + date.toUTCString() + "; SameSite=strict; Secure"; + } + return setCookie(...WTerminal.splitToArguments(argLine)); + }); + //removecookies + WTerminal.terminalAddCommand("removecookies", function(term) { + function removeCookie(name) { + document.cookie = name + "=0; max-age=-1" + "; SameSite=strict; Secure"; + } + if (document.cookie == '') { + term.printError("No cookies found."); + return; + } + const cookies = document.cookie.split(/; */); + term.printLn("Cookies found: " + cookies.length); + cookies.forEach(c => { + const name = c.split('=')[0]; + term.printLn("removing: " + name); + removeCookie(name); + }); + }); + //cookies + WTerminal.terminalAddCommand("cookies", + function(term, argLine) { + if (document.cookie === '') { + if (!argLine.includes("-s")) term.printError("No cookies found.") + } else { + term.printList(document.cookie.split(/; */), false); + } + }, + function(term) { + term.printLn("Usage:"); + term.printLn(" cookies //Prints all cookies."); + term.printLn(" cookies -s //(silent)Prints only cookies, no error."); + }); + //doCookiesWork + WTerminal.terminalAddCommand("docookieswork", (term) => { + function getCookie(name, defaultValue = undefined) { + if (name === null || name === undefined || typeof name != "string" || name == '') { + console.log('error: cookie needs a name'); + return; + } + let pair = document.cookie.split(/; */).find((row) => row.startsWith(name + '=')) + //console.log("cookie pair: ", pair); + if (pair === undefined) + return defaultValue; + else + return pair.split('=')[1]; + } + function setCookie(name, value = 1, days = 7) { + let date = new Date(); + date.setDate(date.getDate() + days); // add x days to date + document.cookie = name + "=" + value + "; expires=" + date.toUTCString() + "; SameSite=strict; Secure"; + } + function removeCookie(name) { + document.cookie = name + "=0; max-age=-1" + "; SameSite=strict; Secure"; + } + const name = "testCookie"; + setCookie(name); + let itWorks = getCookie(name) !== undefined; + if (itWorks) { + removeCookie(name); + } + term.printLn(itWorks); + return itWorks; + }); + } + + //init + if (document.body) { + initTerminalCookieCommands(); + } else { + window.addEventListener("load", initTerminalCookieCommands); + } +} diff --git a/src/js/termext/ext-eval.js b/src/js/termext/ext-eval.js new file mode 100644 index 0000000..50dea3a --- /dev/null +++ b/src/js/termext/ext-eval.js @@ -0,0 +1,32 @@ +/* Author: Ward Truyen +* Version: 1.0.0 +* About: This adds the eval command to the terminal. +*/ +{ + const evalHelp = function(term) { + term.printLn("Uses the function eval(string) on the argLine"); + } + const evalRun = function(term, argLine) { + try { + const result = eval(argLine); + term.printVar(result, '`' + argLine + '`'); + return result; + } catch (error) { + term.printError(`Eval error: \`${argLine}\` -> ${error.message}`); + } + }; + + const initTerminalEvalCommand = function() { + if (WTerminal === undefined) { //is WTerminal not available? + console.error("WTerminal is missing!"); + return; + } + WTerminal.terminalAddCommand("eval", evalRun, evalHelp); + }; + //init + if (document.body) { + initTerminalEvalCommand(); + } else { + window.addEventListener("load", initTerminalEvalCommand); + } +} diff --git a/src/js/termext/ext-featuretest.js b/src/js/termext/ext-featuretest.js new file mode 100644 index 0000000..dc4eb79 --- /dev/null +++ b/src/js/termext/ext-featuretest.js @@ -0,0 +1,42 @@ +/* Author: Ward Truyen +* Version: 1.0.0 +* About: This adds the test command to the terminal. +* current test is to get local variables +*/ +{ + const help = function(term) { + term.printLn("Runs a test (nothing)."); + } + const run = function(term) { + term.printLn("Feature test warning: Under construction, can have unexpected results, errors and crashes."); + // todo: add test ... like throw errors and stuff + + // throw {name : "NotImplementedError", message : "too lazy to implement"}; + // throw new Error("too lazy to implement", "some name perhaps?"); + // class TerminalError extends Error{ + // constructor(msg, name="TerminalError"){ + // super(msg); + // this.name = name; + // } + // } + // throw new TerminalError("my message", "MyName"); + let num = 1; + num.toPrecision(500); + }; + + const addExtention = function() { + if (WTerminal === undefined) { //is WTerminal not available? + console.error("AddExtention Error: WTerminal is missing!"); + return; + } + WTerminal.terminalAddCommand("featuretest", run, help); + //add alias + WTerminal.terminalAddAlias("ft", "featuretest"); + }; + //init + if (document.body) { + addExtention(); + } else { + window.addEventListener("load", addExtention); + } +} diff --git a/src/js/termext/ext-passw.js b/src/js/termext/ext-passw.js new file mode 100644 index 0000000..0a6e563 --- /dev/null +++ b/src/js/termext/ext-passw.js @@ -0,0 +1,96 @@ +/* Author: Ward Truyen +* Version: 1.0.0 +* About: This adds a password generatiing- and testing function +*/ +{ + + const generatePasswordHelp = function(term) { + term.printLn("Generates a new password and prints it out. But also copies it for you."); + } + const generatePassword = function(term, argLine) { + const UPPERCASE_LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + const LOWERCASE_LETTERS = "abcdefghijklmnopqrstuvwxyz"; + const NUMBERS = "1234567890"; + const SPECIAL_CHARACTERS = "!@#$%^&*()_-+=,./<>\\|[]{}"; + const ALL_CHARACTERS = UPPERCASE_LETTERS + LOWERCASE_LETTERS + NUMBERS + SPECIAL_CHARACTERS; + let passwd = ""; + let length = 15; + for (let i = 0; i < length; i++) { + passwd += ALL_CHARACTERS.charAt(Math.floor(Math.random() * ALL_CHARACTERS.length)); + } + const el = document.createElement('span'); + el.innerText = passwd; + // el.style.fontSize = 'x-large'; + el.style.paddingLeft = '1em'; + term.printLn("Generated password(length=" + length + "):", el); + navigator.clipboard.writeText(passwd); + }; + + const isPasswordSafeHelp = function(term) { + term.printLn("Checks if a password is posted as breached by pwnedpasswords.com"); + }; + const isPasswordSafe = function(term, argLine) { + const sha1 = function(string) { + const buffer = new TextEncoder("utf-8").encode(string); + return crypto.subtle.digest("SHA-1", buffer).then(function(buffer) { + // Get the hex code + let hexCodes = []; + const padding = '00000000' + const view = new DataView(buffer); + for (let i = 0; i < view.byteLength; i += 4) { + // Using getUint32 reduces the number of iterations needed (we process 4 bytes each time) + const value = view.getUint32(i) + // toString(16) will give the hex representation of the number without padding + const stringValue = value.toString(16) + // We use concatenation and slice for padding + const paddedValue = (padding + stringValue).slice(-padding.length) + hexCodes.push(paddedValue); + } + // Join all the hex strings into one + return hexCodes.join(""); + }); + }; + const password = argLine; + term.printLn("Checking password, please wait..."); + + sha1(password).then((hash) => { + fetch("https://api.pwnedpasswords.com/range/" + hash.substr(0, 5)).then((response) => response.text()).then((response) => { + + const respLines = response.split('\n'); + const hashSub = hash.slice(5).toUpperCase(); + let isSafe = true; + // console.log('hash: ' + hashSub); + for (let i in respLines) { + // console.log(i+': '+respLines[i]) + if (respLines[i].substring(0, hashSub.length) == hashSub) { + isSafe = false; + break; + } + } + if (isSafe) { + term.printLn("Password is safe."); + } else { + term.printLn("Password has been breached."); + } + }).catch((err) => { + outputSafe.innerText = "Could not check if password is safe: " + err; + }); + }); + }; + + const initTerminalPasswCommands = function() { + if (WTerminal === undefined) { //is WTerminal not available? + console.error("WTerminal is missing!"); + return; + } + + WTerminal.terminalAddCommand("generatepassword", generatePassword, generatePasswordHelp); + WTerminal.terminalAddCommand("ispasswordsafe", isPasswordSafe, isPasswordSafeHelp); + }; + //init + if (document.body) { + initTerminalPasswCommands(); + } else { + window.addEventListener("load", initTerminalPasswCommands); + } +} diff --git a/src/js/termext/ext-popvar.js b/src/js/termext/ext-popvar.js new file mode 100644 index 0000000..421e141 --- /dev/null +++ b/src/js/termext/ext-popvar.js @@ -0,0 +1,164 @@ +/* Author: Ward Truyen +* Version: 1.0.0 +* About: This adds the test command to the terminal. +* current test is to get local variables +*/ + +{// this code block hides the variables below from other scripts. + const runPopVar = function(term, argLine) { + const PAUSE_SYMBOL = "◼"; + const INTERVAL_TIME = "1000"; // 1000 == 1second + + class PopUpWindow { + static popupCounter = 0; + constructor(variableName, term) { + this.variableName = variableName; + this.printVar = term.printVar; + this._getObjType = term._getObjType; + this._printObject = term._printObject; + this.printLn = term.printLn; + this.options = {}; + let o = this.options; + let to = term.options; + o.printToConsoleLog = false; + o.tpo_unknownObjectPrint = to.tpo_unknownObjectPrint; + o.tpo_objectPrefix = to.tpo_objectPrefix; + o.tpo_specialPrefix = to.tpo_specialPrefix; + o.tpo_maxDepth = to.tpo_maxDepth; + o.tpo_innerMaxLength = to.tpo_innerMaxLength; + this.createPopup(); + this.printVariable(); + this.intervalId = setInterval(() => this.printVariable(), INTERVAL_TIME); + } + + createPopup() { + const t = 2 + PopUpWindow.popupCounter++; + let containerStyle = 'border: 1px solid black; z-index: 9990; position: absolute; background: #ffffffa0; border-radius: 2px; backdrop-filter: blur(3px); box-shadow: 3px 3px 3px #00000066;'; + containerStyle += ` top: ${t}em; left: ${t}em;`; + this.container = WTerminal.createElement('div', { style: containerStyle, title: this.variableName }); + const outputStyle = 'margin: 2px; font-family: Monospace, Incosolata, Courier; font-size: 12px; line-height: 1.05;';// overflow-y: scroll; max-height: ' + (window.innerHeight-80) +'px;'; + this.outputEl = WTerminal.createElement('pre', { style: outputStyle }); + this.btnPauseContinue = WTerminal.createElement('button', { title: "pause" }); + this.btnPauseContinue.innerHTML = PAUSE_SYMBOL; + this.btnPauseContinue.addEventListener("click", () => this.onPausePlay()); + let btnRefresh = WTerminal.createElement('button', { title: "refresh" }); + btnRefresh.innerHTML = '↻'; + btnRefresh.addEventListener("click", () => this.printVariable()); + let btnClose = WTerminal.createElement('button', { title: 'close' }); + btnClose.addEventListener("click", () => this.closePopup()); + btnClose.innerHTML = "✖"; + let headerDiv = WTerminal.createElement('div', { style: "border-bottom: 1px solid black; padding: 2px; background: #00000060" }); + headerDiv.appendChild(btnRefresh); + headerDiv.appendChild(this.btnPauseContinue); + headerDiv.appendChild(document.createTextNode(" Popvar " + PopUpWindow.popupCounter)); + let spanForClose = WTerminal.createElement('span', { style: "float: right;" }, btnClose); + headerDiv.appendChild(spanForClose); + this.container.appendChild(headerDiv); + this.container.appendChild(this.outputEl); + document.body.appendChild(this.container); + + headerDiv.onmousedown = (e) => this.startDrag(e); + } + + onPausePlay() { + if (this.intervalId === 0) { + this.intervalId = setInterval(() => this.printVariable(), INTERVAL_TIME); + this.printVariable(); + this.btnPauseContinue.innerHTML = PAUSE_SYMBOL; + this.btnPauseContinue.title = "pause"; + } else { + clearInterval(this.intervalId); + this.intervalId = 0; + this.btnPauseContinue.innerHTML = "►"; + this.btnPauseContinue.title = "play"; + } + } + + printVariable() { + const oldOutput = this.outputEl; + const outputStyle = 'margin: 2px; font-family: Monospace, Incosolata, Courier; font-size: 12px; line-height: 1.05;';// overflow-y: scroll; max-height: ' + (window.innerHeight-80) +'px;'; + this.outputEl = WTerminal.createElement('pre', { style: outputStyle }); + this.printVar(WTerminal.getGlobalVariable(this.variableName), this.variableName); + oldOutput.replaceWith(this.outputEl); + } + + closePopup() { + document.body.removeChild(this.container); + } + + startDrag(e) { + if (e.button !== 0) return; + e.preventDefault(); + this.pos3 = e.clientX; + this.pos4 = e.clientY; + document.onmouseup = () => this.endDrag(); + document.onmousemove = (e) => this.dragPopup(e); + } + + dragPopup(e) { + // e = e || window.event; + e.preventDefault(); + this.pos1 = this.pos3 - e.clientX; + this.pos2 = this.pos4 - e.clientY; + this.pos3 = e.clientX; + this.pos4 = e.clientY; + this.container.style.top = (this.container.offsetTop - this.pos2) + "px"; + this.container.style.left = (this.container.offsetLeft - this.pos1) + "px"; + } + + endDrag() { + document.onmouseup = null; + document.onmousemove = null; + } + };//class PopUpWindow + + if (globalThis === undefined) { + term.printError("Popvar error: Missing globalThis"); + } else if (argLine == '') { + term.printError("Popvar error: No name"); + } else { + // return new PopUpWindow(argLine, term); + new PopUpWindow(argLine, term); + } + }// runPopvar + + const initTerminalVariableCommands = function() { + const ext = { + popvar: { + run: runPopVar, + help: function(term) { + term.printLn("Creates a popup window with (refreshable) the given variable contents."); + term.printBold("Usage:"); + term.printLn("popvar VARIABLE_NAME //shows VARIABLE_NAME contents in a popup"); + term.printBold("Samples:"); + term.printLn("popvar terminal //Shows the terminal variable contents in a popup"); + } + }, + }; + + const aliases = { + pop: "popvar", + pt: "popvar terminal", + }; + + if (WTerminal === undefined) { //is WTerminal not available? + console.error("WTerminal is missing!"); + return; + } + //add commands in ext + for (let c of Object.keys(ext)) { + WTerminal.terminalAddCommand(c, ext[c].run, ext[c].help); + } + //add aliases + for (let c of Object.keys(aliases)) { + WTerminal.terminalAddAlias(c, aliases[c]); + } + }; + + //init + if (document.body) { + initTerminalVariableCommands(); + } else { + window.addEventListener("load", initTerminalVariableCommands); + } +} diff --git a/src/js/termext/ext-stresstest.js b/src/js/termext/ext-stresstest.js new file mode 100644 index 0000000..771b4b4 --- /dev/null +++ b/src/js/termext/ext-stresstest.js @@ -0,0 +1,40 @@ +/* Author: Ward Truyen +* Version: 1.0.0 +* About: This adds the test command to the terminal. +* current test is to get local variables +*/ + +{// this code block hides the variables below from other scripts. + const initTerminalStressTestCommand = function() { + const help = function(term) { + term.printLn("Runs a stress test on the terminal."); + }; + const run = function(term, argLine) { + const testArgLine = function(str) { + term.printVar(WTerminal.splitToArguments(str), '`' + argLine + '`.splitToArguments()'); + }; + if (argLine != '') testArgLine(argLine); + term.terminalCommand("? && alias && const && option && date && time && uptime && starttime && echo OK", true); + term.terminalCommand("? help && help alias && ? const && ? option && ? date && ? time && ? uptime", true); + term.terminalCommand("thisterminal && gg", true); + term.terminalCommand('gdb && result .2 && result .children.0 && result .innerHTML', true); + term.terminalCommand('eval 10000+4*(1+4*10) + Math.PI', true); + term.terminalCommand('dovar alert "finished stresstest"', true); + // term.terminalCommand('dovar document.getElementById ' + TERMINAL_OUTPUT_ID + ' && result .innerHTML', true); + + return term.lastResult; + }; + //add command + if (WTerminal === undefined) { + console.error("WTerminal is missing!"); + return; + } + WTerminal.terminalAddCommand("stresstest", run, help); + }; + //init + if (document.body) { + initTerminalStressTestCommand(); + } else { + window.addEventListener("load", initTerminalStressTestCommand); + } +} diff --git a/src/js/termext/ext-timerdebug.js b/src/js/termext/ext-timerdebug.js new file mode 100644 index 0000000..7440d40 --- /dev/null +++ b/src/js/termext/ext-timerdebug.js @@ -0,0 +1,45 @@ +/* Author: Ward Truyen +* Version: 1.0.0 +* About: This adds the test command to the terminal. +* current test is to get local variables +*/ +{ + const addExtention = function() { + const help = function(term) { + term.printLn("Installs a logger at setTimeout and setInterval."); + } + const run = function(term) { + //capture setTimeout + const oldSetTimeout = setTimeout; + setTimeout = function() { + const e = new Error('Just for stack trace'); + const result = oldSetTimeout.apply(this, arguments); + if (term) term.printLn(`New timeout ${result} registered.\n from: ${e.stack}`); + else console.log(`New timeout ${result} registered from: ${e.stack}`); + return result; + }; + //capture setInterval + const oldSetInterval = setInterval; + setInterval = function() { + const e = new Error('Just for stack trace'); + const result = oldSetInterval.apply(this, arguments); + if (term) term.printLn(`New interval ${result} registered.\n from: ${e.stack}`); + else console.log(`New interval ${result} registered from: ${e.stack}`); + return result; + } + term.printLn("activated"); + } + + if (WTerminal === undefined) { //is WTerminal not available? + console.error("WTerminal is missing!"); + return; + } + WTerminal.terminalAddCommand("timerdebug", run, help); + }; + //init + if (document.body) { + addExtention(); + } else { + window.addEventListener("load", addExtention); + } +} diff --git a/src/js/termext/ext-variables.js b/src/js/termext/ext-variables.js new file mode 100644 index 0000000..22225ed --- /dev/null +++ b/src/js/termext/ext-variables.js @@ -0,0 +1,348 @@ +/* Author: Ward Truyen +* Version: 1.0.0 +* About: This adds the test command to the terminal. +* current test is to get local variables +*/ + +{// this code block hides the variables below from other scripts. + const initTerminalVariableCommands = function() { + const ext = + { + dovar: { + run: function(term, argLine) { + function _doFunction(gVar, functionKey, args) { + if (typeof gVar === "object" && typeof gVar[functionKey] === "function") { + // term.printVar(args, "do args"); + return gVar[functionKey](...args); + } else { + term.printError("Do error: " + functionKey + " is not a function"); + } + }; + /* To run a function of a Class (like document.getElementById(..)) + * We need the parent- /class-object from witch we call it from, to prevent errors (illegal acces) + * todo: ? --> use apply https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/apply + */ + if (globalThis === undefined) { + term.printError("Do error: Missing globalThis"); + } else if (argLine == '') { + term.printError("Do error: No name"); + } else { + let args = WTerminal.splitToArguments(argLine); + const verbal = args.includes("-v"); + if (verbal) { + args = args.filter(e => e !== '-v'); + } + const gName = args.shift(); //full global function name + args.forEach(e => e = WTerminal.stringToValue(e)); + let pName = gName.split('.'); //last object's parent name + let result; + if (pName.length == 1) { + result = _doFunction(globalThis, gName, args) + } else { + const lastName = pName.pop(); //last object name + result = _doFunction(WTerminal.getGlobalVariable(pName.join('.')), lastName, args); + } + if (verbal) term.printVar(result, gName + "(" + args.join(' ') + ")"); + return result; + } + }, + help: function(term) { + term.printLn("Runs given global function with arguments, and returns result."); + term.printBold("Usage:"); + term.printLn("dovar FUNCTION_NAME [ArgumentS] //Runs function FUNCTION_NAME with optional ArgumentS"); + term.printLn("dovar -v FUNCTION_NAME [ArgumentS] //Same as above but prints result too."); + term.printBold("Samples:"); + term.printLn("dovar window.open https://developer.mozilla.org //Runs function window.open() with the url as argument"); + term.printLn("dovar -v document.getElementById terminal-up //Runs function document.getElementById with terminal-up as argument") + term.printLn("dovar document.body.remove //Warning: removes all page content"); + } + }, + getvar: { + run: function(term, argLine) { + if (globalThis === undefined) { + term.printError("Getvar error: Missing globalThis"); + } else if (argLine == '') { + term.printError("Getvar error: Missing argument: VARIABLE_NAME"); + } else { + let result = {}; + const args = WTerminal.splitToArguments(argLine); + for (const gName of args) { + result[gName] = WTerminal.getGlobalVariable(gName); + } + const keys = Object.keys(result); + if (keys.length == 1) { + return result[keys[0]]; + } else { + return result; + } + } + }, + help: function(term) { + term.printLn("Gets a global variable and returns it."); + term.printLn("The returned result will be found in terminal.lastResult") + term.printBold("Usage:"); + term.printLn("getvar VARIABLE_NAME"); + term.printBold("Samples:"); + term.printLn("getvar terminal //Returns terminal"); + term.printLn("getvar terminal.version //Returns terminal.version"); + term.printLn("getvar var1 var2 var3 //Returns an object with var1 var2 and var3 keys and their global found value."); + term.printLn("getvar document //Returns global document object."); + term.printLn("getvar document.body.children //Returns a list of the children in the body."); + term.printLn("getvar document.head.children //Returns a list of the children in the head."); + } + }, + globals: { + run: function(term) { + if (globalThis !== undefined) { + term.printVar(globalThis, "globalThis"); + } else if (self !== undefined) { + term.printVar(self, "self"); + } else if (global !== undefined) { + term.printVar(global, "global"); + } else { + term.printError("Globals error: No globals!?"); + } + if (document !== undefined && globalThis.document !== document) term.printVar(document, "document"); + }, + help: function(term) { + term.printLn("Prints all global variables."); + } + }, + logvar: { + run: function(term, argLine) { + if (globalThis === undefined) { + term.printError("LogVar error: Missing globalThis"); + } else if (argLine == '') { + term.printError("LogVar error: Missing argument: VARIABLE_NAME"); + } else { + let result = {}; + let args = WTerminal.splitToArguments(argLine); + const returnResult = args.includes("-r"); + if (returnResult) { + args = args.filter(e => e !== '-r'); + } + for (const gName of args) { + result[gName] = WTerminal.getGlobalVariable(gName); + } + if (Object.keys(result).length == 1) { + result = result[Object.keys(result)[0]]; + } + // term.printVar(result); + console.log(result); + if (returnResult) return result; + } + }, + help: function(term) { + term.printLn("Like printvar but Logs variable(s) to console."); + } + }, + printvar: { + run: function(term, argLine) { + if (globalThis === undefined) { + term.printError("PrintVar error: Missing globalThis"); + } else if (argLine == '') { + term.printError("PrintVar error: Missing argument: VARIABLE_NAME"); + } else { + let result = {}; + let args = WTerminal.splitToArguments(argLine); + const returnResult = args.includes("-r"); + if (returnResult) { + args = args.filter(e => e !== '-r'); + } + for (const gName of args) { + result[gName] = WTerminal.getGlobalVariable(gName); + } + const keys = Object.keys(result); + if (keys.length == 1) { + result = result[keys[0]]; + term.printVar(result, keys[0]); + } else { + term.printVar(result); + } + if (returnResult) return result; + } + }, + help: function(term) { + term.printLn("Prints value of global variable."); + term.printBold("Usage:"); + term.printLn("printvar VARIABLE_NAME //Prints variable, returns nothing."); + term.printLn("printvar -r VARIABLE_NAME //Prints variable, returns variable."); + term.printBold("Samples:"); + term.printLn("printvar terminal //Prints terminal"); + term.printLn("printvar terminal.version //Prints terminal.version"); + term.printLn("printvar -r terminal.lastResult //Prints terminal.lastResult and returns it."); + term.printLn("printvar document //Prints global document object."); + term.printLn("printvar document.body.children //Prints body elements."); + term.printLn("printvar document.head.children //Prints head elements."); + } + }, + removevar: { + run: function(term, argLine) { + if (globalThis === undefined) { + term.printError("RemoveVar error: Missing globalThis"); + } else if (argLine == '') { + term.printError("RemoveVar error: Missing arguments"); + } else { + let args = WTerminal.splitToArguments(argLine); + const keys = Object.keys(globalThis); + const verbal = args.includes("-v"); + if (verbal) { + args = args.filter(e => e !== '-v'); + } + let count = 0; + if (args.includes("-a")) { + count = keys.length; + keys.forEach(key => { + if (verbal) term.printLn("removing: " + key) + delete globalThis[key]; + }); + term.printLn("Removed " + count + " keys.") + return; + } else if (args.includes("-n")) { + args = args.filter(e => e !== '-n'); + keys.forEach(key => { + if (globalThis[key] === null || globalThis[key] === undefined) { + if (verbal) term.printLn("removing: " + key) + delete globalThis[key]; + count++; + } + }); + } else if (args.includes("-f")) { + args = args.filter(e => e !== '-f'); + keys.forEach(key => { + if (globalThis[key] !== globalThis && + !(typeof globalThis[key] === "function" && globalThis[key].toString().includes("[native code]"))) { + if (verbal) term.printLn("removing: " + key) + delete globalThis[key]; + count++; + } + }); + } + while (args.length > 0) { + const gName = args.shift(); //full global function name + const pName = gName.split('.'); //last object's parent name + const lastName = pName.pop(); //last object name + let obj = globalThis; + if (pName.length > 0) { + obj = WTerminal.getGlobalVariable(pName.join('.')); //parent object + } + if (obj !== undefined && obj[lastName] !== undefined) { + if (verbal) term.printLn("removing: " + gName) + delete obj[lastName]; + count++; + } else { + if (obj === undefined) { + term.printError("Variable " + pName.join(".") + " = undefined") + } else { + term.printError("Variable " + gName + " = undefined"); + } + } + } + term.printLn("Removed " + (count > 1 ? (count + " keys.") : (count === 0 ? "nothing" : "1 key"))); + } + }, + help: function(term) { + term.printLn("Removes global variables."); + term.printBold("Usage:"); + term.printLn("removevar -v ... //Prints removing VARIABLE_NAME."); + term.printLn("removevar -f //Removes all global variables that are not a parent redefinition or native function."); + term.printLn("removevar -n //Removes all null or undefined global variables."); + term.printLn("removevar -a //Removes ALL global variables."); + term.printLn("removevar VARIABLE_NAMES //Removes the global variables provided.") + term.printBold("Samples:"); + term.printLn("removevar terminal //Removes the global variable terminal."); + term.printLn("removevar var1 var2 //Removes the 2 global variables var1 and var2."); + term.printLn("removevar terminal.history //Removes history from terminal."); + term.printLn("removevar -n terminal.history //Removes history from terminal, and all null or undefined global variables."); + term.printLn("removevar -n -v //Prints variable names of all null or undefined global variables it removes."); + } + }, + setvar: { + run: function(term, argLine) { + if (globalThis === undefined) { + term.printError("Setvar error: Missing globalThis"); + } else { + if (argLine == '') { + term.printError("Setvar error: no name"); + } else { + let args = WTerminal.splitToArguments(argLine); + for (const element of args) { + const keyValuePair = element.split("="); + if(keyValuePair.length < 2) throw new Error("missing arguements."); + const names = keyValuePair[0].split("."); + const value = WTerminal.stringToValue(keyValuePair[1]); + if (names.length == 1) { + globalThis[names[0]] = value; + term.printVar(globalThis[names[0]], keyValuePair[0]); + } else { + let obj = globalThis; + for (let i = 0; i < names.length - 1; i++) { + if (typeof obj[names[i]] === "object") obj = obj[names[i]]; + else { + let rem = names.length - 1 - i; + names.splice(names.length - rem, rem); + let name = names.join('.'); + term.printError(`Variable ${keyValuePair[0]} is not an object!`); + term.printVar(obj[names[i]], name); + return; + }; + } + obj[names[names.length - 1]] = value; + return obj[names[names.length - 1]]; + } + } + } + } + }, + help: function(term) { + term.printBold("Usage:"); + term.printLn("setvar NAME=VALUE //Sets VALUE to a global variable, NAME, and returns it."); + term.printBold("Samples:"); + term.printLn("setvar terminal //Sets terminal to undefined"); + term.printLn("setvar terminal.version=prehistoric //Sets terminal.version to string `prehistoric`"); + term.printLn("setvar myNumber=8 //Sets myNumber to number 8"); + term.printLn("setvar myNumber=(number)-8.1 //Sets myNumber to number -8.1"); + term.printLn("setvar var1 var2 var3 //Creates 3 undefined variables"); + term.printLn("setvar myName=\"Ward Truyen\" //Sets myName to string"); + term.printLn('setvar obj={"1":"test","2":true} //Sets obj to JSON.stringified object'); + term.printLn("setvar myFunc=(function)terminalPrintLn(\"Hello\");//Creates a function"); + term.printLn("setvar myVar=(global)myFunc //Sets(binds) myVar to the contents of myFunc"); + } + }, + }; + const aliases = { + run: "dovar", + pdo: "dovar -v ", + g: "getvar", + gg: "globals", + db: "printvar -r document.body.children", + dh: "printvar -r document.head.children", + gdb: "printvar -r document.body.children ", + gdh: "printvar -r document.head.children ", + result: "printvar -r terminal.lastResult", + error: "printvar terminal.lastError", + pv: "printvar -r ", + terminal: "printvar -r terminal", + // history: "printvar terminal.history", + }; + + //add commands in ext + if (WTerminal === undefined) { + console.error("WTerminal is missing!"); + return; + } + for (let c of Object.keys(ext)) { + WTerminal.terminalAddCommand(c, ext[c].run, ext[c].help); + } + //add aliases + for (let c of Object.keys(aliases)) { + WTerminal.terminalAddAlias(c, aliases[c]); + } + }; + //init + if (document.body) { + initTerminalVariableCommands(); + } else { + window.addEventListener("load", initTerminalVariableCommands); + } +} diff --git a/src/js/theme.js b/src/js/theme.js new file mode 100644 index 0000000..22aa963 --- /dev/null +++ b/src/js/theme.js @@ -0,0 +1,38 @@ +function toggleTheme() { + const theme = "theme-dark";//localStorage.getItem('theme'); + const root = document.querySelector(":root"); + root.classList.toggle(theme); + if (root.classList.contains(theme)) { + localStorage.setItem('theme', theme); + } else { + localStorage.setItem('theme', "theme-light"); + } +} + +function setTheme(themeName) { + console.log("setting theme: " + themeName); + const root = document.querySelector(":root"); + root.classList.add(themeName); + localStorage.setItem('theme', themeName); +} + +function detectTheme() { + const theme = localStorage.getItem('theme'); + if (theme === 'theme-dark' || theme === 'theme-light') { + setTheme(theme); + return; + } + + if (window.matchMedia('(prefers-color-scheme: dark)').matches) { + setTheme('theme-dark'); + return; + } + + if (window.matchMedia('(prefers-color-scheme: light)').matches) { + setTheme('theme-light'); + return; + } + + setTheme('theme-light'); +} +detectTheme(); diff --git a/src/js/wterminal-autoextend.js b/src/js/wterminal-autoextend.js new file mode 100644 index 0000000..1e23a6f --- /dev/null +++ b/src/js/wterminal-autoextend.js @@ -0,0 +1,33 @@ +/* Author: Ward Truyen +* Version: 1.0.0 +* About: This auromaticly adds the other terminal extention scripts. +* THIS SCRIPT DOES NOT WORK FOR A BROWSER-ADDON/EXTENTION! +*/ + +{// this code block hides the variables below from other scripts. + const addScripts = function() { + // location of script files + const extentionScripts = [ + "js/termext/ext-variables.js", + "js/termext/ext-cookies.js", + "js/termext/ext-eval.js", + "js/termext/ext-timerdebug.js", + "js/termext/ext-stresstest.js", + "js/termext/ext-popvar.js", + "js/termext/ext-passw.js", + ]; + + for (let ext of extentionScripts) { + const element = document.createElement("script"); + // element.addAttribute("src", ext); + element.src = ext; + document.head.append(element); + } + }//--> addScripts = function() { + + if (document.body) { + addScripts(); + } else { + window.addEventListener("load", addScripts); + } +} diff --git a/src/js/wterminal.js b/src/js/wterminal.js new file mode 100644 index 0000000..aa969aa --- /dev/null +++ b/src/js/wterminal.js @@ -0,0 +1,1353 @@ +/* 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 
from submitting + }; + + terminalClose() { + const terminalBackground = this.backgroundEl; + if (terminalBackground && terminalBackground.classList.contains(WTerminal.VISIBLE_CLASS)) { + terminalBackground.classList.remove(WTerminal.VISIBLE_CLASS); + } else if (typeof terminalBackground === "undefined") { + this.printError("This is not a dropdown terminal."); + } + }; + + terminalOpen() { + const terminalBackground = this.backgroundEl; + if (terminalBackground === null) return; + if (!terminalBackground.classList.contains(WTerminal.VISIBLE_CLASS)) { + terminalBackground.classList.add(WTerminal.VISIBLE_CLASS); + } + // this.inputTextEl.focus(); + this.outputEl.scrollTop = this.outputEl.scrollHeight; + setTimeout(() => this.inputTextEl.focus(), 100); + }; + + terminalOpenClose() { + const terminalBackground = this.backgroundEl; + if (terminalBackground === null) return; + if (!terminalBackground.classList.contains(WTerminal.VISIBLE_CLASS)) { + terminalBackground.classList.add(WTerminal.VISIBLE_CLASS); + // this.inputTextEl.focus(); + this.outputEl.scrollTop = this.outputEl.scrollHeight; + setInterval(() => this.inputTextEl.focus(), 100); + } else { + terminalBackground.classList.remove(WTerminal.VISIBLE_CLASS); + } + } + + isTerminalOpen() { + const terminalBackground = this.backgroundEl; + if (terminalBackground) + return terminalBackground.classList.contains(WTerminal.VISIBLE_CLASS); + return true; + }; + + terminalConst() { + const c = {}; + c.version = WTerminal.VERSION; + //css & html element relations: + c.backgroundClass = WTerminal.BACKGROUND_CLASS; + c.containerClass = WTerminal.CONTAINER_CLASS; + c.outputClass = WTerminal.OUTPUT_CLASS; + c.inputClass = WTerminal.INPUT_CLASS; + c.visibleClass = WTerminal.VISIBLE_CLASS; + //globals + c.globalLastResult = WTerminal.GLOBAL_LAST_RESULT; + c.globalLastError = WTerminal.GLOBAL_LAST_ERROR; + c.globalHistory = WTerminal.GLOBAL_HISTORY; + //auto insert + c.globalTestVariables = WTerminal.AUTO_INSERT_GLOBAL_TEST_VARIABLES; + c.autoInsertDropdown = WTerminal.AUTO_INSERT_DROPDOWN; + c.printLogo = WTerminal.PRINT_LOGO; + //Options: open/close + c.keyOpen = WTerminal.KEY_OPEN; + c.keyOpenCtrl = WTerminal.KEY_OPEN_CTRL; + c.keyClose = WTerminal.KEY_CLOSE; + c.keyHistory = WTerminal.KEY_HISTORY; + //Options: output + c.defaultPrintToConsoleLog = WTerminal.PRINT_TO_CONSOLE_LOG; + //Options: input + c.defaultSlashCommands = WTerminal.SLASH_COMMANDS; + c.defaultInputStrict = WTerminal.INPUT_STRICT; + c.defaultPrintAliasChange = WTerminal.PRINT_ALIAS_CHANGE; + c.defaultPrintInnerCommands = WTerminal.PRINT_INNER_COMMANDS; + c.defaultPrintCommandReturn = WTerminal.PRINT_COMMAND_RETURN; + c.defaultMaxHistory = WTerminal.MAX_HISTORY; + //Options: extensions + c.defaultPrintExtensionAdd = WTerminal.PRINT_EXTENSION_ADD; + c.defaultPrintAliasAdd = WTerminal.PRINT_ALIAS_ADD; + //Options: TPO aka terminalPrintObject const + c.tpo_defaultUnknownObjectPrint = WTerminal.TPO_UNKNOWN_OBJECT_PRINT; + c.tpo_defaultObjectPrefix = WTerminal.TPO_OBJECT_PREFIX; + c.tpo_defaultSpecialPrefix = WTerminal.TPO_SPECIAL_PREFIX; + c.tpo_defaultMaxDepth = WTerminal.TPO_MAX_DEPTH; + c.tpo_defaultInnerMaxLength = WTerminal.TPO_INNER_MAX_LENGTH; + return c; + } + + //#region input + /* List of standard aliases + replacement + */ + static aliasList = Object.freeze({ //freeze object to make it immutable/const-value + "?": "help", + "??": "help help", + cmdlist: "help", + print: "echo", + version: "const version ", + }); + + /* List of standard commands + functions + */ + static commandList = Object.freeze({ //freeze object to make it immutable/const-value + alias: { + run: function(term) { + term.printList(WTerminal.aliasList); + term.printList(term.aliasExtensionList); + }, + help: function(term) { + term.printBold("Usage:"); + term.printLn("alias //Prints all aliases"); + term.printBold("About alias usage:"); + term.printLn("An alias-name has no spaces, but the alias-value can have multiple spaces."); + term.printLn("When the terminal detects an alias-name as the first input word, it gets replaced to the alias-value."); + term.printLn("When the alias-value contains a space, then the arguments will be"); + term.printLn(" joined with the alias-value without a space joining them."); + term.printBold("Samples of using an alias:"); + term.printLn("? //Changes to: `help`"); + term.printLn("? clear //Changes to: `help clear`"); + term.printLn("version //Changes to: `const version ` (command, space at the end)"); + term.printLn("version clear //Changes to: `const version clear`"); + term.printLn("terminal //Changes to: `printvar -r terminal` (command, no space at the end)"); + } + }, + clear: { + run: function(term) { + term.clearOutput(); + }, + help: function(term) { + term.printLn("Clears terminal output."); + } + }, + close: { + run: function(term) { + term.terminalClose(); + }, + help: function(term) { + term.printLn("Closes dropdown terminal."); + } + }, + const: { + run: function(term, argLine) { + const c = term.terminalConst(); + + if (argLine == "") { + term.printVar(c, "const"); + return c; + } else if (c[argLine] !== undefined) { + term.printVar(c[argLine], argLine); + return c[argLine]; + } + }, + help: function(term) { + term.printBold("Usage:"); + term.printLn("const //Prints & returns all constant variables."); + term.printLn("const version //Prints & returns only the version constant."); + } + }, + date: { + run: function(term) { + const d = new Date().toDateString(); + term.printLn("The date is " + d); + return d; + }, + help: function(term) { + term.printLn("Returns the date."); + } + }, + echo: { + run: function(term, argLine) { + if (argLine.charAt(0) == '-') { + if (argLine.charAt(1) == 'r') { + term.printError(argLine.substring(3)); + return; + } else if (argLine.charAt(1) == 'g') { + term.printLn(WTerminal.createElement('span', { style: "color: green;" }, argLine.substring(3))); + return; + } else if (argLine.charAt(1) == 'b') { + term.printLn(WTerminal.createElement('span', { style: "color: blue;" }, argLine.substring(3))); + return; + } + } + term.printLn(argLine); + }, + help: function(term) { + term.printLn("Prints arguments."); + term.printBold("Usage:"); + term.printLn('echo hello //Prints "hello".'); + term.printLn('echo -r hello //Prints "hello", but in red.'); + term.printLn('echo -g hello //Prints "hello", but in green.'); + term.printLn('echo -b hello //Prints "hello", but in blue.'); + } + }, + height: { + run: function(term, argLine) { + if (argLine === "undefined") { + term.outputEl.style.minHeight = ''; // sample 40em (40 lines) + term.outputEl.style.maxHeight = ''; + } else if (argLine) { + term.outputEl.style.minHeight = argLine; // sample 40em (40 lines) + term.outputEl.style.maxHeight = argLine; + } else { + if (term.outputEl.style.minHeight) + term.printLn("height: " + term.outputEl.style.minHeight); + else + term.printLn("at original height."); + // term.printLn("height: " + getComputedStyle(term.outputEl).getPropertyValue('heiht')); + } + }, + help: function(term) { + term.printLn("prints or sets the height of the terminal-output element") + term.printLn("height // prints the terminal-output height") + term.printLn("height 40em // sets terminal-output height to 40 lines") + term.printLn("height undefined // sets terminal-output height to original value") + } + }, + help: { + run: function(term, argLine) { + if (argLine == '') { + if (term.backgroundEl) { + term.print(`Open the terminal with ${(term.options.keyOpenCtrl ? "CTRL + " : "")}the ${term.options.keyOpen}-key. `); + term.printLn(`Close the terminal with the ${term.options.keyClose}-key. `); + } + term.printLn(`Use the ${term.options.keyHistory}-key to get previous command. `) + term.printLn(`Type a command, optionally add some arguments, and press enter or click on submit.`); + term.printBold("Basic commands:"); + Object.keys(WTerminal.commandList).forEach((command, i) => { + if (i == 0) { + term.print(" " + command); + } else if (i % 10 == 0) { + term.printLn(", "); + term.print(" " + command); + } else { + term.print(", " + command); + } + }); + term.printLn(); + term.printBold("Extension commands:"); + Object.keys(term.commandListExtension).forEach((command, i) => { + if (i == 0) { + term.print(" " + command); + } else if (i % 10 == 0) { + term.printLn(", "); + term.print(" " + command); + } else { + term.print(", " + command); + } + }); + term.printLn(); + term.printBold("Using help:"); + term.printLn("help [COMMAND_NAME] //Prints help, optionally on a command"); + term.printBold("Samples of help:"); + term.printLn("help //Prints this help again."); + term.printLn("help help //Prints more help."); + term.printLn("help option //Prints help on the option command."); + term.printLn("help alias //Prints help on the alias command."); + } else { + if (WTerminal.commandList[argLine] !== undefined) { + WTerminal.commandList[argLine].help(term); + } else if (term.commandListExtension[argLine] !== undefined) { + if (typeof term.commandListExtension[argLine].help === "function") { + term.commandListExtension[argLine].help(term); + } else { + term.printLn("No help for command: \"" + argLine + "\""); + } + } else { + term.printError("Help error: Unknown command: \"" + argLine + "\""); + } + } + }, + help: function(term) { + term.printLn("Prints help about the terminal or given command."); + term.printBold("For terminal help use:"); + term.printLn("help"); + term.printBold("For help on a command use:"); + term.printLn("help commandName"); + term.printBold("Samples of help:"); + term.printLn("help //prints terminal help."); + term.printLn("help help //prints this help again."); + term.printLn("help option //prints help on the option command."); + term.printLn("help alias //prints help on the alias command."); + term.printBold("Advanced usage:") + term.printLn("Previous commands are stored in `terminal.history`") + term.printLn("Every command is a function, the result of executing this"); + term.printLn(" will be stored in the global variable `terminal.lastResult`."); + term.printLn("Multiple commands can be chained, separated by `&&`."); + term.printBold("Advanced samples:"); + term.printLn("const version && printvar terminal"); + term.printLn(" //Gets version value, automatically stores it in terminal.lastResult"); + term.printLn(" // then prints out the terminal variable (you will see lastResult in there)"); + term.printLn("printvar -r document.body.children && printvar -r terminal.lastResult.0.children"); + term.printLn(" //Get, print and return html body tag's children"); + term.printLn(" // && print out the children property of first element (of body)."); + term.printLn("gdb && result .0.children // does the same as above"); + } + }, + history: { + run: function(term) { + term.printList(term.history, "history"); + }, + help: function(term) { + term.printLn("prints this terminal's history"); + } + }, + option: { + run: function(term, argLine) { + if (argLine == '') { + term.printList(term.options); + } else { + let args = WTerminal.splitToArguments(argLine); + for (const element of args) { + if (!element.includes("=")) { + throw new Error("Option set error: argument is missing \"=\""); + } + const keyValuePair = element.split("="); + const name = keyValuePair[0]; + if (term.options[name] === undefined) { + throw new Error("Option set error: invalid option name `" + name + "`"); + } + term.options[name] = stringToValue(keyValuePair[1]); + } + return this.options; //todo: make clone for security? + } + }, + help: function(term) { + term.printBold("Usage:"); + term.printLn("option //Prints & returns all the options used."); + term.printLn("option [VARIABLE_NAME=VALUE] //Sets option VARIABLE_NAME to the following value."); + term.printBold("Samples:"); + term.printLn("option printAliasChange=true //Sets option printAliasChange to true."); + term.printLn("option printCommandReturn=true //Sets option printCommandReturn to true."); + term.printLn("option tpo_objectPrefix=\"# \" //Sets objectPrefix to `# `.") + } + }, + reload: { + run: function(term) { + location.reload(); + }, + help: function(term) { + term.printLn("Reloads page."); + } + }, + thisterminal: { + run: function(term) { + term.printVar(term, "terminal"); + }, + help: function(term) { + term.printLn("prints this terminal object") + } + }, + terminalnames: { + run: function(term) { + term.printList(Object.keys(WTerminal.terminals)); + }, + help: function(term) { + term.printLn("prints the names of all terminal objects") + } + }, + time: { + run: function(term) { + const d = new Date(); + const t = d.getHours() + ":" + d.getMinutes() + ":" + d.getSeconds(); + term.printLn("The time is " + t); + return t; + }, + help: function(term) { + term.printLn("Returns the current time."); + } + }, + uptime: { + run: function(term) { + const newDate = new Date(); + let diffTime = Math.abs(newDate - term.startupDate); + + const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24)); + if (diffDays > 0) { + term.print(diffDays + " days "); + diffTime -= diffDays * (1000 * 60 * 60 * 24); + } + + const diffHours = Math.floor(diffTime / (1000 * 60 * 60)); + if (diffHours > 0) { + term.print(diffHours + " hours "); + diffTime -= diffHours * (1000 * 60 * 60); + } + + const diffMinutes = Math.floor(diffTime / (1000 * 60)); + if (diffMinutes > 0) { + term.print(diffMinutes + " minutes "); + diffTime -= diffMinutes * (1000 * 60); + } + + const diffSeconds = Math.floor(diffTime / (1000)); + if (diffSeconds > 0) { + term.print(diffSeconds + " seconds "); + diffTime -= diffSeconds * (1000); + } + + if (diffTime > 0) { + term.print(diffTime + " milliseconds "); + } + term.printLn(); + }, + help: function(term) { + term.printLn("Prints how long the terminal has been up."); + } + }, + starttime: { + run: function(term) { + term.printLn("Terminal initialized on ", term.startupDate); + return term.startupDate; + }, + help: function(term) { + term.printLn("Returns when the terminal was started/initialized."); + } + }, + }); + + /* non static -> adds command to this terminal + */ + terminalAddComand(name, run, help) { + if (name === undefined || typeof name !== "string") { + this.printError("TerminalAddCommand error: Variable name must be a string."); + return; + } + if (name === '') { + this.printError("TerminalAddCommand error: Variable name can not be empty."); + return; + } + if (name.includes(' ')) { + this.printError("TerminalAddCommand error: Variable name can have spaces."); + return; + } + if (run === undefined || typeof run !== "function") { + this.printError("TerminalAddCommand error: Variable run must be a function."); + return; + } + if (help !== undefined && typeof run !== "function") { + this.printError("TerminalAddCommand error: Variable help must be a function or undefined."); + return; + } + if (this.options.printExtensionAdd) this.printLn("Command extension added: " + name); + this.commandListExtension[name] = { run: run, help: help }; + } + + /* static -> adds command to all terminals + */ + static terminalAddCommand(name, run, help) { + for (const tName in this.terminals) { + const t = this.terminals[tName]; + t.terminalAddComand(name, run, help); + } + } + + /* non static -> adds alias to this terminal + */ + terminalAddAlias(name, alias) { + if (typeof name !== "string") { + this.printError("AddAlias error: name must be a string."); + return false; + } + if (name.includes(' ')) { + this.printError("AddAlias error: name can not have spaces.") + return false; + } + if (typeof alias !== "string") { + this.printError("AddAlias error: alias must be a string."); + return false; + } + if (this.options.printAliasAdd) this.printLn(`Alias added: ${name} = \`${alias}\``); + this.aliasExtensionList[name] = alias; + return true; + } + + /* static -> adds alias to all terminals + */ + static terminalAddAlias(name, alias) { + for (const tName in this.terminals) { + const t = this.terminals[tName]; + t.terminalAddAlias(name, alias); + } + } + + /* Runs through a commandline searching for subCommands split by && + */ + terminalCommand(cmdLine, isSuperCommand = false) { + if (typeof cmdLine !== "string") { + this.printError("Error: Wrong cmdLine type: " + typeof cmdLine); + return; + } + if (cmdLine == '') return; + + cmdLine = cmdLine.trim(); + // print input + if (isSuperCommand) { + this.printLn(WTerminal.createElement('u', null, 'super-command#'), WTerminal.createElement('b', null, " ", cmdLine)); + } else { + let d = new Date(); + if (this.outputEl.innerHTML != '') this.print('\n'); + this.printLn(WTerminal.createElement('u', null, "(" + d.getHours() + ":" + d.getMinutes() + ":" + d.getSeconds() + ")#"), WTerminal.createElement('b', null, " ", cmdLine)); + } + // execute + let result; + try { + if (cmdLine.includes("&&")) { + // multiple commands + let commands = cmdLine.split("&&"); + // this.printList(commands); + for (let command of commands) { + command = command.trim(); + if (this.options.printInnerCommands) this.printLn(WTerminal.createElement("u", null, "inner-command#"), WTerminal.createElement("b", null, " ", command)); + result = this._terminalCommand(command); + } + } else { + // single command + result = this._terminalCommand(cmdLine); + } + } catch (e) { + // this.printError("Command error: ", e) + this.printError("Command error:"); + this.printVar(e, "error"); + this.lastError = e; + if (WTerminal.GLOBAL_LAST_ERROR) { + if (typeof globalThis.terminal !== "object") { + globalThis.terminal = {}; + } + globalThis.terminal.lastError = e; + } + // return e; + } + // keep a history + if (this.history === undefined || !(this.history instanceof Array)) { + this.history = []; + } + this.history.unshift(cmdLine); + if (this.history.length > this.options.maxHistory) { + this.history.splice(this.options.maxHistory); + } + if (WTerminal.GLOBAL_HISTORY) { + if (typeof globalThis.terminal !== "object") { + globalThis.terminal = {}; + } + if (globalThis.terminal.history === undefined || !(globalThis.terminal.history instanceof Array)) { + globalThis.terminal.history = []; + } + globalThis.terminal.history.unshift(cmdLine); + if (globalThis.terminal.history.length > this.options.maxHistory) { + globalThis.terminal.history.splice(this.options.maxHistory); + } + } + return result; + }; + + /* Only runs subcommands + * For internal use only + */ + _terminalCommand(cmdLine) { + if (cmdLine == '') return; + // get target values + const split = cmdLine.split(' '); + let command = split.shift(); + let argLine = split.join(' '); + + // not strict + if (!this.options.inputStrict) { + command = command.toLowerCase(); + } + // check for alias + let al; + if (WTerminal.aliasList[command]) { + al = WTerminal.aliasList[command]; + } else if (this.aliasExtensionList[command]) { + al = this.aliasExtensionList[command]; + } + if (al !== undefined) { + if (al.includes(" ")) { + //new command and argument + al = al.split(" "); + command = al.shift(); + argLine = (al.join(' ') + argLine).trim(); + } else { + //new command only + command = al; + } + if (this.options.printAliasChange) { + this.printLn("Alias found#", WTerminal.createElement('b', null, command + " " + argLine)); + } + } + // execute + let result; + if (WTerminal.commandList[command]) { + result = WTerminal.commandList[command].run(this, argLine); + } else if (this.commandListExtension[command]) { + result = this.commandListExtension[command].run(this, argLine); + } else { + this.printError("Error: Unknown command: \"" + command + "\""); + this.printLn("Try help"); + return; + } + // print result + if (this.options.printCommandReturn) { + this.printVar(result, `Result of \`${cmdLine}\``); + } + // store result + this.lastResult = result; + if (WTerminal.GLOBAL_LAST_RESULT) { + if (typeof globalThis.terminal !== "object" && typeof result !== "undefined") { + globalThis.terminal = {}; + } + if (typeof globalThis.terminal === 'object' && + (typeof globalThis.terminal.lastResult !== "undefined" || typeof result !== "undefined")) { + globalThis.terminal.lastResult = result; + } + } + return result; + }//-> function _terminalCommand + //#endregion + + /* installs a dropdown Terminal if it doesnt exist + */ + static instalDropdownTerminal() { + if (WTerminal.terminals.dropdown) return; + if (document.body) { + new WTerminal('dropdown', null, null); + } else { + window.addEventListener("load", () => new WTerminal('dropdown', null, null)); + } + } + + /* short for openDropdownTerminal + */ + static open() { + if (WTerminal.terminals.dropdown) { + WTerminal.terminals.dropdown.terminalOpen(); + } else { + if (confirm("Dropdown terminal is not available. Create terminal?")) { + instalDropdownTerminal(); + WTerminal.terminals.dropdown.terminalOpen(); + } + } + } + +}//-> class WTerminal + +//#region init +//Globals +if (WTerminal.AUTO_INSERT_GLOBAL_TEST_VARIABLES) { + globalThis.terminal = {}; + globalThis.terminal.version = WTerminal.VERSION; + globalThis.terminal.terminals = WTerminal.terminals; + globalThis.terminal.wterminal = WTerminal; + // terminal.options = options; + globalThis.terminal.testvars = {}; + const p = globalThis.terminal.testvars; + p.hello = "Hello World!"; + p.multilineStr = `one +two +three`; + p.emptyArray = []; + p.emptyObject = {}; + p.newArray = new Array(); + p.newDate = new Date(); + p.newSet = new Set(); + p.newMap = new Map(); + p.testArray1 = ["three", "two", "one"]; + p.testArray1 = ["three", 2, true, { 1: 1, '2': 2, 'three': 3 }]; + // p.testKeys = { 1:1, '2':2, 'three':3 };// 1 is a number. 2 is a string. => at runtime keys are always strings (even in an Array) +} + +if (WTerminal.AUTO_INSERT_DROPDOWN) { + WTerminal.instalDropdownTerminal(); +} +//#endregion + +/* get a registered WTerminal, preferrably by name, if not it returns the first of the list. + */ +const getWTerminal = function(name) { + if (name === undefined) for (const t in WTerminal.terminals) return WTerminal.terminals[t]; + return WTerminal.terminals[name]; +} diff --git a/src/pong.html b/src/pong.html new file mode 100644 index 0000000..773e3c3 --- /dev/null +++ b/src/pong.html @@ -0,0 +1,57 @@ + + + + + Sample: Ping pong + + + + + + + + + + + + +
+ + Home + Static + Dropdown + Pong + Addon +
+ +
+ + + +

Game sample: Ping pong

+ +
+ +

How to:
+ Use the arrow keys up and down to move, or [W] and [S].
+ Press spacebar or enter, to start a next round, or to pause/resume.

+

To restart the game use the buttons below. Or open the terminal, type "restartgame" and press enter.

+

To get more information about pong open the terminal, type "printgame" and press enter.
+ Or open the terminal, type "popvar game" and press enter (DEBUG_PARENT_OBJECT must be true for this to work).

+ + + + + + +
+ +
+

Author: Ward Truyen

+
+ + + diff --git a/src/snd/glass-knock.mp3 b/src/snd/glass-knock.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..52b49e59e2094f75d6c1c504b8a93ce97513adf2 GIT binary patch literal 10752 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 F{}-+ox{Lq- literal 0 HcmV?d00001 diff --git a/src/snd/short-success.mp3 b/src/snd/short-success.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..ff101229cae3a44679db1e59927af3d7fd85f7f6 GIT binary patch literal 50880 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

-|>gL+heXQ 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 E0Ae=cz5oCK literal 0 HcmV?d00001 diff --git a/src/static-terminal.html b/src/static-terminal.html new file mode 100644 index 0000000..e87f5d4 --- /dev/null +++ b/src/static-terminal.html @@ -0,0 +1,61 @@ + + + + + WTerminal static sample + + + + + + + + + + + +
+ + Home + Static + Dropdown + Pong + Addon +
+ +
+ +

Static-terminal:

+

Below we have created a WTerminal, by using it's constructor with a name ("static") and + element-id("static-terminal") to generate a terminal as a 'static element on our page'.

+
+ +
+ Samples: + + + | + | + +
+

How to add the terminal to a site:

+

Add the CSS and JavaScript files to your site header:
+ <link rel="stylesheet" href="css/wterminal.css" />
+ <script src="js/wterminal.js"></script>
+

+

Where the terminal should be add a div element, with id "static-terminal":
+ <div id="static-terminal"></div> +

+

Finally add a script element with:
+ new WTerminal("static", "static-terminal", null); +

+
+ +
+

Author: Ward Truyen

+
+ + + diff --git a/src/test/asserts.html b/src/test/asserts.html new file mode 100644 index 0000000..0a1bf22 --- /dev/null +++ b/src/test/asserts.html @@ -0,0 +1,38 @@ + + + + + Test Asserts + + + + + + + + + + +
+ + WTerminal testing home +
+ +
+

Test Asserts:

+
+ + + +
+ +
+

Author: Ward Truyen

+
+ + + diff --git a/src/test/css/main.css b/src/test/css/main.css new file mode 100644 index 0000000..31eb115 --- /dev/null +++ b/src/test/css/main.css @@ -0,0 +1,99 @@ +/* Author: Ward Truyen + * Version: 1.3.0 + */ + +html, +body { + margin: 0px; + padding: 0px; + background: radial-gradient(ellipse at top, #8888ff, transparent), + radial-gradient(ellipse at bottom, #ad9fff, transparent); + height: 100%; + display: flex; + flex-direction: column; +} + +main { + margin: auto; + width: 80%; + border: 2px solid darkgreen; + padding: 10px; + background-color: white; + box-shadow: 3px 3px 3px black; +} + +h1 { + margin-top: 4px; + margin-bottom: 8px; + text-decoration: underline; +} + +header { + padding: 2px 16px 4px 16px; + /* width: 100%; */ + border-bottom: 1px solid black; + /* background-image: linear-gradient(#0000008f, #0000004f); */ + background-color: #80808080; + /* linear-gradient(#0000008f, #0000004f);*/ + /* margin-top: 16px; */ + text-shadow: 1px 1px 1px #0000004f; +} + +header a { + /* border: solid 1px black; */ + padding: 2px; + margin: 1px; +} + +footer { + padding: 8px 16px 4px 16px; + /* width: 100%; */ + border-top: 1px solid black; + /* background-color: #0000004f; */ + background-image: linear-gradient(#0000008f, #0000004f); + /* margin-top: 16px; */ + margin-top: auto; + text-shadow: 1px 1px 1px #0000004f; +} + +footer h1, +footer h2, +footer h3, +footer p { + display: inline; + margin: 0px; +} + +.float-right { + float: right; +} + +.text-red { + color: red; +} + +.button-red { + background-color: red; +} + +.button-black { + background-color: black; + color: white; +} + +html.theme-dark { + color-scheme: dark; + background-image: linear-gradient(-10deg, #1010206F 60%, #5050506F); + color: white; +} + +.theme-dark>body>main { + border: 1px solid darkgray; + background-color: black; + box-shadow: 3px 3px 5px #101010C0; +} + +.theme-dark>body>main>h1 { + text-shadow: 0px 0px 3px blue, 2px 2px 6px; +} + diff --git a/src/test/index.html b/src/test/index.html new file mode 100644 index 0000000..4323b90 --- /dev/null +++ b/src/test/index.html @@ -0,0 +1,38 @@ + + + + + WTerminal test home + + + + + + + + +
+ + WTerminal testing home + + +
+ +
+

Tests:

+ +

Others:

+ WTerminal site home +
+ +
+

Author: Ward Truyen

+
+ + + diff --git a/src/test/js/asserts.js b/src/test/js/asserts.js new file mode 100644 index 0000000..87fe3b6 --- /dev/null +++ b/src/test/js/asserts.js @@ -0,0 +1,74 @@ +/* Author: Ward Truyen +* Version: 1.0.0 +* About: A tool for testing purposes +*/ + +class Asserts { + static runTest(name, testFunction, term, pre = " ", printStacktraceAndStop = true) { + term.print(pre+name); + try { + testFunction(); + term.printLn(WTerminal.createElement("span", {style:"color: green;"}, " OK")); + } catch (e) { + term.printError(" failed: " + e.toString()); + if(printStacktraceAndStop){ + term.printLn(WTerminal.createElement("span", {style:"color: blue;"}, e.stack)); + throw e; + } + } + } + + /* Throws error when not true + */ + static isTrue(value) { + if (value === true) return; + throw new Error('assert isTrue failed'); + } + + /* Throws error when not false + */ + static isFalse(value) { + if (value === false) return; + throw new Error('assert isFalse failed'); + } + /* Throws error when not equal + */ + static isEqual(value1, value2) { + if (Asserts._equals(value1, value2)) return; + // if( typeof value1 === "object" || typeof value2 === "object") throw new Error('assert isEqual failed'); + throw new Error('assert isEqual failed\n' + value1 + " != " + value2); + } + + /* returns true when values/objects v1 and v2 are equal + */ + static _equals(v1, v2, _depth = 0) { + if (v1 === v2) return true; // easy peasy + let t = typeof v1 + if (t === "string" || v1 instanceof String && typeof v2 === "string" || v2 instanceof String) return v1.valueOf() === v2.valueOf(); // "Hello" equals to new String("Hello") + if (t !== typeof v2 || v1.constructor.name !== v2.constructor.name) return false; // must be of identical type + t += " " + v1.constructor.name; + switch (t) { + default: // Yet Unkown when i coded this? + throw new Error("Todo: checking of type " + t); + case 'boolean Boolean': + case 'number Number': + case 'object Date': + return v1.valueOf() === v2.valueOf(); + case 'object Array': + if (_depth > 4) throw Error("too deep"); + if (v1.length !== v2.length) return false; + for (let i = 0; i < v1.length; i++) { + if (Asserts._equals(v1[i], v2[i], _depth + 1) === false) return false; + } + return true; + case 'object Object': + if (_depth > 4) throw Error("too deep"); + const keys = Object.keys(v1); + if (keys.length != Object.keys(v2).length) return false; + for (let k in keys) { + if (Asserts._equals(v1[k], v2[k], _depth + 1) === false) return false; + } + return true; + }//-> switch (t) + }//-> static equals(v1, v2) +}//-> class Asserts diff --git a/src/test/js/test-asserts.js b/src/test/js/test-asserts.js new file mode 100644 index 0000000..687eba0 --- /dev/null +++ b/src/test/js/test-asserts.js @@ -0,0 +1,106 @@ +{ + const term = getWTerminal(); + term.printLn("Testing Asserts"); + //isTrue testing + Asserts.runTest(`Asserts.isTrue(true)`, + () => { + try { + Asserts.isTrue(true); + } catch (e) { + throw new Error("Assserts.isTrue(true) failed", e); + } + }, term); + Asserts.runTest(`Asserts.isTrue(false)`, + () => { + let throwsNoError = true; + try { + Asserts.isTrue(false); + } catch (e) { + throwsNoError = false; + } + if (throwsNoError) throw new Error("Assert.isTrue(false) does not throw Error"); + }, term); + // Asserts.runTest('This test should fail', ()=>{throw new Error("To fail test")}, term); + //isFalse testing + Asserts.runTest(`Asserts.isFalse(false)`, + () => { + try { + Asserts.isFalse(false); + } catch (e) { + throw new Error("Assserts.isFalse(false) failed", e); + } + }, term); + Asserts.runTest(`Asserts.isFalse(true)`, + () => { + let throwsNoError = true; + try { + Asserts.isFalse(true); + } catch (e) { + throwsNoError = false; + } + if (throwsNoError) throw new Error("Assert.isFalse(true) does not throw Error"); + }, term); + //isEqual testing + Asserts.runTest(`Asserts.isEqual(true, true)`, + () => { + try { + Asserts.isEqual(true, true); + } catch (e) { + throw new Error("Assserts.isEqual(true, true) failed", e); + } + }, term); + Asserts.runTest(`Asserts.isEqual(true, false)`, + () => { + let throwsNoError = true; + try { + Asserts.isEqual(true, false); + } catch (e) { + throwsNoError = false; + } + if (throwsNoError) throw new Error("Assert.isEqual(true, false) does not throw Error"); + }, term); + //_equals testing + Asserts.runTest(`Asserts.isFalse(Asserts._equals(null, 0))`, + () => Asserts.isFalse(Asserts._equals(null, 0)), term); + Asserts.runTest(`Asserts.isFalse(Asserts._equals(null, undefined))`, + () => Asserts.isFalse(Asserts._equals(null, undefined)), term); + Asserts.runTest(`Asserts.isFalse(Asserts._equals(false, true))`, + () => Asserts.isFalse(Asserts._equals(false, true)), term); + Asserts.runTest(`Asserts.isFalse(Asserts._equals(false, 0))`, + () => Asserts.isFalse(Asserts._equals(false, 0)), term); + Asserts.runTest(`Asserts.isTrue(Asserts._equals(true, true))`, + () => Asserts.isTrue(Asserts._equals(true, true)), term); + Asserts.runTest(`Asserts.isFalse(Asserts._equals(true, 1))`, + () => Asserts.isFalse(Asserts._equals(true, 1)), term); + Asserts.runTest(`Asserts.isFalse(Asserts._equals(0, '0'))`, + () => Asserts.isFalse(Asserts._equals(0, '0')), term); + Asserts.runTest(`Asserts.isFalse(Asserts._equals(0, 1))`, + () => Asserts.isFalse(Asserts._equals(0, 1)), term); + Asserts.runTest(`Asserts.isTrue(Asserts._equals(1, 1))`, + () => Asserts.isTrue(Asserts._equals(1, 1)), term); + Asserts.runTest(`Asserts.isFalse(Asserts._equals(1, '1'))`, + () => Asserts.isFalse(Asserts._equals(1, '1')), term); + Asserts.runTest(`Asserts.isTrue(Asserts._equals(Math.PI, Math.PI))`, + () => Asserts.isTrue(Asserts._equals(Math.PI, Math.PI)), term); + Asserts.runTest(`Asserts.isTrue(Asserts._equals("Hello", "Hello"))`, + () => Asserts.isTrue(Asserts._equals("Hello", "Hello")), term); + Asserts.runTest(`Asserts.isTrue(Asserts._equals("Hello", new String("Hello")))`, + () => Asserts.isTrue(Asserts._equals("Hello", new String("Hello"))), term); + Asserts.runTest(`Asserts.isFalse(Asserts._equals("Hello", "hello"))`, + () => Asserts.isFalse(Asserts._equals("Hello", "hello")), term); + Asserts.runTest(`Asserts.isTrue(Asserts._equals(["Hello", 1], ["Hello", 1]))`, + () => Asserts.isTrue(Asserts._equals(["Hello", 1], ["Hello", 1])), term); + Asserts.runTest(`Asserts.isFalse(Asserts._equals(["Hello", 1], [1, "Hello"]))`, + () => Asserts.isFalse(Asserts._equals(["Hello", 1], [1, "Hello"])), term); + Asserts.runTest(`Asserts.isTrue(Asserts._equals({}, {}))`, + () => Asserts.isTrue(Asserts._equals({}, {})), term); + Asserts.runTest(`Asserts.isTrue(Asserts._equals({ a: "Hello", b: 1 }, { a: "Hello", b: 1 }))`, + () => Asserts.isTrue(Asserts._equals({ a: "Hello", b: 1 }, { a: "Hello", b: 1 })), term); + Asserts.runTest(`Asserts.isTrue(Asserts._equals({ a: "Hello", b: 1 }, { b: 1, a: "Hello" }))`, + () => Asserts.isTrue(Asserts._equals({ a: "Hello", b: 1 }, { b: 1, a: "Hello" })), term); + Asserts.runTest(`Asserts.isFalse(Asserts._equals({ a: "Hello", b: 1 }, { b: 1, a: "Hello", c: undefined }))`, + () => Asserts.isFalse(Asserts._equals({ a: "Hello", b: 1 }, { b: 1, a: "Hello", c: undefined })), term); + Asserts.runTest(`Asserts.isFalse(Asserts._equals({ a: "Hello", b: 1 }, { b: 1, a: "Hello", c: null }))`, + () => Asserts.isFalse(Asserts._equals({ a: "Hello", b: 1 }, { b: 1, a: "Hello", c: null })), term); + term.printLn("Asserts OK"); +} diff --git a/src/test/js/test-splitToArguments.js b/src/test/js/test-splitToArguments.js new file mode 100644 index 0000000..a5741c0 --- /dev/null +++ b/src/test/js/test-splitToArguments.js @@ -0,0 +1,20 @@ +{ + const term = getWTerminal(); + term.printLn("Testing WTerminal.splitToArguments"); + Asserts.runTest(`Asserts.isEqual(WTerminal.splitToArguments("hello world"), ["hello", "world"]);`, + () => Asserts.isEqual(WTerminal.splitToArguments("hello world"), ["hello", "world"]), term); + Asserts.runTest(`Asserts.isEqual(WTerminal.splitToArguments("hello 1"), ["hello", "1"])`, + () => Asserts.isEqual(WTerminal.splitToArguments("hello 1"), ["hello", "1"]), term); + Asserts.runTest(`Asserts.isEqual(WTerminal.splitToArguments('"hello world" 1'), ["hello world", "1"])`, + () => Asserts.isEqual(WTerminal.splitToArguments('"hello world" 1'), ["hello world", "1"]), term); + Asserts.runTest(`Asserts.isEqual(WTerminal.splitToArguments("'hello world' 1"), ["hello world", "1"])`, + () => Asserts.isEqual(WTerminal.splitToArguments("'hello world' 1"), ["hello world", "1"]), term); + // Asserts.runTest('This test should fail', ()=>{throw new Error("To fail test")}, term); + Asserts.runTest(`Asserts.isEqual(WTerminal.splitToArguments("\`hello world\` 1"), ["hello world", "1"])`, + () => Asserts.isEqual(WTerminal.splitToArguments("`hello world` 1"), ["hello world", "1"]), term); + Asserts.runTest(`Asserts.isEqual(WTerminal.splitToArguments("-rf /css/test.css myKitty.js 'my name with spaces'"), + ["-rf", "/css/test.css", "myKitty.js", "my name with spaces"])`, + () => Asserts.isEqual(WTerminal.splitToArguments("-rf /css/test.css myKitty.js 'my name with spaces'"), + ["-rf", "/css/test.css", "myKitty.js", "my name with spaces"]), term); + term.printLn("WTerminal.splitToArguments OK"); +} diff --git a/src/test/splitToArguments.html b/src/test/splitToArguments.html new file mode 100644 index 0000000..f0e7dd7 --- /dev/null +++ b/src/test/splitToArguments.html @@ -0,0 +1,38 @@ + + + + + Test WTerminal.splitToArguements + + + + + + + + + + +
+ + WTerminal testing home +
+ +
+

Test WTerminal.splitToArguements:

+
+ + + +
+ +
+

Author: Ward Truyen

+
+ + + diff --git a/src/test/test-all.html b/src/test/test-all.html new file mode 100644 index 0000000..063a27e --- /dev/null +++ b/src/test/test-all.html @@ -0,0 +1,39 @@ + + + + + Test all + + + + + + + + + + +
+ + WTerminal testing home +
+ +
+

Test all:

+
+ + + + +
+ + + + +