Compare commits
	
		
			109 Commits
		
	
	
		
			5863040f93
			...
			master
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						
						
							
						
						57261fb13a
	
				 | 
					
					
						|||
| 
						
						
							
						
						5c41eab9d7
	
				 | 
					
					
						|||
| 
						
						
							
						
						99c95edd57
	
				 | 
					
					
						|||
| 
						
						
							
						
						7d90142f0f
	
				 | 
					
					
						|||
| 
						
						
							
						
						71e1a744c5
	
				 | 
					
					
						|||
| 
						
						
							
						
						80af5c22d6
	
				 | 
					
					
						|||
| 
						
						
							
						
						7ba17a0161
	
				 | 
					
					
						|||
| 
						
						
							
						
						4cf8c394b9
	
				 | 
					
					
						|||
| 
						
						
							
						
						e225306419
	
				 | 
					
					
						|||
| 
						
						
							
						
						858734384b
	
				 | 
					
					
						|||
| 
						
						
							
						
						278a7f95a9
	
				 | 
					
					
						|||
| 
						
						
							
						
						a8575ab875
	
				 | 
					
					
						|||
| 
						
						
							
						
						081525f5be
	
				 | 
					
					
						|||
| 
						
						
							
						
						6ac2ac5511
	
				 | 
					
					
						|||
| 
						
						
							
						
						f6483489c2
	
				 | 
					
					
						|||
| 
						
						
							
						
						ed5ac1815b
	
				 | 
					
					
						|||
| 
						
						
							
						
						509cb9f4dd
	
				 | 
					
					
						|||
| 
						
						
							
						
						b3684c4d9f
	
				 | 
					
					
						|||
| 
						
						
							
						
						918c589c65
	
				 | 
					
					
						|||
| 
						
						
							
						
						21095a11d6
	
				 | 
					
					
						|||
| 
						
						
							
						
						a22baa4b55
	
				 | 
					
					
						|||
| 
						
						
							
						
						b3e545e0bb
	
				 | 
					
					
						|||
| 
						
						
							
						
						cd76702ab2
	
				 | 
					
					
						|||
| 
						
						
							
						
						977b073b58
	
				 | 
					
					
						|||
| 
						
						
							
						
						46be4836df
	
				 | 
					
					
						|||
| 
						
						
							
						
						05a41b2629
	
				 | 
					
					
						|||
| 
						
						
							
						
						a62ed5bbac
	
				 | 
					
					
						|||
| 
						
						
							
						
						9c9776bacd
	
				 | 
					
					
						|||
| 
						
						
							
						
						086b879ab8
	
				 | 
					
					
						|||
| 
						
						
							
						
						214c349869
	
				 | 
					
					
						|||
| 
						
						
							
						
						3d975c9437
	
				 | 
					
					
						|||
| 
						
						
							
						
						fce8fd40cc
	
				 | 
					
					
						|||
| 
						
						
							
						
						3597ab9420
	
				 | 
					
					
						|||
| 
						
						
							
						
						1635a730e8
	
				 | 
					
					
						|||
| 
						
						
							
						
						a64b1152a1
	
				 | 
					
					
						|||
| 
						
						
							
						
						a011b57ce2
	
				 | 
					
					
						|||
| 
						
						
							
						
						b1ee295345
	
				 | 
					
					
						|||
| 
						
						
							
						
						872f2d7c59
	
				 | 
					
					
						|||
| 
						
						
							
						
						f15d887dcd
	
				 | 
					
					
						|||
| 
						
						
							
						
						841bc00c51
	
				 | 
					
					
						|||
| 
						
						
							
						
						8afe4f8aad
	
				 | 
					
					
						|||
| 
						
						
							
						
						73cc8f448a
	
				 | 
					
					
						|||
| 
						
						
							
						
						4565afe294
	
				 | 
					
					
						|||
| 
						
						
							
						
						3ad8c79de8
	
				 | 
					
					
						|||
| 
						
						
							
						
						12fc3c228a
	
				 | 
					
					
						|||
| 
						
						
							
						
						175533a5e9
	
				 | 
					
					
						|||
| 
						
						
							
						
						a9b46141a9
	
				 | 
					
					
						|||
| 
						
						
							
						
						c38cca3b92
	
				 | 
					
					
						|||
| 
						
						
							
						
						aee7540faa
	
				 | 
					
					
						|||
