diff --git a/.gitignore b/.gitignore
index e154414..cfa5865 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,3 +14,4 @@ node_modules/
cpu*
heap*
profile*
+file_index.json
\ No newline at end of file
diff --git a/cmd/game_wasm/index.html b/cmd/game_wasm/index.html
new file mode 100644
index 0000000..81b8bf3
--- /dev/null
+++ b/cmd/game_wasm/index.html
@@ -0,0 +1,125 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/cmd/game_wasm/main.go b/cmd/game_wasm/main.go
new file mode 100644
index 0000000..f12f0ad
--- /dev/null
+++ b/cmd/game_wasm/main.go
@@ -0,0 +1,160 @@
+//go:build js
+// +build js
+
+package main
+
+import (
+ "bytes"
+ "fmt"
+ "github.com/BigJk/end_of_eden/system/gen"
+ "github.com/BigJk/end_of_eden/system/gen/faces"
+ "github.com/BigJk/end_of_eden/system/localization"
+ "github.com/BigJk/end_of_eden/system/settings"
+ "github.com/BigJk/end_of_eden/system/settings/browser"
+ "github.com/BigJk/end_of_eden/ui/menus/mainmenu"
+ uiset "github.com/BigJk/end_of_eden/ui/menus/settings"
+ "github.com/BigJk/end_of_eden/ui/menus/warning"
+ "github.com/BigJk/end_of_eden/ui/root"
+ "github.com/BigJk/end_of_eden/ui/style"
+ tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/lipgloss"
+ zone "github.com/lrstanley/bubblezone"
+ "github.com/muesli/termenv"
+ "log"
+ _ "net/http/pprof"
+ "os"
+ "strings"
+ "syscall/js"
+ "time"
+)
+
+type MinReadBuffer struct {
+ buf *bytes.Buffer
+}
+
+// For some reason bubbletea doesn't like a Reader that will return 0 bytes instead of blocking,
+// so we use this hacky workaround for now. As javascript is single threaded this should be fine
+// with regard to concurrency.
+func (b *MinReadBuffer) Read(p []byte) (n int, err error) {
+ for b.buf.Len() == 0 {
+ time.Sleep(100 * time.Millisecond)
+ }
+ return b.buf.Read(p)
+}
+
+func (b *MinReadBuffer) Write(p []byte) (n int, err error) {
+ return b.buf.Write(p)
+}
+
+// Creates the bubbletea program and registers the necessary functions in javascript
+func createTeaForJS(model tea.Model, option ...tea.ProgramOption) *tea.Program {
+ // Create buffers for input and output
+ fromJs := &MinReadBuffer{buf: bytes.NewBuffer(nil)}
+ fromGo := bytes.NewBuffer(nil)
+
+ prog := tea.NewProgram(model, append([]tea.ProgramOption{tea.WithInput(fromJs), tea.WithOutput(fromGo)}, option...)...)
+
+ // Register write function in WASM
+ js.Global().Set("bubbletea_write", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
+ fromJs.Write([]byte(args[0].String()))
+ return nil
+ }))
+
+ // Register read function in WASM
+ js.Global().Set("bubbletea_read", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
+ b := make([]byte, fromGo.Len())
+ _, _ = fromGo.Read(b)
+ fromGo.Reset()
+ return string(b)
+ }))
+
+ // Register resize function in WASM
+ js.Global().Set("bubbletea_resize", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
+ width := args[0].Int()
+ height := args[1].Int()
+ prog.Send(tea.WindowSizeMsg{Width: width, Height: height})
+ return nil
+ }))
+
+ return prog
+}
+
+var prog *tea.Program
+var loadStyle = lipgloss.NewStyle().Bold(true).Italic(true).Foreground(style.BaseGray)
+
+func main() {
+ lipgloss.SetColorProfile(termenv.TrueColor)
+
+ fmt.Println(lipgloss.NewStyle().Bold(true).Foreground(style.BaseRed).Render("End Of Eden"))
+
+ // Init settings
+ fmt.Println(loadStyle.Render("Initializing Settings. Please wait..."))
+ {
+ set := browser.Browser{}
+ set.SetDefault("audio", true)
+ set.SetDefault("volume", 1)
+ set.SetDefault("language", "en")
+ settings.SetSettings(set)
+
+ if err := settings.LoadSettings(); err != nil {
+ panic(err)
+ }
+ }
+ fmt.Println(loadStyle.Render("Done!"))
+
+ // Init generators
+ fmt.Println(loadStyle.Render("Initializing Proc-Gen. Please wait..."))
+ {
+ // Init face generator
+ if err := faces.InitGlobal("./assets/gen/faces"); err != nil {
+ panic(err)
+ }
+
+ // Init other gens
+ gen.InitGen()
+ }
+ fmt.Println(loadStyle.Render("Done!"))
+
+ // Init Localization
+ fmt.Println(loadStyle.Render("Initializing Localization. Please wait..."))
+ {
+ if err := localization.Global.AddFolder("./assets/locals"); err != nil {
+ panic(err)
+ }
+ localization.SetCurrent(settings.GetString("language"))
+ }
+ fmt.Println(loadStyle.Render("Done!"))
+
+ log.Println("=================================")
+ log.Println("= Started")
+ log.Println("=================================")
+
+ // Set window title
+ fmt.Println("\033]2;End of Eden\007")
+
+ uiSettings := []uiset.Value{
+ {Key: "audio", Name: "Audio", Description: "Enable or disable audio", Type: uiset.Bool, Val: settings.GetBool("audio"), Min: nil, Max: nil},
+ {Key: "volume", Name: "Volume", Description: "Change the volume", Type: uiset.Float, Val: settings.GetFloat("volume"), Min: 0.0, Max: 2.0},
+ {Key: "language", Name: "Language", Description: fmt.Sprintf("Change the language (supported: %s)", strings.Join(localization.Global.GetLocales(), ", ")), Type: uiset.String, Val: settings.GetString("language")},
+ }
+
+ // Setup game
+ var baseModel tea.Model
+ zones := zone.New()
+ baseModel = root.New(zones, mainmenu.NewModel(zones, settings.GetGlobal(), uiSettings, func(values []uiset.Value) error {
+ for i := range values {
+ settings.Set(values[i].Key, values[i].Val)
+ }
+ localization.SetCurrent(settings.GetString("language"))
+ return settings.SaveSettings()
+ }))
+
+ baseModel = baseModel.(root.Model).PushModel(warning.New(nil, style.RedText.Render("Warning!")+"\n\nThe Browser version is still very experimental. Loading times can be long. Mouse support is clunky. For the best experience, please use the Desktop version!\n\n"+style.GrayTextDarker.Render("Press ESC to continue")))
+
+ // Run game
+ prog = createTeaForJS(baseModel, tea.WithAltScreen(), tea.WithMouseAllMotion(), tea.WithANSICompressor())
+ if _, err := prog.Run(); err != nil {
+ fmt.Printf("Alas, there's been an error: %v", err)
+ os.Exit(1)
+ }
+}
diff --git a/cmd/game_wasm/wasm_exec.js b/cmd/game_wasm/wasm_exec.js
new file mode 100644
index 0000000..e6c8921
--- /dev/null
+++ b/cmd/game_wasm/wasm_exec.js
@@ -0,0 +1,554 @@
+// 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 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 = {
+ go: {
+ // 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) + 1, // setTimeout has been seen to fire up to 1 millisecond early
+ ));
+ 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;
+ };
+ }
+ }
+})();
diff --git a/cmd/game_win/main.go b/cmd/game_win/main.go
index d14cf77..6137e6c 100644
--- a/cmd/game_win/main.go
+++ b/cmd/game_win/main.go
@@ -7,6 +7,7 @@ import (
teadapter "github.com/BigJk/crt/bubbletea"
"github.com/BigJk/crt/shader"
"github.com/BigJk/end_of_eden/cmd/testargs"
+ "github.com/BigJk/end_of_eden/internal/fs"
"github.com/BigJk/end_of_eden/system/audio"
"github.com/BigJk/end_of_eden/system/gen"
"github.com/BigJk/end_of_eden/system/gen/faces"
@@ -183,7 +184,7 @@ func main() {
// Setup grain shader
if settings.GetBool("grain") {
- res, _ := os.ReadFile("./assets/shader/grain.go")
+ res, _ := fs.ReadFile("./assets/shader/grain.go")
grain, err := ebiten.NewShader(res)
if err != nil {
diff --git a/game/lua.go b/game/lua.go
index ce6d950..357ad09 100644
--- a/game/lua.go
+++ b/game/lua.go
@@ -1,12 +1,12 @@
package game
import (
+ "github.com/BigJk/end_of_eden/internal/fs"
"github.com/BigJk/end_of_eden/internal/lua/ludoc"
luhelp2 "github.com/BigJk/end_of_eden/internal/lua/luhelp"
"github.com/BigJk/end_of_eden/system/audio"
"github.com/BigJk/end_of_eden/system/gen/faces"
"github.com/BigJk/end_of_eden/system/localization"
- "io/fs"
"path/filepath"
"strings"
@@ -24,14 +24,19 @@ func SessionAdapter(session *Session) (*lua.LState, *ludoc.Docs) {
mapper := luhelp2.NewMapper(l)
- _ = filepath.Walk("./assets/scripts/libs", func(path string, info fs.FileInfo, _ error) error {
- if info != nil && info.IsDir() || !strings.HasSuffix(path, ".lua") {
+ _ = fs.Walk("./assets/scripts/libs", func(path string, isDir bool) error {
+ if isDir || !strings.HasSuffix(path, ".lua") {
return nil
}
name := strings.Split(filepath.Base(path), ".")[0]
- mod, err := l.LoadFile(path)
+ luaBytes, err := fs.ReadFile(path)
+ if err != nil {
+ return err
+ }
+
+ mod, err := l.LoadString(string(luaBytes))
if err != nil {
session.log.Println("Can't LoadFile module:", path)
return nil
diff --git a/game/mod.go b/game/mod.go
index 0afe0fc..0c8c43f 100644
--- a/game/mod.go
+++ b/game/mod.go
@@ -2,7 +2,7 @@ package game
import (
"encoding/json"
- "os"
+ "github.com/BigJk/end_of_eden/internal/fs"
"path/filepath"
)
@@ -15,7 +15,7 @@ type Mod struct {
}
func ModDescription(folder string) (Mod, error) {
- data, err := os.ReadFile(filepath.Join(folder, "/meta.json"))
+ data, err := fs.ReadFile(filepath.Join(folder, "/meta.json"))
if err != nil {
return Mod{}, err
}
diff --git a/game/resources.go b/game/resources.go
index db86f3e..d99eea4 100644
--- a/game/resources.go
+++ b/game/resources.go
@@ -2,13 +2,12 @@ package game
import (
"fmt"
+ "github.com/BigJk/end_of_eden/internal/fs"
"github.com/BigJk/end_of_eden/internal/lua/ludoc"
luhelp2 "github.com/BigJk/end_of_eden/internal/lua/luhelp"
"github.com/samber/lo"
lua "github.com/yuin/gopher-lua"
- "io/fs"
"log"
- "path/filepath"
"strings"
)
@@ -67,18 +66,20 @@ func NewResourcesManager(state *lua.LState, docs *ludoc.Docs, logger *log.Logger
man.defineDocs(docs)
// Load all local scripts
- _ = filepath.Walk("./assets/scripts", func(path string, info fs.FileInfo, err error) error {
+ _ = fs.Walk("./assets/scripts", func(path string, isDir bool) error {
// Don't load libs
if strings.Contains(path, "scripts/libs") {
return nil
}
- if err != nil {
- return nil
- }
+ if !isDir && strings.HasSuffix(path, ".lua") {
+ luaBytes, err := fs.ReadFile(path)
+ if err != nil {
+ // TODO: error handling
+ panic(err)
+ }
- if !info.IsDir() && strings.HasSuffix(path, ".lua") {
- if err := man.luaState.DoFile(path); err != nil {
+ if err := man.luaState.DoString(string(luaBytes)); err != nil {
// TODO: error handling
panic(err)
}
diff --git a/game/session.go b/game/session.go
index b6c876f..856a8fd 100644
--- a/game/session.go
+++ b/game/session.go
@@ -6,6 +6,7 @@ import (
"encoding/gob"
"errors"
"fmt"
+ "github.com/BigJk/end_of_eden/internal/fs"
"github.com/BigJk/end_of_eden/internal/lua/ludoc"
"github.com/BigJk/end_of_eden/system/gen"
"github.com/BigJk/end_of_eden/system/gen/faces"
@@ -15,10 +16,8 @@ import (
lua "github.com/yuin/gopher-lua"
"golang.org/x/exp/slices"
"io"
- "io/fs"
"log"
"math/rand"
- "os"
"oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/d2layouts/d2dagrelayout"
"oss.terrastruct.com/d2/d2lib"
@@ -330,18 +329,20 @@ func (s *Session) loadMods(mods []string) {
log.Println("Loading mod:", mod.Name)
}
- _ = filepath.Walk(filepath.Join("./mods", mods[i]), func(path string, info fs.FileInfo, err error) error {
+ _ = fs.Walk(filepath.Join("./mods", mods[i]), func(path string, isDir bool) error {
// If we find a locals folder we add it to the localization
- if info.IsDir() && info.Name() == "locals" {
+ if isDir && filepath.Base(path) == "locals" {
_ = localization.Global.AddFolder(path)
}
- if err != nil {
- return nil
- }
+ if isDir && strings.HasSuffix(path, ".lua") {
+ luaBytes, err := fs.ReadFile(path)
+ if err != nil {
+ // TODO: error handling
+ panic(err)
+ }
- if !info.IsDir() && strings.HasSuffix(path, ".lua") {
- if err := s.luaState.DoFile(path); err != nil {
+ if err := s.luaState.DoString(string(luaBytes)); err != nil {
s.logLuaError("ModLoader", "", err)
}
}
@@ -466,7 +467,7 @@ func (s *Session) SetupFight() {
if err != nil {
s.log.Println("Error saving file:", save)
} else {
- if err := os.WriteFile("./session.save", save, 0666); err != nil {
+ if err := fs.WriteFile("./session.save", save); err != nil {
s.log.Println("Error saving file:", save)
}
}
diff --git a/go.mod b/go.mod
index d3857f6..f03809d 100644
--- a/go.mod
+++ b/go.mod
@@ -2,6 +2,12 @@ module github.com/BigJk/end_of_eden
go 1.20
+replace github.com/containerd/console => github.com/containerd/console v1.0.4-0.20230706203907-8f6c4e4faef5
+
+replace github.com/atotto/clipboard => github.com/BigJk/clipboard v0.0.0-20230514080810-a7e7bd3670a5
+
+replace github.com/charmbracelet/bubbletea => github.com/BigJk/bubbletea v0.0.0-20231222122351-25c0e0b15c34
+
require (
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161
github.com/BigJk/crt v0.0.13
@@ -36,6 +42,7 @@ require (
github.com/yuin/gopher-lua v1.1.0
golang.org/x/exp v0.0.0-20221126150942-6ab00d035af9
golang.org/x/sys v0.13.0
+ gopkg.in/yaml.v3 v3.0.1
oss.terrastruct.com/d2 v0.4.1
)
@@ -109,6 +116,5 @@ require (
gonum.org/v1/plot v0.12.0 // indirect
google.golang.org/genproto v0.0.0-20230222225845-10f96fb3dbec // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
- gopkg.in/yaml.v3 v3.0.1 // indirect
oss.terrastruct.com/util-go v0.0.0-20230320053557-dcb5aac7d972 // indirect
)
diff --git a/go.sum b/go.sum
index 925abd5..9bfe9b4 100644
--- a/go.sum
+++ b/go.sum
@@ -47,6 +47,10 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7
git.sr.ht/~sbinet/gg v0.3.1 h1:LNhjNn8DerC8f9DHLz6lS0YYul/b602DUxDgGkd/Aik=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
+github.com/BigJk/bubbletea v0.0.0-20231222122351-25c0e0b15c34 h1:I3ZTYobM/4vLCKnH6KgN1HX1EwnsaD4lbnOoc4YNhFo=
+github.com/BigJk/bubbletea v0.0.0-20231222122351-25c0e0b15c34/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg=
+github.com/BigJk/clipboard v0.0.0-20230514080810-a7e7bd3670a5 h1:bJrh/4V+8y6u05623Zja5vJD7RLM0Km5CdUrMJnPTDY=
+github.com/BigJk/clipboard v0.0.0-20230514080810-a7e7bd3670a5/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/BigJk/crt v0.0.13 h1:qZRw2tuJCuKsk3lC93Vi+Guib//Rs1m/rcGUR/OCHFE=
github.com/BigJk/crt v0.0.13/go.mod h1:0jGrs6QH6A7zDJz91HfLCXG8yKR12sqOUPq8LrOoDbg=
github.com/BigJk/imeji v0.0.2 h1:h4cKLWkVzs+IVs/27DFTYGsGuA1OqQxrYDNtvlojypU=
@@ -70,8 +74,6 @@ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuW
github.com/anthonynsimon/bild v0.13.0 h1:mN3tMaNds1wBWi1BrJq0ipDBhpkooYfu7ZFSMhXt1C8=
github.com/anthonynsimon/bild v0.13.0/go.mod h1:tpzzp0aYkAsMi1zmfhimaDyX1xjn2OUc1AJZK/TF0AE=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
-github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
-github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymanbagabas/go-osc52 v1.0.3/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
@@ -83,9 +85,6 @@ github.com/caarlos0/sshmarshal v0.1.0/go.mod h1:7Pd/0mmq9x/JCzKauogNjSQEhivBclCQ
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/charmbracelet/bubbles v0.15.0 h1:c5vZ3woHV5W2b8YZI1q7v4ZNQaPetfHuoHzx+56Z6TI=
github.com/charmbracelet/bubbles v0.15.0/go.mod h1:Y7gSFbBzlMpUDR/XM9MhZI374Q+1p1kluf1uLl8iK74=
-github.com/charmbracelet/bubbletea v0.23.1/go.mod h1:JAfGK/3/pPKHTnAS8JIE2u9f61BjWTQY57RbT25aMXU=
-github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM=
-github.com/charmbracelet/bubbletea v0.25.0/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg=
github.com/charmbracelet/glamour v0.6.0 h1:wi8fse3Y7nfcabbbDuwolqTqMQPMnVPeZhDM273bISc=
github.com/charmbracelet/glamour v0.6.0/go.mod h1:taqWV4swIMMbWALc0m7AfE9JkPSU8om2538k9ITBxOc=
github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
@@ -108,9 +107,8 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
-github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
-github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY=
-github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
+github.com/containerd/console v1.0.4-0.20230706203907-8f6c4e4faef5 h1:Ig+OPkE3XQrrl+SKsOqAjlkrBN/zrr+Qpw7rCuDjRCE=
+github.com/containerd/console v1.0.4-0.20230706203907-8f6c4e4faef5/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
@@ -298,6 +296,7 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
@@ -582,7 +581,6 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -603,12 +601,14 @@ golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
+golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -619,6 +619,7 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
diff --git a/internal/fs/fileinfo.go b/internal/fs/fileinfo.go
new file mode 100644
index 0000000..5660800
--- /dev/null
+++ b/internal/fs/fileinfo.go
@@ -0,0 +1,16 @@
+package fs
+
+import "path/filepath"
+
+type FileInfo struct {
+ Path string `json:"path"`
+ IsFile bool `json:"IsFile"`
+}
+
+func (fi FileInfo) Name() string {
+ return filepath.Base(fi.Path)
+}
+
+func (fi FileInfo) IsDir() bool {
+ return !fi.IsFile
+}
diff --git a/internal/fs/fs.go b/internal/fs/fs.go
new file mode 100644
index 0000000..1bf5cc5
--- /dev/null
+++ b/internal/fs/fs.go
@@ -0,0 +1,45 @@
+//go:build !js
+// +build !js
+
+package fs
+
+import (
+ "github.com/samber/lo"
+ "io"
+ "os"
+ "path/filepath"
+)
+
+func ReadDir(path string) ([]FileInfo, error) {
+ dir, err := os.ReadDir(path)
+ if err != nil {
+ return nil, err
+ }
+ return lo.Map(dir, func(f os.DirEntry, i int) FileInfo {
+ return FileInfo{
+ Path: filepath.Join(path, f.Name()),
+ IsFile: !f.IsDir(),
+ }
+ }), nil
+}
+
+func OpenFile(name string, flag int, perm os.FileMode) (io.WriteCloser, error) {
+ return os.OpenFile(name, flag, perm)
+}
+
+func ReadFile(path string) ([]byte, error) {
+ return os.ReadFile(path)
+}
+
+func WriteFile(path string, data []byte) error {
+ return os.WriteFile(path, data, 0644)
+}
+
+func Walk(root string, walkFn func(path string, isDir bool) error) error {
+ return filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
+ if err != nil {
+ return err
+ }
+ return walkFn(path, info.IsDir())
+ })
+}
diff --git a/internal/fs/fs_js.go b/internal/fs/fs_js.go
new file mode 100644
index 0000000..73302d9
--- /dev/null
+++ b/internal/fs/fs_js.go
@@ -0,0 +1,115 @@
+//go:build js
+// +build js
+
+package fs
+
+import (
+ "encoding/base64"
+ "encoding/json"
+ "fmt"
+ "github.com/samber/lo"
+ "io"
+ "net/http"
+ "os"
+ "path/filepath"
+ "sort"
+ "strings"
+ "syscall/js"
+)
+
+type noOpWriteCloser struct{}
+
+func (noOpWriteCloser) Write(p []byte) (int, error) {
+ return len(p), nil
+}
+
+func (noOpWriteCloser) Close() error {
+ return nil
+}
+
+var fileIndex = make(map[string]FileInfo)
+
+func init() {
+ data, err := ReadFile("/assets/file_index.json")
+ if err != nil {
+ panic(err)
+ }
+
+ var fis []FileInfo
+ if err := json.Unmarshal(data, &fis); err != nil {
+ panic(err)
+ }
+ for _, fi := range fis {
+ fi.Path = filepath.Clean(fi.Path)
+ fileIndex[fi.Path] = fi
+ }
+
+ js.Global().Set("fsDump", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
+ for _, fi := range fis {
+ fmt.Println(fi.Path)
+ }
+ return nil
+ }))
+}
+
+func ReadDir(path string) ([]FileInfo, error) {
+ cleanPath := filepath.Clean(path)
+ var fis []FileInfo
+ for indexPath := range fileIndex {
+ if strings.HasPrefix(indexPath, cleanPath) {
+ fis = append(fis, fileIndex[path])
+ }
+ }
+ return fis, nil
+}
+
+func ReadFile(path string) ([]byte, error) {
+ // Check for temp file
+ jsRes := js.Global().Call("fsRead", path)
+ if !jsRes.IsNull() && !jsRes.IsUndefined() {
+ return base64.StdEncoding.DecodeString(jsRes.String())
+ }
+
+ // Check for asset
+ res, err := http.Get(path)
+ if err != nil {
+ return nil, err
+ }
+ if res.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("could not load file %s: %s", path, res.Status)
+ }
+
+ body, err := io.ReadAll(res.Body)
+ if err != nil {
+ return nil, err
+ }
+ return body, nil
+}
+
+func OpenFile(name string, flag int, perm os.FileMode) (io.WriteCloser, error) {
+ // TODO: Implement
+ return noOpWriteCloser{}, nil
+}
+
+func WriteFile(path string, data []byte) error {
+ // TODO: error handling
+ _ = js.Global().Call("fsWrite", path, base64.StdEncoding.EncodeToString(data))
+ return nil
+}
+
+func Walk(root string, walkFn func(path string, isDir bool) error) error {
+ keys := lo.Keys(fileIndex)
+ sort.Strings(keys)
+
+ cleanPath := filepath.Clean(root)
+ for _, path := range keys {
+ if !strings.HasPrefix(path, cleanPath) {
+ continue
+ }
+
+ if err := walkFn(path, fileIndex[path].IsDir()); err != nil {
+ return err
+ }
+ }
+ return nil
+}
diff --git a/internal/misc/build_index.sh b/internal/misc/build_index.sh
new file mode 100755
index 0000000..44112da
--- /dev/null
+++ b/internal/misc/build_index.sh
@@ -0,0 +1,43 @@
+#!/bin/bash
+
+# Check if the argument is provided
+if [ -z "$1" ]; then
+ echo "Usage: $0 "
+ exit 1
+fi
+
+folder_path="$1"
+
+# Initialize an empty array
+file_index=()
+
+# Function to walk through the directory recursively
+walk() {
+ local directory="$1"
+ for file in "$directory"/*; do
+ if [ -d "$file" ]; then
+ # If it's a directory, add to the array and recursively call walk function
+ file_index+=(" {\"path\": \"$file\", \"isFile\": false}")
+ walk "$file"
+ elif [ -f "$file" ]; then
+ # If it's a file, add to the array
+ file_index+=(" {\"path\": \"$file\", \"isFile\": true}")
+ fi
+ done
+}
+
+# Start walking through the directory
+walk "$folder_path"
+
+# Create JSON file with the content of the array in the same folder
+{
+ printf '[\n'
+ for ((i = 0; i < ${#file_index[@]}; i++)); do
+ printf '%s' "${file_index[i]}"
+ if [ $i -ne $(( ${#file_index[@]} - 1 )) ]; then
+ printf ','
+ fi
+ printf '\n'
+ done
+ printf ']\n'
+} > "$folder_path/file_index.json"
diff --git a/system/audio/audio.go b/system/audio/audio.go
index 8d9626d..34174da 100644
--- a/system/audio/audio.go
+++ b/system/audio/audio.go
@@ -1,12 +1,13 @@
-//go:build !no_audio
-// +build !no_audio
+//go:build !no_audio && !js
+// +build !no_audio,!js
// Package audio handles all audio playback. It uses the beep library to play audio files.
package audio
import (
+ "github.com/BigJk/end_of_eden/internal/fs"
"github.com/BigJk/end_of_eden/system/settings"
- "io/fs"
+
"log"
"os"
"path/filepath"
@@ -41,11 +42,7 @@ func InitAudio() {
go func() {
wg := &sync.WaitGroup{}
- _ = filepath.Walk("./assets/audio", func(path string, info fs.FileInfo, err error) error {
- if err != nil {
- return nil
- }
-
+ _ = fs.Walk("./assets/audio", func(path string, isDir bool) error {
wg.Add(1)
go func() {
defer wg.Done()
@@ -53,7 +50,7 @@ func InitAudio() {
var streamer beep.StreamSeekCloser
var format beep.Format
- if !info.IsDir() {
+ if !isDir {
if strings.HasSuffix(path, ".mp3") {
f, err := os.Open(path)
if err != nil {
diff --git a/system/audio/audio_js.go b/system/audio/audio_js.go
new file mode 100644
index 0000000..fba3a2d
--- /dev/null
+++ b/system/audio/audio_js.go
@@ -0,0 +1,40 @@
+//go:build js
+// +build js
+
+// Package audio handles all audio playback. It uses the beep library to play audio files.
+package audio
+
+import (
+ "github.com/BigJk/end_of_eden/internal/fs"
+ "github.com/BigJk/end_of_eden/system/settings"
+ "path/filepath"
+ "strings"
+ "syscall/js"
+)
+
+// InitAudio initializes the audio system. Loads all audio files from the assets/audio folder.
+func InitAudio() {}
+
+// Play plays a sound effect. If the sound effect is not loaded, nothing will happen.
+func Play(key string, volumeModifier ...float64) {
+ fs.Walk("./assets/audio", func(path string, isDir bool) error {
+ if !isDir && strings.HasPrefix(filepath.Base(path), key) {
+ js.Global().Call("playSound", path)
+ }
+ return nil
+ })
+}
+
+// PlayMusic plays a music track. If the music track is not loaded, nothing will happen.
+func PlayMusic(key string) {
+ if settings.GetFloat("volume") == 0 {
+ return
+ }
+
+ fs.Walk("./assets/audio", func(path string, isDir bool) error {
+ if !isDir && strings.HasPrefix(filepath.Base(path), key) {
+ js.Global().Call("loopMusic", path)
+ }
+ return nil
+ })
+}
diff --git a/system/audio/empty.go b/system/audio/empty.go
index 403c10c..9c71af3 100644
--- a/system/audio/empty.go
+++ b/system/audio/empty.go
@@ -1,5 +1,5 @@
-//go:build !no_audio
-// +build !no_audio
+//go:build !no_audio && !js
+// +build !no_audio,!js
package audio
diff --git a/system/gen/faces/faces.go b/system/gen/faces/faces.go
index 6c782d8..26f65af 100644
--- a/system/gen/faces/faces.go
+++ b/system/gen/faces/faces.go
@@ -2,9 +2,9 @@ package faces
import (
"fmt"
+ "github.com/BigJk/end_of_eden/internal/fs"
"github.com/samber/lo"
"math/rand"
- "os"
"path/filepath"
"strconv"
"strings"
@@ -63,7 +63,7 @@ func New(dataFolder string) (*FaceGenerator, error) {
data: map[int][][]string{},
}
for i := 0; i < 7; i++ {
- bytes, err := os.ReadFile(filepath.Join(dataFolder, fmt.Sprintf("/Face%d.txt", i)))
+ bytes, err := fs.ReadFile(filepath.Join(dataFolder, fmt.Sprintf("/Face%d.txt", i)))
if err != nil {
return nil, err
}
diff --git a/system/gen/gen.go b/system/gen/gen.go
index fb5af58..b12a4d8 100644
--- a/system/gen/gen.go
+++ b/system/gen/gen.go
@@ -1,9 +1,9 @@
package gen
import (
+ "github.com/BigJk/end_of_eden/internal/fs"
"log"
"math/rand"
- "os"
"strings"
)
@@ -13,14 +13,14 @@ var data = map[string][]string{}
// The data is stored in a map with the type as key and a slice of strings, which are the lines
// of the file, as value.
func InitGen() {
- files, err := os.ReadDir("./assets/gen")
+ files, err := fs.ReadDir("./assets/gen")
if err != nil {
panic(err)
}
for _, file := range files {
if !file.IsDir() && strings.HasSuffix(file.Name(), ".txt") {
- bytes, err := os.ReadFile("./assets/gen/" + file.Name())
+ bytes, err := fs.ReadFile("./assets/gen/" + file.Name())
if err != nil {
log.Println("Error reading file:", err.Error())
}
diff --git a/system/image/cache.go b/system/image/cache.go
index 0464485..b52d93a 100644
--- a/system/image/cache.go
+++ b/system/image/cache.go
@@ -5,8 +5,8 @@ import (
"encoding/base64"
"errors"
"fmt"
+ "github.com/BigJk/end_of_eden/internal/fs"
"github.com/samber/lo"
- "io/ioutil"
"os"
"path/filepath"
"strings"
@@ -20,7 +20,7 @@ func init() {
// search paths, an error is returned.
func hashFile(path string) (string, error) {
for i := range searchPaths {
- data, err := ioutil.ReadFile(filepath.Join(searchPaths[i], path))
+ data, err := fs.ReadFile(filepath.Join(searchPaths[i], path))
if err != nil {
continue
}
@@ -43,7 +43,7 @@ func hash(name string, options Options) (string, error) {
// getCache returns the cached data for the given hash.
func getCache(hash string) (interface{}, error) {
path := filepath.Join("./cache", hash)
- data, err := ioutil.ReadFile(path)
+ data, err := fs.ReadFile(path)
if err != nil {
return nil, err
}
@@ -75,5 +75,5 @@ func setCache(hash string, data interface{}) error {
}
path := filepath.Join("./cache", hash)
- return ioutil.WriteFile(path, []byte(strings.Join(lines, "\n")), 0644)
+ return fs.WriteFile(path, []byte(strings.Join(lines, "\n")))
}
diff --git a/system/image/image.go b/system/image/image.go
index 9278e82..3cd1022 100644
--- a/system/image/image.go
+++ b/system/image/image.go
@@ -3,7 +3,9 @@
package image
import (
+ "bytes"
"errors"
+ "github.com/BigJk/end_of_eden/internal/fs"
"github.com/BigJk/imeji"
"github.com/BigJk/imeji/charmaps"
"github.com/charmbracelet/log"
@@ -11,8 +13,11 @@ import (
"image"
"image/draw"
"image/gif"
+ _ "image/jpeg"
+ _ "image/png"
"os"
"path/filepath"
+ "runtime"
"strings"
)
@@ -44,6 +49,11 @@ func buildOption(options ...Option) (Options, []imeji.Option) {
data.tag += os.Getenv("EOE_IMG_PATTERN")
}
+ if runtime.GOOS == "js" {
+ imejiOptions = append(imejiOptions, imeji.WithTrueColor())
+ data.tag += "truecolor"
+ }
+
switch termenv.DefaultOutput().Profile {
case termenv.TrueColor:
imejiOptions = append(imejiOptions, imeji.WithTrueColor())
@@ -82,7 +92,17 @@ func Fetch(name string, options ...Option) (string, error) {
}
for i := range searchPaths {
- res, err := imeji.FileString(filepath.Join(searchPaths[i], name), imejiOptions...)
+ file, err := fs.ReadFile(filepath.Join(searchPaths[i], name))
+ if err != nil {
+ continue
+ }
+
+ image, _, err := image.Decode(bytes.NewBuffer(file))
+ if err != nil {
+ continue
+ }
+
+ res, err := imeji.ImageString(image, imejiOptions...)
if err == nil {
if err := setCache(hash, res); err != nil {
log.Warn("could not cache image: %s", err)
@@ -113,13 +133,12 @@ func FetchAnimation(name string, options ...Option) ([]string, error) {
var frames []string
for i := range searchPaths {
- f, err := os.Open(filepath.Join(searchPaths[i], name))
+ fileBytes, err := fs.ReadFile(filepath.Join(searchPaths[i], name))
if err != nil {
continue
}
- defer f.Close()
- g, err := gif.DecodeAll(f)
+ g, err := gif.DecodeAll(bytes.NewBuffer(fileBytes))
if err != nil {
continue
}
diff --git a/system/localization/localization.go b/system/localization/localization.go
index bdfa88c..7ce1313 100644
--- a/system/localization/localization.go
+++ b/system/localization/localization.go
@@ -2,8 +2,8 @@ package localization
import (
"fmt"
+ "github.com/BigJk/end_of_eden/internal/fs"
"gopkg.in/yaml.v3"
- "os"
"path/filepath"
"strings"
)
@@ -44,8 +44,8 @@ func (l *Localization) Add(locale string, translations map[string]string) {
// AddFolder adds all locales from the given folder. Will walk through all
// sub-folders and add all .yaml and .yml files.
func (l *Localization) AddFolder(folder string) error {
- return filepath.Walk(folder, func(path string, info os.FileInfo, err error) error {
- if info.IsDir() {
+ return fs.Walk(folder, func(path string, isDir bool) error {
+ if isDir {
return nil
}
@@ -61,7 +61,7 @@ func (l *Localization) AddFolder(folder string) error {
func (l *Localization) AddFile(file string) error {
var parsed map[string]map[string]any
- data, err := os.ReadFile(file)
+ data, err := fs.ReadFile(file)
if err != nil {
return err
}
diff --git a/system/settings/browser/browser.go b/system/settings/browser/browser.go
new file mode 100644
index 0000000..07854c8
--- /dev/null
+++ b/system/settings/browser/browser.go
@@ -0,0 +1,74 @@
+//go:build js
+// +build js
+
+package browser
+
+import (
+ "syscall/js"
+)
+
+type Browser struct{}
+
+func (b Browser) LoadSettings() error {
+ return nil
+}
+
+func (b Browser) SaveSettings() error {
+ return nil
+}
+
+func (b Browser) Get(key string) any {
+ return js.Global().Get("settings").Call("get", key)
+}
+
+func (b Browser) GetString(key string) string {
+ return js.Global().Get("settings").Call("getString", key).String()
+}
+
+func (b Browser) GetStrings(key string) []string {
+ val := js.Global().Get("settings").Call("getStrings", key)
+ if val.Type() == js.TypeObject {
+ var result []string
+ for i := 0; i < val.Length(); i++ {
+ result = append(result, val.Index(i).String())
+ }
+ return result
+ }
+ return nil
+}
+
+func (b Browser) GetInt(key string) int {
+ return js.Global().Get("settings").Call("getInt", key).Int()
+}
+
+func (b Browser) GetFloat(key string) float64 {
+ return js.Global().Get("settings").Call("getFloat", key).Float()
+}
+
+func (b Browser) GetBool(key string) bool {
+ val := js.Global().Get("settings").Call("getBool", key)
+ if val.Type() == js.TypeBoolean {
+ return val.Bool()
+ }
+ return false
+}
+
+func (b Browser) Set(key string, value any) {
+ js.Global().Get("settings").Call("set", key, value)
+}
+
+func (b Browser) GetKeys() []string {
+ val := js.Global().Get("settings").Call("getKeys")
+ if val.Type() == js.TypeObject {
+ var result []string
+ for i := 0; i < val.Length(); i++ {
+ result = append(result, val.Index(i).String())
+ }
+ return result
+ }
+ return nil
+}
+
+func (b Browser) SetDefault(key string, value any) {
+ js.Global().Get("settings").Call("setDefault", key, value)
+}
diff --git a/ui/menus/eventview/eventview.go b/ui/menus/eventview/eventview.go
index 8efb0fe..4031105 100644
--- a/ui/menus/eventview/eventview.go
+++ b/ui/menus/eventview/eventview.go
@@ -3,6 +3,7 @@ package eventview
import (
"fmt"
"github.com/BigJk/end_of_eden/game"
+ "github.com/BigJk/end_of_eden/internal/fs"
"github.com/BigJk/end_of_eden/system/audio"
image2 "github.com/BigJk/end_of_eden/system/image"
"github.com/BigJk/end_of_eden/ui"
@@ -17,7 +18,6 @@ import (
"github.com/muesli/reflow/wordwrap"
"github.com/samber/lo"
"math"
- "os"
"strconv"
"strings"
)
@@ -182,7 +182,7 @@ func (m Model) eventUpdateContent() Model {
var res string
if strings.HasSuffix(file, ".ans") {
- ansRes, err := os.ReadFile("./assets/images/" + file)
+ ansRes, err := fs.ReadFile("./assets/images/" + file)
if err != nil {
continue
}
diff --git a/ui/menus/mainmenu/mainmenu.go b/ui/menus/mainmenu/mainmenu.go
index 9d3f649..dbc6d47 100644
--- a/ui/menus/mainmenu/mainmenu.go
+++ b/ui/menus/mainmenu/mainmenu.go
@@ -3,6 +3,7 @@ package mainmenu
import (
"fmt"
"github.com/BigJk/end_of_eden/game"
+ "github.com/BigJk/end_of_eden/internal/fs"
"github.com/BigJk/end_of_eden/system/audio"
image2 "github.com/BigJk/end_of_eden/system/image"
"github.com/BigJk/end_of_eden/system/settings"
@@ -91,8 +92,8 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case ChoiceContinue:
audio.Play("btn_menu")
- if saved, err := os.ReadFile("./session.save"); err == nil {
- f, err := os.OpenFile("./logs/S "+strings.ReplaceAll(time.Now().Format(time.DateTime), ":", "-")+".txt", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
+ if saved, err := fs.ReadFile("./session.save"); err == nil {
+ f, err := fs.OpenFile("./logs/S "+strings.ReplaceAll(time.Now().Format(time.DateTime), ":", "-")+".txt", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
if err != nil {
panic(err)
}
@@ -119,7 +120,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
audio.Play("btn_menu")
_ = os.Mkdir("./logs", 0777)
- f, err := os.OpenFile("./logs/S "+strings.ReplaceAll(time.Now().Format(time.DateTime), ":", "-")+".txt", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
+ f, err := fs.OpenFile("./logs/S "+strings.ReplaceAll(time.Now().Format(time.DateTime), ":", "-")+".txt", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
if err != nil {
panic(err)
}
diff --git a/ui/menus/mods/mods.go b/ui/menus/mods/mods.go
index afc0c92..3e602be 100644
--- a/ui/menus/mods/mods.go
+++ b/ui/menus/mods/mods.go
@@ -2,6 +2,7 @@ package mods
import (
"github.com/BigJk/end_of_eden/game"
+ "github.com/BigJk/end_of_eden/internal/fs"
"github.com/BigJk/end_of_eden/system/audio"
"github.com/BigJk/end_of_eden/system/settings"
"github.com/BigJk/end_of_eden/ui"
@@ -13,7 +14,6 @@ import (
zone "github.com/lrstanley/bubblezone"
"github.com/samber/lo"
"log"
- "os"
"path/filepath"
"sort"
)
@@ -182,7 +182,7 @@ func (m Model) modActive(mod string) bool {
}
func (m Model) fetchMods() Model {
- entries, err := os.ReadDir("./mods")
+ entries, err := fs.ReadDir("./mods")
if err != nil {
log.Println("Error while reading mods directory:", err)
return m
diff --git a/ui/menus/warning/warning.go b/ui/menus/warning/warning.go
new file mode 100644
index 0000000..906baf0
--- /dev/null
+++ b/ui/menus/warning/warning.go
@@ -0,0 +1,39 @@
+package warning
+
+import (
+ "github.com/BigJk/end_of_eden/ui"
+ "github.com/BigJk/end_of_eden/ui/components"
+ tea "github.com/charmbracelet/bubbletea"
+)
+
+type Model struct {
+ ui.MenuBase
+
+ parent tea.Model
+ text string
+}
+
+func New(parent tea.Model, text string) Model {
+ return Model{parent: parent, text: text}
+}
+
+func (m Model) Init() tea.Cmd {
+ return nil
+}
+
+func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ switch msg := msg.(type) {
+ case tea.WindowSizeMsg:
+ m.Size = msg
+ case tea.KeyMsg:
+ if msg.Type == tea.KeyEscape {
+ return m.parent, nil
+ }
+ }
+
+ return m, nil
+}
+
+func (m Model) View() string {
+ return components.Error(m.Size.Width, m.Size.Height, m.text)
+}
diff --git a/ui/root/root.go b/ui/root/root.go
index 9387b3d..956fed8 100644
--- a/ui/root/root.go
+++ b/ui/root/root.go
@@ -51,6 +51,8 @@ func (m Model) Init() tea.Cmd {
}
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ var cmds []tea.Cmd
+
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.size = msg
@@ -73,6 +75,10 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
m.stack[curIndex], cmd = m.stack[curIndex].Update(msg)
+ if cmd != nil {
+ cmds = append(cmds, cmd)
+ }
+
if menu, ok := m.stack[curIndex].(ui.Menu); ok && !menu.HasSize() {
return m, tea.Batch(cmd, func() tea.Msg {
return m.size
@@ -80,10 +86,18 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
if m.stack[curIndex] == nil {
+ // If we remove the top model, we need to send a window size message to the new top model
+ // to avoid the layout to be broken.
+ cmds = append(cmds, func() tea.Msg {
+ return tea.WindowSizeMsg{
+ Width: m.size.Width,
+ Height: m.size.Height,
+ }
+ })
m.stack = m.stack[:len(m.stack)-1]
}
- return m, cmd
+ return m, tea.Batch(cmds...)
}
func (m Model) View() string {