105 Commits

Author SHA1 Message Date
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
4cf8c394b9 xA: bump Fyne to 2.6.0
All checks were successful
Arch Linux AUR Success
OpenBSD 7.6 Success
Alpine 3.21 Success
Alpine 3.20 Abandoned
Not much has actually changed.
2025-04-26 15:04:49 +02:00
e225306419 CMakeLists.txt: actually name an option 2025-01-12 11:02:41 +01:00
858734384b xC: regard more characters as highlight delimiters
All checks were successful
Alpine 3.20 Success
Arch Linux AUR Success
OpenBSD 7.5 Success
Almost 10 years of a poor decision.
2025-01-08 06:40:53 +01:00
278a7f95a9 Port the integration test from expect to wdye 2025-01-08 06:40:52 +01:00
a8575ab875 Bump version, update NEWS
All checks were successful
Alpine 3.20 Success
Arch Linux AUR Success
OpenBSD 7.5 Success
A few annoying bugs have been fixed.
2024-12-19 03:15:34 +01:00
081525f5be Update README.adoc 2024-12-19 03:08:14 +01:00
6ac2ac5511 xT: improve MSYS2 build
The static Qt 6 package is unusable.
2024-12-19 03:08:13 +01:00
f6483489c2 xC: fix crash with too many topic formatting items
All checks were successful
Arch Linux AUR Success
OpenBSD 7.5 Success
Alpine 3.20 Success
Manually constructed formatters have no sentinel value.

This is a one-line change in relay_prepare_channel_buffer_update(),
however the whole block of "Relay output" code has been moved down,
resolving one TODO and rendering two function prototypes unnecessary.
2024-12-18 11:48:17 +01:00
ed5ac1815b xT: figure out basic packaging
All checks were successful
Alpine 3.20 Success
Arch Linux AUR Success
OpenBSD 7.5 Success
2024-12-17 06:34:45 +01:00
509cb9f4dd xC: fix newer Readline, allow stdin streaming
All checks were successful
Alpine 3.20 Success
Arch Linux AUR Success
OpenBSD 7.5 Success
Also update NEWS.
2024-12-17 03:31:00 +01:00
b3684c4d9f Bump liberty
All checks were successful
Alpine 3.20 Success
Arch Linux AUR Success
OpenBSD 7.5 Success
2024-12-16 09:37:29 +01:00
918c589c65 Add a Qt Widgets frontend to xC
All checks were successful
Alpine 3.20 Success
Arch Linux AUR Success
OpenBSD 7.5 Success
This is very much a work in progress, though functional.

Qt Widgets are basically non-working on Android,
though Qt Quick requires a radically different approach.
2024-12-15 06:57:28 +01:00
21095a11d6 xM: fix build regression
All checks were successful
Alpine 3.20 Success
Arch Linux AUR Success
OpenBSD 7.5 Success
2024-11-26 13:10:05 +01:00
a22baa4b55 xA: prevent sound playback GC
All checks were successful
Alpine 3.20 Success
Arch Linux AUR Success
OpenBSD 7.5 Success
The beep sound could be cut short.
2024-11-14 16:48:44 +01:00
b3e545e0bb xP: bump copyright years
All checks were successful
Alpine 3.20 Success
Arch Linux AUR Success
OpenBSD 7.5 Success
2024-11-14 16:27:56 +01:00
cd76702ab2 xA/xW: dehighlight current buffer appropriately 2024-11-14 16:14:54 +01:00
977b073b58 xA: enforce internal icon from the start 2024-11-14 16:14:49 +01:00
46be4836df xW: print the separator line at the end of buffer 2024-11-14 13:50:51 +01:00
05a41b2629 xA/xM/xW: refresh renamed buffers correctly
Rendering takes the current buffer into account,
so change its value before using it, not afterwards.

The order happened to not matter on at least Windows,
because we just queue a message.
2024-11-14 11:41:09 +01:00
a62ed5bbac xA/xM: refresh buffer list on dehighlight 2024-11-14 11:41:08 +01:00
9c9776bacd xA: make the log effectively read-only
All checks were successful
Arch Linux AUR Success
OpenBSD 7.5 Success
Alpine 3.20 Success
2024-11-13 10:29:11 +01:00
086b879ab8 xA: add a "generate" target to the Makefile
So that Fyne tools can be run without building the default binary.
2024-11-12 17:11:23 +01:00
214c349869 xA: limit buffer length
All checks were successful
Arch Linux AUR Success
OpenBSD 7.5 Success
Alpine 3.20 Success
2024-11-12 16:19:53 +01:00
3d975c9437 xA: downgrade Go version requirement
All checks were successful
Alpine 3.20 Success
Arch Linux AUR Success
OpenBSD 7.5 Success
We need 1.22 for the "for" loop variable scope change.
2024-11-12 13:53:55 +01:00
fce8fd40cc Bump xP dependencies
Some checks failed
Alpine 3.20 Scripts failed
Arch Linux AUR Success
OpenBSD 7.5 Success
2024-11-12 12:59:33 +01:00
3597ab9420 Update README.adoc 2024-11-12 12:41:09 +01:00
1635a730e8 Add a Fyne frontend for xC
Some checks failed
Alpine 3.20 Scripts failed
Arch Linux AUR Scripts failed
OpenBSD 7.5 Success
It is fairly mediocre all around, but also generally usable,
natively covering mobile platforms.
2024-11-12 12:02:10 +01:00
a64b1152a1 Bump liberty 2024-11-11 21:42:28 +01:00
a011b57ce2 Bump liberty
All checks were successful
Alpine 3.20 Success
Arch Linux AUR Success
OpenBSD 7.5 Success
2024-11-07 11:07:36 +01:00
b1ee295345 xP: update variable name 2024-11-04 07:40:14 +01:00
872f2d7c59 Fix calloc argument order
All checks were successful
Alpine 3.20 Success
Arch Linux AUR Success
OpenBSD 7.5 Success
2024-08-08 09:13:25 +02:00
f15d887dcd Bump liberty
All checks were successful
Alpine 3.20 Success
Arch Linux AUR Success
OpenBSD 7.5 Success
2024-08-08 09:02:25 +02:00
841bc00c51 xP: cleanup
I had forgotten about the auto-redraw system.
2024-07-28 13:42:28 +02:00
8afe4f8aad Improve wording in the last NEWS entry 2024-07-28 13:26:24 +02:00
73cc8f448a Bump version, update NEWS
All checks were successful
Alpine 3.20 Success
Arch Linux AUR Success
OpenBSD 7.5 Success
2024-07-28 07:29:15 +02:00
4565afe294 xC: expand a comment 2024-07-28 07:15:41 +02:00
3ad8c79de8 xC: handle multiline server commands properly
All checks were successful
Alpine 3.20 Success
Arch Linux AUR Success
OpenBSD 7.5 Success
Additional lines might have been passed to the server intact
as part of an argument, but we have /quote for that.
2024-07-28 04:10:30 +02:00
12fc3c228a xP: reset highlight state once reaching buffer end
All checks were successful
Alpine 3.20 Success
Arch Linux AUR Success
OpenBSD 7.5 Success
2024-07-28 03:44:37 +02:00
175533a5e9 xP: don't interrupt IME composition
All checks were successful
Alpine 3.20 Success
Arch Linux AUR Success
OpenBSD 7.5 Success
On Vivaldi/macOS, pressing Enter would send the input and still keep
editing it as it was.
2024-07-04 20:06:59 +02:00
a9b46141a9 xS/xN: add test targets
All checks were successful
Alpine 3.19 Success
Arch Linux AUR Success
OpenBSD 7.3 Success
2024-04-10 13:59:33 +02:00
c38cca3b92 Bump liberty
All checks were successful
Arch Linux AUR Success
Alpine 3.19 Success
2024-04-09 17:08:40 +02:00
aee7540faa Update README.adoc and xN usage output 2024-04-04 21:25:17 +02:00
53ba996ec9 Add a simple IRC notifier utility
All checks were successful
Arch Linux AUR Success
2024-04-03 15:56:33 +02:00
d450c6cc5f xP: do not send the Referrer header 2024-03-04 16:15:22 +01:00
f8ea1634c4 Bump liberty 2024-03-04 16:15:22 +01:00
ef257cd575 xP: avoid expensive updates/refreshes 2024-01-06 23:44:11 +01:00
69eccc7065 xP: don't let buffers grow indefinitely
Primarily for performance reasons.
2024-01-06 21:17:18 +01:00
13d2ff115b xM: improve the bundle icon a bit 2023-09-04 07:06:03 +02:00
9e4692bb09 xM: generate and use a bundle icon 2023-09-03 02:13:14 +02:00
1c4343058d Add a Cocoa frontend for xC
Some work remains to be done to get it to be even as good
as the Win32 frontend, but it's generally usable.
2023-09-01 01:12:51 +02:00
e5156cddbf xW: render leaked lines a bit more accurately
There is no need to reset all text attributes, just the colour.
2023-08-25 22:48:31 +02:00
34521e61c1 xP/xW: fix buffer rename handling
Maintaining string pointers to the current/last buffer
means that renames invalidate them.
2023-08-25 22:48:31 +02:00
c22dd67fc1 xC: send missing relay events for newly added servers 2023-08-25 22:48:27 +02:00
274d5f03e7 xC: give the /away command a proper handler
Multiple words should be passed to the server as a single argument.
2023-08-25 22:46:43 +02:00
2f19e5a733 xW: improve command sending 2023-07-29 02:15:24 +02:00
b9cdabca5d xC: fix relay handling of missing log files
Intermediate error messages would trash the prepared static buffer.
2023-07-28 04:30:45 +02:00
f60ca43156 xW: do not unnecessarily enter compatibility mode 2023-07-28 04:30:45 +02:00
afe4e61f08 xW: mark a footgun
These messages are used by IsDialogMessage(), and use the WM_USER range.
2023-07-27 23:08:16 +02:00
8d9d1c60ec xW: make Up/Down go through input history
The input field isn't multiline, so this doesn't pose an issue.
Otherwise, we'd have to check if we're on the top line first.
2023-07-27 16:35:54 +02:00
8c1464822b xW: don't delay sending out pongs 2023-07-27 16:19:32 +02:00
fcd1b8e011 xW: improve beeping
This adds yet another build dependency,
but it's better than the alternatives of handling it in code.
2023-07-27 16:06:41 +02:00
3d345987c3 xW: cleanup 2023-07-27 02:37:20 +02:00
3e37efd9cd xW: show a connect dialog when run without args 2023-07-27 01:28:52 +02:00
efb25b8aae xW: un-highlight the icon when activating buffers 2023-07-26 16:07:21 +02:00
e72793e315 xW: make newline before unread marker conditional 2023-07-26 16:07:20 +02:00
5a412ab6e2 xW: handle WM_SYSCOLORCHANGE 2023-07-26 16:07:20 +02:00
81bc578773 xW: add missing date change handling 2023-07-26 03:59:25 +02:00
100de5ac2d xC: fix Readline 6.3 compatibility 2023-07-24 07:59:22 +02:00
c157d3369f xP: make Page Up/Down in editor scroll the buffer
Just like in xW recently.  It is unlikely that the user would want
to use these keys to move the cursor.  Ctrl+Home/End still work,
as does holding Up/Down arrows.

Also stop using the deprecated and somewhat cryptic keyCode.
2023-07-23 00:20:32 +02:00
8b5ea67aff xW: fix Clang build 2023-07-21 12:37:01 +02:00
6f02af814f xW: store the largest program icon in PNG format
This shaves off about half a megabyte.
2023-07-16 08:35:39 +02:00
90859107c8 xW: set version information 2023-07-15 23:35:46 +02:00
0219dbd026 Add a Win32 frontend for xC
This has been more of an exercise.  The performance of Msftedit.dll
is rather abysmal, and its interface isn't the most accomodating.

That said, the frontend is quite usable, at least on Windows 10+.
2023-07-15 17:00:21 +02:00
1da4699a7a Cleanup 2023-07-09 09:35:16 +02:00
9e993c50e6 xC: don't crash with unknown terminals
It would be possible to avoid using cur_term fields in this case,
but the program would likely be of little use anyway.
2023-07-07 10:43:15 +02:00
b3e9218b23 Fix Cygwin build warnings 2023-07-07 09:53:20 +02:00
bc8867eb22 Fix the integration test
IRCv3 capabilities broke it a bit.

Also change it so that it doesn't destroy existing configuration.
2023-07-05 00:16:55 +02:00
ec33adba35 Update README.adoc 2023-07-04 23:50:01 +02:00
6f596f1dcb Move project version to file, add xS manual page
So far Go applications remain independent to handle Nix's inability
to easily combine them with the CMake part.

There is also no "install" target, because any packagers will want to
adjust installation paths manually, and there is no configure step.
2023-07-04 23:26:05 +02:00
abcff46dc4 xC: fix an OpenBSD build warning
sys/cdefs.h makes _XOPEN_SOURCE cause _POSIX_C_SOURCE to be overriden.
2023-07-04 06:31:02 +02:00
8d9ce92758 README.adoc: update package information 2023-07-01 22:02:10 +02:00
4bb9449e47 Fix the static analysis test
Adjust its query so that it doesn't cause a particular false positive.
2023-06-16 19:45:12 +02:00
50f70f93bb xC: fix a harmless copy-paste error 2023-06-13 09:02:20 +02:00
3f9a365d36 xC: improve the --format mode
Avoid having formatting spill over the rest of the line,
by placing the automatic formatting reset before newlines.

Also handle longer lines properly.
2023-05-22 04:44:01 +02:00
9932b35a10 xP: highlight hovered buffer list items
To make it apparent which one would be closed by a middle click.
2023-04-14 10:58:19 +02:00
af5f209c53 xP: make middle click close buffers
As if they were tabs, to save pointless typing.
2023-04-13 04:26:40 +02:00
6bfe577f1b xP: make the buffer list selectable by Vimium 2023-04-05 23:10:41 +02:00
1079189381 xP: render date changes as they happen 2023-01-25 00:31:57 +01:00
c58b772905 xP: use the correct log function 2023-01-25 00:28:03 +01:00
26ed2dbc77 xC: fully synchronize input history with frontends
The missing parts were:

 - frontends to client
 - client to frontends after the initial sync
 - frontend to other frontends
2022-10-05 00:55:59 +02:00
4b7258cba0 xP: fix ESC H detection on Macintosh systems 2022-10-04 20:17:31 +02:00
9dc3dd02f3 xP: disable WebSocket compression on Safari
Wildly known to be broken.
2022-10-04 01:17:35 +02:00
a7c3ed7cc1 xC: clean up 2022-09-30 18:30:03 +02:00
807a8c37d9 Bump liberty, improve fallback manual page output 2022-09-30 18:17:23 +02:00
c4707e2803 xC/xP: send buffer input history during sync
This transfer is currenly quite simplistic,
but it paves the way for further extensions.
2022-09-30 17:36:29 +02:00
46d68eacce Move protocol code generators to liberty
This part of the project is now more or less stable.
2022-09-30 03:24:24 +02:00
86278c154c Clean up protocol code generators 2022-09-30 03:24:13 +02:00
941ee2f10c xP: fix automatic scrolling down
Showing channel logs cancelled the AbortController forever.
Thus store it within vnodes.
2022-09-28 21:29:08 +02:00
5b57e9b41b xC/xP: fix unseen message counting
xC: advance unread message counters even with leaked messages,
and don't unnecessarily set the highlighted flag.  Plus clean up.

xP: make leaked non-unimportant messages advance the counter
for unimportant messages, so that the buffer doesn't get emboldened.
2022-09-28 21:20:59 +02:00
4d99690b89 xS: parse project version from CMakeLists.txt 2022-09-27 23:48:12 +02:00
7c74e6615d xD: use SHA-256 for certificate fingerprints
Just like xS.  2.0.0 is the ideal time for such a breaking change.
2022-09-26 13:58:08 +02:00
614fd98fc1 Update README 2022-09-26 13:42:45 +02:00
65 changed files with 9696 additions and 2196 deletions

View File

@@ -1,6 +1,11 @@
# Ubuntu 18.04 LTS and OpenBSD 6.4
cmake_minimum_required (VERSION 3.10)
project (xK VERSION 1.5.0
file (READ xK-version project_version)
configure_file (xK-version xK-version.tag COPYONLY)
string (STRIP "${project_version}" project_version)
project (xK VERSION "${project_version}"
DESCRIPTION "IRC daemon, bot, TUI client and its web frontend" LANGUAGES C)
# Options
@@ -18,9 +23,6 @@ if ("${CMAKE_C_COMPILER_ID}" MATCHES "GNU" OR CMAKE_COMPILER_IS_GNUCC)
set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wextra -Wno-unused-function")
endif ()
# Version
set (project_version "${PROJECT_VERSION}")
# Try to append commit ID if it follows a version tag. It might be nicer if
# we could also detect dirty worktrees but that's very hard to get right.
# If we didn't need this for CPack, we could use add_custom_command to generate
@@ -162,14 +164,15 @@ add_custom_target (replies DEPENDS ${PROJECT_BINARY_DIR}/xD-replies.c)
add_custom_command (OUTPUT xC-proto.c
COMMAND env LC_ALL=C awk
-f ${PROJECT_SOURCE_DIR}/xC-gen-proto.awk
-f ${PROJECT_SOURCE_DIR}/xC-gen-proto-c.awk
${PROJECT_SOURCE_DIR}/xC-proto > xC-proto.c
-f ${PROJECT_SOURCE_DIR}/liberty/tools/lxdrgen.awk
-f ${PROJECT_SOURCE_DIR}/liberty/tools/lxdrgen-c.awk
-v PrefixCamel=Relay
${PROJECT_SOURCE_DIR}/xC.lxdr > xC-proto.c
DEPENDS
${PROJECT_SOURCE_DIR}/xC-gen-proto.awk
${PROJECT_SOURCE_DIR}/xC-gen-proto-c.awk
${PROJECT_SOURCE_DIR}/xC-proto
COMMENT "Generating xC relay protocol code")
${PROJECT_SOURCE_DIR}/liberty/tools/lxdrgen.awk
${PROJECT_SOURCE_DIR}/liberty/tools/lxdrgen-c.awk
${PROJECT_SOURCE_DIR}/xC.lxdr
COMMENT "Generating xC relay protocol code" VERBATIM)
add_custom_target (xC-proto DEPENDS ${PROJECT_BINARY_DIR}/xC-proto.c)
# Build
@@ -207,6 +210,11 @@ if (BUILD_TESTING)
add_test (NAME custom-static-analysis
COMMAND ${PROJECT_SOURCE_DIR}/test-static)
endif ()
option (BUILD_TESTING_WDYE "Build the integration test" OFF)
if (BUILD_TESTING_WDYE)
add_subdirectory (liberty/tools/wdye)
add_test (NAME integration COMMAND wdye "${PROJECT_SOURCE_DIR}/test.lua")
endif ()
# Various clang-based diagnostics, loads of fake positives and spam
file (GLOB clang_tidy_sources *.c)
@@ -260,8 +268,9 @@ foreach (page xB xC xD)
else ()
set (ASCIIMAN ${PROJECT_SOURCE_DIR}/liberty/tools/asciiman.awk)
add_custom_command (OUTPUT ${page_output}
COMMAND env LC_ALL=C awk -f ${ASCIIMAN}
"${PROJECT_SOURCE_DIR}/${page}.adoc" > ${page_output}
COMMAND env LC_ALL=C asciidoc-release-version=${project_version}
awk -f ${ASCIIMAN} "${PROJECT_SOURCE_DIR}/${page}.adoc"
> ${page_output}
DEPENDS ${page}.adoc ${ASCIIMAN}
COMMENT "Generating man page for ${page}" VERBATIM)
endif ()