| 
						
						
							
						
						53ba996ec9
	
				 | 
					
					
						|||
| 
						
						
							
						
						d450c6cc5f
	
				 | 
					
					
						|||
| 
						
						
							
						
						f8ea1634c4
	
				 | 
					
					
						|||
| 
						
						
							
						
						ef257cd575
	
				 | 
					
					
						|||
| 
						
						
							
						
						69eccc7065
	
				 | 
					
					
						|||
| 
						
						
							
						
						13d2ff115b
	
				 | 
					
					
						|||
| 
						
						
							
						
						9e4692bb09
	
				 | 
					
					
						|||
| 
						
						
							
						
						1c4343058d
	
				 | 
					
					
						|||
| 
						
						
							
						
						e5156cddbf
	
				 | 
					
					
						|||
| 
						
						
							
						
						34521e61c1
	
				 | 
					
					
						|||
| 
						
						
							
						
						c22dd67fc1
	
				 | 
					
					
						|||
| 
						
						
							
						
						274d5f03e7
	
				 | 
					
					
						|||
| 
						
						
							
						
						2f19e5a733
	
				 | 
					
					
						|||
| 
						
						
							
						
						b9cdabca5d
	
				 | 
					
					
						|||
| 
						
						
							
						
						f60ca43156
	
				 | 
					
					
						|||
| 
						
						
							
						
						afe4e61f08
	
				 | 
					
					
						|||
| 
						
						
							
						
						8d9d1c60ec
	
				 | 
					
					
						|||
| 
						
						
							
						
						8c1464822b
	
				 | 
					
					
						|||
| 
						
						
							
						
						fcd1b8e011
	
				 | 
					
					
						|||
| 
						
						
							
						
						3d345987c3
	
				 | 
					
					
						|||
| 
						
						
							
						
						3e37efd9cd
	
				 | 
					
					
						|||
| 
						
						
							
						
						efb25b8aae
	
				 | 
					
					
						|||
| 
						
						
							
						
						e72793e315
	
				 | 
					
					
						|||
| 
						
						
							
						
						5a412ab6e2
	
				 | 
					
					
						|||
| 
						
						
							
						
						81bc578773
	
				 | 
					
					
						|||
| 
						
						
							
						
						100de5ac2d
	
				 | 
					
					
						|||
| 
						
						
							
						
						c157d3369f
	
				 | 
					
					
						|||
| 
						
						
							
						
						8b5ea67aff
	
				 | 
					
					
						|||
| 
						
						
							
						
						6f02af814f
	
				 | 
					
					
						|||
| 
						
						
							
						
						90859107c8
	
				 | 
					
					
						|||
| 
						
						
							
						
						0219dbd026
	
				 | 
					
					
						|||
| 
						
						
							
						
						1da4699a7a
	
				 | 
					
					
						|||
| 
						
						
							
						
						9e993c50e6
	
				 | 
					
					
						|||
| 
						
						
							
						
						b3e9218b23
	
				 | 
					
					
						|||
| 
						
						
							
						
						bc8867eb22
	
				 | 
					
					
						|||
| 
						
						
							
						
						ec33adba35
	
				 | 
					
					
						|||
| 
						
						
							
						
						6f596f1dcb
	
				 | 
					
					
						|||
| 
						
						
							
						
						abcff46dc4
	
				 | 
					
					
						|||
| 
						
						
							
						
						8d9ce92758
	
				 | 
					
					
						|||
| 
						
						
							
						
						4bb9449e47
	
				 | 
					
					
						|||
| 
						
						
							
						
						50f70f93bb
	
				 | 
					
					
						|||
| 
						
						
							
						
						3f9a365d36
	
				 | 
					
					
						|||
| 
						
						
							
						
						9932b35a10
	
				 | 
					
					
						|||
| 
						
						
							
						
						af5f209c53
	
				 | 
					
					
						|||
| 
						
						
							
						
						6bfe577f1b
	
				 | 
					
					
						|||
| 
						
						
							
						
						1079189381
	
				 | 
					
					
						|||
| 
						
						
							
						
						c58b772905
	
				 | 
					
					
						|||
| 
						
						
							
						
						26ed2dbc77
	
				 | 
					
					
						|||
| 
						
						
							
						
						4b7258cba0
	
				 | 
					
					
						|||
| 
						
						
							
						
						9dc3dd02f3
	
				 | 
					
					
						|||
