From fb50992ce4962fb652409bde2398951121ee719c Mon Sep 17 00:00:00 2001 From: mvvasilev Date: Sat, 8 Jun 2024 10:09:34 +0300 Subject: [PATCH] Add web stuff ( broken ) --- .gitignore | 4 +- go.mod | 7 +- go.sum | 8 + server/main.go | 7 + web/beep.wav | Bin 0 -> 4172 bytes web/index.html | 16 ++ web/tcell.html | 13 ++ web/tcell.js | 284 +++++++++++++++++++++++ web/termstyle.css | 121 ++++++++++ web/wasm_exec.js | 561 ++++++++++++++++++++++++++++++++++++++++++++++ 10 files changed, 1019 insertions(+), 2 deletions(-) create mode 100644 server/main.go create mode 100644 web/beep.wav create mode 100644 web/index.html create mode 100644 web/tcell.html create mode 100644 web/tcell.js create mode 100644 web/termstyle.css create mode 100644 web/wasm_exec.js diff --git a/.gitignore b/.gitignore index fa464d2..d87f3db 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,6 @@ last_light last_light.exe *.prof -__debug_bin* \ No newline at end of file +__debug_bin* + +web/last-light.wasm \ No newline at end of file diff --git a/go.mod b/go.mod index 1076aff..7a33583 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module mvvasilev/last_light -go 1.22.2 +go 1.22.4 require ( github.com/gdamore/tcell/v2 v2.7.4 @@ -12,7 +12,12 @@ require ( github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect github.com/rivo/uniseg v0.4.3 // indirect + github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636 // indirect + github.com/shurcooL/go-goon v1.0.0 // indirect + github.com/shurcooL/goexec v0.0.0-20230709021537-96bada04ea2b // indirect + golang.org/x/mod v0.8.0 // indirect golang.org/x/sys v0.19.0 // indirect golang.org/x/term v0.19.0 // indirect golang.org/x/text v0.14.0 // indirect + golang.org/x/tools v0.6.0 // indirect ) diff --git a/go.sum b/go.sum index f198c9c..c0456e9 100644 --- a/go.sum +++ b/go.sum @@ -11,10 +11,17 @@ github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.3 h1:utMvzDsuh3suAEnhH0RdHmoPbU648o6CvXxTx4SBMOw= github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636 h1:aSISeOcal5irEhJd1M+IrApc0PdcN7e7Aj4yuEnOrfQ= +github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= +github.com/shurcooL/go-goon v1.0.0 h1:BCQPvxGkHHJ4WpBO4m/9FXbITVIsvAm/T66cCcCGI7E= +github.com/shurcooL/go-goon v1.0.0/go.mod h1:2wTHMsGo7qnpmqA8ADYZtP4I1DD94JpXGQ3Dxq2YQ5w= +github.com/shurcooL/goexec v0.0.0-20230709021537-96bada04ea2b h1:637/WtTYN6u1wzt0dCpGdBJHIggB8inZei6q60AZwjk= +github.com/shurcooL/goexec v0.0.0-20230709021537-96bada04ea2b/go.mod h1:YrZDETqiwAqnKsivK9+sxwhS9rjMR+2NWGy8TATNb6k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= @@ -47,5 +54,6 @@ golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/server/main.go b/server/main.go new file mode 100644 index 0000000..8766b89 --- /dev/null +++ b/server/main.go @@ -0,0 +1,7 @@ +package main + +import "net/http" + +func main() { + http.ListenAndServe(`:8080`, http.FileServer(http.Dir(`./web`))) +} diff --git a/web/beep.wav b/web/beep.wav new file mode 100644 index 0000000000000000000000000000000000000000..96cefd444f226364492dafefa185869327f74182 GIT binary patch literal 4172 zcmeI#SvQqY90u^?uFF0`UnQI)^O$4in1{%mp+QA+QBhQgWS(b{nNSp|&(rh#_kQ2Q zIjmLI>h5fdWi1!y=h=I|@AHrFx7mEpTP*gA(bmlT(hK<2;)O5YL5qBQSymGt*NO<6|SkLj(PNJ>4I>Iy>ICx4nD&rltAytHzfvo;TD#d-~+@ zqlXXf->a*=d*^n|t(!NlS65Y5R$MDDD=jH0E-JiQP>_EmFE=+QJ1Z+QBRxGWHPw-l zlAM&7m=GTy7Z)27V~>u$d^svIG9n^8EG#rMBqTUEC@3&6z~BGUB|kr#&1SXw`ug~I zdq*Ao*xUWOv-M?TeRXANabbRLc4lgFVr+C|XmFsfx2L=7LuW^OTPuL@H7THrPXpm2 z5Iz9&uK<_>U?zZRX{ined`XE31jfe3*zNQY7_NZYX9BI(=)<4;v~6!~Zmg{?E8Fzc z3|HPutdHV_T8hDcc%g%3^*~wr9$gTn}6?4%`PG zxK?%Gnod7(Rc;3k8|r@G65I}4_|XFwAP3IRX2`xuX4uaEBExp-t8ipHomas{rtX4M zrVkorvqvuJSu^7$$TKqY$WD8(9Xn^9%>MmudNT85hF81s67W3FJnw*$E2QU1t84e) zztPdVk5@SAsHfU%F zz(QVd0R|aDV{8nL(b0!hi3Ou#DyXu8ORuaFVWaARO$O!Z0Fmm=Zx0 ziHRuX5}C$npJAj^NvY)$vFi{12d=A{NyvxGr}N{rKpT_->1c_QgRG1N))`jKA4)_J Ap#T5? literal 0 HcmV?d00001 diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..eceeeac --- /dev/null +++ b/web/index.html @@ -0,0 +1,16 @@ + + + + + + last-light + + + + +
+

+		
+ + + \ No newline at end of file diff --git a/web/tcell.html b/web/tcell.html new file mode 100644 index 0000000..8f3cf70 --- /dev/null +++ b/web/tcell.html @@ -0,0 +1,13 @@ + + + + + Tcell + + + + +

+		
+	
+
\ No newline at end of file
diff --git a/web/tcell.js b/web/tcell.js
new file mode 100644
index 0000000..92adbbc
--- /dev/null
+++ b/web/tcell.js
@@ -0,0 +1,284 @@
+// Copyright 2024 The TCell Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use file except in compliance with the License.
+// You may obtain a copy of the license at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+const wasmFilePath = "last-light.wasm";
+const term = document.getElementById("terminal");
+var width = 80;
+var height = 24;
+const beepAudio = new Audio("beep.wav");
+
+var cx = -1;
+var cy = -1;
+var cursorClass = "cursor-blinking-block";
+var cursorColor = "";
+
+var content; // {data: row[height], dirty: bool}
+// row = {data: element[width], previous: span}
+// dirty/[previous being null] indicates if previous (or entire terminal) needs to be recalculated.
+// dirty is true/null if terminal/previous need to be re-calculated/shown
+
+function initialize() {
+  resize(width, height); // initialize content
+  show(); // then show the screen
+}
+
+function resize(w, h) {
+  width = w;
+  height = h;
+  content = { data: new Array(height), dirty: true };
+  for (let i = 0; i < height; i++) {
+    content.data[i] = { data: new Array(width), previous: null };
+  }
+
+  clearScreen();
+}
+
+function clearScreen(fg, bg) {
+  if (fg) {
+    term.style.color = intToHex(fg);
+  }
+  if (bg) {
+    term.style.backgroundColor = intToHex(bg);
+  }
+
+  content.dirty = true;
+  for (let i = 0; i < height; i++) {
+    content.data[i].previous = null; // we set the row to be recalculated later
+    for (let j = 0; j < width; j++) {
+      content.data[i].data[j] = document.createTextNode(" "); // set the entire row to spaces.
+    }
+  }
+}
+
+function drawCell(x, y, s, fg, bg, attrs, us, uc) {
+  var span = document.createElement("span");
+  var use = false;
+
+  if ((attrs & (1 << 2)) != 0) {
+    // reverse video
+    var temp = bg;
+    bg = fg;
+    fg = temp;
+    use = true;
+  }
+  if (fg != -1) {
+    span.style.color = intToHex(fg);
+    use = true;
+  }
+  if (bg != -1) {
+    span.style.backgroundColor = intToHex(bg);
+    use = true;
+  }
+
+  // NB: these has to be updated if Attrs.go changes
+  if (attrs != 0) {
+    use = true;
+    if ((attrs & 1) != 0) {
+      span.classList.add("bold");
+    }
+    if ((attrs & (1 << 4)) != 0) {
+      span.classList.add("dim");
+    }
+    if ((attrs & (1 << 5)) != 0) {
+      span.classList.add("italic");
+    }
+    if ((attrs & (1 << 6)) != 0) {
+      span.classList.add("strikethrough");
+    }
+  }
+  if (us != 0) {
+    use = true;
+    if (us == 1) {
+      span.classList.add("underline");
+    } else if (us == 2) {
+      span.classList.add("double_underline");
+    } else if (us == 3) {
+      span.classList.add("curly_underline");
+    } else if (us == 4) {
+      span.classList.add("dotted_underline");
+    } else if (us == 5) {
+      span.classList.add("dashed_underline");
+    }
+    if (uc != -1) {
+      span.style.textDecorationColor = intToHex(uc);
+    }
+  }
+
+  if ((attrs & (1 << 1)) != 0) {
+    var blink = document.createElement("span");
+    blink.classList.add("blink");
+    var textnode = document.createTextNode(s);
+    blink.appendChild(textnode);
+    span.appendChild(blink);
+  } else {
+    var textnode = document.createTextNode(s);
+    span.appendChild(textnode);
+  }
+
+  content.dirty = true; // invalidate terminal- new cell
+  content.data[y].previous = null; // invalidate row- new row
+  content.data[y].data[x] = use ? span : textnode;
+}
+
+function show() {
+  if (!content.dirty) {
+    return; // no new draws; no need to update
+  }
+
+  displayCursor();
+
+  term.innerHTML = "";
+  content.data.forEach((row) => {
+    if (row.previous == null) {
+      row.previous = document.createElement("span");
+      row.data.forEach((c) => {
+        row.previous.appendChild(c);
+      });
+      row.previous.appendChild(document.createTextNode("\n"));
+    }
+    term.appendChild(row.previous);
+  });
+
+  content.dirty = false;
+}
+
+function showCursor(x, y) {
+  content.dirty = true;
+
+  if (!(cx < 0 || cy < 0)) {
+    // if original position is a valid cursor position
+    content.data[cy].previous = null;
+    if (content.data[cy].data[cx].classList) {
+      content.data[cy].data[cx].classList.remove(cursorClass);
+    }
+  }
+
+  cx = x;
+  cy = y;
+}
+
+function displayCursor() {
+  content.dirty = true;
+
+  if (!(cx < 0 || cy < 0)) {
+    // if new position is a valid cursor position
+    content.data[cy].previous = null;
+
+    if (!content.data[cy].data[cx].classList) {
+      var span = document.createElement("span");
+      span.appendChild(content.data[cy].data[cx]);
+      content.data[cy].data[cx] = span;
+    }
+
+    if (cursorColor != "") {
+      term.style.setProperty("--cursor-color", cursorColor);
+    } else {
+      term.style.setProperty("--cursor-color", "lightgrey");
+    }
+
+    content.data[cy].data[cx].classList.add(cursorClass);
+  }
+}
+
+function setCursorStyle(newClass, newColor) {
+  if (newClass == cursorClass && newColor == cursorColor) {
+    return;
+  }
+
+  if (!(cx < 0 || cy < 0)) {
+    // mark cursor row as dirty; new class has been applied to (cx, cy)
+    content.dirty = true;
+    content.data[cy].previous = null;
+
+    if (content.data[cy].data[cx].classList) {
+      content.data[cy].data[cx].classList.remove(cursorClass);
+    }
+
+    // adding the new class will be dealt with when displayCursor() is called
+  }
+
+  cursorClass = newClass;
+  cursorColor = newColor;
+}
+
+function beep() {
+  beepAudio.currentTime = 0;
+  beepAudio.play();
+}
+
+function setTitle(title) {
+  document.title = title;
+}
+
+function intToHex(n) {
+  return "#" + n.toString(16).padStart(6, "0");
+}
+
+initialize();
+
+let fontwidth = term.clientWidth / width;
+let fontheight = term.clientHeight / height;
+
+document.addEventListener("keydown", (e) => {
+  onKeyEvent(e.key, e.shiftKey, e.altKey, e.ctrlKey, e.metaKey);
+});
+
+term.addEventListener("click", (e) => {
+  onMouseClick(
+    Math.min((e.offsetX / fontwidth) | 0, width - 1),
+    Math.min((e.offsetY / fontheight) | 0, height - 1),
+    e.which,
+    e.shiftKey,
+    e.altKey,
+    e.ctrlKey
+  );
+});
+
+term.addEventListener("mousemove", (e) => {
+  onMouseMove(
+    Math.min((e.offsetX / fontwidth) | 0, width - 1),
+    Math.min((e.offsetY / fontheight) | 0, height - 1),
+    e.which,
+    e.shiftKey,
+    e.altKey,
+    e.ctrlKey
+  );
+});
+
+term.addEventListener("focus", (e) => {
+  onFocus(true);
+});
+
+term.addEventListener("blur", (e) => {
+  onFocus(false);
+});
+
+term.tabIndex = 0;
+
+document.addEventListener("paste", (e) => {
+  e.preventDefault();
+  var text = (e.originalEvent || e).clipboardData.getData("text/plain");
+  onPaste(true);
+  for (let i = 0; i < text.length; i++) {
+    onKeyEvent(text.charAt(i), false, false, false, false);
+  }
+  onPaste(false);
+});
+
+const go = new Go();
+WebAssembly.instantiateStreaming(fetch(wasmFilePath), go.importObject).then(
+  (result) => {
+    go.run(result.instance);
+  }
+);
diff --git a/web/termstyle.css b/web/termstyle.css
new file mode 100644
index 0000000..c5aad67
--- /dev/null
+++ b/web/termstyle.css
@@ -0,0 +1,121 @@
+* {
+  margin: 0;
+  padding: 0;
+  border: 0;
+  outline: 0;
+  font-family: "Menlo", "Andale Mono", "Courier New", Monospace;
+}
+
+#terminal-container {
+  text-align: center;
+}
+
+#terminal {
+  background-color: black;
+  color: green;
+  display: inline-block;
+
+  /* Copy paste! */
+  user-select: none;
+  -webkit-user-select: none;
+  -khtml-user-select: none;
+  -moz-user-select: none;
+  -ms-user-select: none;
+  --cursor-color: lightgrey;
+}
+
+/* Style attributes */
+
+.bold {
+  font-weight: bold;
+}
+
+.blink {
+  animation: blinker 1s step-start infinite;
+}
+
+.underline {
+  text-decoration: underline;
+}
+
+.dim {
+  filter: brightness(50);
+}
+
+.italic {
+  font-style: italic;
+}
+
+.strikethrough {
+  text-decoration: line-through;
+}
+
+.double_underline {
+  text-decoration: underline double;
+}
+
+.curly_underline {
+  text-decoration: underline wavy;
+}
+
+.dotted_underline {
+  text-decoration: underline dotted;
+}
+
+.dashed_underline {
+  text-decoration: underline dashed;
+}
+
+/* Cursor styles */
+
+.cursor-steady-block {
+  background-color: var(--cursor-color) !important;
+}
+.cursor-blinking-block {
+  animation: blinking-block 1s step-start infinite !important;
+}
+@keyframes blinking-block {
+  50% {
+    background-color: var(--cursor-color);
+  }
+}
+
+.cursor-steady-underline {
+  text-decoration: underline var(--cursor-color) !important;
+}
+.cursor-blinking-underline {
+  animation: blinking-underline 1s step-start infinite !important;
+}
+@keyframes blinking-underline {
+  50% {
+    text-decoration: underline var(--cursor-color);
+  }
+}
+
+.cursor-steady-bar {
+  margin-left: -2px;
+}
+.cursor-steady-bar:before {
+  content: " ";
+  width: 2px;
+  background-color: var(--cursor-color) !important;
+  display: inline-block;
+}
+.cursor-blinking-bar {
+  margin-left: -2px;
+}
+.cursor-blinking-bar:before {
+  content: " ";
+  width: 2px;
+  background-color: var(--cursor-color) !important;
+  display: inline-block;
+  animation: blinker 1s step-start infinite;
+}
+
+/* General animations */
+
+@keyframes blinker {
+  50% {
+    opacity: 0;
+  }
+}
diff --git a/web/wasm_exec.js b/web/wasm_exec.js
new file mode 100644
index 0000000..bc6f210
--- /dev/null
+++ b/web/wasm_exec.js
@@ -0,0 +1,561 @@
+// Copyright 2018 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+"use strict";
+
+(() => {
+	const enosys = () => {
+		const err = new Error("not implemented");
+		err.code = "ENOSYS";
+		return err;
+	};
+
+	if (!globalThis.fs) {
+		let outputBuf = "";
+		globalThis.fs = {
+			constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused
+			writeSync(fd, buf) {
+				outputBuf += decoder.decode(buf);
+				const nl = outputBuf.lastIndexOf("\n");
+				if (nl != -1) {
+					console.log(outputBuf.substring(0, nl));
+					outputBuf = outputBuf.substring(nl + 1);
+				}
+				return buf.length;
+			},
+			write(fd, buf, offset, length, position, callback) {
+				if (offset !== 0 || length !== buf.length || position !== null) {
+					callback(enosys());
+					return;
+				}
+				const n = this.writeSync(fd, buf);
+				callback(null, n);
+			},
+			chmod(path, mode, callback) { callback(enosys()); },
+			chown(path, uid, gid, callback) { callback(enosys()); },
+			close(fd, callback) { callback(enosys()); },
+			fchmod(fd, mode, callback) { callback(enosys()); },
+			fchown(fd, uid, gid, callback) { callback(enosys()); },
+			fstat(fd, callback) { callback(enosys()); },
+			fsync(fd, callback) { callback(null); },
+			ftruncate(fd, length, callback) { callback(enosys()); },
+			lchown(path, uid, gid, callback) { callback(enosys()); },
+			link(path, link, callback) { callback(enosys()); },
+			lstat(path, callback) { callback(enosys()); },
+			mkdir(path, perm, callback) { callback(enosys()); },
+			open(path, flags, mode, callback) { callback(enosys()); },
+			read(fd, buffer, offset, length, position, callback) { callback(enosys()); },
+			readdir(path, callback) { callback(enosys()); },
+			readlink(path, callback) { callback(enosys()); },
+			rename(from, to, callback) { callback(enosys()); },
+			rmdir(path, callback) { callback(enosys()); },
+			stat(path, callback) { callback(enosys()); },
+			symlink(path, link, callback) { callback(enosys()); },
+			truncate(path, length, callback) { callback(enosys()); },
+			unlink(path, callback) { callback(enosys()); },
+			utimes(path, atime, mtime, callback) { callback(enosys()); },
+		};
+	}
+
+	if (!globalThis.process) {
+		globalThis.process = {
+			getuid() { return -1; },
+			getgid() { return -1; },
+			geteuid() { return -1; },
+			getegid() { return -1; },
+			getgroups() { throw enosys(); },
+			pid: -1,
+			ppid: -1,
+			umask() { throw enosys(); },
+			cwd() { throw enosys(); },
+			chdir() { throw enosys(); },
+		}
+	}
+
+	if (!globalThis.crypto) {
+		throw new Error("globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)");
+	}
+
+	if (!globalThis.performance) {
+		throw new Error("globalThis.performance is not available, polyfill required (performance.now only)");
+	}
+
+	if (!globalThis.TextEncoder) {
+		throw new Error("globalThis.TextEncoder is not available, polyfill required");
+	}
+
+	if (!globalThis.TextDecoder) {
+		throw new Error("globalThis.TextDecoder is not available, polyfill required");
+	}
+
+	const encoder = new TextEncoder("utf-8");
+	const decoder = new TextDecoder("utf-8");
+
+	globalThis.Go = class {
+		constructor() {
+			this.argv = ["js"];
+			this.env = {};
+			this.exit = (code) => {
+				if (code !== 0) {
+					console.warn("exit code:", code);
+				}
+			};
+			this._exitPromise = new Promise((resolve) => {
+				this._resolveExitPromise = resolve;
+			});
+			this._pendingEvent = null;
+			this._scheduledTimeouts = new Map();
+			this._nextCallbackTimeoutID = 1;
+
+			const setInt64 = (addr, v) => {
+				this.mem.setUint32(addr + 0, v, true);
+				this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true);
+			}
+
+			const setInt32 = (addr, v) => {
+				this.mem.setUint32(addr + 0, v, true);
+			}
+
+			const getInt64 = (addr) => {
+				const low = this.mem.getUint32(addr + 0, true);
+				const high = this.mem.getInt32(addr + 4, true);
+				return low + high * 4294967296;
+			}
+
+			const loadValue = (addr) => {
+				const f = this.mem.getFloat64(addr, true);
+				if (f === 0) {
+					return undefined;
+				}
+				if (!isNaN(f)) {
+					return f;
+				}
+
+				const id = this.mem.getUint32(addr, true);
+				return this._values[id];
+			}
+
+			const storeValue = (addr, v) => {
+				const nanHead = 0x7FF80000;
+
+				if (typeof v === "number" && v !== 0) {
+					if (isNaN(v)) {
+						this.mem.setUint32(addr + 4, nanHead, true);
+						this.mem.setUint32(addr, 0, true);
+						return;
+					}
+					this.mem.setFloat64(addr, v, true);
+					return;
+				}
+
+				if (v === undefined) {
+					this.mem.setFloat64(addr, 0, true);
+					return;
+				}
+
+				let id = this._ids.get(v);
+				if (id === undefined) {
+					id = this._idPool.pop();
+					if (id === undefined) {
+						id = this._values.length;
+					}
+					this._values[id] = v;
+					this._goRefCounts[id] = 0;
+					this._ids.set(v, id);
+				}
+				this._goRefCounts[id]++;
+				let typeFlag = 0;
+				switch (typeof v) {
+					case "object":
+						if (v !== null) {
+							typeFlag = 1;
+						}
+						break;
+					case "string":
+						typeFlag = 2;
+						break;
+					case "symbol":
+						typeFlag = 3;
+						break;
+					case "function":
+						typeFlag = 4;
+						break;
+				}
+				this.mem.setUint32(addr + 4, nanHead | typeFlag, true);
+				this.mem.setUint32(addr, id, true);
+			}
+
+			const loadSlice = (addr) => {
+				const array = getInt64(addr + 0);
+				const len = getInt64(addr + 8);
+				return new Uint8Array(this._inst.exports.mem.buffer, array, len);
+			}
+
+			const loadSliceOfValues = (addr) => {
+				const array = getInt64(addr + 0);
+				const len = getInt64(addr + 8);
+				const a = new Array(len);
+				for (let i = 0; i < len; i++) {
+					a[i] = loadValue(array + i * 8);
+				}
+				return a;
+			}
+
+			const loadString = (addr) => {
+				const saddr = getInt64(addr + 0);
+				const len = getInt64(addr + 8);
+				return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len));
+			}
+
+			const timeOrigin = Date.now() - performance.now();
+			this.importObject = {
+				_gotest: {
+					add: (a, b) => a + b,
+				},
+				gojs: {
+					// Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters)
+					// may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported
+					// function. A goroutine can switch to a new stack if the current stack is too small (see morestack function).
+					// This changes the SP, thus we have to update the SP used by the imported function.
+
+					// func wasmExit(code int32)
+					"runtime.wasmExit": (sp) => {
+						sp >>>= 0;
+						const code = this.mem.getInt32(sp + 8, true);
+						this.exited = true;
+						delete this._inst;
+						delete this._values;
+						delete this._goRefCounts;
+						delete this._ids;
+						delete this._idPool;
+						this.exit(code);
+					},
+
+					// func wasmWrite(fd uintptr, p unsafe.Pointer, n int32)
+					"runtime.wasmWrite": (sp) => {
+						sp >>>= 0;
+						const fd = getInt64(sp + 8);
+						const p = getInt64(sp + 16);
+						const n = this.mem.getInt32(sp + 24, true);
+						fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n));
+					},
+
+					// func resetMemoryDataView()
+					"runtime.resetMemoryDataView": (sp) => {
+						sp >>>= 0;
+						this.mem = new DataView(this._inst.exports.mem.buffer);
+					},
+
+					// func nanotime1() int64
+					"runtime.nanotime1": (sp) => {
+						sp >>>= 0;
+						setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000);
+					},
+
+					// func walltime() (sec int64, nsec int32)
+					"runtime.walltime": (sp) => {
+						sp >>>= 0;
+						const msec = (new Date).getTime();
+						setInt64(sp + 8, msec / 1000);
+						this.mem.setInt32(sp + 16, (msec % 1000) * 1000000, true);
+					},
+
+					// func scheduleTimeoutEvent(delay int64) int32
+					"runtime.scheduleTimeoutEvent": (sp) => {
+						sp >>>= 0;
+						const id = this._nextCallbackTimeoutID;
+						this._nextCallbackTimeoutID++;
+						this._scheduledTimeouts.set(id, setTimeout(
+							() => {
+								this._resume();
+								while (this._scheduledTimeouts.has(id)) {
+									// for some reason Go failed to register the timeout event, log and try again
+									// (temporary workaround for https://github.com/golang/go/issues/28975)
+									console.warn("scheduleTimeoutEvent: missed timeout event");
+									this._resume();
+								}
+							},
+							getInt64(sp + 8),
+						));
+						this.mem.setInt32(sp + 16, id, true);
+					},
+
+					// func clearTimeoutEvent(id int32)
+					"runtime.clearTimeoutEvent": (sp) => {
+						sp >>>= 0;
+						const id = this.mem.getInt32(sp + 8, true);
+						clearTimeout(this._scheduledTimeouts.get(id));
+						this._scheduledTimeouts.delete(id);
+					},
+
+					// func getRandomData(r []byte)
+					"runtime.getRandomData": (sp) => {
+						sp >>>= 0;
+						crypto.getRandomValues(loadSlice(sp + 8));
+					},
+
+					// func finalizeRef(v ref)
+					"syscall/js.finalizeRef": (sp) => {
+						sp >>>= 0;
+						const id = this.mem.getUint32(sp + 8, true);
+						this._goRefCounts[id]--;
+						if (this._goRefCounts[id] === 0) {
+							const v = this._values[id];
+							this._values[id] = null;
+							this._ids.delete(v);
+							this._idPool.push(id);
+						}
+					},
+
+					// func stringVal(value string) ref
+					"syscall/js.stringVal": (sp) => {
+						sp >>>= 0;
+						storeValue(sp + 24, loadString(sp + 8));
+					},
+
+					// func valueGet(v ref, p string) ref
+					"syscall/js.valueGet": (sp) => {
+						sp >>>= 0;
+						const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16));
+						sp = this._inst.exports.getsp() >>> 0; // see comment above
+						storeValue(sp + 32, result);
+					},
+
+					// func valueSet(v ref, p string, x ref)
+					"syscall/js.valueSet": (sp) => {
+						sp >>>= 0;
+						Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32));
+					},
+
+					// func valueDelete(v ref, p string)
+					"syscall/js.valueDelete": (sp) => {
+						sp >>>= 0;
+						Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16));
+					},
+
+					// func valueIndex(v ref, i int) ref
+					"syscall/js.valueIndex": (sp) => {
+						sp >>>= 0;
+						storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16)));
+					},
+
+					// valueSetIndex(v ref, i int, x ref)
+					"syscall/js.valueSetIndex": (sp) => {
+						sp >>>= 0;
+						Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24));
+					},
+
+					// func valueCall(v ref, m string, args []ref) (ref, bool)
+					"syscall/js.valueCall": (sp) => {
+						sp >>>= 0;
+						try {
+							const v = loadValue(sp + 8);
+							const m = Reflect.get(v, loadString(sp + 16));
+							const args = loadSliceOfValues(sp + 32);
+							const result = Reflect.apply(m, v, args);
+							sp = this._inst.exports.getsp() >>> 0; // see comment above
+							storeValue(sp + 56, result);
+							this.mem.setUint8(sp + 64, 1);
+						} catch (err) {
+							sp = this._inst.exports.getsp() >>> 0; // see comment above
+							storeValue(sp + 56, err);
+							this.mem.setUint8(sp + 64, 0);
+						}
+					},
+
+					// func valueInvoke(v ref, args []ref) (ref, bool)
+					"syscall/js.valueInvoke": (sp) => {
+						sp >>>= 0;
+						try {
+							const v = loadValue(sp + 8);
+							const args = loadSliceOfValues(sp + 16);
+							const result = Reflect.apply(v, undefined, args);
+							sp = this._inst.exports.getsp() >>> 0; // see comment above
+							storeValue(sp + 40, result);
+							this.mem.setUint8(sp + 48, 1);
+						} catch (err) {
+							sp = this._inst.exports.getsp() >>> 0; // see comment above
+							storeValue(sp + 40, err);
+							this.mem.setUint8(sp + 48, 0);
+						}
+					},
+
+					// func valueNew(v ref, args []ref) (ref, bool)
+					"syscall/js.valueNew": (sp) => {
+						sp >>>= 0;
+						try {
+							const v = loadValue(sp + 8);
+							const args = loadSliceOfValues(sp + 16);
+							const result = Reflect.construct(v, args);
+							sp = this._inst.exports.getsp() >>> 0; // see comment above
+							storeValue(sp + 40, result);
+							this.mem.setUint8(sp + 48, 1);
+						} catch (err) {
+							sp = this._inst.exports.getsp() >>> 0; // see comment above
+							storeValue(sp + 40, err);
+							this.mem.setUint8(sp + 48, 0);
+						}
+					},
+
+					// func valueLength(v ref) int
+					"syscall/js.valueLength": (sp) => {
+						sp >>>= 0;
+						setInt64(sp + 16, parseInt(loadValue(sp + 8).length));
+					},
+
+					// valuePrepareString(v ref) (ref, int)
+					"syscall/js.valuePrepareString": (sp) => {
+						sp >>>= 0;
+						const str = encoder.encode(String(loadValue(sp + 8)));
+						storeValue(sp + 16, str);
+						setInt64(sp + 24, str.length);
+					},
+
+					// valueLoadString(v ref, b []byte)
+					"syscall/js.valueLoadString": (sp) => {
+						sp >>>= 0;
+						const str = loadValue(sp + 8);
+						loadSlice(sp + 16).set(str);
+					},
+
+					// func valueInstanceOf(v ref, t ref) bool
+					"syscall/js.valueInstanceOf": (sp) => {
+						sp >>>= 0;
+						this.mem.setUint8(sp + 24, (loadValue(sp + 8) instanceof loadValue(sp + 16)) ? 1 : 0);
+					},
+
+					// func copyBytesToGo(dst []byte, src ref) (int, bool)
+					"syscall/js.copyBytesToGo": (sp) => {
+						sp >>>= 0;
+						const dst = loadSlice(sp + 8);
+						const src = loadValue(sp + 32);
+						if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) {
+							this.mem.setUint8(sp + 48, 0);
+							return;
+						}
+						const toCopy = src.subarray(0, dst.length);
+						dst.set(toCopy);
+						setInt64(sp + 40, toCopy.length);
+						this.mem.setUint8(sp + 48, 1);
+					},
+
+					// func copyBytesToJS(dst ref, src []byte) (int, bool)
+					"syscall/js.copyBytesToJS": (sp) => {
+						sp >>>= 0;
+						const dst = loadValue(sp + 8);
+						const src = loadSlice(sp + 16);
+						if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) {
+							this.mem.setUint8(sp + 48, 0);
+							return;
+						}
+						const toCopy = src.subarray(0, dst.length);
+						dst.set(toCopy);
+						setInt64(sp + 40, toCopy.length);
+						this.mem.setUint8(sp + 48, 1);
+					},
+
+					"debug": (value) => {
+						console.log(value);
+					},
+				}
+			};
+		}
+
+		async run(instance) {
+			if (!(instance instanceof WebAssembly.Instance)) {
+				throw new Error("Go.run: WebAssembly.Instance expected");
+			}
+			this._inst = instance;
+			this.mem = new DataView(this._inst.exports.mem.buffer);
+			this._values = [ // JS values that Go currently has references to, indexed by reference id
+				NaN,
+				0,
+				null,
+				true,
+				false,
+				globalThis,
+				this,
+			];
+			this._goRefCounts = new Array(this._values.length).fill(Infinity); // number of references that Go has to a JS value, indexed by reference id
+			this._ids = new Map([ // mapping from JS values to reference ids
+				[0, 1],
+				[null, 2],
+				[true, 3],
+				[false, 4],
+				[globalThis, 5],
+				[this, 6],
+			]);
+			this._idPool = [];   // unused ids that have been garbage collected
+			this.exited = false; // whether the Go program has exited
+
+			// Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory.
+			let offset = 4096;
+
+			const strPtr = (str) => {
+				const ptr = offset;
+				const bytes = encoder.encode(str + "\0");
+				new Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes);
+				offset += bytes.length;
+				if (offset % 8 !== 0) {
+					offset += 8 - (offset % 8);
+				}
+				return ptr;
+			};
+
+			const argc = this.argv.length;
+
+			const argvPtrs = [];
+			this.argv.forEach((arg) => {
+				argvPtrs.push(strPtr(arg));
+			});
+			argvPtrs.push(0);
+
+			const keys = Object.keys(this.env).sort();
+			keys.forEach((key) => {
+				argvPtrs.push(strPtr(`${key}=${this.env[key]}`));
+			});
+			argvPtrs.push(0);
+
+			const argv = offset;
+			argvPtrs.forEach((ptr) => {
+				this.mem.setUint32(offset, ptr, true);
+				this.mem.setUint32(offset + 4, 0, true);
+				offset += 8;
+			});
+
+			// The linker guarantees global data starts from at least wasmMinDataAddr.
+			// Keep in sync with cmd/link/internal/ld/data.go:wasmMinDataAddr.
+			const wasmMinDataAddr = 4096 + 8192;
+			if (offset >= wasmMinDataAddr) {
+				throw new Error("total length of command line and environment variables exceeds limit");
+			}
+
+			this._inst.exports.run(argc, argv);
+			if (this.exited) {
+				this._resolveExitPromise();
+			}
+			await this._exitPromise;
+		}
+
+		_resume() {
+			if (this.exited) {
+				throw new Error("Go program has already exited");
+			}
+			this._inst.exports.resume();
+			if (this.exited) {
+				this._resolveExitPromise();
+			}
+		}
+
+		_makeFuncWrapper(id) {
+			const go = this;
+			return function () {
+				const event = { id: id, this: this, args: arguments };
+				go._pendingEvent = event;
+				go._resume();
+				return event.result;
+			};
+		}
+	}
+})();