View File

@@ -1,4 +1,4 @@
Copyright (c) 2014 - 2022, 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.

43
NEWS
View File

@@ -1,4 +1,37 @@
2.0.0 (Unreleased)
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
* xC: fixed keyboard EOF behaviour with Readline >= 8.0
* xC: made it possible to stream commands into the binary
* xM/xW: various bugfixes
* Added a Fyne frontend for xC called xA
* Added a Qt Widgets frontend for xC called xT
2.0.0 (2024-07-28) "Perfect Is the Enemy of Good"
* xD: now using SHA-256 for client certificate fingerprints
* xD: implemented WALLOPS, choosing to make it target even non-operators
@@ -14,6 +47,8 @@
* xC: replaced behaviour.save_on_quit with general.autosave
* xC: the servers.*.command configuration option now supports multiple lines
* xC: improved pager integration capabilities
* xC: unsolicited JOINs will no longer automatically activate the buffer
@@ -29,8 +64,14 @@
* Added a web frontend for xC called xP
* Added a Win32 frontend for xC called xW
* Added a Cocoa frontend for xC called xM
* Added a Go port of xD called xS
* Added a simple notifier called xN
1.5.0 (2021-12-21) "The Show Must Go On"

View File

@@ -1,9 +1,10 @@
xK
==
'xK' (chat kit) is an IRC software suite consisting of a daemon, bot, terminal
client, and a web frontend for the client. It's all you're ever going to
need for chatting, so long as you can make do with slightly minimalist software.
'xK' (chat kit) is an IRC software suite consisting of a daemon, bot, notifier,
terminal client, and web/Windows/macOS/Linux/FreeBSD/Android/iOS frontends
for the client. It's all you're ever going to need for chatting, so long as
you can make do with slightly minimalist software.
They're all lean on dependencies, and offer a maximally permissive licence.
@@ -32,6 +33,11 @@ including link:xC.adoc#_key_bindings[keyboard shortcuts].
image::xP.webp[align="center"]
xA, xT, xW, xM
--------------
Fyne, Qt Widgets, Win32, Cocoa frontends for 'xC'.
Using them is not recommended.
xD
--
The IRC daemon. It is designed for use as a regular user application rather
@@ -49,7 +55,15 @@ besides the total number of connections and mode `+l`, or server linking
xS
--
The IRC daemon again, this time ported to Go, additionally supporting WEBIRC.
The IRC daemon again, this time ported to Go, additionally supporting WEBIRC,
and thus ideal for pairing with, e.g.,
https://github.com/kiwiirc/webircgateway[].
Any further development, such as P10 or TS6 linking for IRC services,
or plugin support for arbitrary bridges, will happen here.
xN
--
The IRC notifier, should you ever need to send automated messages from a script.
xB
--
@@ -64,8 +78,10 @@ that easily to any program).
Packages
--------
Regular releases are sporadic. git master should be stable enough. You can get
a package with the latest development version from Archlinux's AUR.
Regular releases are sporadic. git master should be stable enough.
You can get a package with the latest development version using Arch Linux's
https://aur.archlinux.org/packages/xk-git[AUR],
or as a https://git.janouch.name/p/nixexprs[Nix derivation].
Building
--------
@@ -120,27 +136,60 @@ 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
endpoint as the third command line argument in this case.
xA
~~
The Fyne frontend supports all of Linux, FreeBSD, Windows, macOS, Android, and
iOS natively, albeit somewhat poorly. Only use `fyne` or `fyne-cross` after
running `make generate` first.
xT
~~
The Qt Widgets frontend is a separate CMake subproject. It generally supports
all desktop operating systems. To avoid having to specify the relay address
each time you run it, pass it on the command line.
xW
~~
The Win32 frontend is a separate CMake subproject that should be compiled
using MinGW-w64. To avoid having to specify the relay address each time you
run it, create a shortcut for the executable and include the address in its
_Target_ field:
C:\...\xW.exe 127.0.0.1 9000
It works reasonably well starting with Windows 7.
xM
~~
The Cocoa frontend is a separate CMake subproject that requires Xcode to build.
It is currently not that usable. The relay address can either be passed on
the command line, or preset in the _defaults_ database:
$ defaults write name.janouch.xM relayHost 127.0.0.1
$ defaults write name.janouch.xM relayPort 9000
Client Certificates
-------------------
'xC' will use the SASL EXTERNAL method to authenticate using the TLS client
certificate specified by the respective server's `tls_cert` option if you add
`sasl` to the `capabilities` option and the server supports this.
'xD' uses SHA-1 fingerprints of TLS client certificates to authenticate users.
To get the fingerprint from a certificate file in the required form, use:
'xD' and 'xS' use SHA-256 fingerprints of TLS client certificates
to authenticate users. To get the fingerprint from a certificate file
in the required form, use:
$ openssl x509 -in public.pem -outform DER | sha1sum
$ openssl x509 -in public.pem -outform DER | sha256sum
Custom Key Bindings in xC
-------------------------

Submodule liberty updated: 34460ca715...31ae400852

50
test
View File

@@ -1,50 +0,0 @@
#!/usr/bin/expect -f
# Very basic end-to-end testing for CI
# Run the daemon to test against
system ./xD --write-default-cfg
spawn ./xD -d
# 10 seconds is a bit too much
set timeout 5
spawn ./xC
# Fuck this Tcl shit, I want the exit code
expect_after {
eof {
puts ""
puts "Child exited prematurely"
exit 1
}
}
# Connect to the daemon
send "/server add localhost\n"
expect "]"
send "/set servers.localhost.addresses = \"localhost\"\n"
expect "Option changed"
send "/disconnect\n"
expect "]"
send "/connect\n"
expect "Connection established"
# Try some chatting
send "/join #test\n"
expect "has joined"
send "Hello\n"
expect "Hello"
# Attributes
send "\x1bmbBold text! \x1bmc0,5And colors.\n"
expect "]"
# Try basic commands
send "/set\n"
expect "]"
send "/help\n"
expect "]"
# Quit
send "/quit\n"
expect "Shutting down"

View File