| 
						
						
							
						
						a7c3ed7cc1
	
				 | 
					
					
						|||
| 
						
						
							
						
						807a8c37d9
	
				 | 
					
					
						|||
| 
						
						
							
						
						c4707e2803
	
				 | 
					
					
						|||
| 
						
						
							
						
						46d68eacce
	
				 | 
					
					
						|||
| 
						
						
							
						
						86278c154c
	
				 | 
					
					
						|||
| 
						
						
							
						
						941ee2f10c
	
				 | 
					
					
						|||
| 
						
						
							
						
						5b57e9b41b
	
				 | 
					
					
						|||
| 
						
						
							
						
						4d99690b89
	
				 | 
					
					
						|||
| 
						
						
							
						
						7c74e6615d
	
				 | 
					
					
						|||
| 
						
						
							
						
						614fd98fc1
	
				 | 
					
					
						
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -2,6 +2,7 @@
 | 
			
		||||
/build
 | 
			
		||||
 | 
			
		||||
# Qt Creator files
 | 
			
		||||
/.qtcreator
 | 
			
		||||
/CMakeLists.txt.user*
 | 
			
		||||
/xK.config
 | 
			
		||||
/xK.files
 | 
			
		||||
 
 | 
			
		||||
@@ -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 ()
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								LICENSE
									
									
									
									
									
								
							
							
						
						@@ -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
									
									
									
									
									
								
							
							
						
						@@ -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"
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										77
									
								
								README.adoc
									
									
									
									
									
								
							
							
						
						@@ -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
 | 
			
		||||
-------------------------
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								liberty
									
									
									
									
									
								
							
							
								
								
								
								
								
							
						
						
							
								
								
									
										50
									
								
								test
									
									
									
									
									
								
							
							
						
						@@ -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"
 | 
			
		||||
							
								
								
									
										12
									
								
								test-static
									
									
									
									
									
								
							
							
						
						@@ -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
									
								
							
							
						
						@@ -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)
 | 
			
		||||
							
								
								
									
										6
									
								
								xA/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,6 @@
 | 
			
		||||
/xA
 | 
			
		||||
/xA.apk
 | 
			
		||||
/proto.go
 | 
			
		||||
/FyneApp.toml
 | 
			
		||||
