~taiite/public-inbox

This thread contains a patchset. You're looking at the original emails, but you may wish to use the patch review UI. Review patch
3 2

[PATCH senpai 0/2] Add support for running on the web, connecting to websockets

Details
Message ID
<163814615496.3472.12166640118295783938-0@git.sr.ht>
DKIM signature
missing
Download raw message
Experimental support for running Senpai on the web, with GOOS=js
GOARCH=wasm + xterm.js

Give it a try!

You'll need a binary IRCv3
websocket that accepts connections from your origin.

You can put it
in the hash of the URL to pass it to Senpai on startup.

e.g. `python
-m http.server` -> http://localhost:8000/#wss://example.net/some/socket
The binary websocket limitation makes it tricky to use soju, which only
offers text.

The app doesn't seem to receive messages when the socket
is used in text mode..

A demo is currently running at
https://cyberspace.baby/senpai.

If you give me a bell in #senpai I'll
pass you a config block to use a real soju session on cyberspace.baby..
currently going through a binary websocket proxy.

mooff (2):
  Make history prefetch limit configurable
  Add support for running on the web, connecting to websockets

 Makefile                 |   5 +
 app.go                   |  22 +-
 cmd/senpai/main.go       |   7 +-
 cmd/senpai/signals_js.go |   5 +
 cmd/senpai/signals_x.go  |  21 ++
 config.go                |  17 ++
 go.mod                   |   7 +-
 go.sum                   |  67 ++++-
 web/.gitignore           |   3 +
 web/index.html           | 261 ++++++++++++++++
 web/package-lock.json    |  39 +++
 web/package.json         |   6 +
 web/wasm_exec.js         | 636 +++++++++++++++++++++++++++++++++++++++
 13 files changed, 1072 insertions(+), 24 deletions(-)
 create mode 100644 cmd/senpai/signals_js.go
 create mode 100644 cmd/senpai/signals_x.go
 create mode 100644 web/.gitignore
 create mode 100644 web/index.html
 create mode 100644 web/package-lock.json
 create mode 100644 web/package.json
 create mode 100644 web/wasm_exec.js

-- 
2.32.0

[PATCH senpai 1/2] Make history prefetch limit configurable

Details
Message ID
<163814615496.3472.12166640118295783938-1@git.sr.ht>
In-Reply-To
<163814615496.3472.12166640118295783938-0@git.sr.ht> (view parent)
DKIM signature
missing
Download raw message
Patch: +18 -1
From: mooff <mooff@awful.cooking>

history {
  prefetch-limit 500
}
---
 app.go    |  2 +-
 config.go | 17 +++++++++++++++++
 2 files changed, 18 insertions(+), 1 deletion(-)

