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 0000000..96cefd4
Binary files /dev/null and b/web/beep.wav differ
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;
+ };
+ }
+ }
+})();