8 Commits

Author SHA1 Message Date
77aaf87860 xC: hackfix Readline 8.3
All checks were successful
Alpine 3.22 Success
Arch Linux AUR Success
OpenBSD 7.8 Success
2025-11-19 11:03:55 +01:00
57261fb13a xA: bump Fyne and other dependencies
Some checks failed
Alpine 3.21 Scripts failed
Arch Linux AUR Success
OpenBSD 7.8 Success
2025-11-03 17:58:32 +01:00
5c41eab9d7 xA: add a Makefile rule for Android 2025-11-03 17:58:32 +01:00
99c95edd57 Update .gitignore for newer Qt Creator 2025-11-03 17:58:31 +01:00
7d90142f0f xP: fix alternative browsers on iOS
As a rule, they use the same stupid and broken WebKit.
2025-11-03 17:58:08 +01:00
71e1a744c5 xP: embed web resources, tame browser caching
All checks were successful
Alpine 3.21 Success
Arch Linux AUR Success
OpenBSD 7.6 Success
2025-07-09 22:15:13 +02:00
80af5c22d6 Add an xC relay protocol analyzer
All checks were successful
Alpine 3.21 Success
Arch Linux AUR Success
OpenBSD 7.6 Success
2025-05-15 14:14:53 +02:00
7ba17a0161 Make the relay acknowledge all received commands
All checks were successful
Alpine 3.21 Success
Arch Linux AUR Success
OpenBSD 7.6 Success
To that effect, bump liberty and the xC relay protocol version.
Relay events have been reordered to improve forward compatibility.

Also prevent use-after-free when serialization fails.

xP now slightly throttles activity notifications,
and indicates when there are unacknowledged commands.
2025-05-10 12:08:51 +02:00
24 changed files with 498 additions and 144 deletions

1
.gitignore vendored
View File

@@ -2,6 +2,7 @@
/build
# Qt Creator files
/.qtcreator
/CMakeLists.txt.user*
/xK.config
/xK.files

View File

@@ -1,4 +1,4 @@
Copyright (c) 2014 - 2024, Přemysl Eric Janouch <p@janouch.name>
Copyright (c) 2014 - 2025, Přemysl Eric Janouch <p@janouch.name>
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.

16
NEWS
View File

@@ -1,3 +1,19 @@
Unreleased
* xC: added more characters as nickname delimiters,
so that @nick works as a highlight
* xC: prevented rare crashes in relay code
* xP: added a network lag indicator to the user interface
* xP: started embedding the necessary web resources,
and making sure that the files have unique paths after change,
so that stale copies are not cached by browsers indefinitely
* Bumped relay protocol version
2.1.0 (2024-12-19) "Bunnyrific"
* xC: fixed a crash when the channel topic had too many formatting items

View File

@@ -136,12 +136,12 @@ The precondition for running 'xC' frontends is enabling its relay interface:
/set general.relay_bind = "127.0.0.1:9000"
To build the web server, you'll need to install the Go compiler, and run `make`
from the _xP_ directory. Then start it from the _public_ subdirectory,
and navigate to the adress you gave it as its first argument--in the following
example, that would be http://localhost:8080[]:
To build the web server, install the Go compiler, and run `make`
from the _xP_ directory. Then start the resulting binary, and navigate to
the adress you give it as its first argument--in the following example,
that would be http://localhost:8080[]:
$ ../xP 127.0.0.1:8080 127.0.0.1:9000
$ ./xP 127.0.0.1:8080 127.0.0.1:9000
For remote use, it's recommended to put 'xP' behind a reverse proxy, with TLS,
and some form of HTTP authentication. Pass the external URL of the WebSocket

Submodule liberty updated: af889b733e...31ae400852

1
xA/.gitignore vendored
View File