diff --git a/app.go b/app.go
index f50f8bd..52397b1 100644
--- a/app.go
+++ b/app.go
@@ -615,7 +615,7 @@ func (app *App) handleIRCEvent(netID string, ev interface{}) {
			s.Join(channel, "")
		}
		s.NewHistoryRequest("").
			WithLimit(1000).
			WithLimit(app.cfg.HistoryPrefetch).
			Targets(app.lastCloseTime, msg.TimeOrNow())
		body := "Connected to the server"
		if s.Nick() != app.cfg.Nick {
diff --git a/config.go b/config.go
index d232cf4..a0d2d2f 100644
--- a/config.go
+++ b/config.go
@@ -70,6 +70,8 @@ type Config struct {

	Colors ConfigColors

	HistoryPrefetch int

	Debug bool
}

@@ -100,6 +102,7 @@ func Defaults() (cfg Config, err error) {
		Colors: ConfigColors{
			Prompt: Color(tcell.ColorDefault),
		},
		HistoryPrefetch: 1000,
		Debug: false,
	}

@@ -191,6 +194,20 @@ func unmarshal(filename string, cfg *Config) (err error) {
			if err := d.ParseParams(&cfg.OnHighlightPath); err != nil {
				return err
			}
		case "history":
			for _, child := range d.Children {
				switch child.Name {
				case "prefetch-limit":
					var prefetch string
					if err := child.ParseParams(&prefetch); err != nil {
						return err
					}

					if cfg.HistoryPrefetch, err = strconv.Atoi(prefetch); err != nil {
						return err
					}
				}
			}
		case "pane-widths":
			for _, child := range d.Children {
				switch child.Name {
-- 
2.32.0

[PATCH senpai 2/2] Add support for running on the web, connecting to websockets

Details
Message ID
<163814615496.3472.12166640118295783938-2@git.sr.ht>
In-Reply-To
<163814615496.3472.12166640118295783938-0@git.sr.ht> (view parent)
DKIM signature
missing
Download raw message
Patch: +1054 -23
From: mooff <mooff@awful.cooking>

---
 Makefile                 |   5 +
 app.go                   |  20 +-
 cmd/senpai/main.go       |   7 +-
 cmd/senpai/signals_js.go |   5 +
 cmd/senpai/signals_x.go  |  21 ++
 go.mod                   |   7 +-
 go.sum                   |  67 ++++-
 web/.gitignore           |   3 +
 web/index.html           | 261 ++++++++++++++++
 web/package-lock.json    |  39 +++
 web/package.json         |   6 +
 web/wasm_exec.js         | 636 +++++++++++++++++++++++++++++++++++++++
 12 files changed, 1054 insertions(+), 23 deletions(-)
 create mode 100644 cmd/senpai/signals_js.go
 create mode 100644 cmd/senpai/signals_x.go
 create mode 100644 web/.gitignore
 create mode 100644 web/index.html
 create mode 100644 web/package-lock.json
 create mode 100644 web/package.json
 create mode 100644 web/wasm_exec.js

diff --git a/Makefile b/Makefile
index da150ec..0b97630 100644
--- a/Makefile
+++ b/Makefile
@@ -18,6 +18,11 @@ doc/senpai.1: doc/senpai.1.scd
doc/senpai.5: doc/senpai.5.scd
	$(SCDOC) < doc/senpai.5.scd > doc/senpai.5

web: js
	cd web && npm install --silent
js:
	GOOS=js GOARCH=wasm $(GO) build -o web/senpai.wasm $(GOFLAGS) ./cmd/senpai

clean:
	$(RM) -rf senpai doc/senpai.1 doc/senpai.5
install: all
diff --git a/app.go b/app.go
index 52397b1..c7777bb 100644
--- a/app.go
+++ b/app.go
@@ -1,6 +1,7 @@
package senpai

import (
	"context"
	"crypto/tls"
	"errors"
	"fmt"
@@ -14,6 +15,7 @@ import (
	"git.sr.ht/~taiite/senpai/irc"
	"git.sr.ht/~taiite/senpai/ui"
	"github.com/gdamore/tcell/v2"
	"nhooyr.io/websocket"
)

const eventChanSize = 64
@@ -311,8 +313,17 @@ func (app *App) connect(netID string) net.Conn {
	}
}

func (app *App) tryConnect() (conn net.Conn, err error) {
func (app *App) tryConnect() (net.Conn, error) {
	addr := app.cfg.Addr
	if strings.HasPrefix(addr, "wss://") || strings.HasPrefix(addr, "ws://") {
		return app.tryConnectWebSocket()
	}
	return app.tryConnectTCP()
}

func (app *App) tryConnectTCP() (conn net.Conn, err error) {
	addr := app.cfg.Addr

	colonIdx := strings.LastIndexByte(addr, ':')
	bracketIdx := strings.LastIndexByte(addr, ']')
	if colonIdx <= bracketIdx {
@@ -346,6 +357,13 @@ func (app *App) tryConnect() (conn net.Conn, err error) {
	return
}

func (app *App) tryConnectWebSocket() (net.Conn, error) {
	conn, _, err := websocket.Dial(context.TODO(), app.cfg.Addr, &websocket.DialOptions{
		Subprotocols: []string{"binary.ircv3.net", "binary"},
	})
	return websocket.NetConn(context.TODO(), conn, websocket.MessageBinary), err
}

func (app *App) debugOutputMessages(netID string, out chan<- irc.Message) chan<- irc.Message {
	debugOut := make(chan irc.Message, cap(out))
	go func() {
diff --git a/cmd/senpai/main.go b/cmd/senpai/main.go
index 665cacc..92d4c86 100644
--- a/cmd/senpai/main.go
+++ b/cmd/senpai/main.go
@@ -6,10 +6,8 @@ import (
	"io/ioutil"
	"math/rand"
	"os"
	"os/signal"
	"path"
	"strings"
	"syscall"
	"time"

	"git.sr.ht/~taiite/senpai"
@@ -54,11 +52,8 @@ func main() {
	app.SwitchToBuffer(lastNetID, lastBuffer)
	app.SetLastClose(getLastStamp())

	sigCh := make(chan os.Signal, 1)
	signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)

	go func() {
		<-sigCh
		<-closeSignal()
		app.Close()
	}()

diff --git a/cmd/senpai/signals_js.go b/cmd/senpai/signals_js.go
new file mode 100644
index 0000000..509b20f
--- /dev/null
+++ b/cmd/senpai/signals_js.go
@@ -0,0 +1,5 @@
package main

func closeSignal() chan struct{} {
	return make(chan struct{}) // never fires
}
diff --git a/cmd/senpai/signals_x.go b/cmd/senpai/signals_x.go
new file mode 100644
index 0000000..3b88aff
--- /dev/null
+++ b/cmd/senpai/signals_x.go
@@ -0,0 +1,21 @@
// +build !js

package main

import (
	"os"
	"os/signal"
	"syscall"
)

func closeSignal() chan struct{} {
	osClose := make(chan os.Signal, 1)
	signal.Notify(osClose, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)

	genericClose := make(chan struct{}, 1)
	go func() {
		<-osClose
		genericClose <- struct{}{}
	}()
	return genericClose
}
diff --git a/go.mod b/go.mod
index d3c9b55..d0d120a 100644
--- a/go.mod
+++ b/go.mod
@@ -5,10 +5,13 @@ go 1.16
require (
	git.sr.ht/~emersion/go-scfg v0.0.0-20201019143924-142a8aa629fc
	github.com/gdamore/tcell/v2 v2.3.11
	github.com/mattn/go-runewidth v0.0.10
	github.com/mattn/go-runewidth v0.0.13
	golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf
	golang.org/x/time v0.0.0-20210611083556-38a9dc6acbc6
	mvdan.cc/xurls/v2 v2.3.0
	nhooyr.io/websocket v1.8.7
)

replace github.com/gdamore/tcell/v2 => github.com/hhirtz/tcell/v2 v2.3.12-0.20210807133752-5d743c3ab0c9
replace github.com/gdamore/tcell/v2 => github.com/awfulcooking/tcell/v2 v2.4.1-0.20211128170204-5ebcb5571e5d

replace golang.org/x/term => github.com/awfulcooking/term v0.0.0-20211128155416-2652f7c0d88b
diff --git a/go.sum b/go.sum
index 856f865..bce6b18 100644
--- a/go.sum
+++ b/go.sum
@@ -1,33 +1,72 @@
git.sr.ht/~emersion/go-scfg v0.0.0-20201019143924-142a8aa629fc h1:51BD67xFX+bozd3ZRuOUfalrhx4/nQSh6A9lI08rYOk=
git.sr.ht/~emersion/go-scfg v0.0.0-20201019143924-142a8aa629fc/go.mod h1:t+Ww6SR24yYnXzEWiNlOY0AFo5E9B73X++10lrSpp4U=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/awfulcooking/tcell/v2 v2.4.1-0.20211128170204-5ebcb5571e5d h1:GGONl5jDMT0wtMWEEu4sH67JkaMCYqTcyW2Xha9uy/o=
github.com/awfulcooking/tcell/v2 v2.4.1-0.20211128170204-5ebcb5571e5d/go.mod h1:X4k/PfBso5eCwoulBKD17GlepBi4ecMbxPM1O1EG2is=
github.com/awfulcooking/term v0.0.0-20211128155416-2652f7c0d88b h1:xzEg9LjSS8aku4qx07hjNn8YxdPrKjOwaxouhImrOlc=
github.com/awfulcooking/term v0.0.0-20211128155416-2652f7c0d88b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/hhirtz/tcell/v2 v2.3.12-0.20210807133752-5d743c3ab0c9 h1:YE0ZsDHfDGR0MeB6YLSGW8tjoxOXZKX3XbB0ytGDX4M=
github.com/hhirtz/tcell/v2 v2.3.12-0.20210807133752-5d743c3ab0c9/go.mod h1:cTTuF84Dlj/RqmaCIV5p4w8uG1zWdk0SF6oBpwHp4fU=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/klauspost/compress v1.10.3 h1:OP96hzwJVBIHYU52pVTI6CczrxPvrGfgqF9N5eTO0Q8=
github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac=
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-runewidth v0.0.10 h1:CoZ3S2P7pvtP45xOtBw+/mDL2z0RKI576gSkzRRpdGg=
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf h1:MZ2shdL+ZM/XzY3ZGOnh4Nlpnxz5GSOhOmtHo3iPU6M=
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211113001501-0c823b97ae02 h1:7NCfEGl0sfUojmX78nK9pBJuUlSZWEJA/TwASvfiPLo=
golang.org/x/sys v0.0.0-20211113001501-0c823b97ae02/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20210611083556-38a9dc6acbc6 h1:Vv0JUPWTyeqUq42B2WJ1FeIDjjvGKoA2Ss+Ts0lAVbs=
golang.org/x/time v0.0.0-20210611083556-38a9dc6acbc6/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
mvdan.cc/xurls/v2 v2.3.0 h1:59Olnbt67UKpxF1EwVBopJvkSUBmgtb468E4GVWIZ1I=
mvdan.cc/xurls/v2 v2.3.0/go.mod h1:AjuTy7gEiUArFMjgBBDU4SMxlfUYsRokpJQgNWOt3e4=
nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g=
nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=
diff --git a/web/.gitignore b/web/.gitignore
new file mode 100644
index 0000000..1800848
--- /dev/null
+++ b/web/.gitignore
@@ -0,0 +1,3 @@
*.wasm
node_modules/
upload.sh
diff --git a/web/index.html b/web/index.html
new file mode 100644
index 0000000..9663b6e
--- /dev/null
+++ b/web/index.html
@@ -0,0 +1,261 @@
<!doctype html>
<html>
<head>
	<meta charset="utf-8">
	<title>Senpai</title>

	<link rel="stylesheet" href="node_modules/xterm/css/xterm.css" />
	<script src="node_modules/xterm/lib/xterm.js"></script>
	<script src="node_modules/xterm-addon-fit/lib/xterm-addon-fit.js"></script>
</head>

<body>
	<style>
		* { box-sizing: border-box; padding: 0px; margin: 0px; }

		:root, body, #terminal {
			position: absolute;
			top: 0px;
			left: 0px;
			width: 100%;
			height: 100%;
			overflow: hidden;

			background: #000;
			color: #eeeeee;
		}
	</style>
	
	<div id="terminal"></div>

	<script src="wasm_exec.js"></script>
	<script>
		const fallbackName = randUsername();
		const fallbackSocket = location.hash.slice(1) || `wss://${location.host}/bounce`;

		const senpaiConfig = sessionStorage['senpaiConfig'] || localStorage['senpaiConfig'] || `
			address ${fallbackSocket}
			nickname ${fallbackName}
			pane-widths {
				channels 17
			}
			history {
				prefetch-limit 30
			}
		`;

		function randUsername() {
			const names = [
				"alice", "bob", "jane", "visual", "studio", "code", "jaffa",
				"web", "cyber", "develop", "tools", "mozilla", "firefox",
				"happy", "juice", "cola", "shake", "tree", "sun", "radiant",
				"tmux", "git", "sunshine", "duck", "cat", "dog", "fish", "penguin",
			];
			return names[Math.floor(Math.random()*names.length)];
		}
	</script>
	<script>
		const goStubFS = globalThis.fs; // defined in wasm_exec.js

		const configFD = 42; // used for /config/senpai/senpai.scfg
		const termFD = 69;   // used for /dev/tty

		const fds = {};

		let nextFD = 100;

		const files = {
			"/config.scfg": senpaiConfig,
		};

		const senpaiFS = {
			open(path, flags, mode, callback) {
				console.info('fs open', path, flags, mode, callback);
				if (path == '/dev/tty')
					return callback(null, termFD);
				else if (!(path in files))
					return callback({code: "ENOENT"});

				const fd = nextFD++;
				fds[fd] = {path, flags, mode, position: 0};

				callback(null, fd);
			},
			async read(fd, buffer, offset, length, position, callback) {
				let numBytes = 0;

				if (fd === termFD) { // read any input from xterm.js
					await waitReadAvailable();

					numBytes = Math.min(readBuffer.length, position+length);
					buffer.set(readBuffer.slice(position, numBytes), offset);

					resetReadBuffer();
					return callback(null, numBytes, buffer);
				}

				const desc = fds[fd];
				if (position != null)
					desc.position = position;
				
				const data = new TextEncoder().encode(files[desc.path]);

				numBytes = length;
				if (desc.position + numBytes > data.length)
					numBytes = Math.max(0, data.length - desc.position);

				buffer.set(data.slice(desc.position, numBytes), offset);
				desc.position += numBytes;

				callback(null, numBytes, buffer);
			},
			writeSync(fd, buf) {
				if (fd == 1 || fd == 2) { // stdout, stderr
					return goStubFS.writeSync(fd, buf);
				} else if (fd == termFD) { // for xterm.js tty
					term.write(buf);
				} else {
					console.error("Write to fd", "|", fd, buf);
				}
				return buf.length;
			},
			mkdir: (path, perm, callback) => callback(null),
		};

		globalThis.fs = Object.assign({}, goStubFS, senpaiFS);
	</script>

	<script>
		globalThis["golang.org/x/term"] = {
			isTerminal: (fd) => true,
			getSize: (fd) => ({width: term.cols, height: term.rows}),
			makeRaw: (fd) => false,
			getState: (fd) => {},
			restore: (fd, state) => null,
		};

		// look at numSizeRequests in console to see how often it happens
		let numSizeRequests = 0;
		globalThis["golang.org/x/term"].getSize = () => {
			numSizeRequests++;
			return {width: term.cols, height: term.rows};
		}
	</script>

	<script>
		const baseTheme = {
			foreground: '#F8F8F8',
			background: '#2D2E2C',
			selection: '#5DA5D533',
			black: '#1E1E1D',
			brightBlack: '#262625',
			red: '#CE5C5C',
			brightRed: '#FF7272',
			green: '#5BCC5B',
			brightGreen: '#72FF72',
			yellow: '#CCCC5B',
			brightYellow: '#FFFF72',
			blue: '#5D5DD3',
			brightBlue: '#7279FF',
			magenta: '#BC5ED1',
			brightMagenta: '#E572FF',
			cyan: '#5DA5D5',
			brightCyan: '#72F0FF',
			white: '#F8F8F8',
			brightWhite: '#FFFFFF'
		};

		const snazzyTheme = {
			foreground: '#eff0eb',
			background: '#282a36',
			selection: '#97979b33',
			black: '#282a36',
			brightBlack: '#686868',
			red: '#ff5c57',
			brightRed: '#ff5c57',
			green: '#5af78e',
			brightGreen: '#5af78e',
			yellow: '#f3f99d',
			brightYellow: '#f3f99d',
			blue: '#57c7ff',
			brightBlue: '#57c7ff',
			magenta: '#ff6ac1',
			brightMagenta: '#ff6ac1',
			cyan: '#9aedfe',
			brightCyan: '#9aedfe',
			white: '#f1f1f0',
			brightWhite: '#eff0eb'
		};

		const theme = null;
	</script>

	<script>
		const term = new Terminal({ theme, scrollback: 0 });
		const fitAddon = new FitAddon.FitAddon();

		term.loadAddon(fitAddon);

		function fit() {
			fitAddon.fit();
			// todo: notify senpai, make tcell ask terminal less often
		}

		window.addEventListener('resize', fit);
		setTimeout(fit, 0);

		term.open(document.querySelector('#terminal'));
		term.focus();

		let readBuffer = new Uint8Array;
		let _readAvailable;

		function waitReadAvailable() {
			if (readBuffer.length)
				return Promise.resolve();

			return new Promise(resolve => _readAvailable = resolve);
		}

		term.onData(data => {
			const len = data.length;
			const bytes = new Uint8Array(len);

			for (let i=0; i<len; ++i)
				bytes[i] = data.charCodeAt(i) & 255;

			readBuffer = new Uint8Array([...readBuffer, ...bytes]);
			_readAvailable?.();
		});

		term.onBinary(bytes => {
			readBuffer = new Uint8Array([...readBuffer, ...bytes]);
			_readAvailable?.();
		});

		function resetReadBuffer() {
			readBuffer = new Uint8Array;
		}
	</script>

	<script>
		(async () => {
			const blob = await fetch('senpai.wasm');

			globalThis.go = new Go();
			go.argv = ["senpai", "-config", "/config.scfg"];
			go.env.TERM = "xterm-256color";
			go.env.XDG_CACHE_HOME = "/cache";
			go.env.LC_ALL = "UTF-8";

			const {module, instance} = await WebAssembly.instantiateStreaming(blob, go.importObject);

			globalThis.senpai = module;
			globalThis.instance = instance;

			go.run(instance);
		})();
	</script>
</body>

</html>
diff --git a/web/package-lock.json b/web/package-lock.json
new file mode 100644
index 0000000..bc87443
--- /dev/null
+++ b/web/package-lock.json
@@ -0,0 +1,39 @@
{
  "name": "web",
  "lockfileVersion": 2,
  "requires": true,
  "packages": {
    "": {
      "dependencies": {
        "xterm": "^4.15.0",
        "xterm-addon-fit": "^0.5.0"
      }
    },
    "node_modules/xterm": {
      "version": "4.15.0",
      "resolved": "https://registry.npmjs.org/xterm/-/xterm-4.15.0.tgz",
      "integrity": "sha512-Ik1GoSq1yqKZQ2LF37RPS01kX9t4TP8gpamUYblD09yvWX5mEYuMK4CcqH6+plgiNEZduhTz/UrcaWs97gOlOw=="
    },
    "node_modules/xterm-addon-fit": {
      "version": "0.5.0",
      "resolved": "https://registry.npmjs.org/xterm-addon-fit/-/xterm-addon-fit-0.5.0.tgz",
      "integrity": "sha512-DsS9fqhXHacEmsPxBJZvfj2la30Iz9xk+UKjhQgnYNkrUIN5CYLbw7WEfz117c7+S86S/tpHPfvNxJsF5/G8wQ==",
      "peerDependencies": {
        "xterm": "^4.0.0"
      }
    }
  },
  "dependencies": {
    "xterm": {
      "version": "4.15.0",
      "resolved": "https://registry.npmjs.org/xterm/-/xterm-4.15.0.tgz",
      "integrity": "sha512-Ik1GoSq1yqKZQ2LF37RPS01kX9t4TP8gpamUYblD09yvWX5mEYuMK4CcqH6+plgiNEZduhTz/UrcaWs97gOlOw=="
    },
    "xterm-addon-fit": {
      "version": "0.5.0",
      "resolved": "https://registry.npmjs.org/xterm-addon-fit/-/xterm-addon-fit-0.5.0.tgz",
      "integrity": "sha512-DsS9fqhXHacEmsPxBJZvfj2la30Iz9xk+UKjhQgnYNkrUIN5CYLbw7WEfz117c7+S86S/tpHPfvNxJsF5/G8wQ==",
      "requires": {}
    }
  }
}
diff --git a/web/package.json b/web/package.json
new file mode 100644
index 0000000..d4b845e
--- /dev/null
+++ b/web/package.json
@@ -0,0 +1,6 @@
{
  "dependencies": {
    "xterm": "^4.15.0",
    "xterm-addon-fit": "^0.5.0"
  }
}
diff --git a/web/wasm_exec.js b/web/wasm_exec.js
new file mode 100644
index 0000000..5651141
--- /dev/null
+++ b/web/wasm_exec.js
@@ -0,0 +1,636 @@
// 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.

(() => {
	// Map multiple JavaScript environments to a single common API,
	// preferring web standards over Node.js API.
	//
	// Environments considered:
	// - Browsers
	// - Node.js
	// - Electron
	// - Parcel
	// - Webpack

	if (typeof global !== "undefined") {
		// global already exists
	} else if (typeof window !== "undefined") {
		window.global = window;
	} else if (typeof self !== "undefined") {
		self.global = self;
	} else {
		throw new Error("cannot export Go (neither global, window nor self is defined)");
	}

	if (!global.require && typeof require !== "undefined") {
		global.require = require;
	}

	if (!global.fs && global.require) {
		const fs = require("fs");
		if (typeof fs === "object" && fs !== null && Object.keys(fs).length !== 0) {
			global.fs = fs;
		}
	}

	const enosys = () => {
		const err = new Error("not implemented");
		err.code = "ENOSYS";
		return err;
	};

	if (!global.fs) {
		let outputBuf = "";
		global.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.substr(0, nl));
					outputBuf = outputBuf.substr(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 (!global.process) {
		global.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 (!global.crypto && global.require) {
		const nodeCrypto = require("crypto");
		global.crypto = {
			getRandomValues(b) {
				nodeCrypto.randomFillSync(b);
			},
		};
	}
	if (!global.crypto) {
		throw new Error("global.crypto is not available, polyfill required (getRandomValues only)");
	}

	if (!global.performance) {
		global.performance = {
			now() {
				const [sec, nsec] = process.hrtime();
				return sec * 1000 + nsec / 1000000;
			},
		};
	}

	if (!global.TextEncoder && global.require) {
		global.TextEncoder = require("util").TextEncoder;
	}
	if (!global.TextEncoder) {
		throw new Error("global.TextEncoder is not available, polyfill required");
	}

	if (!global.TextDecoder && global.require) {
		global.TextDecoder = require("util").TextDecoder;
	}
	if (!global.TextDecoder) {
		throw new Error("global.TextDecoder is not available, polyfill required");
	}

	// End of polyfills for common API.

	const encoder = new TextEncoder("utf-8");
	const decoder = new TextDecoder("utf-8");

	global.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,
				global,
				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],
				[global, 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;
			};
		}
	}

	if (
		typeof module !== "undefined" &&
		global.require &&
		global.require.main === module &&
		global.process &&
		global.process.versions &&
		!global.process.versions.electron
	) {
		if (process.argv.length < 3) {
			console.error("usage: go_js_wasm_exec [wasm binary] [arguments]");
			process.exit(1);
		}

		const go = new Go();
		go.argv = process.argv.slice(2);
		go.env = Object.assign({ TMPDIR: require("os").tmpdir() }, process.env);
		go.exit = process.exit;
		WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject).then((result) => {
			process.on("exit", (code) => { // Node.js exits if no event handler is pending
				if (code === 0 && !go.exited) {
					// deadlock, make Go print error and stack traces
					go._pendingEvent = { id: 0 };
					go._resume();
				}
			});
			return go.run(result.instance);
		}).catch((err) => {
			console.error(err);
			process.exit(1);
		});
	}
})();
-- 
2.32.0

Re: [PATCH senpai 2/2] Add support for running on the web, connecting to websockets

Details
Message ID
<CG34MQVT5OZK.XBERCE1VGP87@vps807759>
In-Reply-To
<163814615496.3472.12166640118295783938-2@git.sr.ht> (view parent)
DKIM signature
pass
Download raw message
Time to deprecate gamja i guess?

Jokes aside, nice project, this looks cool (and it works? it seems so)
I think you should set this up in a new repo with a Makefile and the HTML/JS boilerplate.

I don't have a lot of ideas about the text vs binary websocket issue..
What happens when you don't negotiate any sub-protocols?
It might also be possible for soju to accept the binary sub-protocol.

> @@ -346,6 +357,13 @@ func (app *App) tryConnect() (conn net.Conn, err error) {
>  	return
>  }
>  
> +func (app *App) tryConnectWebSocket() (net.Conn, error) {
> +	conn, _, err := websocket.Dial(context.TODO(), app.cfg.Addr, &websocket.DialOptions{
> +		Subprotocols: []string{"binary.ircv3.net", "binary"},

what happens if you remove this line (thus don't negotiate any sub-protocol)?

> +	})
> +	return websocket.NetConn(context.TODO(), conn, websocket.MessageBinary), err

MessageBinary should also be changed to MessageText then

> +}
> +
>  func (app *App) debugOutputMessages(netID string, out chan<- irc.Message) chan<- irc.Message {
>  	debugOut := make(chan irc.Message, cap(out))
>  	go func() {
> diff --git a/cmd/senpai/signals_x.go b/cmd/senpai/signals_x.go
> new file mode 100644
> index 0000000..3b88aff
> --- /dev/null
> +++ b/cmd/senpai/signals_x.go
> @@ -0,0 +1,21 @@
> +// +build !js
> +
> +package main
> +
> +import (
> +	"os"
> +	"os/signal"
> +	"syscall"
> +)
> +
> +func closeSignal() chan struct{} {
> +	osClose := make(chan os.Signal, 1)
> +	signal.Notify(osClose, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
> +
> +	genericClose := make(chan struct{}, 1)
> +	go func() {
> +		<-osClose
> +		genericClose <- struct{}{}
> +	}()

pro-tip: have closeSignal() return a chan interface{} instead, so this extra goroutine is not needed.
Reply to thread Export thread (mbox)