aboutsummaryrefslogtreecommitdiff
path: root/xP
diff options
context:
space:
mode:
Diffstat (limited to 'xP')
-rw-r--r--xP/go.mod2
-rw-r--r--xP/public/xP.js46
-rw-r--r--xP/xP.go74
3 files changed, 101 insertions, 21 deletions
diff --git a/xP/go.mod b/xP/go.mod
index dca4d10..b79feff 100644
--- a/xP/go.mod
+++ b/xP/go.mod
@@ -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)]),
diff --git a/xP/xP.go b/xP/xP.go
index 188fd5d..7e0c386 100644
--- a/xP/xP.go
+++ b/xP/xP.go
@@ -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))