@@ -1,4 +1,5 @@
/xA
/xA.apk
/proto.go
/FyneApp.toml
/*.png

View File

@@ -32,5 +32,7 @@ proto.go: $(tools)/lxdrgen.awk $(tools)/lxdrgen-go.awk ../xC.lxdr
xA: xA.go ../xK-version $(generated)
go build -ldflags "-X 'main.projectVersion=$$(cat ../xK-version)'" -o $@ \
-gcflags=all="-N -l"
xA.apk: $(generated)
fyne package -os android
clean:
rm -f $(outputs)

View File

@@ -1,25 +1,23 @@
module janouch.name/xK/xA
go 1.23.0
toolchain go1.24.0
go 1.24.0
require (
fyne.io/fyne/v2 v2.6.0
github.com/ebitengine/oto/v3 v3.3.3
fyne.io/fyne/v2 v2.7.0
github.com/ebitengine/oto/v3 v3.4.0
)
require (
fyne.io/systray v1.11.0 // indirect
fyne.io/systray v1.11.1-0.20250603113521-ca66a66d8b58 // indirect
github.com/BurntSushi/toml v1.5.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/ebitengine/purego v0.8.2 // indirect
github.com/fredbi/uri v1.1.0 // indirect
github.com/ebitengine/purego v0.9.0 // indirect
github.com/fredbi/uri v1.1.1 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/fyne-io/gl-js v0.1.0 // indirect
github.com/fyne-io/glfw-js v0.2.0 // indirect
github.com/fyne-io/gl-js v0.2.0 // indirect
github.com/fyne-io/glfw-js v0.3.0 // indirect
github.com/fyne-io/image v0.1.1 // indirect
github.com/fyne-io/oksvg v0.1.0 // indirect
github.com/fyne-io/oksvg v0.2.0 // indirect
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 // indirect
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20250301202403-da16c1255728 // indirect
github.com/go-text/render v0.2.0 // indirect
@@ -27,20 +25,20 @@ require (
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/hack-pad/go-indexeddb v0.3.2 // indirect
github.com/hack-pad/safejs v0.1.1 // indirect
github.com/jeandeaual/go-locale v0.0.0-20250421151639-a9d6ed1b3d45 // indirect
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade // indirect
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
github.com/nicksnyder/go-i18n/v2 v2.6.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rymdport/portal v0.4.1 // indirect
github.com/rymdport/portal v0.4.2 // indirect
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect
github.com/stretchr/testify v1.10.0 // indirect
github.com/yuin/goldmark v1.7.10 // indirect
golang.org/x/image v0.26.0 // indirect
golang.org/x/net v0.39.0 // indirect
golang.org/x/sys v0.32.0 // indirect
golang.org/x/text v0.24.0 // indirect
github.com/stretchr/testify v1.11.1 // indirect
github.com/yuin/goldmark v1.7.13 // indirect
golang.org/x/image v0.32.0 // indirect
golang.org/x/net v0.46.0 // indirect
golang.org/x/sys v0.37.0 // indirect
golang.org/x/text v0.30.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

View File

@@ -1,30 +1,30 @@
fyne.io/fyne/v2 v2.6.0 h1:Rywo9yKYN4qvNuvkRuLF+zxhJYWbIFM+m4N4KV4p1pQ=
fyne.io/fyne/v2 v2.6.0/go.mod h1:YZt7SksjvrSNJCwbWFV32WON3mE1Sr7L41D29qMZ/lU=
fyne.io/systray v1.11.0 h1:D9HISlxSkx+jHSniMBR6fCFOUjk1x/OOOJLa9lJYAKg=
fyne.io/systray v1.11.0/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs=
fyne.io/fyne/v2 v2.7.0 h1:GvZSpE3X0liU/fqstInVvRsaboIVpIWQ4/sfjDGIGGQ=
fyne.io/fyne/v2 v2.7.0/go.mod h1:xClVlrhxl7D+LT+BWYmcrW4Nf+dJTvkhnPgji7spAwE=
fyne.io/systray v1.11.1-0.20250603113521-ca66a66d8b58 h1:eA5/u2XRd8OUkoMqEv3IBlFYSruNlXD8bRHDiqm0VNI=
fyne.io/systray v1.11.1-0.20250603113521-ca66a66d8b58/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs=
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/ebitengine/oto/v3 v3.3.3 h1:m6RV69OqoXYSWCDsHXN9rc07aDuDstGHtait7HXSM7g=
github.com/ebitengine/oto/v3 v3.3.3/go.mod h1:MZeb/lwoC4DCOdiTIxYezrURTw7EvK/yF863+tmBI+U=
github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=
github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/ebitengine/oto/v3 v3.4.0 h1:br0PgASsEWaoWn38b2Goe7m1GKFYfNgnsjSd5Gg+/bQ=
github.com/ebitengine/oto/v3 v3.4.0/go.mod h1:IOleLVD0m+CMak3mRVwsYY8vTctQgOM0iiL6S7Ar7eI=
github.com/ebitengine/purego v0.9.0 h1:mh0zpKBIXDceC63hpvPuGLiJ8ZAa3DfrFTudmfi8A4k=
github.com/ebitengine/purego v0.9.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g=
github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw=
github.com/fredbi/uri v1.1.0 h1:OqLpTXtyRg9ABReqvDGdJPqZUxs8cyBDOMXBbskCaB8=
github.com/fredbi/uri v1.1.0/go.mod h1:aYTUoAXBOq7BLfVJ8GnKmfcuURosB1xyHDIfWeC/iW4=
github.com/fredbi/uri v1.1.1 h1:xZHJC08GZNIUhbP5ImTHnt5Ya0T8FI2VAwI/37kh2Ko=
github.com/fredbi/uri v1.1.1/go.mod h1:4+DZQ5zBjEwQCDmXW5JdIjz0PUA+yJbvtBv+u+adr5o=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/fyne-io/gl-js v0.1.0 h1:8luJzNs0ntEAJo+8x8kfUOXujUlP8gB3QMOxO2mUdpM=
github.com/fyne-io/gl-js v0.1.0/go.mod h1:ZcepK8vmOYLu96JoxbCKJy2ybr+g1pTnaBDdl7c3ajI=
github.com/fyne-io/glfw-js v0.2.0 h1:8GUZtN2aCoTPNqgRDxK5+kn9OURINhBEBc7M4O1KrmM=
github.com/fyne-io/glfw-js v0.2.0/go.mod h1:Ri6te7rdZtBgBpxLW19uBpp3Dl6K9K/bRaYdJ22G8Jk=
github.com/fyne-io/gl-js v0.2.0 h1:+EXMLVEa18EfkXBVKhifYB6OGs3HwKO3lUElA0LlAjs=
github.com/fyne-io/gl-js v0.2.0/go.mod h1:ZcepK8vmOYLu96JoxbCKJy2ybr+g1pTnaBDdl7c3ajI=
github.com/fyne-io/glfw-js v0.3.0 h1:d8k2+Y7l+zy2pc7wlGRyPfTgZoqDf3AI4G+2zOWhWUk=
github.com/fyne-io/glfw-js v0.3.0/go.mod h1:Ri6te7rdZtBgBpxLW19uBpp3Dl6K9K/bRaYdJ22G8Jk=
github.com/fyne-io/image v0.1.1 h1:WH0z4H7qfvNUw5l4p3bC1q70sa5+YWVt6HCj7y4VNyA=
github.com/fyne-io/image v0.1.1/go.mod h1:xrfYBh6yspc+KjkgdZU/ifUC9sPA5Iv7WYUBzQKK7JM=
github.com/fyne-io/oksvg v0.1.0 h1:7EUKk3HV3Y2E+qypp3nWqMXD7mum0hCw2KEGhI1fnBw=
github.com/fyne-io/oksvg v0.1.0/go.mod h1:dJ9oEkPiWhnTFNCmRgEze+YNprJF7YRbpjgpWS4kzoI=
github.com/fyne-io/oksvg v0.2.0 h1:mxcGU2dx6nwjJsSA9PCYZDuoAcsZ/OuJlvg/Q9Njfo8=
github.com/fyne-io/oksvg v0.2.0/go.mod h1:dJ9oEkPiWhnTFNCmRgEze+YNprJF7YRbpjgpWS4kzoI=
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 h1:5BVwOaUSBTlVZowGO6VZGw2H/zl9nrd3eCZfYV+NfQA=
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20250301202403-da16c1255728 h1:RkGhqHxEVAvPM0/R+8g7XRwQnHatO0KAuVcwHo8q9W8=
@@ -43,8 +43,8 @@ github.com/hack-pad/go-indexeddb v0.3.2 h1:DTqeJJYc1usa45Q5r52t01KhvlSN02+Oq+tQb
github.com/hack-pad/go-indexeddb v0.3.2/go.mod h1:QvfTevpDVlkfomY498LhstjwbPW6QC4VC/lxYb0Kom0=
github.com/hack-pad/safejs v0.1.1 h1:d5qPO0iQ7h2oVtpzGnLExE+Wn9AtytxIfltcS2b9KD8=
github.com/hack-pad/safejs v0.1.1/go.mod h1:HdS+bKF1NrE72VoXZeWzxFOVQVUSqZJAG0xNCnb+Tio=
github.com/jeandeaual/go-locale v0.0.0-20250421151639-a9d6ed1b3d45 h1:vFdvrlsVU+p/KFBWTq0lTG4fvWvG88sawGlCzM+RUEU=
github.com/jeandeaual/go-locale v0.0.0-20250421151639-a9d6ed1b3d45/go.mod h1:ZDXo8KHryOWSIqnsb/CiDq7hQUYryCgdVnxbj8tDG7o=
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade h1:FmusiCI1wHw+XQbvL9M+1r/C3SPqKrmBaIOYwVfQoDE=
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade/go.mod h1:ZDXo8KHryOWSIqnsb/CiDq7hQUYryCgdVnxbj8tDG7o=
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe972w/cYF+FjW34v27+9Vo5106B4M=
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -59,24 +59,24 @@ github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA=
github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rymdport/portal v0.4.1 h1:2dnZhjf5uEaeDjeF/yBIeeRo6pNI2QAKm7kq1w/kbnA=
github.com/rymdport/portal v0.4.1/go.mod h1:kFF4jslnJ8pD5uCi17brj/ODlfIidOxlgUDTO5ncnC4=
github.com/rymdport/portal v0.4.2 h1:7jKRSemwlTyVHHrTGgQg7gmNPJs88xkbKcIL3NlcmSU=
github.com/rymdport/portal v0.4.2/go.mod h1:kFF4jslnJ8pD5uCi17brj/ODlfIidOxlgUDTO5ncnC4=
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiYWEedbTT0qnSxrCjsVbb7yKY1KE=
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q=
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqdkI8FSpFyZDtCVBc2VmejdNrm5rRQ=
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/yuin/goldmark v1.7.10 h1:S+LrtBjRmqMac2UdtB6yyCEJm+UILZ2fefI4p7o0QpI=
github.com/yuin/goldmark v1.7.10/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
golang.org/x/image v0.26.0 h1:4XjIFEZWQmCZi6Wv8BoxsDhRU3RVnLX04dToTDAEPlY=
golang.org/x/image v0.26.0/go.mod h1:lcxbMFAovzpnJxzXS3nyL83K27tmqtKzIJpctK8YO5c=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA=
github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
golang.org/x/image v0.32.0 h1:6lZQWq75h7L5IWNk0r+SCpUJ6tUVd3v4ZHnbRKLkUDQ=
golang.org/x/image v0.32.0/go.mod h1:/R37rrQmKXtO6tYXAjtDLwQgFLHmhW+V6ayXlxzP2Pc=
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@@ -337,9 +337,14 @@ func relaySend(data RelayCommandData, callback callback) bool {
CommandSeq: commandSeq,
Data: data,
}
if callback != nil {
commandCallbacks[m.CommandSeq] = callback
if callback == nil {
callback = func(err string, response *RelayResponseData) {
if response == nil {
showErrorMessage(err)
}
}
}
commandCallbacks[m.CommandSeq] = callback
commandSeq++
// TODO(p): Handle errors better.

80
xC.c
View File

@@ -653,10 +653,15 @@ input_rl_buffer_destroy (void *input, input_buffer_t input_buffer)
HISTORY_STATE *state = history_get_history_state ();
history_set_history_state (buffer->history);
// TODO: Actually figure out why these cause crashes later.
#if RL_READLINE_VERSION <= 0x0802
rl_clear_history ();
// rl_clear_history just removes history entries,
// we have to reclaim memory for their actual container ourselves
free (buffer->history->entries);
#endif
free (buffer->history);
buffer->history = NULL;
@@ -1818,6 +1823,7 @@ struct client
uint32_t event_seq; ///< Outgoing message counter
bool initialized; ///< Initial sync took place
bool closing; ///< We're closing the connection
struct poller_fd socket_event; ///< The socket can be read/written to
};
@@ -1875,7 +1881,7 @@ enum server_state
IRC_CONNECTED, ///< Trying to register
IRC_REGISTERED, ///< We can chat now
IRC_CLOSING, ///< Flushing output before shutdown
IRC_HALF_CLOSED ///< Connection shutdown from our side
IRC_HALF_CLOSED ///< Connection shut down from our side
};
/// Convert an IRC identifier character to lower-case
@@ -2263,14 +2269,6 @@ struct app_context
struct str_map servers; ///< Our servers
// Relay:
int relay_fd; ///< Listening socket FD
struct client *clients; ///< Our relay clients
/// A single message buffer to prepare all outcoming messages within
struct relay_event_message relay_message;
// Events:
struct poller_fd tty_event; ///< Terminal input event
@@ -2322,6 +2320,14 @@ struct app_context
char *editor_filename; ///< The file being edited by user
int terminal_suspended; ///< Terminal suspension level
// Relay:
int relay_fd; ///< Listening socket FD
struct client *clients; ///< Our relay clients
/// A single message buffer to prepare all outcoming messages within
struct relay_event_message relay_message;
// Plugins:
struct plugin *plugins; ///< Loaded plugins
@@ -2392,8 +2398,6 @@ app_context_init (struct app_context *self)
self->config = config_make ();
poller_init (&self->poller);
self->relay_fd = -1;
self->servers = str_map_make ((str_map_free_fn) server_unref);
self->servers.key_xfrm = tolower_ascii_strxfrm;
@@ -2417,6 +2421,8 @@ app_context_init (struct app_context *self)
self->nick_palette =
filter_color_cube_for_acceptable_nick_colors (&self->nick_palette_len);
self->relay_fd = -1;
}
static void
@@ -4152,8 +4158,11 @@ client_kill (struct client *c)
static void
client_update_poller (struct client *c, const struct pollfd *pfd)
{
// In case of closing without any data in the write buffer,
// we don't actually need to be able to write to the socket,
// but the condition should be quick to satisfy.
int new_events = POLLIN;
if (c->write_buffer.len)
if (c->write_buffer.len || c->closing)
new_events |= POLLOUT;
hard_assert (new_events != 0);
@@ -4168,9 +4177,7 @@ relay_send (struct client *c)
{
struct relay_event_message *m = &c->ctx->relay_message;
m->event_seq = c->event_seq++;
// TODO: Also don't try sending anything if half-closed.
if (!c->initialized || c->socket_fd == -1)
if (!c->initialized || c->closing || c->socket_fd == -1)
return;
// liberty has msg_{reader,writer} already, but they use 8-byte lengths.
@@ -4180,12 +4187,18 @@ relay_send (struct client *c)
|| (frame_len = c->write_buffer.len - frame_len_pos - 4) > UINT32_MAX)
{
print_error ("serialization failed, killing client");
client_kill (c);
return;
// We can't kill the client immediately,
// because more relay_send() calls may follow.
c->write_buffer.len = frame_len_pos;
c->closing = true;
}
else
{
uint32_t len = htonl (frame_len);
memcpy (c->write_buffer.str + frame_len_pos, &len, sizeof len);
}
uint32_t len = htonl (frame_len);
memcpy (c->write_buffer.str + frame_len_pos, &len, sizeof len);
client_update_poller (c, NULL);
}
@@ -15604,28 +15617,31 @@ client_process_message (struct client *c,
return true;
}
bool acknowledge = true;
switch (m->data.command)
{
case RELAY_COMMAND_HELLO:
c->initialized = true;
if (m->data.hello.version != RELAY_VERSION)
{
// TODO: This should send back an error message and shut down.
log_global_error (c->ctx,
"Protocol version mismatch, killing client");
return false;
relay_prepare_error (c->ctx,
m->command_seq, "Protocol version mismatch");
relay_send (c);
c->closing = true;
return true;
}
c->initialized = true;
client_resync (c);
break;
case RELAY_COMMAND_PING:
relay_prepare_response (c->ctx, m->command_seq)
->data.command = RELAY_COMMAND_PING;
relay_send (c);
break;
case RELAY_COMMAND_ACTIVE:
reset_autoaway (c->ctx);
break;
case RELAY_COMMAND_BUFFER_COMPLETE:
acknowledge = false;
client_process_buffer_complete (c, m->command_seq, buffer,
&m->data.buffer_complete);
break;
@@ -15639,13 +15655,21 @@ client_process_message (struct client *c,
buffer_toggle_unimportant (c->ctx, buffer);
break;
case RELAY_COMMAND_BUFFER_LOG:
acknowledge = false;
client_process_buffer_log (c, m->command_seq, buffer);
break;
default:
acknowledge = false;
log_global_debug (c->ctx, "Unhandled client command");
relay_prepare_error (c->ctx, m->command_seq, "Unknown command");
relay_send (c);
}
if (acknowledge)
{
relay_prepare_response (c->ctx, m->command_seq)
->data.command = m->data.command;
relay_send (c);
}
return true;
}
@@ -15667,7 +15691,7 @@ client_process_buffer (struct client *c)
break;
struct relay_command_message m = {};
bool ok = client_process_message (c, &r, &m);
bool ok = c->closing || client_process_message (c, &r, &m);
relay_command_message_free (&m);
if (!ok)
return false;
@@ -15739,7 +15763,11 @@ on_client_ready (const struct pollfd *pfd, void *user_data)
{
struct client *c = user_data;
if (client_try_read (c) && client_try_write (c))
{
client_update_poller (c, pfd);
if (c->closing && !c->write_buffer.len)
client_kill (c);
}
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

53
xC.lxdr
View File

@@ -1,7 +1,8 @@
// Backwards-compatible protocol version.
const VERSION = 1;
const VERSION = 2;
// From the frontend to the relay.
// All commands receive either an Event.RESPONSE, or an Event.ERROR.
struct CommandMessage {
// The command sequence number will be repeated in responses
// in the respective fields.
@@ -32,13 +33,10 @@ struct CommandMessage {
// XXX: Perhaps this should rather be handled through a /buffer command.
case BUFFER_TOGGLE_UNIMPORTANT:
string buffer_name;
case PING_RESPONSE:
u32 event_seq;
// Only these commands may produce Event.RESPONSE, as below,
// but any command may produce an error.
case PING:
void;
case PING_RESPONSE:
u32 event_seq;
case BUFFER_COMPLETE:
string buffer_name;
string text;
@@ -52,6 +50,9 @@ struct CommandMessage {
struct EventMessage {
u32 event_seq;
union EventData switch (enum Event {
ERROR,
RESPONSE,
PING,
BUFFER_LINE,
BUFFER_UPDATE,
@@ -64,12 +65,28 @@ struct EventMessage {
SERVER_UPDATE,
SERVER_RENAME,
SERVER_REMOVE,
ERROR,
RESPONSE,
} event) {
// Restriction: command_seq strictly follows the sequence received
// by the relay, across both of these replies.
case ERROR:
u32 command_seq;
string error;
case RESPONSE:
u32 command_seq;
union ResponseData switch (Command command) {
case BUFFER_COMPLETE:
u32 start;
string completions<>;
case BUFFER_LOG:
// UTF-8, but not guaranteed.
u8 log<>;
default:
// Reception acknowledged.
void;
} data;
case PING:
void;
case BUFFER_LINE:
string buffer_name;
// Whether the line should also be displayed in the active buffer.
@@ -188,23 +205,5 @@ struct EventMessage {
string new;
case SERVER_REMOVE:
string server_name;
// Restriction: command_seq strictly follows the sequence received
// by the relay, across both of these replies.
case ERROR:
u32 command_seq;
string error;
case RESPONSE:
u32 command_seq;
union ResponseData switch (Command command) {
case PING:
void;
case BUFFER_COMPLETE:
u32 start;
string completions<>;
case BUFFER_LOG:
// UTF-8, but not guaranteed.
u8 log<>;
} data;
} data;
};

View File

@@ -173,8 +173,11 @@ class RelayRPC {
func send(data: RelayCommandData, callback: Callback? = nil) {
self.commandSeq += 1
let m = RelayCommandMessage(commandSeq: self.commandSeq, data: data)
if let callback = callback {
self.commandCallbacks[m.commandSeq] = callback
self.commandCallbacks[m.commandSeq] = callback ?? { error, data in
if data == nil {
NSSound.beep()
Logger().warning("\(error)")
}
}
var w = RelayWriter()

View File

@@ -247,16 +247,16 @@ func main() {
flag.PrintDefaults()
}
flag.Parse()
if flag.NArg() < 1 {
flag.Usage()
os.Exit(2)
}
if *version {
fmt.Printf("%s %s\n", projectName, projectVersion)
return
}
if flag.NArg() < 1 {
flag.Usage()
os.Exit(2)
}
text, err := io.ReadAll(os.Stdin)
if err != nil {
log.Fatalln(err)

View File

@@ -1,6 +1,6 @@
module janouch.name/xK/xP
go 1.21
go 1.22
toolchain go1.23.2

View File

@@ -1,4 +1,4 @@
// Copyright (c) 2022 - 2024, 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)]),

View File

@@ -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
@@ -167,9 +176,11 @@ func handleWS(w http.ResponseWriter, r *http.Request) {
}
// AppleWebKit can be broken with compression.
if agent := r.UserAgent(); strings.Contains(agent, " Version/") &&
(strings.HasPrefix(agent, "Mozilla/5.0 (Macintosh; ") ||
strings.HasPrefix(agent, "Mozilla/5.0 (iPhone; ")) {
// It would be more reliable to check for 'ApplePaySession' in window in JS,
// and have us disable compression based on a query parameter.
if agent := r.UserAgent(); (strings.Contains(agent, " Version/") &&
strings.HasPrefix(agent, "Mozilla/5.0 (Macintosh; ")) ||
strings.HasPrefix(agent, "Mozilla/5.0 (iPhone; ") {
opts.CompressionMode = websocket.CompressionDisabled
}
@@ -240,21 +251,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 +272,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 +333,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))

2
xR/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/xR
/proto.go

17
xR/Makefile Normal file
View File

@@ -0,0 +1,17 @@
.POSIX:
AWK = env LC_ALL=C awk
tools = ../liberty/tools
generated = proto.go
outputs = xR $(generated)
all: $(outputs)
generate: $(generated)
proto.go: $(tools)/lxdrgen.awk $(tools)/lxdrgen-go.awk ../xC.lxdr
$(AWK) -f $(tools)/lxdrgen.awk -f $(tools)/lxdrgen-go.awk \
-v PrefixCamel=Relay ../xC.lxdr > $@
xR: xR.go ../xK-version $(generated)
go build -ldflags "-X 'main.projectVersion=$$(cat ../xK-version)'" -o $@ \
-gcflags=all="-N -l"
clean:
rm -f $(outputs)

5
xR/go.mod Normal file
View File

@@ -0,0 +1,5 @@
module janouch.name/xK/xR
go 1.23.0
toolchain go1.24.0

41
xR/xR.adoc Normal file
View File

@@ -0,0 +1,41 @@
xR(1)
=====
:doctype: manpage
:manmanual: xK Manual
:mansource: xK {release-version}
Name
----
xR - xC relay protocol analyzer
Synopsis
--------
*xR* [_OPTION_]... RELAY-ADDRESS...
Description
-----------
*xR* connects to an *xC* relay and prints all incoming events one per line
in JSON format. The JSON objects have two additional fields:
when::
The time of reception (or sending) as a nanosecond precision
RFC 3339 UTC timestamp.
raw::
The incoming event (or outgoing command) in raw binary form.
Options
-------
*-debug*::
Print any outgoing commands as well, which may help in debugging any issues.
*-version*::
Output version information and exit.
Reporting bugs
--------------
Use https://git.janouch.name/p/xK to report bugs, request features,
or submit pull requests.
See also
--------
*xC*(1)

134
xR/xR.go Normal file
View File

@@ -0,0 +1,134 @@
// Copyright (c) 2025, Přemysl Eric Janouch <p@janouch.name>
// SPDX-License-Identifier: 0BSD
package main
import (
"encoding/binary"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"log"
"net"
"os"
"time"
)
var (
debug = flag.Bool("debug", false, "enable debug output")
version = flag.Bool("version", false, "show version and exit")
projectName = "xR"
projectVersion = "?"
)
func now() string {
return time.Now().UTC().Format(time.RFC3339Nano)
}
func relayReadFrame(r io.Reader) bool {
var length uint32
if err := binary.Read(
r, binary.BigEndian, &length); errors.Is(err, io.EOF) {
return false
} else if err != nil {
log.Fatalln("Event receive failed: " + err.Error())
}
b := make([]byte, length)
if _, err := io.ReadFull(r, b); errors.Is(err, io.EOF) {
return false
} else if err != nil {
log.Fatalln("Event receive failed: " + err.Error())
}
m := struct {
When string `json:"when"`
Binary []byte `json:"raw"`
RelayEventMessage
}{
When: now(),
Binary: b,
}
if after, ok := m.RelayEventMessage.ConsumeFrom(b); !ok {
log.Println("Event deserialization failed")
} else if len(after) != 0 {
log.Println("Event deserialization failed: trailing data")
return true
}
j, err := json.Marshal(m)
if err != nil {
log.Fatalln("Event marshalling failed: " + err.Error())
}
fmt.Printf("%s\n", j)
return true
}
func run(addressConnect string) {
conn, err := net.Dial("tcp", addressConnect)
if err != nil {
log.Println("Connection failed: " + err.Error())
return
}
defer conn.Close()
// We can only support this one protocol version
// that proto.go has been generated for.
m := RelayCommandMessage{CommandSeq: 0, Data: RelayCommandData{
Variant: &RelayCommandDataHello{Version: RelayVersion},
}}
b, ok := m.AppendTo(make([]byte, 4))
if !ok {
log.Fatalln("Command serialization failed")
}
binary.BigEndian.PutUint32(b[:4], uint32(len(b)-4))
if _, err := conn.Write(b); err != nil {
log.Fatalln("Command send failed: " + err.Error())
}
// You can differentiate the direction by the presence
// of .data.command or .data.event.
if *debug {
j, err := json.Marshal(struct {
When string `json:"when"`
Binary []byte `json:"raw"`
RelayCommandMessage
}{
When: now(),
Binary: b,
RelayCommandMessage: m,
})
if err != nil {
log.Fatalln("Command marshalling failed: " + err.Error())
}
fmt.Printf("%s\n", j)
}
for relayReadFrame(conn) {
}
}
func main() {
flag.Usage = func() {
fmt.Fprintf(flag.CommandLine.Output(),
"Usage: %s [OPTION...] CONNECT\n\n", os.Args[0])
flag.PrintDefaults()
}
flag.Parse()
if *version {
fmt.Printf("%s %s (relay protocol version %d)\n",
projectName, projectVersion, RelayVersion)
return
}
if flag.NArg() != 1 {
flag.Usage()
os.Exit(1)
}
// TODO(p): This program should be able to run as a filter as well.
run(flag.Arg(0))
}

View File

@@ -179,6 +179,14 @@ beep()
// --- Networking --------------------------------------------------------------
static void
on_relay_generic_response(
std::wstring error, const Relay::ResponseData *response)
{
if (!response)
show_error_message(QString::fromStdWString(error));
}
static void
relay_send(Relay::CommandData *data, Callback callback = {})
{
@@ -190,6 +198,8 @@ relay_send(Relay::CommandData *data, Callback callback = {})
if (callback)
g.command_callbacks[m.command_seq] = std::move(callback);
else
g.command_callbacks[m.command_seq] = on_relay_generic_response;
auto len = qToBigEndian<uint32_t>(w.data.size());
auto prefix = reinterpret_cast<const char *>(&len);

View File

@@ -221,6 +221,14 @@ relay_try_write(std::wstring &error)
return true;
}
static void
on_relay_generic_response(
std::wstring error, const Relay::ResponseData *response)
{
if (!response)
show_error_message(error.c_str());
}
static void
relay_send(Relay::CommandData *data, Callback callback = {})
{
@@ -232,6 +240,8 @@ relay_send(Relay::CommandData *data, Callback callback = {})
if (callback)
g.command_callbacks[m.command_seq] = std::move(callback);
else
g.command_callbacks[m.command_seq] = on_relay_generic_response;
uint32_t len = htonl(w.data.size());
uint8_t *prefix = reinterpret_cast<uint8_t *>(&len);