@@ -1,8 +1,16 @@
#!/bin/sh
# We don't use printf's percent notation with our custom logging mechanism,
# so the compiler cannot check it for us like it usually does
# so the compiler cannot check it for us like it usually does.
#
# In clang-query terms, the string we're interested in can be found through:
# set traversal IgnoreUnlessSpelledInSource
# set output dump
# match callExpr(callee(functionDecl(
# hasName("log_full"))),
# hasArgument(5, stringLiteral().bind("format")))
# However, the tool is too restricted to be useful in a shell script.
perl -n0777 - "$(dirname "$0")"/xC.c <<-'END'
while (/\blog_[^ ]+\s*\([^"()]*"[^"]*%[^%][^"]*"/gm) {
while (/\blog_[^ ]+\s*\([^"()]*"[^"]*%\w[^"]*"/gm) {
my ($p, $m) = ($`, $&);
printf "$ARGV:%d: suspicious log format string: %s...\n",
(1 + $p =~ tr/\n//), ($m =~ s/\s+/ /rg);

72
test.lua Normal file
View File

@@ -0,0 +1,72 @@
#!/usr/bin/env wdye
-- Very basic end-to-end testing for CI
function exec (...)
local p = wdye.spawn(...)
local out = wdye.expect(p:eof {function (p) return p[0] end})
if not out then
error "exec() timeout"
end
local status = p:wait()
if status ~= 0 then
io.write(out, "\n")
error("exit status " .. status)
end
return out:gsub("%s+$", "")
end
local temp = exec {"mktemp", "-d"}
local atexit = {}
setmetatable(atexit, {__gc = function () exec {"rm", "-rf", "--", temp} end})
local env = {XDG_CONFIG_HOME=temp, TERM="xterm"}
exec {"./xD", "--write-default-cfg", environ=env}
-- Run the daemon to test against (assuming the default port 6667)
local xD = wdye.spawn {"./xD", "-d", environ=env}
local xC = wdye.spawn {"./xC", environ=env}
function send (...) xC:send(...) end
function expect (string)
wdye.expect(xC:exact {string},
wdye.timeout {5, function (p) error "xC timeout" end},
xC:eof {function (p) error "xC exited prematurely" end})
end
-- Connect to the daemon
send "/server add localhost\n"
expect "]"
send "/set servers.localhost.addresses = \"localhost\"\n"
expect "Option changed"
send "/disconnect\n"
expect "]"
send "/connect\n"
expect "Welcome to"
-- Try some chatting
send "/join #test\n"
expect "has joined"
send "Hello\n"
expect "Hello"
-- Attributes
send "\x1bmbBold text! \x1bmc0,5And colors.\n"
expect "]"
-- Try basic commands
send "/set\n"
expect "]"
send "/help\n"
expect "]"
-- Quit
send "/quit\n"
expect "Shutting down"
local s1 = xC:wait()
assert(s1 == 0, "xC exited abnormally: " .. s1)
-- Send SIGINT (^C)
xD:send "\003"
local s2 = xD:wait()
assert(s2 == 0, "xD exited abnormally: " .. s2)

5
xA/.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
/xA
/proto.go
/FyneApp.toml
/*.png
/beep.raw

36
xA/Makefile Normal file
View File

@@ -0,0 +1,36 @@
.POSIX:
.SUFFIXES:
.SUFFIXES: .png .svg
AWK = env LC_ALL=C awk
tools = ../liberty/tools
generated = FyneApp.toml xA.png xA-highlighted.png beep.raw proto.go
outputs = xA $(generated)
all: $(outputs)
generate: $(generated)
FyneApp.toml: ../xK-version
printf "\
[Details]\n\
Icon = 'xA.png'\n\
Name = 'xA'\n\
ID = 'name.janouch.xA'\n\
Version = '$$(cat ../xK-version)'\n\
Build = 1\n\
\n\
[LinuxAndBSD]\n\
GenericName = 'IRC Client'\n\
Categories = ['Network', 'Chat', 'IRCClient']\n" > $@
.svg.png:
rsvg-convert --output=$@ -- $<
beep.raw:
sox -Dr 44100 -c 1 -e signed-integer -b 16 -L -n $@ \
synth 0.1 0 25 triangle 800 vol 0.5 fade t 0 -0 0.005 pad 0 0.05
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 > $@
xA: xA.go ../xK-version $(generated)
go build -ldflags "-X 'main.projectVersion=$$(cat ../xK-version)'" -o $@ \
-gcflags=all="-N -l"
clean:
rm -f $(outputs)

46
xA/go.mod Normal file
View File

@@ -0,0 +1,46 @@
module janouch.name/xK/xA
go 1.23.0
toolchain go1.24.0
require (
fyne.io/fyne/v2 v2.6.0
github.com/ebitengine/oto/v3 v3.3.3
)
require (
fyne.io/systray v1.11.0 // 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/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/image v0.1.1 // indirect
github.com/fyne-io/oksvg v0.1.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
github.com/go-text/typesetting v0.3.0 // indirect
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/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/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
gopkg.in/yaml.v3 v3.0.1 // indirect
)

84
xA/go.sum Normal file
View File

@@ -0,0 +1,84 @@
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=
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/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/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/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/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=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20250301202403-da16c1255728/go.mod h1:SyRD8YfuKk+ZXlDqYiqe1qMSqjNgtHzBTG810KUagMc=
github.com/go-text/render v0.2.0 h1:LBYoTmp5jYiJ4NPqDc2pz17MLmA3wHw1dZSVGcOdeAc=
github.com/go-text/render v0.2.0/go.mod h1:CkiqfukRGKJA5vZZISkjSYrcdtgKQWRa2HIzvwNN5SU=
github.com/go-text/typesetting v0.3.0 h1:OWCgYpp8njoxSRpwrdd1bQOxdjOXDj9Rqart9ML4iF4=
github.com/go-text/typesetting v0.3.0/go.mod h1:qjZLkhRgOEYMhU9eHBr3AR4sfnGJvOXNLt8yRAySFuY=
github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066 h1:qCuYC+94v2xrb1PoS4NIDe7DGYtLnU2wWiQe9a1B1c0=
github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y=
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=
github.com/hack-pad/go-indexeddb v0.3.2 h1:DTqeJJYc1usa45Q5r52t01KhvlSN02+Oq+tQbSBI91A=
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/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=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/nicksnyder/go-i18n/v2 v2.6.0 h1:C/m2NNWNiTB6SK4Ao8df5EWm3JETSTIGNXBpMJTxzxQ=
github.com/nicksnyder/go-i18n/v2 v2.6.0/go.mod h1:88sRqr0C6OPyJn0/KRNaEz1uWorjxIKP7rUUcvycecE=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
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/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=
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=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

23
xA/xA-highlighted.svg Normal file
View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg version="1.1" width="512" height="512" viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="background" x1="0" y1="0" x2="0" y2="1">
<stop stop-color="#ffaa00" offset="0" />
<stop stop-color="#ffffff" offset="1" />
</linearGradient>
<clipPath id="clip">
<rect x="0" y="0" width="1" height="1" />
</clipPath>
</defs>
<rect x="0" y="0" width="512" height="512" fill="url(#background)" />
<g transform="translate(64, 64) scale(384)" stroke-linecap="square">
<g clip-path="url(#clip)">
<path stroke="#ff0000" stroke-width="0.125"
d="M 0.25,1.1 0.65,-0.1 M 0.75,1.1 0.35,-0.1 M 0.225,0.75 0.775,0.75" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 781 B

1651
xA/xA.go Normal file

File diff suppressed because it is too large Load Diff

23
xA/xA.svg Normal file
View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg version="1.1" width="512" height="512" viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="background" x1="0" y1="0" x2="0" y2="1">
<stop stop-color="#ffaa00" offset="0" />
<stop stop-color="#ff0000" offset="1" />
</linearGradient>
<clipPath id="clip">
<rect x="0" y="0" width="1" height="1" />
</clipPath>
</defs>
<rect x="0" y="0" width="512" height="512" fill="url(#background)" />
<g transform="translate(64, 64) scale(384)" stroke-linecap="square">
<g clip-path="url(#clip)">
<path stroke="#ffffff" stroke-width="0.125"
d="M 0.25,1.1 0.65,-0.1 M 0.75,1.1 0.35,-0.1 M 0.225,0.75 0.775,0.75" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 781 B

4
xB.c
View File

@@ -1009,7 +1009,7 @@ is_valid_plugin_name (const char *name)
if (!*name)
return false;
for (const char *p = name; *p; p++)
if (!isgraph (*p) || *p == '/')
if (!isgraph ((uint8_t) *p) || *p == '/')
return false;
return true;
}
@@ -1213,7 +1213,7 @@ parse_bot_command (const char *s, const char *command, const char **following)
s += command_len;
// Expect a word boundary, so that we don't respond to invalid things
if (isalnum (*s))
if (isalnum ((uint8_t) *s))
return false;
// Ignore any initial spaces; the rest is the command's argument

View File

@@ -1,325 +0,0 @@
# xC-gen-proto-c.awk: C backend for xC-gen-proto.awk.
#
# Copyright (c) 2022, Přemysl Eric Janouch <p@janouch.name>
# SPDX-License-Identifier: 0BSD
#
# Neither *_new() nor *_destroy() functions are provided, because they'd only
# be useful for top-levels, and are merely extra malloc()/free() calls.
# Users are expected to reuse buffers.
#
# Similarly, no constructors are produced--those are easy to write manually.
#
# All arrays are deserialized zero-terminated, so u8<> and i8<> can be directly
# used as C strings.
#
# All types must be able to dispose partially zero values going from the back,
# i.e., in the reverse order of deserialization.
function define_internal(name, ctype) {
Types[name] = "internal"
CodegenCType[name] = ctype
}
function define_int(shortname, ctype) {
define_internal(shortname, ctype)
CodegenSerialize[shortname] = \
"\tstr_pack_" shortname "(w, %s);\n"
CodegenDeserialize[shortname] = \
"\tif (!msg_unpacker_" shortname "(r, &%s))\n" \
"\t\treturn false;\n"
}
function define_sint(size) { define_int("i" size, "int" size "_t") }
function define_uint(size) { define_int("u" size, "uint" size "_t") }
function codegen_begin() {
define_sint("8")
define_sint("16")
define_sint("32")
define_sint("64")
define_uint("8")
define_uint("16")
define_uint("32")
define_uint("64")
define_internal("string", "struct str")
CodegenDispose["string"] = "\tstr_free(&%s);\n"
CodegenSerialize["string"] = \
"\tif (!proto_string_serialize(&%s, w))\n" \
"\t\treturn false;\n"
CodegenDeserialize["string"] = \
"\tif (!proto_string_deserialize(&%s, r))\n" \
"\t\treturn false;\n"
define_internal("bool", "bool")
CodegenSerialize["bool"] = \
"\tstr_pack_u8(w, !!%s);\n"
CodegenDeserialize["bool"] = \
"\t{\n" \
"\t\tuint8_t v = 0;\n" \
"\t\tif (!msg_unpacker_u8(r, &v))\n" \
"\t\t\treturn false;\n" \
"\t\t%s = !!v;\n" \
"\t}\n"
print "// This file directly depends on liberty.c, but doesn't include it."
print ""
print "static bool"
print "proto_string_serialize(const struct str *s, struct str *w) {"
print "\tif (s->len > UINT32_MAX)"
print "\t\treturn false;"
print "\tstr_pack_u32(w, s->len);"
print "\tstr_append_str(w, s);"
print "\treturn true;"
print "}"
print ""
print "static bool"
print "proto_string_deserialize(struct str *s, struct msg_unpacker *r) {"
print "\tuint32_t len = 0;"
print "\tif (!msg_unpacker_u32(r, &len))"
print "\t\treturn false;"
print "\tif (msg_unpacker_get_available(r) < len)"
print "\t\treturn false;"
print "\t*s = str_make();"
print "\tstr_append_data(s, r->data + r->offset, len);"
print "\tr->offset += len;"
print "\tif (!utf8_validate (s->str, s->len))"
print "\t\treturn false;"
print "\treturn true;"
print "}"
}
function codegen_constant(name, value) {
print ""
print "enum { " PrefixUpper name " = " value " };"
}
function codegen_enum_value(name, subname, value, cg) {
append(cg, "fields",
"\t" PrefixUpper toupper(cameltosnake(name)) "_" subname \
" = " value ",\n")
}
function codegen_enum(name, cg, ctype) {
ctype = "enum " PrefixLower cameltosnake(name)
print ""
print ctype " {"
print cg["fields"] "};"
# XXX: This should also check if it isn't out-of-range for any reason,
# but our usage of sprintf() stands in the way a bit.
CodegenSerialize[name] = "\tstr_pack_i8(w, %s);\n"
CodegenDeserialize[name] = \
"\t{\n" \
"\t\tint8_t v = 0;\n" \
"\t\tif (!msg_unpacker_i8(r, &v) || !v)\n" \
"\t\t\treturn false;\n" \
"\t\t%s = v;\n" \
"\t}\n"
CodegenCType[name] = ctype
for (i in cg)
delete cg[i]
}
function codegen_struct_tag(d, cg, f) {
f = "self->" d["name"]
append(cg, "fields", "\t" CodegenCType[d["type"]] " " d["name"] ";\n")
append(cg, "dispose", sprintf(CodegenDispose[d["type"]], f))
append(cg, "serialize", sprintf(CodegenSerialize[d["type"]], f))
# Do not deserialize here, that would be out of order.
}
function codegen_struct_field(d, cg, f, dispose, serialize, deserialize) {
f = "self->" d["name"]
dispose = CodegenDispose[d["type"]]
serialize = CodegenSerialize[d["type"]]
deserialize = CodegenDeserialize[d["type"]]
if (!d["isarray"]) {
append(cg, "fields", "\t" CodegenCType[d["type"]] " " d["name"] ";\n")
append(cg, "dispose", sprintf(dispose, f))
append(cg, "serialize", sprintf(serialize, f))
append(cg, "deserialize", sprintf(deserialize, f))
return
}
append(cg, "fields",
"\t" CodegenCType["u32"] " " d["name"] "_len;\n" \
"\t" CodegenCType[d["type"]] " *" d["name"] ";\n")
if (dispose)
append(cg, "dispose", "\tif (" f ")\n" \
"\t\tfor (size_t i = 0; i < " f "_len; i++)\n" \
indent(indent(sprintf(dispose, f "[i]"))))
append(cg, "dispose", "\tfree(" f ");\n")
append(cg, "serialize", sprintf(CodegenSerialize["u32"], f "_len"))
if (d["type"] == "u8" || d["type"] == "i8") {
append(cg, "serialize",
"\tstr_append_data(w, " f ", " f "_len);\n")
} else if (serialize) {
append(cg, "serialize",
"\tfor (size_t i = 0; i < " f "_len; i++)\n" \
indent(sprintf(serialize, f "[i]")))
}
append(cg, "deserialize", sprintf(CodegenDeserialize["u32"], f "_len") \
"\tif (!(" f " = calloc(" f "_len + 1, sizeof *" f ")))\n" \
"\t\treturn false;\n")
if (d["type"] == "u8" || d["type"] == "i8") {
append(cg, "deserialize",
"\tif (msg_unpacker_get_available(r) < " f "_len)\n" \
"\t\treturn false;\n" \
"\tmemcpy(" f ", r->data + r->offset, " f "_len);\n" \
"\tr->offset += " f "_len;\n")
} else if (deserialize) {
append(cg, "deserialize",
"\tfor (size_t i = 0; i < " f "_len; i++)\n" \
indent(sprintf(deserialize, f "[i]")))
}
}
function codegen_struct(name, cg, ctype, funcname) {
ctype = "struct " PrefixLower cameltosnake(name)
print ""
print ctype " {"
print cg["fields"] "};"
if (cg["dispose"]) {
funcname = PrefixLower cameltosnake(name) "_free"
print ""
print "static void\n" funcname "(" ctype " *self) {"
print cg["dispose"] "}"
CodegenDispose[name] = "\t" funcname "(&%s);\n"
}
if (cg["serialize"]) {
funcname = PrefixLower cameltosnake(name) "_serialize"
print ""
print "static bool\n" \
funcname "(\n\t\t" ctype " *self, struct str *w) {"
print cg["serialize"] "\treturn true;"
print "}"
CodegenSerialize[name] = "\tif (!" funcname "(&%s, w))\n" \
"\t\treturn false;\n"
}
if (cg["deserialize"]) {
funcname = PrefixLower cameltosnake(name) "_deserialize"
print ""
print "static bool\n" \
funcname "(\n\t\t" ctype " *self, struct msg_unpacker *r) {"
print cg["deserialize"] "\treturn true;"
print "}"
CodegenDeserialize[name] = "\tif (!" funcname "(&%s, r))\n" \
"\t\treturn false;\n"
}
CodegenCType[name] = ctype
for (i in cg)
delete cg[i]
}
function codegen_union_tag(d, cg) {
cg["tagtype"] = d["type"]
cg["tagname"] = d["name"]
append(cg, "fields", "\t" CodegenCType[d["type"]] " " d["name"] ";\n")
}
function codegen_union_struct( \
name, casename, cg, scg, structname, fieldname, fullcasename) {
# Don't generate obviously useless structs.
fullcasename = toupper(cameltosnake(cg["tagtype"])) "_" casename
if (!scg["dispose"] && !scg["deserialize"]) {
append(cg, "structless", "\tcase " PrefixUpper fullcasename ":\n")
for (i in scg)
delete scg[i]
return
}
# And thus not all generated structs are present in Types.
structname = name "_" casename
fieldname = tolower(casename)
codegen_struct(structname, scg)
append(cg, "fields", "\t" CodegenCType[structname] " " fieldname ";\n")
if (CodegenDispose[structname])
append(cg, "dispose", "\tcase " PrefixUpper fullcasename ":\n" \
indent(sprintf(CodegenDispose[structname], "self->" fieldname)) \
"\t\tbreak;\n")
# With no de/serialization code, this will simply recognize the tag.
append(cg, "serialize", "\tcase " PrefixUpper fullcasename ":\n" \
indent(sprintf(CodegenSerialize[structname], "self->" fieldname)) \
"\t\tbreak;\n")
append(cg, "deserialize", "\tcase " PrefixUpper fullcasename ":\n" \
indent(sprintf(CodegenDeserialize[structname], "self->" fieldname)) \
"\t\tbreak;\n")
}
function codegen_union(name, cg, f, ctype, funcname) {
ctype = "union " PrefixLower cameltosnake(name)
print ""
print ctype " {"
print cg["fields"] "};"
f = "self->" cg["tagname"]
if (cg["dispose"]) {
funcname = PrefixLower cameltosnake(name) "_free"
print ""
print "static void\n" funcname "(" ctype " *self) {"
print "\tswitch (" f ") {"
if (cg["structless"])
print cg["structless"] \
indent(sprintf(CodegenDispose[cg["tagtype"]], f)) "\t\tbreak;"
print cg["dispose"] "\tdefault:"
print "\t\tbreak;"
print "\t}"
print "}"
CodegenDispose[name] = "\t" funcname "(&%s);\n"
}
if (cg["serialize"]) {
funcname = PrefixLower cameltosnake(name) "_serialize"
print ""
print "static bool\n" \
funcname "(\n\t\t" ctype " *self, struct str *w) {"
print "\tswitch (" f ") {"
if (cg["structless"])
print cg["structless"] \
indent(sprintf(CodegenSerialize[cg["tagtype"]], f)) "\t\tbreak;"
print cg["serialize"] "\tdefault:"
print "\t\treturn false;"
print "\t}"
print "\treturn true;"
print "}"
CodegenSerialize[name] = "\tif (!" funcname "(&%s, w))\n" \
"\t\treturn false;\n"
}
if (cg["deserialize"]) {
funcname = PrefixLower cameltosnake(name) "_deserialize"
print ""
print "static bool\n" \
funcname "(\n\t\t" ctype " *self, struct msg_unpacker *r) {"
print sprintf(CodegenDeserialize[cg["tagtype"]], f)
print "\tswitch (" f ") {"
if (cg["structless"])
print cg["structless"] "\t\tbreak;"
print cg["deserialize"] "\tdefault:"
print "\t\treturn false;"
print "\t}"
print "\treturn true;"
print "}"
CodegenDeserialize[name] = "\tif (!" funcname "(&%s, r))\n" \
"\t\treturn false;\n"
}
CodegenCType[name] = ctype
for (i in cg)
delete cg[i]
}

View File

@@ -1,519 +0,0 @@
# xC-gen-proto-go.awk: Go backend for xC-gen-proto.awk.
#
# Copyright (c) 2022, Přemysl Eric Janouch <p@janouch.name>
# SPDX-License-Identifier: 0BSD
#
# This backend also enables proxying to other endpoints using JSON.
function define_internal(name, gotype) {
Types[name] = "internal"
CodegenGoType[name] = gotype
}
function define_sint(size, shortname, gotype) {
shortname = "i" size
gotype = "int" size
define_internal(shortname, gotype)
CodegenAppendJSON[shortname] = \
"\tb = strconv.AppendInt(b, int64(%s), 10)\n"
if (size == 8) {
CodegenSerialize[shortname] = "\tdata = append(data, uint8(%s))\n"
CodegenDeserialize[shortname] = \
"\tif len(data) >= 1 {\n" \
"\t\t%s, data = int8(data[0]), data[1:]\n" \
"\t} else {\n" \
"\t\treturn nil, false\n" \
"\t}\n"
return
}
CodegenSerialize[shortname] = \
"\tdata = binary.BigEndian.AppendUint" size "(data, uint" size "(%s))\n"
CodegenDeserialize[shortname] = \
"\tif len(data) >= " (size / 8) " {\n" \
"\t\t%s = " gotype "(binary.BigEndian.Uint" size "(data))\n" \
"\t\tdata = data[" (size / 8) ":]\n" \
"\t} else {\n" \
"\t\treturn nil, false\n" \
"\t}\n"
}
function define_uint(size, shortname, gotype) {
# Both []byte and []uint8 luckily marshal as base64-encoded JSON strings,
# so there's no need to rename the type as an exception.
shortname = "u" size
gotype = "uint" size
define_internal(shortname, gotype)
CodegenAppendJSON[shortname] = \
"\tb = strconv.AppendUint(b, uint64(%s), 10)\n"
if (size == 8) {
CodegenSerialize[shortname] = "\tdata = append(data, %s)\n"
CodegenDeserialize[shortname] = \
"\tif len(data) >= 1 {\n" \
"\t\t%s, data = data[0], data[1:]\n" \
"\t} else {\n" \
"\t\treturn nil, false\n" \
"\t}\n"
return
}
CodegenSerialize[shortname] = \
"\tdata = binary.BigEndian.AppendUint" size "(data, %s)\n"
CodegenDeserialize[shortname] = \
"\tif len(data) >= " (size / 8) " {\n" \
"\t\t%s = binary.BigEndian.Uint" size "(data)\n" \
"\t\tdata = data[" (size / 8) ":]\n" \
"\t} else {\n" \
"\t\treturn nil, false\n" \
"\t}\n"
}
function codegen_begin() {
define_sint("8")
define_sint("16")
define_sint("32")
define_sint("64")
define_uint("8")
define_uint("16")
define_uint("32")
define_uint("64")
define_internal("bool", "bool")
CodegenAppendJSON["bool"] = \
"\tb = strconv.AppendBool(b, %s)\n"
CodegenSerialize["bool"] = \
"\tif %s {\n" \
"\t\tdata = append(data, 1)\n" \
"\t} else {\n" \
"\t\tdata = append(data, 0)\n" \
"\t}\n"
CodegenDeserialize["bool"] = \
"\tif data, ok = protoConsumeBoolFrom(data, &%s); !ok {\n" \
"\t\treturn nil, ok\n" \
"\t}\n"
define_internal("string", "string")
CodegenSerialize["string"] = \
"\tif data, ok = protoAppendStringTo(data, %s); !ok {\n" \
"\t\treturn nil, ok\n" \
"\t}\n"
CodegenDeserialize["string"] = \
"\tif data, ok = protoConsumeStringFrom(data, &%s); !ok {\n" \
"\t\treturn nil, ok\n" \
"\t}\n"
print "package main"
print ""
print "import ("
print "\t`encoding/base64`"
print "\t`encoding/binary`"
print "\t`encoding/json`"
print "\t`errors`"
print "\t`math`"
print "\t`strconv`"
print "\t`unicode/utf8`"
print ")"
print ""
print "// protoConsumeBoolFrom tries to deserialize a boolean value"
print "// from the beginning of a byte stream. When successful,"
print "// it returns a subslice with any data that might follow."
print "func protoConsumeBoolFrom(data []byte, b *bool) ([]byte, bool) {"
print "\tif len(data) < 1 {"
print "\t\treturn nil, false"
print "\t}"
print "\tif data[0] != 0 {"
print "\t\t*b = true"
print "\t} else {"
print "\t\t*b = false"
print "\t}"
print "\treturn data[1:], true"
print "}"
print ""
print "// protoAppendStringTo tries to serialize a string value,"
print "// appending it to the end of a byte stream."
print "func protoAppendStringTo(data []byte, s string) ([]byte, bool) {"
print "\tif len(s) > math.MaxUint32 {"
print "\t\treturn nil, false"
print "\t}"
print "\tdata = binary.BigEndian.AppendUint32(data, uint32(len(s)))"
print "\treturn append(data, s...), true"
print "}"
print ""
print "// protoConsumeStringFrom tries to deserialize a string value"
print "// from the beginning of a byte stream. When successful,"
print "// it returns a subslice with any data that might follow."
print "func protoConsumeStringFrom(data []byte, s *string) ([]byte, bool) {"
print "\tif len(data) < 4 {"
print "\t\treturn nil, false"
print "\t}"
print "\tlength := binary.BigEndian.Uint32(data)"
print "\tif data = data[4:]; uint64(len(data)) < uint64(length) {"
print "\t\treturn nil, false"
print "\t}"
print "\t*s = string(data[:length])"
print "\tif !utf8.ValidString(*s) {"
print "\t\treturn nil, false"
print "\t}"
print "\treturn data[length:], true"
print "}"
print ""
print "// protoUnmarshalEnumJSON converts a JSON fragment to an integer,"
print "// ensuring that it's within the expected range of enum values."
print "func protoUnmarshalEnumJSON(data []byte) (int64, error) {"
print "\tvar n int64"
print "\tif err := json.Unmarshal(data, &n); err != nil {"
print "\t\treturn 0, err"
print "\t} else if n > math.MaxInt8 || n < math.MinInt8 {"
print "\t\treturn 0, errors.New(`integer out of range`)"
print "\t} else {"
print "\t\treturn n, nil"
print "\t}"
print "}"
print ""
}
function codegen_constant(name, value) {
print "const " PrefixCamel snaketocamel(name) " = " value
print ""
}
function codegen_enum_value(name, subname, value, cg, goname) {
goname = PrefixCamel name snaketocamel(subname)
append(cg, "fields",
"\t" goname " = " value "\n")
append(cg, "stringer",
"\tcase " goname ":\n" \
"\t\treturn `" snaketocamel(subname) "`\n")
append(cg, "marshal",
goname ",\n")
append(cg, "unmarshal",
"\tcase `" snaketocamel(subname) "`:\n" \
"\t\t*v = " goname "\n")
}
function codegen_enum(name, cg, gotype, fields) {
gotype = PrefixCamel name
print "type " gotype " int8"
print ""
print "const ("
print cg["fields"] ")"
print ""
print "func (v " gotype ") String() string {"
print "\tswitch v {"
print cg["stringer"] "\tdefault:"
print "\t\treturn strconv.Itoa(int(v))"
print "\t}"
print "}"
print ""
CodegenIsMarshaler[name] = 1
fields = cg["marshal"]
sub(/,\n$/, ":", fields)
gsub(/\n/, "\n\t", fields)
print "func (v " gotype ") MarshalJSON() ([]byte, error) {"
print "\tswitch v {"
print indent("case " fields)
print "\t\treturn []byte(`\"` + v.String() + `\"`), nil"
print "\t}"
print "\treturn json.Marshal(int(v))"
print "}"
print ""
print "func (v *" gotype ") UnmarshalJSON(data []byte) error {"
print "\tvar s string"
print "\tif json.Unmarshal(data, &s) == nil {"
print "\t\t// Handled below."
print "\t} else if n, err := protoUnmarshalEnumJSON(data); err != nil {"
print "\t\treturn err"
print "\t} else {"
print "\t\t*v = " gotype "(n)"
print "\t\treturn nil"
print "\t}"
print ""
print "\tswitch s {"
print cg["unmarshal"] "\tdefault:"
print "\t\treturn errors.New(`unrecognized value: ` + s)"
print "\t}"
print "\treturn nil"
print "}"
print ""
# XXX: This should also check if it isn't out-of-range for any reason,
# but our usage of sprintf() stands in the way a bit.
CodegenSerialize[name] = "\tdata = append(data, uint8(%s))\n"
CodegenDeserialize[name] = \
"\tif len(data) >= 1 {\n" \
"\t\t%s, data = " gotype "(data[0]), data[1:]\n" \
"\t} else {\n" \
"\t\treturn nil, false\n" \
"\t}\n"
CodegenGoType[name] = gotype
for (i in cg)
delete cg[i]
}
function codegen_marshal(type, f, marshal) {
if (CodegenAppendJSON[type])
return sprintf(CodegenAppendJSON[type], f)
# Complex types are json.Marshalers, there's no need to json.Marshal(&f).
if (CodegenIsMarshaler[type])
marshal = f ".MarshalJSON()"
else
marshal = "json.Marshal(" f ")"
return \
"\tif j, err := " marshal "; err != nil {\n" \
"\t\treturn nil, err\n" \
"\t} else {\n" \
"\t\tb = append(b, j...)\n" \
"\t}\n"
}
function codegen_struct_field_marshal(d, cg, camel, f, marshal) {
camel = snaketocamel(d["name"])
f = "s." camel
if (!d["isarray"]) {
append(cg, "marshal",
"\tb = append(b, `,\"" decapitalize(camel) "\":`...)\n" \
codegen_marshal(d["type"], f))
return
}
# Note that we do not produce `null` for nil slices, unlike encoding/json.
# And arrays never get deserialized as such.
if (d["type"] == "u8") {
append(cg, "marshal",
"\tb = append(b, `,\"" decapitalize(camel) "\":\"`...)\n" \
"\tb = append(b, base64.StdEncoding.EncodeToString(" f ")...)\n" \
"\tb = append(b, '\"')\n")
return
}
append(cg, "marshal",
"\tb = append(b, `,\"" decapitalize(camel) "\":[`...)\n" \
"\tfor i := 0; i < len(" f "); i++ {\n" \
"\t\tif i > 0 {\n" \
"\t\t\tb = append(b, ',')\n" \
"\t\t}\n" \
indent(codegen_marshal(d["type"], f "[i]")) \
"\t}\n" \
"\tb = append(b, ']')\n")
}
function codegen_struct_field(d, cg, camel, f, serialize, deserialize) {
codegen_struct_field_marshal(d, cg)
camel = snaketocamel(d["name"])
f = "s." camel
serialize = CodegenSerialize[d["type"]]
deserialize = CodegenDeserialize[d["type"]]
if (!d["isarray"]) {
append(cg, "fields", "\t" camel " " CodegenGoType[d["type"]] \
" `json:\"" decapitalize(camel) "\"`\n")
append(cg, "serialize", sprintf(serialize, f))
append(cg, "deserialize", sprintf(deserialize, f))
return
}
append(cg, "fields", "\t" camel " []" CodegenGoType[d["type"]] \
" `json:\"" decapitalize(camel) "\"`\n")
# XXX: This should also check if it isn't out-of-range for any reason.
append(cg, "serialize",
sprintf(CodegenSerialize["u32"], "uint32(len(" f "))"))
if (d["type"] == "u8") {
append(cg, "serialize",
"\tdata = append(data, " f "...)\n")
} else {
append(cg, "serialize",
"\tfor i := 0; i < len(" f "); i++ {\n" \
indent(sprintf(serialize, f "[i]")) \
"\t}\n")
}
append(cg, "deserialize",
"\t{\n" \
"\t\tvar length uint32\n" \
indent(sprintf(CodegenDeserialize["u32"], "length")))
if (d["type"] == "u8") {
append(cg, "deserialize",
"\t\tif uint64(len(data)) < uint64(length) {\n" \
"\t\t\treturn nil, false\n" \
"\t\t}\n" \
"\t\t" f ", data = data[:length], data[length:]\n" \
"\t}\n")
} else {
append(cg, "deserialize",
"\t\t" f " = make([]" CodegenGoType[d["type"]] ", length)\n" \
"\t}\n" \
"\tfor i := 0; i < len(" f "); i++ {\n" \
indent(sprintf(deserialize, f "[i]")) \
"\t}\n")
}
}
function codegen_struct_tag(d, cg, camel, f) {
codegen_struct_field_marshal(d, cg)
camel = snaketocamel(d["name"])
f = "s." camel
append(cg, "fields", "\t" camel " " CodegenGoType[d["type"]] \
" `json:\"" decapitalize(camel) "\"`\n")
append(cg, "serialize", sprintf(CodegenSerialize[d["type"]], f))
# Do not deserialize here, that is already done by the containing union.
}
function codegen_struct(name, cg, gotype) {
gotype = PrefixCamel name
print "type " gotype " struct {\n" cg["fields"] "}\n"
if (cg["marshal"]) {
CodegenIsMarshaler[name] = 1
print "func (s *" gotype ") MarshalJSON() ([]byte, error) {"
print "\tb := []byte{}"
print cg["marshal"] "\tb[0] = '{'"
print "\treturn append(b, '}'), nil"
print "}"
print ""
}
if (cg["serialize"]) {
print "func (s *" gotype ") AppendTo(data []byte) ([]byte, bool) {"
print "\tok := true"
print cg["serialize"] "\treturn data, ok"
print "}"
print ""
CodegenSerialize[name] = \
"\tif data, ok = %s.AppendTo(data); !ok {\n" \
"\t\treturn nil, ok\n" \
"\t}\n"
}
if (cg["deserialize"]) {
print "func (s *" gotype ") ConsumeFrom(data []byte) ([]byte, bool) {"
print "\tok := true"
print cg["deserialize"] "\treturn data, ok"
print "}"
print ""
CodegenDeserialize[name] = \
"\tif data, ok = %s.ConsumeFrom(data); !ok {\n" \
"\t\treturn nil, ok\n" \
"\t}\n"
}
CodegenGoType[name] = gotype
for (i in cg)
delete cg[i]
}
function codegen_union_tag(d, cg) {
cg["tagtype"] = d["type"]
cg["tagname"] = d["name"]
# The tag is implied from the type of struct stored in the interface.
}
function codegen_union_struct(name, casename, cg, scg, structname, init) {
# And thus not all generated structs are present in Types.
structname = name snaketocamel(casename)
codegen_struct(structname, scg)
init = CodegenGoType[structname] "{" snaketocamel(cg["tagname"]) \
": " decapitalize(snaketocamel(cg["tagname"])) "}"
append(cg, "unmarshal",
"\tcase " CodegenGoType[cg["tagtype"]] snaketocamel(casename) ":\n" \
"\t\ts := " init "\n" \
"\t\terr = json.Unmarshal(data, &s)\n" \
"\t\tu.Interface = &s\n")
append(cg, "serialize",
"\tcase *" CodegenGoType[structname] ":\n" \
indent(sprintf(CodegenSerialize[structname], "union")))
append(cg, "deserialize",
"\tcase " CodegenGoType[cg["tagtype"]] snaketocamel(casename) ":\n" \
"\t\ts := " init "\n" \
indent(sprintf(CodegenDeserialize[structname], "s")) \
"\t\tu.Interface = &s\n")
}
function codegen_union(name, cg, gotype, tagfield, tagvar) {
gotype = PrefixCamel name
print "type " gotype " struct {"
print "\tInterface any"
print "}"
print ""
# This cannot be a pointer method, it wouldn't work recursively.
CodegenIsMarshaler[name] = 1
print "func (u " gotype ") MarshalJSON() ([]byte, error) {"
print "\treturn u.Interface.(json.Marshaler).MarshalJSON()"
print "}"
print ""
tagfield = snaketocamel(cg["tagname"])
tagvar = decapitalize(tagfield)
print "func (u *" gotype ") UnmarshalJSON(data []byte) (err error) {"
print "\tvar t struct {"
print "\t\t" tagfield " " CodegenGoType[cg["tagtype"]] \
" `json:\"" tagvar "\"`"
print "\t}"
print "\tif err := json.Unmarshal(data, &t); err != nil {"
print "\t\treturn err"
print "\t}"
print ""
print "\tswitch " tagvar " := t." tagfield "; " tagvar " {"
print cg["unmarshal"] "\tdefault:"
print "\t\terr = errors.New(`unsupported value: ` + " tagvar ".String())"
print "\t}"
print "\treturn err"
print "}"
print ""
# XXX: Consider changing the interface into an AppendTo/ConsumeFrom one,
# that would eliminate these type case switches entirely.
# On the other hand, it would make it possible to send unsuitable structs.
print "func (u *" gotype ") AppendTo(data []byte) ([]byte, bool) {"
print "\tok := true"
print "\tswitch union := u.Interface.(type) {"
print cg["serialize"] "\tdefault:"
print "\t\treturn nil, false"
print "\t}"
print "\treturn data, ok"
print "}"
print ""
CodegenSerialize[name] = \
"\tif data, ok = %s.AppendTo(data); !ok {\n" \
"\t\treturn nil, ok\n" \
"\t}\n"
print "func (u *" gotype ") ConsumeFrom(data []byte) ([]byte, bool) {"
print "\tok := true"
print "\tvar " tagvar " " CodegenGoType[cg["tagtype"]]
print sprintf(CodegenDeserialize[cg["tagtype"]], tagvar)
print "\tswitch " tagvar " {"
print cg["deserialize"] "\tdefault:"
print "\t\treturn nil, false"
print "\t}"
print "\treturn data, ok"
print "}"
print ""
CodegenDeserialize[name] = \
"\tif data, ok = %s.ConsumeFrom(data); !ok {\n" \
"\t\treturn nil, ok\n" \
"\t}\n"
CodegenGoType[name] = gotype
for (i in cg)
delete cg[i]
}

View File

@@ -1,223 +0,0 @@
# xC-gen-proto-js.awk: Javascript backend for xC-gen-proto.awk.
#
# Copyright (c) 2022, Přemysl Eric Janouch <p@janouch.name>
# SPDX-License-Identifier: 0BSD
#
# This backend is currently for decoding the binary format only.
# (JSON is way too expensive to process and transfer.)
#
# Import the resulting script as a Javascript module.
function define_internal(name) {
Types[name] = "internal"
}
function define_sint(size, shortname) {
shortname = "i" size
define_internal(shortname)
CodegenDeserialize[shortname] = "\t%s = r." shortname "()\n"
print ""
print "\t" shortname "() {"
if (size == "64") {
# XXX: 2^53 - 1 must be enough for anyone. BigInts are a PITA.
print "\t\tconst " shortname \
" = Number(this.getBigInt" size "(this.offset))"
} else {
print "\t\tconst " shortname " = this.getInt" size "(this.offset)"
}
print "\t\tthis.offset += " (size / 8)
print "\t\treturn " shortname
print "\t}"
}
function define_uint(size, shortname) {
shortname = "u" size
define_internal(shortname)
CodegenDeserialize[shortname] = "\t%s = r." shortname "()\n"
print ""
print "\t" shortname "() {"
if (size == "64") {
# XXX: 2^53 - 1 must be enough for anyone. BigInts are a PITA.
print "\t\tconst " shortname \
" = Number(this.getBigUint" size "(this.offset))"
} else {
print "\t\tconst " shortname " = this.getUint" size "(this.offset)"
}
print "\t\tthis.offset += " (size / 8)
print "\t\treturn " shortname
print "\t}"
}
function codegen_begin() {
print "export class Reader extends DataView {"
print "\tconstructor() {"
print "\t\tsuper(...arguments)"
print "\t\tthis.offset = 0"
print "\t\tthis.decoder = new TextDecoder('utf-8', {fatal: true})"
print "\t}"
print ""
print "\tget empty() {"
print "\t\treturn this.byteLength <= this.offset"
print "\t}"
print ""
print "\trequire(len) {"
print "\t\tif (this.byteLength - this.offset < len)"
print "\t\t\tthrow `Premature end of data`"
print "\t\treturn this.byteOffset + this.offset"
print "\t}"
define_internal("string")
CodegenDeserialize["string"] = "\t%s = r.string()\n"
print ""
print "\tstring() {"
print "\t\tconst len = this.getUint32(this.offset)"
print "\t\tthis.offset += 4"
print "\t\tconst array = new Uint8Array("
print "\t\t\tthis.buffer, this.require(len), len)"
print "\t\tthis.offset += len"
print "\t\treturn this.decoder.decode(array)"
print "\t}"
define_internal("bool")
CodegenDeserialize["bool"] = "\t%s = r.bool()\n"
print ""
print "\tbool() {"
print "\t\tconst u8 = this.getUint8(this.offset)"
print "\t\tthis.offset += 1"
print "\t\treturn u8 != 0"
print "\t}"
define_sint("8")
define_sint("16")
define_sint("32")
define_sint("64")
define_uint("8")
define_uint("16")
define_uint("32")
define_uint("64")
print "}"
}
function codegen_constant(name, value) {
print ""
print "export const " decapitalize(snaketocamel(name)) " = " value
}
function codegen_enum_value(name, subname, value, cg) {
append(cg, "fields", "\t" snaketocamel(subname) ": " value ",\n")
}
function codegen_enum(name, cg) {
print ""
print "export const " name " = Object.freeze({"
print cg["fields"] "})"
CodegenDeserialize[name] = "\t%s = r.i8()\n"
for (i in cg)
delete cg[i]
}
function codegen_struct_field(d, cg, camel, f, deserialize) {
camel = decapitalize(snaketocamel(d["name"]))
f = "s." camel
append(cg, "fields", "\t" camel "\n")
deserialize = CodegenDeserialize[d["type"]]
if (!d["isarray"]) {
append(cg, "deserialize", sprintf(deserialize, f))
return
}
append(cg, "deserialize",
"\t{\n" \
indent(sprintf(CodegenDeserialize["u32"], "const len")))
if (d["type"] == "u8") {
append(cg, "deserialize",
"\t\t" f " = new Uint8Array(\n" \
"\t\t\tr.buffer, r.require(len), len)\n" \
"\t\tr.offset += len\n" \
"\t}\n")
return
}
if (d["type"] == "i8") {
append(cg, "deserialize",
"\t\t" f " = new Int8Array(\n" \
"\t\t\tr.buffer, r.require(len), len)\n" \
"\t\tr.offset += len\n" \
"\t}\n")
return
}
append(cg, "deserialize",
"\t\t" f " = new Array(len)\n" \
"\t}\n" \
"\tfor (let i = 0; i < " f ".length; i++)\n" \
indent(sprintf(deserialize, f "[i]")))
}
function codegen_struct_tag(d, cg) {
append(cg, "fields", "\t" decapitalize(snaketocamel(d["name"])) "\n")
# Do not deserialize here, that is already done by the containing union.
}
function codegen_struct(name, cg) {
print ""
print "export class " name " {"
print cg["fields"] cg["methods"]
print "\tstatic deserialize(r) {"
print "\t\tconst s = new " name "()"
print indent(cg["deserialize"]) "\t\treturn s"
print "\t}"
print "}"
CodegenDeserialize[name] = "\t%s = " name ".deserialize(r)\n"
for (i in cg)
delete cg[i]
}
function codegen_union_tag(d, cg) {
cg["tagtype"] = d["type"]
cg["tagname"] = d["name"]
}
function codegen_union_struct(name, casename, cg, scg, structname) {
append(scg, "methods",
"\n" \
"\tconstructor() {\n" \
"\t\tthis." decapitalize(snaketocamel(cg["tagname"])) \
" = " cg["tagtype"] "." snaketocamel(casename) "\n" \
"\t}\n")
# And thus not all generated structs are present in Types.
structname = name snaketocamel(casename)
codegen_struct(structname, scg)
append(cg, "deserialize",
"\tcase " cg["tagtype"] "." snaketocamel(casename) ":\n" \
"\t{\n" \
indent(sprintf(CodegenDeserialize[structname], "const s")) \
"\t\treturn s\n" \
"\t}\n")
}
function codegen_union(name, cg, tagvar) {
tagvar = decapitalize(snaketocamel(cg["tagname"]))
print ""
print "export function deserialize" name "(r) {"
print sprintf(CodegenDeserialize[cg["tagtype"]], "const " tagvar) \
"\tswitch (" tagvar ") {"
print cg["deserialize"] "\tdefault:"
print "\t\tthrow `Unknown " cg["tagtype"] " (${tagvar})`"
print "\t}"
print "}"
CodegenDeserialize[name] = "\t%s = deserialize" name "(r)\n"
for (i in cg)
delete cg[i]
}

View File

@@ -1,305 +0,0 @@
# xC-gen-proto.awk: an XDR-derived code generator for network protocols.
#
# Copyright (c) 2022, Přemysl Eric Janouch <p@janouch.name>
# SPDX-License-Identifier: 0BSD
#
# You may read RFC 4506 for context, however it is only a source of inspiration.
# Grammar is easy to deduce from the parser.
#
# Native types: bool, u{8,16,32,64}, i{8,16,32,64}, string
#
# Don't define any new types, unless you hate yourself, then it's okay to do so.
# Backends tend to be a pain in the arse, for different reasons.
#
# All numbers are encoded in big-endian byte order.
# Booleans are one byte each.
# Strings must be valid UTF-8, use u8<> to lift that restriction.
# String and array lengths are encoded as u32.
# Enumeration values automatically start at 1, and are encoded as i8.
# Any struct or union field may be a variable-length array.
#
# Message framing is done externally, but also happens to prefix u32 lengths,
# unless this role is already filled by, e.g., WebSocket.
#
# Usage: env LC_ALL=C awk -f xC-gen-proto.awk -f xC-gen-proto-{c,go,js}.awk \
# xC-proto > xC-proto.{c,go,js} | {clang-format,gofmt,...}
# --- Utilities ----------------------------------------------------------------
function cameltosnake(s) {
while (match(s, /[[:lower:]][[:upper:]]/)) {
s = substr(s, 1, RSTART) "_" \
tolower(substr(s, RSTART + 1, RLENGTH - 1)) \
substr(s, RSTART + RLENGTH)
}
return tolower(s)
}
function snaketocamel(s) {
s = toupper(substr(s, 1, 1)) tolower(substr(s, 2))
while (match(s, /_[[:alnum:]]/)) {
s = substr(s, 1, RSTART - 1) \
toupper(substr(s, RSTART + 1, RLENGTH - 1)) \
substr(s, RSTART + RLENGTH)
}
return s
}
function decapitalize(s) {
if (match(s, /[[:upper:]][[:lower:]]/)) {
return tolower(substr(s, 1, 1)) substr(s, 2)
}
return s
}
function indent(s) {
if (!s)
return s
gsub(/\n/, "\n\t", s)
sub(/\t*$/, "", s)
return "\t" s
}
function append(a, key, value) {
a[key] = a[key] value
}
# --- Parsing ------------------------------------------------------------------
function fatal(message) {
print "// " FILENAME ":" FNR ": fatal error: " message
print FILENAME ":" FNR ": fatal error: " message > "/dev/stderr"
exit 1
}
function skipcomment() {
do {
if (match($0, /[*][/]/)) {
$0 = substr($0, RSTART + RLENGTH)
return
}
} while (getline > 0)
fatal("unterminated block comment")
}
function nexttoken() {
do {
if (match($0, /^[[:space:]]+/)) {
$0 = substr($0, RLENGTH + 1)
} else if (match($0, /^[/][/].*/)) {
$0 = ""
} else if (match($0, /^[/][*]/)) {
$0 = substr($0, RLENGTH + 1)
skipcomment()
} else if (match($0, /^[[:alpha:]][[:alnum:]_]*/)) {
Token = substr($0, 1, RLENGTH)
$0 = substr($0, RLENGTH + 1)
return Token
} else if (match($0, /^(0[xX][0-9a-fA-F]+|[1-9][0-9]*)/)) {
Token = substr($0, 1, RLENGTH)
$0 = substr($0, RLENGTH + 1)
return Token
} else if (/./) {
Token = substr($0, 1, 1)
$0 = substr($0, 2)
return Token
}
} while (/./ || getline > 0)
Token = ""
return Token
}
function expect(v) {
if (!v)
fatal("broken expectations at `" Token "' before `" $0 "'")
return v
}
function accept(what) {
if (Token != what)
return 0
nexttoken()
return 1
}
function identifier( v) {
if (Token !~ /^[[:alpha:]]/)
return 0
v = Token
nexttoken()
return v
}
function number( v) {
if (Token !~ /^[0-9]/)
return 0
v = Token
nexttoken()
return v
}
function readnumber( ident) {
ident = identifier()
if (!ident)
return expect(number())
if (!(ident in Consts))
fatal("unknown constant: " ident)
return Consts[ident]
}
function defconst( ident, num) {
if (!accept("const"))
return 0
ident = expect(identifier())
expect(accept("="))
num = readnumber()
if (ident in Consts)
fatal("constant redefined: " ident)
Consts[ident] = num
codegen_constant(ident, num)
return 1
}
function readtype( ident) {
ident = deftype()
if (ident)
return ident
ident = identifier()
if (!ident)
return 0
if (!(ident in Types))
fatal("unknown type: " ident)
return ident
}
function defenum( name, ident, value, cg) {
delete cg[0]
name = expect(identifier())
expect(accept("{"))
while (!accept("}")) {
ident = expect(identifier())
value = value + 1
if (accept("="))
value = readnumber()
if (!value)
fatal("enumeration values cannot be zero")
if (value < -128 || value > 127)
fatal("enumeration value out of range")
expect(accept(","))
append(EnumValues, name, SUBSEP ident)
if (EnumValues[name, ident]++)
fatal("duplicate enum value: " ident)
codegen_enum_value(name, ident, value, cg)
}
Types[name] = "enum"
codegen_enum(name, cg)
return name
}
function readfield(out, nonvoid) {
nonvoid = !accept("void")
if (nonvoid) {
out["type"] = expect(readtype())
out["name"] = expect(identifier())
# TODO: Consider supporting XDR's VLA length limits here.
# TODO: Consider supporting XDR's fixed-length syntax for string limits.
out["isarray"] = accept("<") && expect(accept(">"))
}
expect(accept(";"))
return nonvoid
}
function defstruct( name, d, cg) {
delete d[0]
delete cg[0]
name = expect(identifier())
expect(accept("{"))
while (!accept("}")) {
if (readfield(d))
codegen_struct_field(d, cg)
}
Types[name] = "struct"
codegen_struct(name, cg)
return name
}
function defunion( name, tag, tagtype, tagvalue, cg, scg, d, a, i, unseen) {
delete cg[0]
delete scg[0]
delete d[0]
name = expect(identifier())
expect(accept("switch"))
expect(accept("("))
tag["type"] = tagtype = expect(readtype())
tag["name"] = expect(identifier())
expect(accept(")"))
if (Types[tagtype] != "enum")
fatal("not an enum type: " tagtype)
codegen_union_tag(tag, cg)
split(EnumValues[tagtype], a, SUBSEP)
for (i in a)
unseen[a[i]]++
expect(accept("{"))
while (!accept("}")) {
if (accept("case")) {
if (tagvalue)
codegen_union_struct(name, tagvalue, cg, scg)
tagvalue = expect(identifier())
expect(accept(":"))
if (!unseen[tagvalue]--)
fatal("no such value or duplicate case: " tagtype "." tagvalue)
codegen_struct_tag(tag, scg)
} else if (tagvalue) {
if (readfield(d))
codegen_struct_field(d, scg)
} else {
fatal("union fields must fall under a case")
}
}
if (tagvalue)
codegen_union_struct(name, tagvalue, cg, scg)
# What remains non-zero in unseen[2..] is simply not recognized/allowed.
Types[name] = "union"
codegen_union(name, cg)
return name
}
function deftype() {
if (accept("enum"))
return defenum()
if (accept("struct"))
return defstruct()
if (accept("union"))
return defunion()
return 0
}
BEGIN {
PrefixLower = "relay_"
PrefixUpper = "RELAY_"
PrefixCamel = "Relay"
print "// Generated by xC-gen-proto.awk. DO NOT MODIFY."
codegen_begin()
nexttoken()
while (Token != "") {
expect(defconst() || deftype())
expect(accept(";"))
}
}

1094
xC.c

File diff suppressed because it is too large Load Diff

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,
@@ -59,16 +60,33 @@ struct EventMessage {
BUFFER_RENAME,
BUFFER_REMOVE,
BUFFER_ACTIVATE,
BUFFER_INPUT,
BUFFER_CLEAR,
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.
@@ -153,6 +171,9 @@ struct EventMessage {
string buffer_name;
case BUFFER_ACTIVATE:
string buffer_name;
case BUFFER_INPUT:
string buffer_name;
string text;
case BUFFER_CLEAR:
string buffer_name;
@@ -184,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;
};

8
xD.c
View File

@@ -49,7 +49,7 @@ static struct simple_config_item g_config_table[] =
{ "tls_key", NULL, "Server TLS private key (PEM)" },
{ "tls_ciphers", DEFAULT_CIPHERS, "OpenSSL cipher list" },
{ "operators", NULL, "IRCop TLS client cert. SHA-1 fingerprints" },
{ "operators", NULL, "IRCop TLS client cert. SHA-256 fingerprints" },
{ "max_connections", "0", "Global connection limit" },
{ "ping_interval", "180", "Interval between PINGs (sec)" },
@@ -296,7 +296,7 @@ irc_is_valid_user_mask (const char *mask)
static bool
irc_is_valid_fingerprint (const char *fp)
{
return irc_regex_match ("^[a-fA-F0-9]{40}$", fp);
return irc_regex_match ("^[a-fA-F0-9]{64}$", fp);
}
// --- Clients (equals users) --------------------------------------------------
@@ -1005,8 +1005,8 @@ client_get_ssl_cert_fingerprint (struct client *c)
if (i2d_X509 (peer_cert, &p) < 0)
return NULL;
unsigned char hash[SHA_DIGEST_LENGTH];
SHA1 (cert, cert_len, hash);
unsigned char hash[SHA256_DIGEST_LENGTH];
SHA256 (cert, cert_len, hash);
struct str fingerprint = str_make ();
for (size_t i = 0; i < sizeof hash; i++)

4
xF.svg
View File

@@ -1,8 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg version="1.1" width="48" height="48" viewBox="0 0 48 48"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="background" x1="0" y1="0" x2="1" y2="1">

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

1
xK-version Normal file
View File

@@ -0,0 +1 @@
2.1.0

45
xM/CMakeLists.txt Normal file
View File

@@ -0,0 +1,45 @@
# Swift language support
cmake_minimum_required (VERSION 3.15)
file (READ ../xK-version project_version)
configure_file (../xK-version xK-version.tag COPYONLY)
string (STRIP "${project_version}" project_version)
# There were two issues when building this from the main CMakeLists.txt:
# a) renaming main.swift to xM.swift requires removing top-level statements,
# b) there is a "redefinition of module 'FFI'" error.
project (xM VERSION "${project_version}"
DESCRIPTION "Cocoa frontend for xC" LANGUAGES Swift)
set (root "${PROJECT_SOURCE_DIR}/..")
add_custom_command (OUTPUT xC-proto.swift
COMMAND env LC_ALL=C awk
-f ${root}/liberty/tools/lxdrgen.awk
-f ${root}/liberty/tools/lxdrgen-swift.awk
-v PrefixCamel=Relay
${root}/xC.lxdr > xC-proto.swift
DEPENDS
${root}/liberty/tools/lxdrgen.awk
${root}/liberty/tools/lxdrgen-swift.awk
${root}/xC.lxdr
COMMENT "Generating xC relay protocol code" VERBATIM)
set (MACOSX_BUNDLE_GUI_IDENTIFIER name.janouch.${PROJECT_NAME})
set (MACOSX_BUNDLE_ICON_FILE xM.icns)
# Avoid including binary files in the repository by generating icons in code.
# sips(1) + Javascript + iconutil(1) could probably also be used.
find_program (SWIFT_EXECUTABLE swift REQUIRED)
set (icon "${PROJECT_BINARY_DIR}/${MACOSX_BUNDLE_ICON_FILE}")
add_custom_command (OUTPUT "${icon}"
COMMAND ${SWIFT_EXECUTABLE} "${PROJECT_SOURCE_DIR}/gen-icon.swift" "${icon}"
DEPENDS gen-icon.swift
COMMENT "Generating xM application icon" VERBATIM)
set_source_files_properties ("${icon}" PROPERTIES
MACOSX_PACKAGE_LOCATION Resources)
# Other requirements: macOS 10.14 for Network, and macOS 11 for Logger.
set (CMAKE_Swift_LANGUAGE_VERSION 5)
add_executable (xM MACOSX_BUNDLE
main.swift "${icon}" "${PROJECT_BINARY_DIR}/xC-proto.swift")

99
xM/gen-icon.swift Normal file
View File

@@ -0,0 +1,99 @@
// gen-icon.swift: generate a program icon for xM in the Apple icon format
//
// Copyright (c) 2023, Přemysl Eric Janouch <p@janouch.name>
// SPDX-License-Identifier: 0BSD
//
// As an odd regression, AppKit may be necessary for JIT linking.
import AppKit
// NSGraphicsContext mostly just weirdly wraps over Quartz,
// so we do it all in Quartz directly.
import CoreGraphics
import Foundation
import ImageIO
import UniformTypeIdentifiers
// Apple uses something that's close to a "quintic superellipse" in their icons,
// but doesn't quite match. Either way, it looks better than rounded rectangles.
func addSquircle(context: CGContext, bounds: CGRect) {
context.move(to: CGPoint(x: bounds.maxX, y: bounds.midY))
for theta in stride(from: 0.0, to: .pi * 2, by: .pi / 1e4) {
let x = pow(abs(cos(theta)), 2 / 5.0) * bounds.width / 2
* CGFloat(signOf: cos(theta), magnitudeOf: 1) + bounds.midX
let y = pow(abs(sin(theta)), 2 / 5.0) * bounds.height / 2
* CGFloat(signOf: sin(theta), magnitudeOf: 1) + bounds.midY
context.addLine(to: CGPoint(x: x, y: y))
}
context.closePath()
}
func drawIcon(scale: CGFloat) -> CGImage? {
let size = CGSizeMake(1024, 1024)
let colorspace = CGColorSpaceCreateDeviceRGB()
let context = CGContext(data: nil,
width: Int(size.width * scale), height: Int(size.height * scale),
bitsPerComponent: 8, bytesPerRow: 0, space: colorspace,
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)!
context.scaleBy(x: scale, y: scale)
let bounds = CGRectMake(100, 100, size.width - 200, size.height - 200)
addSquircle(context: context, bounds: bounds)
let squircle = context.path!
// Gradients don't draw shadows, so draw it separately.
context.saveGState()
context.setShadow(offset: CGSizeMake(0, -12).applying(context.ctm),
blur: 28 * scale, color: CGColor(gray: 0, alpha: 0.5))
context.setFillColor(CGColor(red: 1, green: 0x55p-8, blue: 0, alpha: 1))
context.fillPath()
context.restoreGState()
context.saveGState()
context.addPath(squircle)
context.clip()
context.drawLinearGradient(
CGGradient(colorsSpace: colorspace, colors: [
CGColor(red: 1, green: 0x00p-8, blue: 0, alpha: 1),
CGColor(red: 1, green: 0xaap-8, blue: 0, alpha: 1)
] as CFArray, locations: [0, 1])!,
start: CGPointMake(0, 100), end: CGPointMake(0, size.height - 100),
options: CGGradientDrawingOptions(rawValue: 0))
context.restoreGState()
context.move(to: CGPoint(x: size.width * 0.30, y: size.height * 0.30))
context.addLine(to: CGPoint(x: size.width * 0.30, y: size.height * 0.70))
context.addLine(to: CGPoint(x: size.width * 0.575, y: size.height * 0.425))
context.move(to: CGPoint(x: size.width * 0.70, y: size.height * 0.30))
context.addLine(to: CGPoint(x: size.width * 0.70, y: size.height * 0.70))
context.addLine(to: CGPoint(x: size.width * 0.425, y: size.height * 0.425))
context.setLineWidth(80)
context.setLineCap(.round)
context.setLineJoin(.round)
context.setStrokeColor(CGColor.white)
context.strokePath()
return context.makeImage()
}
if CommandLine.arguments.count != 2 {
print("Usage: \(CommandLine.arguments.first!) OUTPUT.icns")
exit(EXIT_FAILURE)
}
let filename = CommandLine.arguments[1]
let macOSSizes: Array<CGFloat> = [16, 32, 128, 256, 512]
let icns = CGImageDestinationCreateWithURL(
URL(fileURLWithPath: filename) as CFURL,
UTType.icns.identifier as CFString, macOSSizes.count * 2, nil)!
for size in macOSSizes {
CGImageDestinationAddImage(icns, drawIcon(scale: size / 1024.0)!, nil)
CGImageDestinationAddImage(icns, drawIcon(scale: size / 1024.0 * 2)!, [
kCGImagePropertyDPIWidth: 144,
kCGImagePropertyDPIHeight: 144,
] as CFDictionary)
}
if !CGImageDestinationFinalize(icns) {
print("ICNS finalization failed.")
exit(EXIT_FAILURE)
}

1376
xM/main.swift Normal file

File diff suppressed because it is too large Load Diff

3
xN/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
/xN
/xN.1
/irc.go

21
xN/Makefile Normal file
View File

@@ -0,0 +1,21 @@
.POSIX:
.SUFFIXES:
AWK = env LC_ALL=C awk
outputs = irc.go xN xN.1
all: $(outputs)
# If we want to keep module dependencies separate, we don't have many options.
# Symlinking seems to work good enough.
irc.go: ../xS/irc.go
ln -sf ../xS/irc.go $@
xN: xN.go ../xK-version irc.go
go build -ldflags "-X 'main.projectVersion=$$(cat ../xK-version)'" -o $@
xN.1: ../xK-version ../liberty/tools/asciiman.awk xN.adoc
env "asciidoc-release-version=$$(cat ../xK-version)" \
$(AWK) -f ../liberty/tools/asciiman.awk xN.adoc > $@
test: all
go test
clean:
rm -f $(outputs)

3
xN/go.mod Normal file
View File

@@ -0,0 +1,3 @@
module janouch.name/xK/xN
go 1.19

86
xN/xN.adoc Normal file
View File

@@ -0,0 +1,86 @@
xN(1)
=====
:doctype: manpage
:manmanual: xK Manual
:mansource: xK {release-version}
Name
----
xN - IRC notifier
Synopsis
--------
*xN* [_OPTION_]... IRC-URL...
Description
-----------
*xN* is a simple IRC notifier, sending the text it receives on its standard
input to all IRC targets specified by its command line arguments.
The input text is forced to validate as UTF-8, and it is _not_ split
automatically to comply with the maximum IRC message length.
Thus, make sure to make the lines short, or they will be trimmed by the servers.
*xN* does not attempt to appease flood detectors.
Options
-------
*-debug*::
Print incoming IRC traffic, which may help in debugging any issues.
*-version*::
Output version information and exit.
URL format
----------
*xN* accepts URLs describing IRC users and channels, roughly as specified by
the Internet Draft _draft-butcher-irc-url-04.txt_. Note, however, that close
to no validation is done on these, and you should not pass URLs from untrusted
sources, so as to avoid command or parameter injection.
Any provided username will be propagated to the nickname, username,
and realname. The default value for these is the name of your system user.
As an extension, you may use the following options:
*skipjoin*::
Do not JOIN channels before sending messages to them.
This requires channels to allow external messages
(which are disabled by channel mode *+n*).
*usenotice*::
Send a NOTICE rather than a PRIVMSG, which distinguishes automated messages,
and is more appropriate for bots.
Examples
--------
$ uptime | xN 'irc://uptime@localhost/%23watch?skipjoin&usenotice'
Send *uptime*(1) information as an external notice to channel *#watch*
on the local server, using the standard port *6667*.
$ fortune -s | xN ircs://ohayou@irc.libera.chat/john,isuser
Greet user *john* with a fortune for this day. In compliance with _RFC 7194_,
the default TLS port is assumed to be *6697*.
$ xN 'ircs://agent:Password123@irc.cia.gov:1337/#hq?key=123456' <<EOF
The red fox trots quietly at midnight.
EOF
Connect over TLS to *irc.cia.gov* on port *1337*, use *Password123*
as the server password, register as user *agent*,
join channel *#hq* using the channel key *123456*,
and send a very secret message.
Reporting bugs
--------------
Use https://git.janouch.name/p/xK to report bugs, request features,
or submit pull requests.
See also
--------
_Uniform Resource Locator Schemes for Internet Relay Chat Entities_,
https://datatracker.ietf.org/doc/html/draft-butcher-irc-url-04[].
_Default Port for Internet Relay Chat (IRC) via TLS/SSL_,
https://datatracker.ietf.org/doc/html/rfc7194[].

279
xN/xN.go Normal file
View File

@@ -0,0 +1,279 @@
//
// Copyright (c) 2024, 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.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
//
// xN is a simple IRC notifier.
package main
import (
"bufio"
"bytes"
"crypto/tls"
"errors"
"flag"
"fmt"
"io"
"log"
"net"
"net/url"
"os"
"os/user"
"strconv"
"strings"
)
const projectName = "xN"
var projectVersion = "?"
var debugMode = false
type parameters struct {
conn net.Conn // underlying network connection
username string // nickname + username + realname
password string // server password
target string // where to send text
isuser bool // the target is a user rather than a channel
chankey string // channel key
skipjoin bool // whether to send external messages to channels
usenotice bool // whether to use NOTICE rather than PRIVMSG
message []string // lines of the message to send
}
func (p *parameters) send(command string, args ...string) error {
var buf bytes.Buffer
buf.WriteString(command)
for i, arg := range args {
buf.WriteRune(' ')
if i+1 == len(args) {
buf.WriteRune(':')
}
buf.WriteString(arg)
}
buf.WriteString("\r\n")
_, err := p.conn.Write(buf.Bytes())
return err
}
const (
rplWELCOME = "001"
errNICKNAMEINUSE = "433"
)
func notify(p *parameters) error {
// The intro should comfortably fit in the TCP send buffer whole.
if p.password != "" {
p.send("PASS", p.password)
}
p.send("USER", p.username, "0", "*", p.username)
p.send("NICK", p.username)
scanner, nickCounter, issue := bufio.NewScanner(p.conn), 1, ""
for scanner.Scan() {
if debugMode {
log.Println(scanner.Text())
}
m := ircParseMessage(scanner.Text())
switch m.command {
case "PING":
p.send("PONG", m.params...)
case rplWELCOME:
if !p.isuser && !p.skipjoin {
if p.chankey != "" {
p.send("JOIN", p.target, p.chankey)
} else {
p.send("JOIN", p.target)
}
}
for _, line := range p.message {
if p.usenotice {
p.send("NOTICE", p.target, line)
} else {
p.send("PRIVMSG", p.target, line)
}
}
p.send("QUIT")
case errNICKNAMEINUSE:
p.send("NICK", fmt.Sprintf("%s%d", p.username, nickCounter))
nickCounter++
default:
// Prevent hanging on unsuccessful registrations.
numeric, _ := strconv.Atoi(m.command)
if numeric >= 400 && numeric <= 599 {
if len(m.params) > 1 {
issue = strings.Join(m.params[1:], " ")
} else {
issue = strings.Join(m.params, " ")
}
p.send("QUIT")
}
}
}
if err := scanner.Err(); err != nil {
return err
}
if issue != "" {
return errors.New(issue)
}
return nil
}
func parse(rawURL string, text []byte) (
p parameters, connect func() (net.Conn, error), err error) {
u, err := url.Parse(rawURL)
if err != nil {
return p, nil, err
} else if !u.IsAbs() || u.Opaque != "" {
return p, nil, errors.New("need an absolute URL")
} else if u.Path == "/" && u.Fragment != "" {
// Try to handle the common but degenerate case.
fragment := "%23" + u.Fragment
u.Fragment, u.RawFragment = "", ""
if u, err = url.Parse(u.String() + fragment); err != nil {
return p, nil, err
}
}
// Figure out registration details.
p.username = projectName
if u, _ := user.Current(); u != nil {
p.username = u.Username
}
if u.User.Username() != "" {
p.username = u.User.Username()
}
p.password, _ = u.User.Password()
// Figure out the target, which for our intents must accept messages.
path, _ := strings.CutPrefix(u.Path, "/")
elements := strings.Split(path, ",")
if path == "" || elements[0] == "" {
return p, nil, errors.New("unspecified entity")
}
// The last entity type wins.
p.target, p.isuser = elements[0], false
for _, typ := range elements[1:] {
switch typ {
case "isuser":
p.isuser = true
case "ischannel":
p.isuser = false
case "isserver":
// We do not support network names, and this is the default.
default:
return p, nil, errors.New("unsupported type: " + typ)
}
}
if p.isuser {
if i := strings.IndexAny(p.target, "!@"); i != -1 {
p.target = p.target[:i]
}
} else if !strings.HasPrefix(p.target, "#") {
// TODO(p): We should consult RPL_ISUPPORT rather than guess,
// though other prefixes are rare.
p.target = "#" + p.target
}
// Note that the draft RFC wants these to be case-insensitive.
p.chankey = u.Query().Get("key")
// Being able to skip channel join is our own requirement and invention,
// as are notices (names taken from Travis CI configuration).
p.skipjoin = u.Query().Has("skipjoin")
p.usenotice = u.Query().Has("usenotice")
// Ensure valid LF-separated UTF-8, and split it at lines.
sanitized := strings.ReplaceAll(string([]rune(string(text))), "\r", "\n")
for _, line := range strings.Split(sanitized, "\n") {
if line != "" {
p.message = append(p.message, line)
}
}
hostname, port := u.Hostname(), u.Port()
switch u.Scheme {
case "irc":
if port == "" {
port = "6667"
}
connect = func() (net.Conn, error) {
return net.Dial("tcp", net.JoinHostPort(hostname, port))
}
case "ircs":
if port == "" {
port = "6697"
}
connect = func() (net.Conn, error) {
return tls.Dial("tcp", net.JoinHostPort(hostname, port), nil)
}
default:
err = errors.New("unsupported scheme: " + u.Scheme)
}
return
}
// notifyByURL sends the given text to the IRC server specified by a URL.
// See draft-butcher-irc-url-04.txt for the URL scheme specification
// this function loosely follows.
func notifyByURL(rawURL string, text []byte) error {
p, connect, err := parse(rawURL, text)
if p.conn, err = connect(); err != nil {
return err
}
defer p.conn.Close()
return notify(&p)
}
func main() {
flag.BoolVar(&debugMode, "debug", false, "run in verbose debug mode")
version := flag.Bool("version", false, "show version and exit")
flag.Usage = func() {
f := flag.CommandLine.Output()
fmt.Fprintf(f, "Usage: %s [OPTION]... URL...\n", os.Args[0])
flag.PrintDefaults()
}
flag.Parse()
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)
}
status := 0
for _, rawURL := range flag.Args() {
if err := notifyByURL(rawURL, text); err != nil {
status = 1
var ue *url.Error
if errors.As(err, &ue) {
log.Println(err)
} else {
log.Printf("notify %q: %s\n", rawURL, err)
}
}
}
os.Exit(status)
}

98
xN/xN_test.go Normal file
View File

@@ -0,0 +1,98 @@
//
// Copyright (c) 2024, 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.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
//
package main
import "testing"
func TestParseURL(t *testing.T) {
for _, rawURL := range []string{
`irc:server/channel`,
`irc://server/channel,isnetwork`,
`ircs://ssl.ircnet.io`,
`ircs://ssl.ircnet.io/`,
`http://server/path`,
} {
if _, _, err := parse(rawURL, nil); err == nil {
t.Errorf("%q should not parse\n", rawURL)
}
}
for _, test := range []struct {
rawURL string
p parameters
}{
{
rawURL: `irc://uptime@localhost/%23watch?skipjoin&usenotice`,
p: parameters{
username: "uptime",
target: "#watch",
skipjoin: true,
usenotice: true,
},
},
{
rawURL: `ircs://ohayou@irc.libera.chat/john,isuser,isserver`,
p: parameters{
username: "ohayou",
target: "john",
isuser: true,
},
},
{
rawURL: `ircs://agent:Password123@irc.cia.gov:1337/#hq?key=123456`,
p: parameters{
username: "agent",
password: "Password123",
target: "#hq",
chankey: "123456",
},
},
} {
p, _, err := parse(test.rawURL, nil)
if err != nil {
t.Errorf("%q should parse, got: %s\n", test.rawURL, err)
continue
}
if p.username != test.p.username {
t.Errorf("%q: username: %v ≠ %v\n",
test.rawURL, p.username, test.p.username)
}
if p.password != test.p.password {
t.Errorf("%q: password: %v ≠ %v\n",
test.rawURL, p.password, test.p.password)
}
if p.target != test.p.target {
t.Errorf("%q: target: %v ≠ %v\n",
test.rawURL, p.target, test.p.target)
}
if p.isuser != test.p.isuser {
t.Errorf("%q: isuser: %v ≠ %v\n",
test.rawURL, p.isuser, test.p.isuser)
}
if p.chankey != test.p.chankey {
t.Errorf("%q: chankey: %v ≠ %v\n",
test.rawURL, p.chankey, test.p.chankey)
}
if p.skipjoin != test.p.skipjoin {
t.Errorf("%q: skipjoin: %v ≠ %v\n",
test.rawURL, p.skipjoin, test.p.skipjoin)
}
if p.usenotice != test.p.usenotice {
t.Errorf("%q: usenotice: %v ≠ %v\n",
test.rawURL, p.usenotice, test.p.usenotice)
}
}
}

View File

@@ -2,15 +2,17 @@
.SUFFIXES:
AWK = env LC_ALL=C awk
tools = ../liberty/tools
outputs = xP proto.go public/proto.js public/mithril.js
all: $(outputs) public/ircfmt.woff2
xP: xP.go proto.go
go build -o $@
proto.go: ../xC-gen-proto.awk ../xC-gen-proto-go.awk ../xC-proto
$(AWK) -f ../xC-gen-proto.awk -f ../xC-gen-proto-go.awk ../xC-proto > $@
public/proto.js: ../xC-gen-proto.awk ../xC-gen-proto-js.awk ../xC-proto
$(AWK) -f ../xC-gen-proto.awk -f ../xC-gen-proto-js.awk ../xC-proto > $@
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 > $@
public/proto.js: $(tools)/lxdrgen.awk $(tools)/lxdrgen-mjs.awk ../xC.lxdr
$(AWK) -f $(tools)/lxdrgen.awk -f $(tools)/lxdrgen-mjs.awk ../xC.lxdr > $@
public/ircfmt.woff2: gen-ircfmt.awk
$(AWK) -v Output=$@ -f gen-ircfmt.awk
public/mithril.js:

View File

@@ -1,10 +1,7 @@
module janouch.name/xK/xP
go 1.18
go 1.22
require nhooyr.io/websocket v1.8.7
toolchain go1.23.2
require (
github.com/klauspost/compress v1.15.9 // indirect
golang.org/x/sys v0.0.0-20210423082822-04245dca01da // indirect
)
require nhooyr.io/websocket v1.8.17

View File

@@ -1,62 +1,2 @@
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/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
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 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0=
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8=
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo=
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 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
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/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/compress v1.15.9 h1:wKRjX6JRtDdrE9qwa4b/Cip7ACOshUI4smpCQanqjSY=
github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
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 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
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-20210423082822-04245dca01da h1:b3NXsE2LusjYGGjL5bxEVZZORm/YEFFrWFjR8eFrw/c=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/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 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
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/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g=
nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=
nhooyr.io/websocket v1.8.17 h1:KEVeLJkUywCKVsnLIDlD/5gtayKp8VoCkksHCGGfT9Y=
nhooyr.io/websocket v1.8.17/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c=

View File

@@ -120,6 +120,9 @@ button:hover:active {
.item.activity {
font-weight: bold;
}
.item:hover {
background: #f8f8f8;
}
.item.current {
font-style: italic;
background: #eee;

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
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,12 +202,31 @@ 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
b.highlighted = false
}
function bufferPopExcessLines(b) {
// Let "new" messages be, if only because pulling the log file
// is much more problematic in the web browser than in xC.
// TODO: Make the limit configurable, or extract general.backlog_limit.
const old = b.lines.length - b.newMessages - b.newUnimportantMessages
b.lines.splice(0, old - 1000)
}
function bufferActivate(name) {
rpc.send({command: 'BufferActivate', bufferName: name})
}
@@ -288,12 +318,19 @@ rpcEventHandlers.set(Relay.Event.BufferLine, e => {
b.lines.push({...line})
if (!(visible || e.leakToActive) ||
b.newMessages || b.newUnimportantMessages) {
if (line.isUnimportant)
if (line.isUnimportant || e.leakToActive)
b.newUnimportantMessages++
else
b.newMessages++
}
// XXX: In its unkeyed diff algorithm, Mithril.js can only efficiently
// deal with common prefixes, i.e., indefinitely growing buffers.
// But we don't want to key all children of Buffer,
// so only trim buffers while they are, or once they become invisible.
if (e.bufferName != bufferCurrent)
bufferPopExcessLines(b)
if (e.leakToActive) {
let bc = buffers.get(bufferCurrent)
bc.lines.push({...line, leaked: true})
@@ -336,7 +373,7 @@ rpcEventHandlers.set(Relay.Event.BufferStats, e => {
if (b === undefined)
return
b.newMessages = e.newMessages,
b.newMessages = e.newMessages
b.newUnimportantMessages = e.newUnimportantMessages
b.highlighted = e.highlighted
})
@@ -344,6 +381,11 @@ rpcEventHandlers.set(Relay.Event.BufferStats, e => {
rpcEventHandlers.set(Relay.Event.BufferRename, e => {
buffers.set(e.new, buffers.get(e.bufferName))
buffers.delete(e.bufferName)
if (e.bufferName === bufferCurrent)
bufferCurrent = e.new
if (e.bufferName === bufferLast)
bufferLast = e.new
})
rpcEventHandlers.set(Relay.Event.BufferRemove, e => {
@@ -354,8 +396,16 @@ rpcEventHandlers.set(Relay.Event.BufferRemove, e => {
rpcEventHandlers.set(Relay.Event.BufferActivate, e => {
let old = buffers.get(bufferCurrent)
if (old !== undefined)
if (old !== undefined) {
bufferResetStats(old)
bufferPopExcessLines(old)
}
// Initial sync: trim all buffers to our limit, just for consistency.
if (bufferCurrent === undefined) {
for (let b of buffers.values())
bufferPopExcessLines(b)
}
bufferLast = bufferCurrent
let b = buffers.get(e.bufferName)
@@ -387,6 +437,15 @@ rpcEventHandlers.set(Relay.Event.BufferActivate, e => {
}
})
rpcEventHandlers.set(Relay.Event.BufferInput, e => {
let b = buffers.get(e.bufferName)
if (b === undefined)
return
if (b.historyAt == b.history.length)
b.historyAt++
b.history.push(e.text)
})
rpcEventHandlers.set(Relay.Event.BufferClear, e => {
let b = buffers.get(e.bufferName)
if (b !== undefined)
@@ -453,14 +512,23 @@ let BufferList = {
classes.push('highlighted')
highlighted = true
}
return m('.item', {
// The role makes it selectable in VIM-like browser extensions.
return m('.item[role=tab]', {
onclick: event => bufferActivate(name),
onauxclick: event => {
if (event.button == 1)
rpc.send({
command: 'BufferInput',
bufferName: name,
text: '/buffer close',
})
},
class: classes.join(' '),
}, displayName)
})
updateIcon(rpc.ws === undefined ? null : highlighted)
return m('.list', {}, items)
return m('.list[role=tablist]', {}, items)
},
}
@@ -483,7 +551,8 @@ let Content = {
while ((match = re.exec(text)) !== null) {
if (end < match.index)
a.push(m('span', attrs, text.substring(end, match.index)))
a.push(m('a[target=_blank]', {href: match[0], ...attrs}, match[0]))
a.push(m('a[target=_blank][rel=noreferrer]',
{href: match[0], ...attrs}, match[0]))
end = re.lastIndex
}
if (end < text.length)
@@ -566,12 +635,6 @@ let Topic = {
}
let Buffer = {
controller: new AbortController(),
onbeforeremove: vnode => {
Buffer.controller.abort()
},
onupdate: vnode => {
if (bufferAutoscroll)
vnode.dom.scrollTop = vnode.dom.scrollHeight
@@ -579,8 +642,28 @@ let Buffer = {
oncreate: vnode => {
Buffer.onupdate(vnode)
vnode.state.controller = new AbortController()
window.addEventListener('resize', event => Buffer.onupdate(vnode),
{signal: Buffer.controller.signal})
{signal: vnode.state.controller.signal})
Buffer.setDateChangeTimeout(vnode)
},
onremove: vnode => {
vnode.state.controller.abort()
clearTimeout(vnode.state.dateChangeTimeout)
},
setDateChangeTimeout: vnode => {
let midnight = new Date()
midnight.setHours(24, 0, 0, 0)
// Note that this doesn't handle time zone changes correctly.
vnode.state.dateChangeTimeout = setTimeout(() => {
m.redraw()
Buffer.setDateChangeTimeout(vnode)
}, midnight - new Date())
},
view: vnode => {
@@ -632,6 +715,10 @@ let Buffer = {
const dom = event.target
bufferAutoscroll =
dom.scrollTop + dom.clientHeight + 1 >= dom.scrollHeight
let b = buffers.get(bufferCurrent)
if (b !== undefined && b.highlighted && !bufferAutoscroll)
b.highlighted = false
}}, lines)
},
}
@@ -647,7 +734,8 @@ let Log = {
while ((match = re.exec(text)) !== null) {
if (end < match.index)
a.push(text.substring(end, match.index))
a.push(m('a[target=_blank]', {href: match[0]}, match[0]))
a.push(m('a[target=_blank][rel=noreferrer]',
{href: match[0]}, match[0]))
end = re.lastIndex
}
if (end < text.length)
@@ -932,10 +1020,10 @@ let Input = {
onKeyDown: event => {
// TODO: And perhaps on other actions, too.
rpc.send({command: 'Active'})
notifyActive()
let b = buffers.get(bufferCurrent)
if (b === undefined)
if (b === undefined || event.isComposing)
return
let textarea = event.currentTarget
@@ -981,10 +1069,23 @@ let Input = {
} else if (!event.altKey && !event.ctrlKey && !event.metaKey &&
!event.shiftKey) {
handled = true
switch (event.keyCode) {
case 9: success = Input.complete(b, textarea); break
case 13: success = Input.submit(b, textarea); break
default: handled = false
switch (event.key) {
case 'PageUp':
Array.from(document.getElementsByClassName('buffer'))
.forEach(b => b.scrollBy(0, -b.clientHeight))
break
case 'PageDown':
Array.from(document.getElementsByClassName('buffer'))
.forEach(b => b.scrollBy(0, +b.clientHeight))
break
case 'Tab':
success = Input.complete(b, textarea);
break
case 'Enter':
success = Input.submit(b, textarea);
break
default:
handled = false
}
}
if (!success)
@@ -1024,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)]),
@@ -1049,6 +1156,13 @@ let lastWasEscape = false
document.addEventListener('keydown', event => {
event.escapePrefix = lastWasEscape
if (lastWasEscape) {
// https://www.w3.org/TR/uievents-key/#keys-modifier
// https://bugzilla.mozilla.org/show_bug.cgi?id=1232918
if (["Alt", "AltGraph", "CapsLock", "Control", "Fn", "FnLock",
"Meta", "NumLock", "ScrollLock", "Shift", "Symbol", "SymbolLock",
"Hyper", "Super", "OS"].indexOf(event.key) != -1)
return
lastWasEscape = false
} else if (event.code == 'Escape' &&
navigator.userAgentData?.platform === 'macOS') {

102
xP/xP.go
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,23 +6,33 @@ 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"
"os"
"strings"
"time"
"nhooyr.io/websocket"
)
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
@@ -74,12 +84,12 @@ func relayMakeReceiver(ctx context.Context, conn net.Conn) <-chan []byte {
go func() {
defer close(p)
for {
j := relayReadFrame(r)
if j == nil {
b := relayReadFrame(r)
if b == nil {
return
}
select {
case p <- j:
case p <- b:
case <-ctx.Done():
return
}
@@ -144,8 +154,7 @@ func clientWriteError(ctx context.Context, ws *websocket.Conn, err error) bool {
b, ok := (&RelayEventMessage{
EventSeq: 0,
Data: RelayEventData{
Interface: RelayEventDataError{
Event: RelayEventError,
Variant: &RelayEventDataError{
CommandSeq: 0,
Error: err.Error(),
},
@@ -159,13 +168,21 @@ func clientWriteError(ctx context.Context, ws *websocket.Conn, err error) bool {
}
func handleWS(w http.ResponseWriter, r *http.Request) {
ws, err := websocket.Accept(w, r, &websocket.AcceptOptions{
opts := &websocket.AcceptOptions{
InsecureSkipVerify: true,
// Note that Safari can be broken with compression.
CompressionMode: websocket.CompressionContextTakeover,
CompressionMode: websocket.CompressionContextTakeover,
// This is for the payload; set higher to avoid overhead.
CompressionThreshold: 64 << 10,
})
}
// 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; ")) {
opts.CompressionMode = websocket.CompressionDisabled
}
ws, err := websocket.Accept(w, r, opts)
if err != nil {
log.Println("Client rejected: " + err.Error())
return
@@ -232,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>
@@ -254,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(),
@@ -286,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))
@@ -295,5 +355,5 @@ func main() {
WriteTimeout: 60 * time.Second,
MaxHeaderBytes: 32 << 10,
}
log.Fatal(s.ListenAndServe())
log.Fatalln(s.ListenAndServe())
}

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))
}

1
xS/.gitignore vendored
View File

@@ -1,2 +1,3 @@
/xS
/xS-replies.go
/xS.1

View File

@@ -2,12 +2,17 @@
.SUFFIXES:
AWK = env LC_ALL=C awk
outputs = xS xS-replies.go
outputs = xS xS-replies.go xS.1
all: $(outputs)
xS: xS.go xS-replies.go
go build -o $@
xS: xS.go xS-replies.go ../xK-version
go build -ldflags "-X 'main.projectVersion=$$(cat ../xK-version)'" -o $@
xS-replies.go: xS-gen-replies.awk xS-replies
$(AWK) -f xS-gen-replies.awk xS-replies > $@
xS.1: ../xK-version ../liberty/tools/asciiman.awk xS.adoc
env "asciidoc-release-version=$$(cat ../xK-version)" \
$(AWK) -f ../liberty/tools/asciiman.awk xS.adoc > $@
test: all
go test
clean:
rm -f $(outputs)

132
xS/irc.go Normal file
View File

@@ -0,0 +1,132 @@
package main
import (
"path/filepath"
"regexp"
"strings"
)
func ircToLower(c byte) byte {
switch c {
case '[':
return '{'
case ']':
return '}'
case '\\':
return '|'
case '~':
return '^'
}
if c >= 'A' && c <= 'Z' {
return c + ('a' - 'A')
}
return c
}
func ircToUpper(c byte) byte {
switch c {
case '{':
return '['
case '}':
return ']'
case '|':
return '\\'
case '^':
return '~'
}
if c >= 'a' && c <= 'z' {
return c - ('a' - 'A')
}
return c
}
// Convert identifier to a canonical form for case-insensitive comparisons.
// ircToUpper is used so that statically initialized maps can be in uppercase.
func ircToCanon(ident string) string {
var canon []byte
for _, c := range []byte(ident) {
canon = append(canon, ircToUpper(c))
}
return string(canon)
}
func ircEqual(s1, s2 string) bool {
return ircToCanon(s1) == ircToCanon(s2)
}
func ircFnmatch(pattern string, s string) bool {
pattern, s = ircToCanon(pattern), ircToCanon(s)
// FIXME: This should not support [] ranges and handle '/' specially.
// We could translate the pattern to a regular expression.
matched, _ := filepath.Match(pattern, s)
return matched
}
var reMsg = regexp.MustCompile(
`^(?:@([^ ]*) +)?(?::([^! ]*)(?:!([^@]*)@([^ ]*))? +)?([^ ]+)(.*)?$`)
var reArgs = regexp.MustCompile(`:.*| [^: ][^ ]*`)
type message struct {
tags map[string]string // IRC 3.2 message tags
nick string // optional nickname
user string // optional username
host string // optional hostname or IP address
command string // command name
params []string // arguments
}
func ircUnescapeMessageTag(value string) string {
var buf []byte
escape := false
for i := 0; i < len(value); i++ {
if escape {
switch value[i] {
case ':':
buf = append(buf, ';')
case 's':
buf = append(buf, ' ')
case 'r':
buf = append(buf, '\r')
case 'n':
buf = append(buf, '\n')
default:
buf = append(buf, value[i])
}
escape = false
} else if value[i] == '\\' {
escape = true
} else {
buf = append(buf, value[i])
}
}
return string(buf)
}
func ircParseMessageTags(tags string, out map[string]string) {
for _, tag := range strings.Split(tags, ";") {
if tag == "" {
// Ignore empty.
} else if equal := strings.IndexByte(tag, '='); equal < 0 {
out[tag] = ""
} else {
out[tag[:equal]] = ircUnescapeMessageTag(tag[equal+1:])
}
}
}
func ircParseMessage(line string) *message {
m := reMsg.FindStringSubmatch(line)
if m == nil {
return nil
}
msg := message{nil, m[2], m[3], m[4], m[5], nil}
if m[1] != "" {
msg.tags = make(map[string]string)
ircParseMessageTags(m[1], msg.tags)
}
for _, x := range reArgs.FindAllString(m[6], -1) {
msg.params = append(msg.params, x[1:])
}
return &msg
}

57
xS/xS.adoc Normal file
View File

@@ -0,0 +1,57 @@
xS(1)
=====
:doctype: manpage
:manmanual: xK Manual
:mansource: xK {release-version}
Name
----
xS - IRC daemon
Synopsis
--------
*xS* [_OPTION_]...
Description
-----------
*xS* is a basic IRC daemon for single-server networks, suitable for testing
and private use. When run without a configuration file, it will start listening
on the standard port 6667 and the "any" address.
Options
-------
*-debug*::
Do not daemonize, print more information on the standard error stream
to help debug various issues.
*-systemd*::
Log using the format specified in *sd-daemon*(3).
*-h*, *-help*::
Display a help message and exit.
*-version*::
Output version information and exit.
*-writedefaultcfg*::
Write a configuration file with defaults, show its path and exit.
+
The file will be appropriately commented.
Files
-----
*xS* follows the XDG Base Directory Specification.
_~/.config/xS/xS.conf_::
_/etc/xdg/xS/xS.conf_::
The daemon's configuration file. Use the *-writedefaultcfg* option
to create a new one for editing.
Reporting bugs
--------------
Use https://git.janouch.name/p/xK to report bugs, request features,
or submit pull requests.
See also
--------
*sd-daemon*(3)

136
xS/xS.go
View File

@@ -40,13 +40,11 @@ import (
"time"
)
var debugMode = false
const projectName = "xS"
const (
projectName = "xS"
// TODO: Consider using the same version number for all subprojects.
projectVersion = "0"
)
var projectVersion = "?"
var debugMode = false
// --- Logging -----------------------------------------------------------------
@@ -458,132 +456,6 @@ func (fd *floodDetector) check() bool {
return count <= fd.limit
}
// --- IRC protocol ------------------------------------------------------------
func ircToLower(c byte) byte {
switch c {
case '[':
return '{'
case ']':
return '}'
case '\\':
return '|'
case '~':
return '^'
}
if c >= 'A' && c <= 'Z' {
return c + ('a' - 'A')
}
return c
}
func ircToUpper(c byte) byte {
switch c {
case '{':
return '['
case '}':
return ']'
case '|':
return '\\'
case '^':
return '~'
}
if c >= 'a' && c <= 'z' {
return c - ('a' - 'A')
}
return c
}
// Convert identifier to a canonical form for case-insensitive comparisons.
// ircToUpper is used so that statically initialized maps can be in uppercase.
func ircToCanon(ident string) string {
var canon []byte
for _, c := range []byte(ident) {
canon = append(canon, ircToUpper(c))
}
return string(canon)
}
func ircEqual(s1, s2 string) bool {
return ircToCanon(s1) == ircToCanon(s2)
}
func ircFnmatch(pattern string, s string) bool {
pattern, s = ircToCanon(pattern), ircToCanon(s)
// FIXME: This should not support [] ranges and handle '/' specially.
// We could translate the pattern to a regular expression.
matched, _ := filepath.Match(pattern, s)
return matched
}
var reMsg = regexp.MustCompile(
`^(?:@([^ ]*) +)?(?::([^! ]*)(?:!([^@]*)@([^ ]*))? +)?([^ ]+)(.*)?$`)
var reArgs = regexp.MustCompile(`:.*| [^: ][^ ]*`)
type message struct {
tags map[string]string // IRC 3.2 message tags
nick string // optional nickname
user string // optional username
host string // optional hostname or IP address
command string // command name
params []string // arguments
}
func ircUnescapeMessageTag(value string) string {
var buf []byte
escape := false
for i := 0; i < len(value); i++ {
if escape {
switch value[i] {
case ':':
buf = append(buf, ';')
case 's':
buf = append(buf, ' ')
case 'r':
buf = append(buf, '\r')
case 'n':
buf = append(buf, '\n')
default:
buf = append(buf, value[i])
}
escape = false
} else if value[i] == '\\' {
escape = true
} else {
buf = append(buf, value[i])
}
}
return string(buf)
}
func ircParseMessageTags(tags string, out map[string]string) {
for _, tag := range splitString(tags, ";", true /* ignoreEmpty */) {
if equal := strings.IndexByte(tag, '='); equal < 0 {
out[tag] = ""
} else {
out[tag[:equal]] = ircUnescapeMessageTag(tag[equal+1:])
}
}
}
func ircParseMessage(line string) *message {
m := reMsg.FindStringSubmatch(line)
if m == nil {
return nil
}
msg := message{nil, m[2], m[3], m[4], m[5], nil}
if m[1] != "" {
msg.tags = make(map[string]string)
ircParseMessageTags(m[1], msg.tags)
}
for _, x := range reArgs.FindAllString(m[6], -1) {
msg.params = append(msg.params, x[1:])
}
return &msg
}
// --- IRC token validation ----------------------------------------------------
// Everything as per RFC 2812

164
xT/CMakeLists.txt Normal file
View File

@@ -0,0 +1,164 @@
# As per Qt 6.8 documentation, at least 3.16 is necessary
cmake_minimum_required (VERSION 3.21.1)
file (READ ../xK-version project_version)
configure_file (../xK-version xK-version.tag COPYONLY)
string (STRIP "${project_version}" project_version)
# This is an entirely separate CMake project.
project (xT VERSION "${project_version}"
DESCRIPTION "Qt frontend for xC" LANGUAGES CXX)
set (CMAKE_CXX_STANDARD 17)
set (CMAKE_CXX_STANDARD_REQUIRED ON)
find_package (Qt6 REQUIRED COMPONENTS Widgets Network Multimedia)
qt_standard_project_setup ()
add_compile_options ("$<$<CXX_COMPILER_ID:MSVC>:/utf-8>")
add_compile_options ("$<$<CXX_COMPILER_ID:GNU>:-Wall;-Wextra>")
add_compile_options ("$<$<CXX_COMPILER_ID:Clang>:-Wall;-Wextra>")
set (project_config "${PROJECT_BINARY_DIR}/config.h")
configure_file ("${PROJECT_SOURCE_DIR}/config.h.in" "${project_config}")
include_directories ("${PROJECT_SOURCE_DIR}" "${PROJECT_BINARY_DIR}")
# Produce a beep sample
find_program (sox_EXECUTABLE sox REQUIRED)
set (beep "${PROJECT_BINARY_DIR}/beep.wav")
add_custom_command (OUTPUT "${beep}"
COMMAND ${sox_EXECUTABLE} -b 16 -Dr 44100 -n "${beep}"
synth 0.1 0 25 triangle 800 vol 0.5 fade t 0 -0 0.005 pad 0 0.05
COMMENT "Generating a beep sample" VERBATIM)
set_property (SOURCE "${beep}" APPEND PROPERTY QT_RESOURCE_ALIAS beep.wav)
# Rasterize SVG icons
set (root "${PROJECT_SOURCE_DIR}/..")
set (CMAKE_MODULE_PATH "${root}/liberty/cmake")
include (IconUtils)
# It might generally be better to use QtSvg, though it is an extra dependency.
# The icon_to_png macro is not intended to be used like this.
foreach (icon xT xT-highlighted)
icon_to_png (${icon} "${PROJECT_SOURCE_DIR}/${icon}.svg"
48 "${PROJECT_BINARY_DIR}/resources" icon_png)
set_property (SOURCE "${icon_png}"
APPEND PROPERTY QT_RESOURCE_ALIAS "${icon}.png")
list (APPEND icon_rsrc_list "${icon_png}")
endforeach ()
if (APPLE)
set (MACOSX_BUNDLE_ICON_FILE xT.icns)
icon_to_icns ("${PROJECT_SOURCE_DIR}/xT.svg"
"${MACOSX_BUNDLE_ICON_FILE}" icon_icns)
else ()
# The largest size is mainly for an appropriately sized Windows icon
set (icon_base "${PROJECT_BINARY_DIR}/icons")
set (icon_png_list)
foreach (icon_size 16 32 48 256)
icon_to_png (xT "${PROJECT_SOURCE_DIR}/xT.svg"
${icon_size} "${icon_base}" icon_png)
list (APPEND icon_png_list "${icon_png}")
endforeach ()
add_custom_target (icons ALL DEPENDS ${icon_png_list})
if (WIN32)
list (REMOVE_ITEM icon_png_list "${icon_png}")
set (icon_ico "${PROJECT_BINARY_DIR}/xT.ico")
icon_for_win32 ("${icon_ico}" "${icon_png_list}" "${icon_png}")
set (resource_file "${PROJECT_BINARY_DIR}/xT.rc")
list (APPEND project_sources "${resource_file}")
add_custom_command (OUTPUT "${resource_file}"
COMMAND ${CMAKE_COMMAND} -E echo "1 ICON \"xT.ico\""
> ${resource_file} VERBATIM)
set_property (SOURCE "${resource_file}"
APPEND PROPERTY OBJECT_DEPENDS ${icon_ico})
endif ()
endif ()
# Build the main executable and link it
find_program (awk_EXECUTABLE awk ${find_program_REQUIRE})
add_custom_command (OUTPUT xC-proto.cpp
COMMAND ${CMAKE_COMMAND} -E env LC_ALL=C ${awk_EXECUTABLE}
-f ${root}/liberty/tools/lxdrgen.awk
-f ${root}/liberty/tools/lxdrgen-cpp.awk
-v PrefixCamel=Relay
${root}/xC.lxdr > xC-proto.cpp
DEPENDS
${root}/liberty/tools/lxdrgen.awk
${root}/liberty/tools/lxdrgen-cpp.awk
${root}/xC.lxdr
COMMENT "Generating xC relay protocol code" VERBATIM)
add_custom_target (xC-proto DEPENDS ${PROJECT_BINARY_DIR}/xC-proto.cpp)
list (APPEND project_sources "${root}/liberty/tools/lxdrgen-cpp-qt.cpp")
qt_add_executable (xT
xT.cpp ${project_config} ${project_sources} "${icon_icns}")
add_dependencies (xT xC-proto)
qt_add_resources (xT "rsrc" PREFIX / FILES "${beep}" ${icon_rsrc_list})
target_link_libraries (xT PRIVATE Qt6::Widgets Qt6::Network Qt6::Multimedia)
set_target_properties (xT PROPERTIES WIN32_EXECUTABLE ON MACOSX_BUNDLE ON
MACOSX_BUNDLE_GUI_IDENTIFIER name.janouch.xT)
# https://stackoverflow.com/questions/79079161 and resolved in Qt Creator 16.
set (QT_QML_GENERATE_QMLLS_INI ON)
# The files to be installed
include (GNUInstallDirs)
if (ANDROID)
install (TARGETS xT DESTINATION .)
elseif (APPLE OR WIN32)
install (TARGETS xT
BUNDLE DESTINATION .
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
# XXX: QTBUG-127075, which can be circumvented by manually running
# macdeployqt on xT.app before the install.
qt_generate_deploy_app_script (TARGET xT OUTPUT_SCRIPT deploy_xT)
install (SCRIPT "${deploy_xT}")
else ()
install (TARGETS xT DESTINATION ${CMAKE_INSTALL_BINDIR})
install (FILES ../LICENSE DESTINATION ${CMAKE_INSTALL_DOCDIR})
install (FILES xT.svg
DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/scalable/apps)
install (DIRECTORY ${icon_base}
DESTINATION ${CMAKE_INSTALL_DATADIR})
install (FILES xT.desktop
DESTINATION ${CMAKE_INSTALL_DATADIR}/applications)
endif ()
# Within MSYS2, windeployqt doesn't copy the compiler runtime,
# which is always linked dynamically by the Qt binaries.
# TODO(p): Consider whether or not to use MSYS2 to cross-compile, and how.
if (WIN32)
install (CODE [=[
set (bindir "${CMAKE_INSTALL_PREFIX}/bin")
execute_process (COMMAND cygpath -m /
OUTPUT_VARIABLE cygroot OUTPUT_STRIP_TRAILING_WHITESPACE)
if (cygroot)
execute_process (COMMAND ldd "${bindir}/xT.exe"
OUTPUT_VARIABLE ldd_output OUTPUT_STRIP_TRAILING_WHITESPACE)
string (REGEX MATCHALL " /mingw64/bin/[^ ]+ " libs "${ldd_output}")
foreach (lib ${libs})
string (STRIP "${lib}" lib)
file (COPY "${cygroot}${lib}" DESTINATION "${bindir}")
endforeach()
endif ()
]=])
endif ()
# CPack
set (CPACK_PACKAGE_VENDOR "Premysl Eric Janouch")
set (CPACK_PACKAGE_CONTACT "Přemysl Eric Janouch <p@janouch.name>")
set (CPACK_RESOURCE_FILE_LICENSE "${PROJECT_SOURCE_DIR}/../LICENSE")
set (CPACK_GENERATOR "TGZ;ZIP")
set (CPACK_PACKAGE_FILE_NAME
"${PROJECT_NAME}-${PROJECT_VERSION}-${CMAKE_SYSTEM_NAME}-${CMAKE_SYSTEM_PROCESSOR}")
set (CPACK_PACKAGE_INSTALL_DIRECTORY "${PROJECT_NAME} ${PROJECT_VERSION}")
set (CPACK_SOURCE_GENERATOR "TGZ;ZIP")
set (CPACK_SOURCE_IGNORE_FILES "/build;/CMakeLists.txt.user")
set (CPACK_SOURCE_PACKAGE_FILE_NAME "${PROJECT_NAME}-${PROJECT_VERSION}")
include (CPack)

7
xT/config.h.in Normal file
View File

@@ -0,0 +1,7 @@
#ifndef CONFIG_H
#define CONFIG_H
#define PROJECT_NAME "${PROJECT_NAME}"
#define PROJECT_VERSION "${project_version}"
#endif // ! CONFIG_H

29
xT/xT-highlighted.svg Normal file
View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg version="1.1" width="48" height="48" viewBox="0 0 48 48"
xmlns="http://www.w3.org/2000/svg">
<defs>
<radialGradient id="green-x">
<stop stop-color="hsl(66, 100%, 80%)" offset="0" />
<stop stop-color="hsl(66, 100%, 50%)" offset="1" />
</radialGradient>
<radialGradient id="orange">
<stop stop-color="hsl(36, 100%, 60%)" offset="0" />
<stop stop-color="hsl(23, 100%, 60%)" offset="1" />
</radialGradient>
<filter id="shadow" x="-50%" y="-50%" width="200%" height="200%">
<feDropShadow dx="0" dy="0" stdDeviation="0.05"
flood-color="rgba(0, 0, 0, .5)" />
</filter>
</defs>
<!-- XXX: librsvg screws up shadows on rotated objects. -->
<g filter="url(#shadow)" transform="translate(24 3) scale(16)">
<path fill="url(#orange)" stroke="hsl(36, 100%, 20%)" stroke-width="0.1"
d="M-.8 0 H.8 V.5 H.25 V2.625 H-.25 V.5 H-.8 Z" />
</g>
<g filter="url(#shadow)" transform="translate(24 28) rotate(-45) scale(16)">
<path fill="url(#green-x)" stroke="hsl(66, 100%, 20%)" stroke-width="0.1"
d="M-.25 -1 H.25 V-.25 H1 V.25 H.25 V1 H-.25 V.25 H-1 V-.25 H-.25 Z" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

1734
xT/xT.cpp Normal file

File diff suppressed because it is too large Load Diff

8
xT/xT.desktop Normal file
View File

@@ -0,0 +1,8 @@
[Desktop Entry]
Type=Application
Name=xT
GenericName=IRC Client
Icon=xT
Exec=xT
StartupNotify=false
Categories=Network;Chat;IRCClient;

29
xT/xT.svg Normal file
View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg version="1.1" width="48" height="48" viewBox="0 0 48 48"
xmlns="http://www.w3.org/2000/svg">
<defs>
<radialGradient id="grey-x">
<stop stop-color="hsl(66, 0%, 90%)" offset="0" />
<stop stop-color="hsl(66, 0%, 80%)" offset="1" />
</radialGradient>
<radialGradient id="orange">
<stop stop-color="hsl(36, 100%, 60%)" offset="0" />
<stop stop-color="hsl(23, 100%, 60%)" offset="1" />
</radialGradient>
<filter id="shadow" x="-50%" y="-50%" width="200%" height="200%">
<feDropShadow dx="0" dy="0" stdDeviation="0.05"
flood-color="rgba(0, 0, 0, .5)" />
</filter>
</defs>
<!-- XXX: librsvg screws up shadows on rotated objects. -->
<g filter="url(#shadow)" transform="translate(24 28) rotate(-45) scale(16)">
<path fill="url(#grey-x)" stroke="hsl(66, 0%, 30%)" stroke-width="0.1"
d="M-.25 -1 H.25 V-.25 H1 V.25 H.25 V1 H-.25 V.25 H-1 V-.25 H-.25 Z" />
</g>
<g filter="url(#shadow)" transform="translate(24 3) scale(16)">
<path fill="url(#orange)" stroke="hsl(36, 100%, 20%)" stroke-width="0.1"
d="M-.8 0 H.8 V.5 H.25 V2.625 H-.25 V.5 H-.8 Z" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

11
xW/.clang-format Normal file
View File

@@ -0,0 +1,11 @@
BasedOnStyle: LLVM
ColumnLimit: 80
IndentWidth: 4
TabWidth: 4
UseTab: ForContinuationAndIndentation
AlwaysBreakAfterReturnType: AllDefinitions
BreakBeforeBraces: Linux
SpaceAfterCStyleCast: true
AlignAfterOpenBracket: DontAlign
AlignOperands: DontAlign
SpacesBeforeTrailingComments: 2

85
xW/CMakeLists.txt Normal file
View File

@@ -0,0 +1,85 @@
# The last version with Windows XP support is 3.13, we want to keep that
cmake_minimum_required (VERSION 3.10)
file (READ ../xK-version project_version)
configure_file (../xK-version xK-version.tag COPYONLY)
string (STRIP "${project_version}" project_version)
# This is an entirely separate CMake project--the main executables only build
# on Windows within Cygwin, and this Windows executable only builds on Linux
# cross-compiled, so you'd want to build them independently anyway.
project (xW VERSION "${project_version}"
DESCRIPTION "Win32 frontend for xC" LANGUAGES CXX)
set (CMAKE_CXX_STANDARD 17)
add_definitions (-DUNICODE -D_UNICODE)
add_compile_options ("$<$<CXX_COMPILER_ID:MSVC>:/utf-8>")
add_compile_options ("$<$<CXX_COMPILER_ID:GNU>:-Wall;-Wextra>")
add_compile_options ("$<$<CXX_COMPILER_ID:Clang>:-Wall;-Wextra>")
add_link_options ("$<$<CXX_COMPILER_ID:GNU>:-static;-municode>")
add_link_options ("$<$<CXX_COMPILER_ID:Clang>:-static;-municode>")
set (project_config ${PROJECT_BINARY_DIR}/config.h)
configure_file (${PROJECT_SOURCE_DIR}/config.h.in ${project_config})
include_directories (${PROJECT_SOURCE_DIR} ${PROJECT_BINARY_DIR})
# Produce a beep sample
if (NOT ${CMAKE_VERSION} VERSION_LESS 3.18.0)
set (find_program_REQUIRE REQUIRED)
endif ()
find_program (sox_EXECUTABLE sox ${find_program_REQUIRE})
add_custom_command (OUTPUT beep.wav
COMMAND ${sox_EXECUTABLE} -b 16 -Dr 44100 -n beep.wav
synth 0.1 0 25 triangle 800 vol 0.5 fade t 0 -0 0.005 pad 0 0.05
COMMENT "Generating a beep sample" VERBATIM)
# Rasterize SVG icons
set (root "${PROJECT_SOURCE_DIR}/..")
set (CMAKE_MODULE_PATH ${root}/liberty/cmake)
include (IconUtils)
set (icon_ico_list)
foreach (icon xW xW-highlighted)
set (icon_png_list)
foreach (icon_size 16 32 48)
icon_to_png (${icon} ${PROJECT_SOURCE_DIR}/${icon}.svg
${icon_size} ${PROJECT_BINARY_DIR}/icons icon_png)
list (APPEND icon_png_list ${icon_png})
endforeach ()
icon_to_png (${icon} ${PROJECT_SOURCE_DIR}/${icon}.svg
256 ${PROJECT_BINARY_DIR}/icons icon_png)
set (icon_ico ${PROJECT_BINARY_DIR}/${icon}.ico)
icon_for_win32 (${icon_ico} "${icon_png_list}" "${icon_png}")
list (APPEND icon_ico_list ${icon_ico})
endforeach ()
set_property (SOURCE xW.rc
APPEND PROPERTY OBJECT_DEPENDS ${icon_ico_list} beep.wav)
# Build the main executable and link it
find_program (awk_EXECUTABLE awk ${find_program_REQUIRE})
add_custom_command (OUTPUT xC-proto.cpp
COMMAND ${CMAKE_COMMAND} -E env LC_ALL=C ${awk_EXECUTABLE}
-f ${root}/liberty/tools/lxdrgen.awk
-f ${root}/liberty/tools/lxdrgen-cpp.awk
-v PrefixCamel=Relay
${root}/xC.lxdr > xC-proto.cpp
DEPENDS
${root}/liberty/tools/lxdrgen.awk
${root}/liberty/tools/lxdrgen-cpp.awk
${root}/xC.lxdr
COMMENT "Generating xC relay protocol code" VERBATIM)
add_custom_target (xC-proto DEPENDS ${PROJECT_BINARY_DIR}/xC-proto.cpp)
add_executable (xW WIN32 xW.cpp xW.rc xW.manifest ${project_config}
${root}/liberty/tools/lxdrgen-cpp-win32.cpp)
target_link_libraries (xW comctl32 ws2_32 winmm)
add_dependencies (xW xC-proto)
# At least with MinGW, this is a fully independent portable executable
install (TARGETS xW DESTINATION .)
set (CPACK_GENERATOR ZIP)
include (CPack)

14
xW/config.h.in Normal file
View File

@@ -0,0 +1,14 @@
#ifndef CONFIG_H
#define CONFIG_H
#define PROJECT_NAME "${PROJECT_NAME}"
#define PROJECT_VERSION "${project_version}"
#define PROJECT_DESCRIPTION "${PROJECT_DESCRIPTION}"
#define PROJECT_AUTHOR "Přemysl Eric Janouch"
#define PROJECT_MAJOR (${PROJECT_VERSION_MAJOR}-0)
#define PROJECT_MINOR (${PROJECT_VERSION_MINOR}-0)
#define PROJECT_PATCH (${PROJECT_VERSION_PATCH}-0)
#define PROJECT_TWEAK (${PROJECT_VERSION_TWEAK}-0)
#endif // ! CONFIG_H

24
xW/xW-highlighted.svg Normal file
View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg version="1.1" width="48" height="48" viewBox="0 0 48 48"
xmlns="http://www.w3.org/2000/svg">
<defs>
<clipPath id="outer">
<rect x="-1" y="-0.15" width="5" height="3.30" />
</clipPath>
<clipPath id="inner">
<rect x="-1" y="0" width="5" height="3" />
</clipPath>
</defs>
<g transform="translate(6, 6) scale(12)" stroke-linecap="square">
<g clip-path="url(#outer)">
<path stroke="#ffffff" stroke-width="1.5" d="M 0.5,0 2.5,3" />
<path stroke="#ffffff" stroke-width="1.5" d="M 0.5,3 2.5,0" />
</g>
<g clip-path="url(#inner)">
<path stroke="#ff0000" stroke-width="0.9" d="M 0.5,0 2.5,3" />
<path stroke="#ff0000" stroke-width="0.9" d="M 0.5,3 2.5,0" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 806 B

18
xW/xW-resources.h Normal file
View File

@@ -0,0 +1,18 @@
#define IDI_ICON 1
#define IDI_HIGHLIGHTED 2
#define IDR_BEEP 3
#define IDA_ACCELERATORS 10
// Named after input_add_functions() in xC.
#define ID_PREVIOUS_BUFFER 11
#define ID_NEXT_BUFFER 12
#define ID_SWITCH_BUFFER 13
#define ID_GOTO_HIGHLIGHT 14
#define ID_GOTO_ACTIVITY 15
#define ID_TOGGLE_UNIMPORTANT 16
#define ID_DISPLAY_FULL_LOG 17
#define IDD_CONNECT 20
#define IDC_STATIC 21
#define IDC_HOST 22
#define IDC_PORT 23

2035
xW/xW.cpp Normal file

File diff suppressed because it is too large Load Diff

25
xW/xW.manifest Normal file
View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity name="xW" version="1.0.0.0" type="win32" />
<dependency>
<dependentAssembly>
<assemblyIdentity name="Microsoft.Windows.Common-Controls"
version="6.0.0.0" type="win32" processorArchitecture="*"
publicKeyToken="6595b64144ccf1df" language="*" />
</dependentAssembly>
</dependency>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
<!-- Windows 8.1 -->
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />
<!-- Windows 8 -->
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />
<!-- Windows 7 -->
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />
<!-- Windows Vista -->
<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}" />
</application>
</compatibility>
</assembly>

80
xW/xW.rc Normal file
View File

@@ -0,0 +1,80 @@
#include <windows.h>
#include "xW-resources.h"
// https://devblogs.microsoft.com/oldnewthing/20190607-00/?p=102569
// For UTF-8 literals to work in both MinGW and Microsoft resource compilers,
// the pragma needs to be in this file, and before they're included.
#pragma code_page(65001)
#include "config.h"
// Beware of this madness https://gitlab.kitware.com/cmake/cmake/-/issues/23066
CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "xW.manifest"
IDI_ICON ICON "xW.ico"
IDI_HIGHLIGHTED ICON "xW-highlighted.ico"
IDR_BEEP WAVE "beep.wav"
IDA_ACCELERATORS ACCELERATORS
BEGIN
"^p", ID_PREVIOUS_BUFFER
"^n", ID_NEXT_BUFFER
VK_F5, ID_PREVIOUS_BUFFER, VIRTKEY
VK_F6, ID_NEXT_BUFFER, VIRTKEY
VK_PRIOR, ID_PREVIOUS_BUFFER, CONTROL, VIRTKEY
VK_NEXT, ID_NEXT_BUFFER, CONTROL, VIRTKEY
VK_TAB, ID_SWITCH_BUFFER, CONTROL, VIRTKEY
// These are proper, but llvm-rc won't accept them (GitHub #64002).
#ifndef __clang__
"!", ID_GOTO_HIGHLIGHT, ALT
"a", ID_GOTO_ACTIVITY, ALT
"H", ID_TOGGLE_UNIMPORTANT, ALT
"h", ID_DISPLAY_FULL_LOG, ALT
#endif
END
// https://devblogs.microsoft.com/oldnewthing/20050204-00/?p=36523
// https://devblogs.microsoft.com/oldnewthing/20050207-00/?p=36513
//
// Note that this is still not the right font to use in newest Windows,
// that would be 9pt Segoe UI, as described in:
// https://learn.microsoft.com/en-us/windows/win32/uxguide/vis-fonts
// or even better yet, NONCLIENTMETRICS::lfMessageFont.
IDD_CONNECT DIALOGEX 0, 0, 150, 64
STYLE DS_SHELLFONT | DS_MODALFRAME | DS_CENTER \
| WS_POPUP | WS_CAPTION | WS_SYSMENU
CAPTION "Connect to Relay"
FONT 8, "MS Shell Dlg", 400 /*FW_NORMAL*/, 0 /*FALSE*/, 0x1 /*DEFAULT_CHARSET*/
BEGIN
LTEXT "&Host:", IDC_STATIC, 7, 10, 18, 8
EDITTEXT IDC_HOST, 39, 7, 104, 14, ES_AUTOHSCROLL
LTEXT "&Port:", IDC_STATIC, 7, 28, 18, 8
EDITTEXT IDC_PORT, 39, 25, 104, 14, ES_AUTOHSCROLL
DEFPUSHBUTTON "&Connect", IDOK, 39, 43, 50, 14
PUSHBUTTON "E&xit", IDCANCEL, 93, 43, 50, 14
END
VS_VERSION_INFO VERSIONINFO
FILEVERSION PROJECT_MAJOR, PROJECT_MINOR, PROJECT_PATCH, PROJECT_TWEAK
PRODUCTVERSION PROJECT_MAJOR, PROJECT_MINOR, PROJECT_PATCH, PROJECT_TWEAK
FILETYPE VFT_APP
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904B0"
BEGIN
VALUE "CompanyName", PROJECT_AUTHOR
VALUE "FileDescription", PROJECT_DESCRIPTION
VALUE "FileVersion", PROJECT_VERSION
VALUE "InternalName", PROJECT_NAME
VALUE "LegalCopyright", PROJECT_AUTHOR
VALUE "OriginalFilename", PROJECT_NAME ".exe"
VALUE "ProductName", PROJECT_NAME
VALUE "ProductVersion", PROJECT_VERSION
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x409, 1200
END
END

24
xW/xW.svg Normal file
View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg version="1.1" width="48" height="48" viewBox="0 0 48 48"
xmlns="http://www.w3.org/2000/svg">
<defs>
<clipPath id="outer">
<rect x="-1" y="-0.15" width="5" height="3.30" />
</clipPath>
<clipPath id="inner">
<rect x="-1" y="0" width="5" height="3" />
</clipPath>
</defs>
<g transform="translate(6, 6) scale(12)" stroke-linecap="square">
<g clip-path="url(#outer)">
<path stroke="#ffffff" stroke-width="1.5" d="M 0.5,0 2.5,3" />
<path stroke="#ffffff" stroke-width="1.5" d="M 0.5,3 2.5,0" />
</g>
<g clip-path="url(#inner)">
<path stroke="#000000" stroke-width="0.2" d="M 0,0 2,3 M 1,0 3,3" />
<path stroke="#ff6600" stroke-width="0.3" d="M 0,3 2,0 M 1,3 3,0" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 818 B