/*.png
 | 
			
		||||
/beep.raw
 | 
			
		||||
							
								
								
									
										38
									
								
								xA/Makefile
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,38 @@
 | 
			
		||||
.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"
 | 
			
		||||
xA.apk: $(generated)
 | 
			
		||||
	fyne package -os android
 | 
			
		||||
clean:
 | 
			
		||||
	rm -f $(outputs)
 | 
			
		||||
							
								
								
									
										44
									
								
								xA/go.mod
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,44 @@
 | 
			
		||||
module janouch.name/xK/xA
 | 
			
		||||
 | 
			
		||||
go 1.24.0
 | 
			
		||||
 | 
			
		||||
require (
 | 
			
		||||
	fyne.io/fyne/v2 v2.7.0
 | 
			
		||||
	github.com/ebitengine/oto/v3 v3.4.0
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
require (
 | 
			
		||||
	fyne.io/systray v1.11.1-0.20250603113521-ca66a66d8b58 // indirect
 | 
			
		||||
	github.com/BurntSushi/toml v1.5.0 // indirect
 | 
			
		||||
	github.com/davecgh/go-spew v1.1.1 // indirect
 | 
			
		||||
	github.com/ebitengine/purego v0.9.0 // indirect
 | 
			
		||||
	github.com/fredbi/uri v1.1.1 // indirect
 | 
			
		||||
	github.com/fsnotify/fsnotify v1.9.0 // indirect
 | 
			
		||||
	github.com/fyne-io/gl-js v0.2.0 // indirect
 | 
			
		||||
	github.com/fyne-io/glfw-js v0.3.0 // indirect
 | 
			
		||||
	github.com/fyne-io/image v0.1.1 // indirect
 | 
			
		||||
	github.com/fyne-io/oksvg v0.2.0 // indirect
 | 
			
		||||
	github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 // indirect
 | 
			
		||||
	github.com/go-gl/glfw/v3.3/glfw v0.0.0-20250301202403-da16c1255728 // indirect
 | 
			
		||||
	github.com/go-text/render v0.2.0 // indirect
 | 
			
		||||
	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-20250612000132-0ef82f21eade // indirect
 | 
			
		||||
	github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect
 | 
			
		||||
	github.com/kr/text v0.2.0 // indirect
 | 
			
		||||
	github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
 | 
			
		||||
	github.com/nicksnyder/go-i18n/v2 v2.6.0 // indirect
 | 
			
		||||
	github.com/pmezard/go-difflib v1.0.0 // indirect
 | 
			
		||||
	github.com/rymdport/portal v0.4.2 // indirect
 | 
			
		||||
	github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect
 | 
			
		||||
	github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect
 | 
			
		||||
	github.com/stretchr/testify v1.11.1 // indirect
 | 
			
		||||
	github.com/yuin/goldmark v1.7.13 // indirect
 | 
			
		||||
	golang.org/x/image v0.32.0 // indirect
 | 
			
		||||
	golang.org/x/net v0.46.0 // indirect
 | 
			
		||||
	golang.org/x/sys v0.37.0 // indirect
 | 
			
		||||
	golang.org/x/text v0.30.0 // indirect
 | 
			
		||||
	gopkg.in/yaml.v3 v3.0.1 // indirect
 | 
			
		||||
)
 | 
			
		||||
							
								
								
									
										84
									
								
								xA/go.sum
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,84 @@
 | 
			
		||||
fyne.io/fyne/v2 v2.7.0 h1:GvZSpE3X0liU/fqstInVvRsaboIVpIWQ4/sfjDGIGGQ=
 | 
			
		||||
fyne.io/fyne/v2 v2.7.0/go.mod h1:xClVlrhxl7D+LT+BWYmcrW4Nf+dJTvkhnPgji7spAwE=
 | 
			
		||||
fyne.io/systray v1.11.1-0.20250603113521-ca66a66d8b58 h1:eA5/u2XRd8OUkoMqEv3IBlFYSruNlXD8bRHDiqm0VNI=
 | 
			
		||||
fyne.io/systray v1.11.1-0.20250603113521-ca66a66d8b58/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs=
 | 
			
		||||
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
 | 
			
		||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
 | 
			
		||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 | 
			
		||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 | 
			
		||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 | 
			
		||||
github.com/ebitengine/oto/v3 v3.4.0 h1:br0PgASsEWaoWn38b2Goe7m1GKFYfNgnsjSd5Gg+/bQ=
 | 
			
		||||
github.com/ebitengine/oto/v3 v3.4.0/go.mod h1:IOleLVD0m+CMak3mRVwsYY8vTctQgOM0iiL6S7Ar7eI=
 | 
			
		||||
github.com/ebitengine/purego v0.9.0 h1:mh0zpKBIXDceC63hpvPuGLiJ8ZAa3DfrFTudmfi8A4k=
 | 
			
		||||
github.com/ebitengine/purego v0.9.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
 | 
			
		||||
github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g=
 | 
			
		||||
github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw=
 | 
			
		||||
github.com/fredbi/uri v1.1.1 h1:xZHJC08GZNIUhbP5ImTHnt5Ya0T8FI2VAwI/37kh2Ko=
 | 
			
		||||
github.com/fredbi/uri v1.1.1/go.mod h1:4+DZQ5zBjEwQCDmXW5JdIjz0PUA+yJbvtBv+u+adr5o=
 | 
			
		||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
 | 
			
		||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
 | 
			
		||||
github.com/fyne-io/gl-js v0.2.0 h1:+EXMLVEa18EfkXBVKhifYB6OGs3HwKO3lUElA0LlAjs=
 | 
			
		||||
github.com/fyne-io/gl-js v0.2.0/go.mod h1:ZcepK8vmOYLu96JoxbCKJy2ybr+g1pTnaBDdl7c3ajI=
 | 
			
		||||
github.com/fyne-io/glfw-js v0.3.0 h1:d8k2+Y7l+zy2pc7wlGRyPfTgZoqDf3AI4G+2zOWhWUk=
 | 
			
		||||
github.com/fyne-io/glfw-js v0.3.0/go.mod h1:Ri6te7rdZtBgBpxLW19uBpp3Dl6K9K/bRaYdJ22G8Jk=
 | 
			
		||||
github.com/fyne-io/image v0.1.1 h1:WH0z4H7qfvNUw5l4p3bC1q70sa5+YWVt6HCj7y4VNyA=
 | 
			
		||||
github.com/fyne-io/image v0.1.1/go.mod h1:xrfYBh6yspc+KjkgdZU/ifUC9sPA5Iv7WYUBzQKK7JM=
 | 
			
		||||
github.com/fyne-io/oksvg v0.2.0 h1:mxcGU2dx6nwjJsSA9PCYZDuoAcsZ/OuJlvg/Q9Njfo8=
 | 
			
		||||
github.com/fyne-io/oksvg v0.2.0/go.mod h1:dJ9oEkPiWhnTFNCmRgEze+YNprJF7YRbpjgpWS4kzoI=
 | 
			
		||||
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 h1:5BVwOaUSBTlVZowGO6VZGw2H/zl9nrd3eCZfYV+NfQA=
 | 
			
		||||
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw=
 | 
			
		||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20250301202403-da16c1255728 h1:RkGhqHxEVAvPM0/R+8g7XRwQnHatO0KAuVcwHo8q9W8=
 | 
			
		||||
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-20250612000132-0ef82f21eade h1:FmusiCI1wHw+XQbvL9M+1r/C3SPqKrmBaIOYwVfQoDE=
 | 
			
		||||
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade/go.mod h1:ZDXo8KHryOWSIqnsb/CiDq7hQUYryCgdVnxbj8tDG7o=
 | 
			
		||||
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe972w/cYF+FjW34v27+9Vo5106B4M=
 | 
			
		||||
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw=
 | 
			
		||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 | 
			
		||||
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.2 h1:7jKRSemwlTyVHHrTGgQg7gmNPJs88xkbKcIL3NlcmSU=
 | 
			
		||||
github.com/rymdport/portal v0.4.2/go.mod h1:kFF4jslnJ8pD5uCi17brj/ODlfIidOxlgUDTO5ncnC4=
 | 
			
		||||
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiYWEedbTT0qnSxrCjsVbb7yKY1KE=
 | 
			
		||||
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q=
 | 
			
		||||
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqdkI8FSpFyZDtCVBc2VmejdNrm5rRQ=
 | 
			
		||||
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE=
 | 
			
		||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
 | 
			
		||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
 | 
			
		||||
github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA=
 | 
			
		||||
github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
 | 
			
		||||
golang.org/x/image v0.32.0 h1:6lZQWq75h7L5IWNk0r+SCpUJ6tUVd3v4ZHnbRKLkUDQ=
 | 
			
		||||
golang.org/x/image v0.32.0/go.mod h1:/R37rrQmKXtO6tYXAjtDLwQgFLHmhW+V6ayXlxzP2Pc=
 | 
			
		||||
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
 | 
			
		||||
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
 | 
			
		||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
 | 
			
		||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
 | 
			
		||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
 | 
			
		||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
 | 
			
		||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 | 
			
		||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
 | 
			
		||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 | 
			
		||||
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
									
								
							
							
						
						@@ -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  | 
							
								
								
									
										23
									
								
								xA/xA.svg
									
									
									
									
									
										Normal 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
									
									
									
									
									
								
							
							
						
						@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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]
 | 
			
		||||
}
 | 
			
		||||
@@ -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]
 | 
			
		||||
}
 | 
			
		||||
@@ -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]
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										305
									
								
								xC-gen-proto.awk
									
									
									
									
									
								
							
							
						
						@@ -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(";"))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -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
									
									
									
									
									
								
							
							
						
						@@ -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
									
									
									
									
									
								
							
							
						
						@@ -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
									
								
							
							
						
						@@ -0,0 +1 @@
 | 
			
		||||
2.1.0
 | 
			
		||||
							
								
								
									
										45
									
								
								xM/CMakeLists.txt
									
									
									
									
									
										Normal 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
									
								
							
							
						
						@@ -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
									
								
							
							
						
						
							
								
								
									
										3
									
								
								xN/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,3 @@
 | 
			
		||||
/xN
 | 
			
		||||
/xN.1
 | 
			
		||||
/irc.go
 | 
			
		||||
							
								
								
									
										21
									
								
								xN/Makefile
									
									
									
									
									
										Normal 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)
 | 
			
		||||
							
								
								
									
										86
									
								
								xN/xN.adoc
									
									
									
									
									
										Normal 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
									
								
							
							
						
						@@ -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
									
								
							
							
						
						@@ -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)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										10
									
								
								xP/Makefile
									
									
									
									
									
								
							
							
						
						@@ -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:
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										64
									
								
								xP/go.sum
									
									
									
									
									
								
							
							
						
						@@ -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=
 | 
			
		||||
 
 | 
			
		||||
@@ -120,6 +120,9 @@ button:hover:active {
 | 
			
		||||
.item.activity {
 | 
			
		||||
	font-weight: bold;
 | 
			
		||||
}
 | 
			
		||||
.item:hover {
 | 
			
		||||
	background: #f8f8f8;
 | 
			
		||||
}
 | 
			
		||||
.item.current {
 | 
			
		||||
	font-style: italic;
 | 
			
		||||
	background: #eee;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										170
									
								
								xP/public/xP.js
									
									
									
									
									
								
							
							
						
						@@ -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') {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										104
									
								
								xP/xP.go
									
									
									
									
									
								
							
							
						
						@@ -1,4 +1,4 @@
 | 
			
		||||
// Copyright (c) 2022, Přemysl Eric Janouch <p@janouch.name>
 | 
			
		||||
// Copyright (c) 2022 - 2025, Přemysl Eric Janouch <p@janouch.name>
 | 
			
		||||
// SPDX-License-Identifier: 0BSD
 | 
			
		||||
 | 
			
		||||
package main
 | 
			
		||||
@@ -6,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,23 @@ 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.
 | 
			
		||||
	// It would be more reliable to check for 'ApplePaySession' in window in JS,
 | 
			
		||||
	// and have us disable compression based on a query parameter.
 | 
			
		||||
	if agent := r.UserAgent(); (strings.Contains(agent, " Version/") &&
 | 
			
		||||
		strings.HasPrefix(agent, "Mozilla/5.0 (Macintosh; ")) ||
 | 
			
		||||
		strings.HasPrefix(agent, "Mozilla/5.0 (iPhone; ") {
 | 
			
		||||
		opts.CompressionMode = websocket.CompressionDisabled
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ws, err := websocket.Accept(w, r, opts)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Println("Client rejected: " + err.Error())
 | 
			
		||||
		return
 | 
			
		||||
@@ -232,21 +251,20 @@ func handleWS(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
 | 
			
		||||
// -----------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
var staticHandler = http.FileServer(http.Dir("."))
 | 
			
		||||
 | 
			
		||||
var page = template.Must(template.New("/").Parse(`<!DOCTYPE html>
 | 
			
		||||
<html>
 | 
			
		||||
<head>
 | 
			
		||||
	<title>xP</title>
 | 
			
		||||
	<meta charset="utf-8" />
 | 
			
		||||
	<meta name="viewport" content="width=device-width, initial-scale=1">
 | 
			
		||||
	<base href="{{ .Root }}/">
 | 
			
		||||
	<link rel="stylesheet" href="xP.css" />
 | 
			
		||||
</head>
 | 
			
		||||
<body>
 | 
			
		||||
	<script src="mithril.js">
 | 
			
		||||
	</script>
 | 
			
		||||
	<script>
 | 
			
		||||
	let proxy = '{{ . }}'
 | 
			
		||||
	let proxy = '{{ .Proxy }}'
 | 
			
		||||
	</script>
 | 
			
		||||
	<script type="module" src="xP.js">
 | 
			
		||||
	</script>
 | 
			
		||||
@@ -254,20 +272,49 @@ var page = template.Must(template.New("/").Parse(`<!DOCTYPE html>
 | 
			
		||||
</html>`))
 | 
			
		||||
 | 
			
		||||
func handleDefault(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	if r.URL.Path != "/" {
 | 
			
		||||
		staticHandler.ServeHTTP(w, r)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	wsURI := addressWS
 | 
			
		||||
	if wsURI == "" {
 | 
			
		||||
		wsURI = fmt.Sprintf("ws://%s/ws", r.Host)
 | 
			
		||||
	}
 | 
			
		||||
	if err := page.Execute(w, wsURI); err != nil {
 | 
			
		||||
 | 
			
		||||
	args := struct {
 | 
			
		||||
		Root  string
 | 
			
		||||
		Proxy string
 | 
			
		||||
	}{
 | 
			
		||||
		Root:  webResourcesHash,
 | 
			
		||||
		Proxy: wsURI,
 | 
			
		||||
	}
 | 
			
		||||
	if err := page.Execute(w, &args); err != nil {
 | 
			
		||||
		log.Println("Template execution failed: " + err.Error())
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func hashFS(root fs.FS) []byte {
 | 
			
		||||
	hasher := sha1.New()
 | 
			
		||||
	callback := func(path string, d fs.DirEntry, err error) error {
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Note that this can be fooled.
 | 
			
		||||
		fmt.Fprintln(hasher, path)
 | 
			
		||||
 | 
			
		||||
		if !d.IsDir() {
 | 
			
		||||
			file, err := root.Open(path)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
			defer file.Close()
 | 
			
		||||
			io.Copy(hasher, file)
 | 
			
		||||
		}
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	if err := fs.WalkDir(root, ".", callback); err != nil {
 | 
			
		||||
		log.Fatalln(err)
 | 
			
		||||
	}
 | 
			
		||||
	return hasher.Sum(nil)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func main() {
 | 
			
		||||
	flag.Usage = func() {
 | 
			
		||||
		fmt.Fprintf(flag.CommandLine.Output(),
 | 
			
		||||
@@ -286,6 +333,21 @@ func main() {
 | 
			
		||||
		addressWS = flag.Arg(2)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	subResources, err := fs.Sub(webResources, "public")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatalln(err)
 | 
			
		||||
	}
 | 
			
		||||
	if *webRoot != "" {
 | 
			
		||||
		subResources = os.DirFS(*webRoot)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// The simplest way of ensuring that web browsers don't use
 | 
			
		||||
	// stale cached copies of our files.
 | 
			
		||||
	webResourcesHash = hex.EncodeToString(hashFS(subResources))
 | 
			
		||||
	http.Handle("/"+webResourcesHash+"/",
 | 
			
		||||
		http.StripPrefix("/"+webResourcesHash+"/",
 | 
			
		||||
			http.FileServerFS(subResources)))
 | 
			
		||||
 | 
			
		||||
	http.Handle("/ws", http.HandlerFunc(handleWS))
 | 
			
		||||
	http.Handle("/", http.HandlerFunc(handleDefault))
 | 
			
		||||
 | 
			
		||||
@@ -295,5 +357,5 @@ func main() {
 | 
			
		||||
		WriteTimeout:   60 * time.Second,
 | 
			
		||||
		MaxHeaderBytes: 32 << 10,
 | 
			
		||||
	}
 | 
			
		||||
	log.Fatal(s.ListenAndServe())
 | 
			
		||||
	log.Fatalln(s.ListenAndServe())
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								xR/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,2 @@
 | 
			
		||||
/xR
 | 
			
		||||
/proto.go
 | 
			
		||||
							
								
								
									
										17
									
								
								xR/Makefile
									
									
									
									
									
										Normal 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
									
								
							
							
						
						@@ -0,0 +1,5 @@
 | 
			
		||||
module janouch.name/xK/xR
 | 
			
		||||
 | 
			
		||||
go 1.23.0
 | 
			
		||||
 | 
			
		||||
toolchain go1.24.0
 | 
			
		||||
							
								
								
									
										41
									
								
								xR/xR.adoc
									
									
									
									
									
										Normal 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
									
								
							
							
						
						@@ -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
									
									
								
							
							
						
						@@ -1,2 +1,3 @@
 | 
			
		||||
/xS
 | 
			
		||||
/xS-replies.go
 | 
			
		||||
/xS.1
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										11
									
								
								xS/Makefile
									
									
									
									
									
								
							
							
						
						@@ -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
									
								
							
							
						
						@@ -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
									
								
							
							
						
						@@ -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
									
									
									
									
									
								
							
							
						
						@@ -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
									
								
							
							
						
						@@ -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
									
								
							
							
						
						@@ -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
									
								
							
							
						
						@@ -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  | 
							
								
								
									
										8
									
								
								xT/xT.desktop
									
									
									
									
									
										Normal 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
									
								
							
							
						
						@@ -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
									
								
							
							
						
						@@ -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
									
								
							
							
						
						@@ -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
									
								
							
							
						
						@@ -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
									
								
							
							
						
						@@ -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
									
								
							
							
						
						@@ -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
 | 
			
		||||
							
								
								
									
										25
									
								
								xW/xW.manifest
									
									
									
									
									
										Normal 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
									
								
							
							
						
						@@ -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
									
								
							
							
						
						@@ -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  |