diff options
Diffstat (limited to 'xP')
-rw-r--r-- | xP/go.mod | 2 | ||||
-rw-r--r-- | xP/public/xP.js | 46 | ||||
-rw-r--r-- | xP/xP.go | 74 |
3 files changed, 101 insertions, 21 deletions
@@ -1,6 +1,6 @@ module janouch.name/xK/xP -go 1.21 +go 1.22 toolchain go1.23.2 diff --git a/xP/public/xP.js b/xP/public/xP.js index 5436a65..33d7d2a 100644 --- a/xP/public/xP.js +++ b/xP/public/xP.js @@ -1,4 +1,4 @@ -// Copyright (c) 2022 - 2023, Přemysl Eric Janouch <p@janouch.name> +// Copyright (c) 2022 - 2025, Přemysl Eric Janouch <p@janouch.name> // SPDX-License-Identifier: 0BSD import * as Relay from './proto.js' @@ -67,18 +67,19 @@ class RelayRPC extends EventTarget { _processOne(message) { let e = message.data + let p switch (e.event) { case Relay.Event.Error: - if (this.promised[e.commandSeq] !== undefined) - this.promised[e.commandSeq].reject(e.error) - else + if ((p = this.promised[e.commandSeq]) === undefined) console.error(`Unawaited error: ${e.error}`) + else if (p !== true) + p.reject(e.error) break case Relay.Event.Response: - if (this.promised[e.commandSeq] !== undefined) - this.promised[e.commandSeq].resolve(e.data) - else + if ((p = this.promised[e.commandSeq]) === undefined) console.error("Unawaited response") + else if (p !== true) + p.resolve(e.data) break default: e.eventSeq = message.eventSeq @@ -95,6 +96,13 @@ class RelayRPC extends EventTarget { this.promised[seq].reject("No response") delete this.promised[seq] } + m.redraw() + } + + get busy() { + for (const seq in this.promised) + return true + return false } send(params) { @@ -110,6 +118,9 @@ class RelayRPC extends EventTarget { this.ws.send(JSON.stringify({commandSeq: seq, data: params})) + this.promised[seq] = true + m.redraw() + // Automagically detect if we want a result. let data = undefined const promise = new Promise( @@ -191,6 +202,17 @@ let bufferAutoscroll = true let servers = new Map() +let lastActive = undefined + +function notifyActive() { + // Reduce unnecessary traffic. + const now = Date.now() + if (lastActive === undefined || (now - lastActive >= 5000)) { + lastActive = now + rpc.send({command: 'Active'}) + } +} + function bufferResetStats(b) { b.newMessages = 0 b.newUnimportantMessages = 0 @@ -998,7 +1020,7 @@ let Input = { onKeyDown: event => { // TODO: And perhaps on other actions, too. - rpc.send({command: 'Active'}) + notifyActive() let b = buffers.get(bufferCurrent) if (b === undefined || event.isComposing) @@ -1103,7 +1125,13 @@ let Main = { return m('.xP', {}, [ overlay, - m('.title', {}, [m('b', {}, `xP`), m(Topic)]), + m('.title', {}, [ + m('span', [ + rpc.busy ? '⋯ ' : undefined, + m('b', {}, `xP`), + ]), + m(Topic), + ]), m('.middle', {}, [m(BufferList), m(BufferContainer)]), m(Status), m('.input', {}, [m(Prompt), m(Input)]), @@ -1,4 +1,4 @@ -// Copyright (c) 2022, Přemysl Eric Janouch <p@janouch.name> +// Copyright (c) 2022 - 2025, Přemysl Eric Janouch <p@janouch.name> // SPDX-License-Identifier: 0BSD package main @@ -6,12 +6,16 @@ package main import ( "bufio" "context" + "crypto/sha1" + "embed" "encoding/binary" + "encoding/hex" "encoding/json" "flag" "fmt" "html/template" "io" + "io/fs" "log" "net" "net/http" @@ -23,7 +27,12 @@ import ( ) var ( - debug = flag.Bool("debug", false, "enable debug output") + debug = flag.Bool("debug", false, "enable debug output") + webRoot = flag.String("webroot", "", "override bundled web resources") + + //go:embed public/* + webResources embed.FS + webResourcesHash string addressBind string addressConnect string @@ -240,21 +249,20 @@ func handleWS(w http.ResponseWriter, r *http.Request) { // ----------------------------------------------------------------------------- -var staticHandler = http.FileServer(http.Dir(".")) - var page = template.Must(template.New("/").Parse(`<!DOCTYPE html> <html> <head> <title>xP</title> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1"> + <base href="{{ .Root }}/"> <link rel="stylesheet" href="xP.css" /> </head> <body> <script src="mithril.js"> </script> <script> - let proxy = '{{ . }}' + let proxy = '{{ .Proxy }}' </script> <script type="module" src="xP.js"> </script> @@ -262,20 +270,49 @@ var page = template.Must(template.New("/").Parse(`<!DOCTYPE html> </html>`)) func handleDefault(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/" { - staticHandler.ServeHTTP(w, r) - return - } - wsURI := addressWS if wsURI == "" { wsURI = fmt.Sprintf("ws://%s/ws", r.Host) } - if err := page.Execute(w, wsURI); err != nil { + + args := struct { + Root string + Proxy string + }{ + Root: webResourcesHash, + Proxy: wsURI, + } + if err := page.Execute(w, &args); err != nil { log.Println("Template execution failed: " + err.Error()) } } +func hashFS(root fs.FS) []byte { + hasher := sha1.New() + callback := func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + + // Note that this can be fooled. + fmt.Fprintln(hasher, path) + + if !d.IsDir() { + file, err := root.Open(path) + if err != nil { + return err + } + defer file.Close() + io.Copy(hasher, file) + } + return nil + } + if err := fs.WalkDir(root, ".", callback); err != nil { + log.Fatalln(err) + } + return hasher.Sum(nil) +} + func main() { flag.Usage = func() { fmt.Fprintf(flag.CommandLine.Output(), @@ -294,6 +331,21 @@ func main() { addressWS = flag.Arg(2) } + subResources, err := fs.Sub(webResources, "public") + if err != nil { + log.Fatalln(err) + } + if *webRoot != "" { + subResources = os.DirFS(*webRoot) + } + + // The simplest way of ensuring that web browsers don't use + // stale cached copies of our files. + webResourcesHash = hex.EncodeToString(hashFS(subResources)) + http.Handle("/"+webResourcesHash+"/", + http.StripPrefix("/"+webResourcesHash+"/", + http.FileServerFS(subResources))) + http.Handle("/ws", http.HandlerFunc(handleWS)) http.Handle("/", http.HandlerFunc(handleDefault)) |