Compare commits
198 Commits
93b66b6a26
...
v2.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
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
|
|||
|
5863040f93
|
|||
|
f891e5ca63
|
|||
|
8344b09c4f
|
|||
|
4d50ed111a
|
|||
|
e15c9cba43
|
|||
|
66370387bc
|
|||
|
2bc3ac7b0d
|
|||
|
2c54f5a5dd
|
|||
|
f2d8de3ab9
|
|||
|
67d52a2d89
|
|||
|
ef3d1cc409
|
|||
|
e6bf88673f
|
|||
|
4a2740313c
|
|||
|
d3628928b9
|
|||
|
1f0e0b1ce4
|
|||
|
919b12510b
|
|||
|
414859d309
|
|||
|
5a7f2d16df
|
|||
|
b8061b665d
|
|||
|
807bdea2ea
|
|||
|
8a689c98b4
|
|||
|
9327333813
|
|||
|
7806d4bd4e
|
|||
|
c0e1cd57b2
|
|||
|
00184811ea
|
|||
|
ec20fdef7b
|
|||
|
21e5d80ab1
|
|||
|
ff243c1d11
|
|||
|
e2ef7d668c
|
|||
|
b979257c3a
|
|||
|
840b646700
|
|||
|
126105fa4f
|
|||
|
e2f3fc2e79
|
|||
|
b55bae50df
|
|||
|
430968e5d5
|
|||
|
d5153fe354
|
|||
|
ee76186bef
|
|||
|
6f39aa6615
|
|||
|
e87cc90b5e
|
|||
|
98b0a4ef3d
|
|||
|
9cf44aa4dd
|
|||
|
b53fc1918f
|
|||
|
92f2f6895b
|
|||
|
c1d2e38840
|
|||
|
f89f21a47f
|
|||
|
fa85ea8208
|
|||
|
b728235b6c
|
|||
|
d31ab67268
|
|||
|
b2b3093e0e
|
|||
|
a551e911ab
|
|||
|
a61789637a
|
|||
|
8968100a28
|
|||
|
3b6c29d676
|
|||
|
b4ee523628
|
|||
|
c3a52b9e4c
|
|||
|
96fc12bc4c
|
|||
|
1493d9998b
|
|||
|
36f77e74fb
|
|||
|
23deca45c9
|
|||
|
62773acaa0
|
|||
|
7e3919e25d
|
|||
|
4bc2f736f2
|
|||
|
add670212f
|
|||
|
95aa89ee97
|
|||
|
0bc2c12eec
|
|||
|
3330683ad6
|
|||
|
0015d26dc8
|
|||
|
7d5e63be1f
|
|||
|
e7d0f2380e
|
|||
|
36529a46fd
|
|||
|
632ac992ab
|
|||
|
d29e2cbfe8
|
|||
|
240fac4d90
|
|||
|
c06894b291
|
|||
|
9eaf78f823
|
|||
|
5f02dddd11
|
|||
|
6f4a3f4657
|
|||
|
6387145adc
|
|||
|
f3cc137342
|
|||
|
8c8e06b015
|
|||
|
d7b6967b6f
|
|||
|
8c3ee80b21
|
|||
|
3a165a595b
|
|||
|
4ba28c6ed3
|
|||
|
45aa0e8dfb
|
|||
|
a2d5995cf5
|
|||
|
2075c38fd1
|
|||
|
88a7b1a2d9
|
|||
|
2341228efd
|
|||
|
2e3005d88b
|
|||
|
2b13f891c9
|
|||
|
d55402234c
|
|||
|
e3149b9abf
|
|||
|
976e7bfbb4
|
|||
|
5fd76ba6f9
|
|||
|
41878a587f
|
|||
|
80089a4d65
|
|||
| 1238233556 | |||
| 9c31fb69df | |||
| a51c247d69 | |||
| f26e6361f3 | |||
| 4073b7329f | |||
| 6421892ef3 | |||
| a1994865a9 | |||
| c285f3a266 | |||
| e2c34afbc6 | |||
| e2c8fb6e33 | |||
| 5c7ac9a92b | |||
| 3fee7e8051 | |||
| 09d7a10b69 | |||
| e9bcd0fa53 | |||
| 3815795d59 | |||
| fd1538251a | |||
| ffad1f15a5 | |||
| 765b741a67 | |||
| ab66a60703 | |||
| 9ee07873ea | |||
| 7ee7dc5f9b | |||
| fea801ac7a | |||
| cbdbfc3d64 | |||
| 3610f98d67 | |||
| e77495f316 | |||
| 2f841d214f | |||
| 051bbedc2f | |||
| 404aa8c9cc | |||
| 90129ee2bc | |||
| 50e7f7dca5 | |||
| 3322fe2851 | |||
| 208a8fcc7e | |||
| 2d287752d4 |
@@ -1,7 +1,12 @@
|
|||||||
# Ubuntu 18.04 LTS and OpenBSD 6.4
|
# Ubuntu 18.04 LTS and OpenBSD 6.4
|
||||||
cmake_minimum_required (VERSION 3.10)
|
cmake_minimum_required (VERSION 3.10)
|
||||||
project (xK VERSION 1.5.0
|
|
||||||
DESCRIPTION "IRC daemon, bot, TUI client and X11/web frontends" LANGUAGES C)
|
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
|
# Options
|
||||||
option (WANT_READLINE "Use GNU Readline for the UI (better)" ON)
|
option (WANT_READLINE "Use GNU Readline for the UI (better)" ON)
|
||||||
@@ -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")
|
set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wextra -Wno-unused-function")
|
||||||
endif ()
|
endif ()
|
||||||
|
|
||||||
# Version
|
|
||||||
set (project_version "${PROJECT_VERSION}")
|
|
||||||
|
|
||||||
# Try to append commit ID if it follows a version tag. It might be nicer if
|
# 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.
|
# 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
|
# 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
|
add_custom_command (OUTPUT xC-proto.c
|
||||||
COMMAND env LC_ALL=C awk
|
COMMAND env LC_ALL=C awk
|
||||||
-f ${PROJECT_SOURCE_DIR}/xC-gen-proto.awk
|
-f ${PROJECT_SOURCE_DIR}/liberty/tools/lxdrgen.awk
|
||||||
-f ${PROJECT_SOURCE_DIR}/xC-gen-proto-c.awk
|
-f ${PROJECT_SOURCE_DIR}/liberty/tools/lxdrgen-c.awk
|
||||||
${PROJECT_SOURCE_DIR}/xC-proto > xC-proto.c
|
-v PrefixCamel=Relay
|
||||||
|
${PROJECT_SOURCE_DIR}/xC.lxdr > xC-proto.c
|
||||||
DEPENDS
|
DEPENDS
|
||||||
${PROJECT_SOURCE_DIR}/xC-gen-proto.awk
|
${PROJECT_SOURCE_DIR}/liberty/tools/lxdrgen.awk
|
||||||
${PROJECT_SOURCE_DIR}/xC-gen-proto-c.awk
|
${PROJECT_SOURCE_DIR}/liberty/tools/lxdrgen-c.awk
|
||||||
${PROJECT_SOURCE_DIR}/xC-proto
|
${PROJECT_SOURCE_DIR}/xC.lxdr
|
||||||
COMMENT "Generating xC relay protocol code")
|
COMMENT "Generating xC relay protocol code" VERBATIM)
|
||||||
add_custom_target (xC-proto DEPENDS ${PROJECT_BINARY_DIR}/xC-proto.c)
|
add_custom_target (xC-proto DEPENDS ${PROJECT_BINARY_DIR}/xC-proto.c)
|
||||||
|
|
||||||
# Build
|
# Build
|
||||||
@@ -225,7 +228,6 @@ add_custom_target (clang-tidy
|
|||||||
# Installation
|
# Installation
|
||||||
install (TARGETS xB xC xD DESTINATION ${CMAKE_INSTALL_BINDIR})
|
install (TARGETS xB xC xD DESTINATION ${CMAKE_INSTALL_BINDIR})
|
||||||
install (FILES LICENSE DESTINATION ${CMAKE_INSTALL_DOCDIR})
|
install (FILES LICENSE DESTINATION ${CMAKE_INSTALL_DOCDIR})
|
||||||
# XXX: our defaults for XDG_DATA_DIRS expect /usr/local/share or /usr/share
|
|
||||||
install (DIRECTORY plugins/xB/
|
install (DIRECTORY plugins/xB/
|
||||||
DESTINATION ${CMAKE_INSTALL_DATADIR}/xB/plugins USE_SOURCE_PERMISSIONS)
|
DESTINATION ${CMAKE_INSTALL_DATADIR}/xB/plugins USE_SOURCE_PERMISSIONS)
|
||||||
install (DIRECTORY plugins/xC/
|
install (DIRECTORY plugins/xC/
|
||||||
@@ -235,7 +237,8 @@ install (DIRECTORY plugins/xC/
|
|||||||
find_program (ASCIIDOCTOR_EXECUTABLE asciidoctor)
|
find_program (ASCIIDOCTOR_EXECUTABLE asciidoctor)
|
||||||
find_program (A2X_EXECUTABLE a2x)
|
find_program (A2X_EXECUTABLE a2x)
|
||||||
if (NOT ASCIIDOCTOR_EXECUTABLE AND NOT A2X_EXECUTABLE)
|
if (NOT ASCIIDOCTOR_EXECUTABLE AND NOT A2X_EXECUTABLE)
|
||||||
message (FATAL_ERROR "Neither asciidoctor nor a2x were found")
|
message (WARNING "Neither asciidoctor nor a2x were found, "
|
||||||
|
"falling back to a substandard manual page generator")
|
||||||
endif ()
|
endif ()
|
||||||
|
|
||||||
foreach (page xB xC xD)
|
foreach (page xB xC xD)
|
||||||
@@ -257,6 +260,14 @@ foreach (page xB xC xD)
|
|||||||
"${PROJECT_SOURCE_DIR}/${page}.adoc"
|
"${PROJECT_SOURCE_DIR}/${page}.adoc"
|
||||||
DEPENDS ${page}.adoc
|
DEPENDS ${page}.adoc
|
||||||
COMMENT "Generating man page for ${page}" VERBATIM)
|
COMMENT "Generating man page for ${page}" VERBATIM)
|
||||||
|
else ()
|
||||||
|
set (ASCIIMAN ${PROJECT_SOURCE_DIR}/liberty/tools/asciiman.awk)
|
||||||
|
add_custom_command (OUTPUT ${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 ()
|
endif ()
|
||||||
endforeach ()
|
endforeach ()
|
||||||
|
|
||||||
|
|||||||
2
LICENSE
2
LICENSE
@@ -1,4 +1,4 @@
|
|||||||
Copyright (c) 2014 - 2022, Přemysl Eric Janouch <p@janouch.name>
|
Copyright (c) 2014 - 2024, Přemysl Eric Janouch <p@janouch.name>
|
||||||
|
|
||||||
Permission to use, copy, modify, and/or distribute this software for any
|
Permission to use, copy, modify, and/or distribute this software for any
|
||||||
purpose with or without fee is hereby granted.
|
purpose with or without fee is hereby granted.
|
||||||
|
|||||||
25
NEWS
25
NEWS
@@ -1,4 +1,6 @@
|
|||||||
Unreleased
|
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
|
* xD: implemented WALLOPS, choosing to make it target even non-operators
|
||||||
|
|
||||||
@@ -8,19 +10,36 @@ Unreleased
|
|||||||
with the exception of editor_command/editor, backlog_helper/pager,
|
with the exception of editor_command/editor, backlog_helper/pager,
|
||||||
and backlog_helper_strip_formatting/pager_strip_formatting
|
and backlog_helper_strip_formatting/pager_strip_formatting
|
||||||
|
|
||||||
|
* xC: all attributes.* configuration options have been made abstract in
|
||||||
|
a subset of the git-config(1) format, and renamed to theme.*,
|
||||||
|
with the exception of attributes.reset, which has no replacement
|
||||||
|
|
||||||
* xC: replaced behaviour.save_on_quit with general.autosave
|
* xC: replaced behaviour.save_on_quit with general.autosave
|
||||||
|
|
||||||
|
* xC: the server *.command configuration option now supports multiple lines
|
||||||
|
|
||||||
* xC: improved pager integration capabilities
|
* xC: improved pager integration capabilities
|
||||||
|
|
||||||
|
* xC: unsolicited JOINs will no longer automatically activate the buffer
|
||||||
|
|
||||||
|
* xC: made Readline insert the longest common completion prefix first,
|
||||||
|
and prevented the possible-completions command from duplicating the prompt
|
||||||
|
|
||||||
* xC: normalized editline's history behaviour, making it a viable frontend
|
* xC: normalized editline's history behaviour, making it a viable frontend
|
||||||
|
|
||||||
* xC: various bugfixes
|
* xC: various bugfixes
|
||||||
|
|
||||||
* xC: added a relay interface, enabled through the general.relay_bind option
|
* xC: added a relay interface, enabled through the general.relay_bind option
|
||||||
|
|
||||||
* Added an experimental X11 frontend for xC called xF
|
* Added a web frontend for xC called xP
|
||||||
|
|
||||||
* Added an experimental 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"
|
1.5.0 (2021-12-21) "The Show Must Go On"
|
||||||
|
|||||||
97
README.adoc
97
README.adoc
@@ -1,9 +1,10 @@
|
|||||||
xK
|
xK
|
||||||
==
|
==
|
||||||
|
|
||||||
'xK' (chat kit) is an IRC software suite consisting of a daemon, bot, terminal
|
'xK' (chat kit) is an IRC software suite consisting of a daemon, bot, notifier,
|
||||||
client, and X11/web frontends for the client. It's all you're ever going to
|
terminal client, and web/Windows/macOS frontends for the client. It's all
|
||||||
need for chatting, so long as you can make do with slightly minimalist software.
|
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.
|
They're all lean on dependencies, and offer a maximally permissive licence.
|
||||||
|
|
||||||
@@ -13,7 +14,7 @@ The IRC client, and the core of 'xK'. It is largely defined by building on top
|
|||||||
of GNU Readline or BSD Editline that have been hacked to death. Its interface
|
of GNU Readline or BSD Editline that have been hacked to death. Its interface
|
||||||
should feel somewhat familiar for weechat or irssi users.
|
should feel somewhat familiar for weechat or irssi users.
|
||||||
|
|
||||||
image::xC.png[align="center"]
|
image::xC.webp[align="center"]
|
||||||
|
|
||||||
It has most features you'd expect of an IRC client, such as being multiserver,
|
It has most features you'd expect of an IRC client, such as being multiserver,
|
||||||
a powerful configuration system, integrated help, text formatting, automatic
|
a powerful configuration system, integrated help, text formatting, automatic
|
||||||
@@ -26,12 +27,11 @@ As a unique bonus, you can launch a full text editor from within.
|
|||||||
xP
|
xP
|
||||||
--
|
--
|
||||||
The web frontend for 'xC', making use of its networked relay interface.
|
The web frontend for 'xC', making use of its networked relay interface.
|
||||||
So far it's quite basic, yet usable.
|
It intentionally differs in that it uses a sans-serif font, and it shows
|
||||||
|
the list of all buffers in a side panel. Otherwise it is a near replica,
|
||||||
|
including link:xC.adoc#_key_bindings[keyboard shortcuts].
|
||||||
|
|
||||||
xF
|
image::xP.webp[align="center"]
|
||||||
--
|
|
||||||
The X11 frontend for 'xC', making use of its networked relay interface.
|
|
||||||
It's currently in development, and hidden behind a CMake option.
|
|
||||||
|
|
||||||
xD
|
xD
|
||||||
--
|
--
|
||||||
@@ -48,8 +48,17 @@ What it notably doesn't support is online changes to configuration, any limits
|
|||||||
besides the total number of connections and mode `+l`, or server linking
|
besides the total number of connections and mode `+l`, or server linking
|
||||||
(which also means no services).
|
(which also means no services).
|
||||||
|
|
||||||
This program has been https://git.janouch.name/p/haven/src/branch/master/hid[
|
xS
|
||||||
ported to Go] in a different project, and development continues over there.
|
--
|
||||||
|
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
|
xB
|
||||||
--
|
--
|
||||||
@@ -64,17 +73,18 @@ that easily to any program).
|
|||||||
|
|
||||||
Packages
|
Packages
|
||||||
--------
|
--------
|
||||||
Regular releases are sporadic. git master should be stable enough. You can get
|
Regular releases are sporadic. git master should be stable enough.
|
||||||
a package with the latest development version from Archlinux's AUR.
|
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
|
Building
|
||||||
--------
|
--------
|
||||||
Build-only dependencies:
|
Build-only dependencies: CMake, pkg-config, awk, liberty (included),
|
||||||
CMake, pkg-config, asciidoctor or asciidoc, awk, liberty (included) +
|
asciidoctor or asciidoc (recommended but optional) +
|
||||||
Common runtime dependencies: openssl +
|
Common runtime dependencies: openssl +
|
||||||
Additionally for 'xC': curses, libffi, readline >= 6.0 or libedit >= 2013-07-12,
|
Additionally for 'xC': curses, libffi, readline >= 6.0 or libedit >= 2013-07-12,
|
||||||
lua >= 5.3 (optional) +
|
lua >= 5.3 (optional) +
|
||||||
Additionally for 'xF': x11, xft
|
|
||||||
|
|
||||||
$ git clone --recursive https://git.janouch.name/p/xK.git
|
$ git clone --recursive https://git.janouch.name/p/xK.git
|
||||||
$ mkdir xK/build
|
$ mkdir xK/build
|
||||||
@@ -117,8 +127,40 @@ as a `forking` type systemd user service.
|
|||||||
|
|
||||||
xP
|
xP
|
||||||
~~
|
~~
|
||||||
Install the Go compiler, and build the server using `make` in its directory,
|
The precondition for running 'xC' frontends is enabling its relay interface:
|
||||||
then run it from within the _public_ subdirectory.
|
|
||||||
|
/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[]:
|
||||||
|
|
||||||
|
$ ../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.
|
||||||
|
|
||||||
|
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
|
Client Certificates
|
||||||
-------------------
|
-------------------
|
||||||
@@ -126,10 +168,11 @@ Client Certificates
|
|||||||
certificate specified by the respective server's `tls_cert` option if you add
|
certificate specified by the respective server's `tls_cert` option if you add
|
||||||
`sasl` to the `capabilities` option and the server supports this.
|
`sasl` to the `capabilities` option and the server supports this.
|
||||||
|
|
||||||
'xD' uses SHA-1 fingerprints of TLS client certificates to authenticate users.
|
'xD' and 'xS' use SHA-256 fingerprints of TLS client certificates
|
||||||
To get the fingerprint from a certificate file in the required form, use:
|
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
|
Custom Key Bindings in xC
|
||||||
-------------------------
|
-------------------------
|
||||||
@@ -165,12 +208,12 @@ properly set-up terminal emulator, it suffices to run:
|
|||||||
/set general.pager = Press Tab here and change +Gb to +Gb1d
|
/set general.pager = Press Tab here and change +Gb to +Gb1d
|
||||||
/set general.date_change_line = "%a %e %b %Y"
|
/set general.date_change_line = "%a %e %b %Y"
|
||||||
/set general.plugin_autoload += "fancy-prompt.lua"
|
/set general.plugin_autoload += "fancy-prompt.lua"
|
||||||
/set attributes.userhost = "\x1b[38;5;109m"
|
/set theme.userhost = "109"
|
||||||
/set attributes.join = "\x1b[38;5;108m"
|
/set theme.join = "108"
|
||||||
/set attributes.part = "\x1b[38;5;138m"
|
/set theme.part = "138"
|
||||||
/set attributes.external = "\x1b[38;5;248m"
|
/set theme.external = "248"
|
||||||
/set attributes.timestamp = "\x1b[48;5;255m\x1b[38;5;250m"
|
/set theme.timestamp = "250 255"
|
||||||
/set attributes.read_marker = "\x1b[38;5;202m"
|
/set theme.read_marker = "202"
|
||||||
|
|
||||||
Configuration profiles
|
Configuration profiles
|
||||||
----------------------
|
----------------------
|
||||||
|
|||||||
8
common.c
8
common.c
@@ -329,7 +329,7 @@ struct socks_connector
|
|||||||
|
|
||||||
SOCKS_DATA_CB (socks_4a_finish)
|
SOCKS_DATA_CB (socks_4a_finish)
|
||||||
{
|
{
|
||||||
uint8_t null, status;
|
uint8_t null = 0, status = 0;
|
||||||
hard_assert (msg_unpacker_u8 (unpacker, &null));
|
hard_assert (msg_unpacker_u8 (unpacker, &null));
|
||||||
hard_assert (msg_unpacker_u8 (unpacker, &status));
|
hard_assert (msg_unpacker_u8 (unpacker, &status));
|
||||||
|
|
||||||
@@ -432,7 +432,7 @@ SOCKS_DATA_CB (socks_5_request_domain)
|
|||||||
|
|
||||||
SOCKS_DATA_CB (socks_5_request_finish)
|
SOCKS_DATA_CB (socks_5_request_finish)
|
||||||
{
|
{
|
||||||
uint8_t version, status, reserved, type;
|
uint8_t version = 0, status = 0, reserved = 0, type = 0;
|
||||||
hard_assert (msg_unpacker_u8 (unpacker, &version));
|
hard_assert (msg_unpacker_u8 (unpacker, &version));
|
||||||
hard_assert (msg_unpacker_u8 (unpacker, &status));
|
hard_assert (msg_unpacker_u8 (unpacker, &status));
|
||||||
hard_assert (msg_unpacker_u8 (unpacker, &reserved));
|
hard_assert (msg_unpacker_u8 (unpacker, &reserved));
|
||||||
@@ -509,7 +509,7 @@ socks_5_request_start (struct socks_connector *self)
|
|||||||
|
|
||||||
SOCKS_DATA_CB (socks_5_userpass_finish)
|
SOCKS_DATA_CB (socks_5_userpass_finish)
|
||||||
{
|
{
|
||||||
uint8_t version, status;
|
uint8_t version = 0, status = 0;
|
||||||
hard_assert (msg_unpacker_u8 (unpacker, &version));
|
hard_assert (msg_unpacker_u8 (unpacker, &version));
|
||||||
hard_assert (msg_unpacker_u8 (unpacker, &status));
|
hard_assert (msg_unpacker_u8 (unpacker, &status));
|
||||||
|
|
||||||
@@ -544,7 +544,7 @@ socks_5_userpass_start (struct socks_connector *self)
|
|||||||
|
|
||||||
SOCKS_DATA_CB (socks_5_auth_finish)
|
SOCKS_DATA_CB (socks_5_auth_finish)
|
||||||
{
|
{
|
||||||
uint8_t version, method;
|
uint8_t version = 0, method = 0;
|
||||||
hard_assert (msg_unpacker_u8 (unpacker, &version));
|
hard_assert (msg_unpacker_u8 (unpacker, &version));
|
||||||
hard_assert (msg_unpacker_u8 (unpacker, &method));
|
hard_assert (msg_unpacker_u8 (unpacker, &method));
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,9 @@
|
|||||||
|
|
||||||
#define PROGRAM_VERSION "${project_version}"
|
#define PROGRAM_VERSION "${project_version}"
|
||||||
|
|
||||||
|
// We use the XDG Base Directory Specification, but may be installed anywhere.
|
||||||
|
#define PROJECT_DATADIR "${CMAKE_INSTALL_FULL_DATADIR}"
|
||||||
|
|
||||||
#cmakedefine HAVE_READLINE
|
#cmakedefine HAVE_READLINE
|
||||||
#cmakedefine HAVE_EDITLINE
|
#cmakedefine HAVE_EDITLINE
|
||||||
#cmakedefine HAVE_LUA
|
#cmakedefine HAVE_LUA
|
||||||
|
|||||||
2
liberty
2
liberty
Submodule liberty updated: f545be725d...f04cc2c61e
4
test
4
test
@@ -1,5 +1,7 @@
|
|||||||
#!/usr/bin/expect -f
|
#!/usr/bin/expect -f
|
||||||
# Very basic end-to-end testing for CI
|
# Very basic end-to-end testing for CI
|
||||||
|
set tempdir [exec mktemp -d]
|
||||||
|
set ::env(XDG_CONFIG_HOME) $tempdir
|
||||||
|
|
||||||
# Run the daemon to test against
|
# Run the daemon to test against
|
||||||
system ./xD --write-default-cfg
|
system ./xD --write-default-cfg
|
||||||
@@ -27,7 +29,7 @@ expect "Option changed"
|
|||||||
send "/disconnect\n"
|
send "/disconnect\n"
|
||||||
expect "]"
|
expect "]"
|
||||||
send "/connect\n"
|
send "/connect\n"
|
||||||
expect "Connection established"
|
expect "Welcome to"
|
||||||
|
|
||||||
# Try some chatting
|
# Try some chatting
|
||||||
send "/join #test\n"
|
send "/join #test\n"
|
||||||
|
|||||||
12
test-static
12
test-static
@@ -1,8 +1,16 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# We don't use printf's percent notation with our custom logging mechanism,
|
# 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'
|
perl -n0777 - "$(dirname "$0")"/xC.c <<-'END'
|
||||||
while (/\blog_[^ ]+\s*\([^"()]*"[^"]*%[^%][^"]*"/gm) {
|
while (/\blog_[^ ]+\s*\([^"()]*"[^"]*%\w[^"]*"/gm) {
|
||||||
my ($p, $m) = ($`, $&);
|
my ($p, $m) = ($`, $&);
|
||||||
printf "$ARGV:%d: suspicious log format string: %s...\n",
|
printf "$ARGV:%d: suspicious log format string: %s...\n",
|
||||||
(1 + $p =~ tr/\n//), ($m =~ s/\s+/ /rg);
|
(1 + $p =~ tr/\n//), ($m =~ s/\s+/ /rg);
|
||||||
|
|||||||
5
xB.c
5
xB.c
@@ -1009,7 +1009,7 @@ is_valid_plugin_name (const char *name)
|
|||||||
if (!*name)
|
if (!*name)
|
||||||
return false;
|
return false;
|
||||||
for (const char *p = name; *p; p++)
|
for (const char *p = name; *p; p++)
|
||||||
if (!isgraph (*p) || *p == '/')
|
if (!isgraph ((uint8_t) *p) || *p == '/')
|
||||||
return false;
|
return false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1019,6 +1019,7 @@ plugin_resolve_relative_filename (const char *filename)
|
|||||||
{
|
{
|
||||||
struct strv paths = strv_make ();
|
struct strv paths = strv_make ();
|
||||||
get_xdg_data_dirs (&paths);
|
get_xdg_data_dirs (&paths);
|
||||||
|
strv_append (&paths, PROJECT_DATADIR);
|
||||||
char *result = resolve_relative_filename_generic
|
char *result = resolve_relative_filename_generic
|
||||||
(&paths, PROGRAM_NAME "/plugins/", filename);
|
(&paths, PROGRAM_NAME "/plugins/", filename);
|
||||||
strv_free (&paths);
|
strv_free (&paths);
|
||||||
@@ -1212,7 +1213,7 @@ parse_bot_command (const char *s, const char *command, const char **following)
|
|||||||
s += command_len;
|
s += command_len;
|
||||||
|
|
||||||
// Expect a word boundary, so that we don't respond to invalid things
|
// Expect a word boundary, so that we don't respond to invalid things
|
||||||
if (isalnum (*s))
|
if (isalnum ((uint8_t) *s))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
// Ignore any initial spaces; the rest is the command's argument
|
// 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_i32(w, %s);\n"
|
|
||||||
CodegenDeserialize[name] = \
|
|
||||||
"\t{\n" \
|
|
||||||
"\t\tint32_t v = 0;\n" \
|
|
||||||
"\t\tif (!msg_unpacker_i32(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,448 +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)
|
|
||||||
|
|
||||||
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) {
|
|
||||||
shortname = "u" size
|
|
||||||
gotype = "uint" size
|
|
||||||
define_internal(shortname, gotype)
|
|
||||||
|
|
||||||
# Both byte and uint8 luckily marshal as base64-encoded JSON strings.
|
|
||||||
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")
|
|
||||||
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/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.MaxInt32 || n < math.MinInt32 {"
|
|
||||||
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 " int"
|
|
||||||
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 ""
|
|
||||||
|
|
||||||
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 json.Marshal(v.String())"
|
|
||||||
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 = binary.BigEndian.AppendUint32(data, uint32(%s))\n"
|
|
||||||
CodegenDeserialize[name] = \
|
|
||||||
"\tif len(data) >= 4 {\n" \
|
|
||||||
"\t\t%s = " gotype "(int32(binary.BigEndian.Uint32(data)))\n" \
|
|
||||||
"\t\tdata = data[4:]\n" \
|
|
||||||
"\t} else {\n" \
|
|
||||||
"\t\treturn nil, false\n" \
|
|
||||||
"\t}\n"
|
|
||||||
|
|
||||||
CodegenGoType[name] = gotype
|
|
||||||
for (i in cg)
|
|
||||||
delete cg[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
function codegen_struct_field(d, cg, camel, f, serialize, deserialize) {
|
|
||||||
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) {
|
|
||||||
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["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.
|
|
||||||
print "func (u " gotype ") MarshalJSON() ([]byte, error) {"
|
|
||||||
print "\treturn json.Marshal(u.Interface)"
|
|
||||||
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]
|
|
||||||
}
|
|
||||||
303
xC-gen-proto.awk
303
xC-gen-proto.awk
@@ -1,303 +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.
|
|
||||||
# Both backends are 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 i32.
|
|
||||||
# Any struct or union field may be a variable-length array.
|
|
||||||
#
|
|
||||||
# Message framing is done externally, but also happens to prefix u32 lengths.
|
|
||||||
#
|
|
||||||
# Usage: env LC_ALL=C awk -v prefix=Relay \
|
|
||||||
# -f xC-gen-proto.awk < xC-proto \
|
|
||||||
# -f xC-gen-proto-{c,go}.awk > xC-proto.{c,go} | {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")
|
|
||||||
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(";"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
123
xC-proto
123
xC-proto
@@ -1,123 +0,0 @@
|
|||||||
// Backwards-compatible protocol version.
|
|
||||||
const VERSION = 1;
|
|
||||||
|
|
||||||
// From the frontend to the relay.
|
|
||||||
struct CommandMessage {
|
|
||||||
u32 command_seq;
|
|
||||||
union CommandData switch (enum Command {
|
|
||||||
HELLO,
|
|
||||||
PING,
|
|
||||||
ACTIVE,
|
|
||||||
BUFFER_COMPLETE,
|
|
||||||
BUFFER_INPUT,
|
|
||||||
BUFFER_ACTIVATE,
|
|
||||||
BUFFER_LOG,
|
|
||||||
} command) {
|
|
||||||
case HELLO:
|
|
||||||
u32 version;
|
|
||||||
case PING:
|
|
||||||
void;
|
|
||||||
case ACTIVE:
|
|
||||||
void;
|
|
||||||
case BUFFER_COMPLETE:
|
|
||||||
string buffer_name;
|
|
||||||
string text;
|
|
||||||
u32 position;
|
|
||||||
case BUFFER_INPUT:
|
|
||||||
string buffer_name;
|
|
||||||
string text;
|
|
||||||
case BUFFER_ACTIVATE:
|
|
||||||
string buffer_name;
|
|
||||||
case BUFFER_LOG:
|
|
||||||
string buffer_name;
|
|
||||||
} data;
|
|
||||||
};
|
|
||||||
|
|
||||||
// From the relay to the frontend.
|
|
||||||
struct EventMessage {
|
|
||||||
u32 event_seq;
|
|
||||||
union EventData switch (enum Event {
|
|
||||||
PING,
|
|
||||||
BUFFER_UPDATE,
|
|
||||||
BUFFER_RENAME,
|
|
||||||
BUFFER_REMOVE,
|
|
||||||
BUFFER_ACTIVATE,
|
|
||||||
BUFFER_LINE,
|
|
||||||
BUFFER_CLEAR,
|
|
||||||
ERROR,
|
|
||||||
RESPONSE,
|
|
||||||
} event) {
|
|
||||||
case PING:
|
|
||||||
void;
|
|
||||||
case BUFFER_UPDATE:
|
|
||||||
string buffer_name;
|
|
||||||
case BUFFER_RENAME:
|
|
||||||
string buffer_name;
|
|
||||||
string new;
|
|
||||||
case BUFFER_REMOVE:
|
|
||||||
string buffer_name;
|
|
||||||
case BUFFER_ACTIVATE:
|
|
||||||
string buffer_name;
|
|
||||||
case BUFFER_LINE:
|
|
||||||
string buffer_name;
|
|
||||||
bool is_unimportant;
|
|
||||||
bool is_highlight;
|
|
||||||
enum Rendition {
|
|
||||||
BARE,
|
|
||||||
INDENT,
|
|
||||||
STATUS,
|
|
||||||
ERROR,
|
|
||||||
JOIN,
|
|
||||||
PART,
|
|
||||||
ACTION,
|
|
||||||
} rendition;
|
|
||||||
// Unix timestamp in milliseconds.
|
|
||||||
u64 when;
|
|
||||||
// Broken-up text, with in-band formatting.
|
|
||||||
union ItemData switch (enum Item {
|
|
||||||
TEXT,
|
|
||||||
RESET,
|
|
||||||
FG_COLOR,
|
|
||||||
BG_COLOR,
|
|
||||||
FLIP_BOLD,
|
|
||||||
FLIP_ITALIC,
|
|
||||||
FLIP_UNDERLINE,
|
|
||||||
FLIP_INVERSE,
|
|
||||||
FLIP_CROSSED_OUT,
|
|
||||||
FLIP_MONOSPACE,
|
|
||||||
} kind) {
|
|
||||||
case TEXT:
|
|
||||||
string text;
|
|
||||||
case RESET:
|
|
||||||
void;
|
|
||||||
case FG_COLOR:
|
|
||||||
i16 color;
|
|
||||||
case BG_COLOR:
|
|
||||||
i16 color;
|
|
||||||
case FLIP_BOLD:
|
|
||||||
case FLIP_ITALIC:
|
|
||||||
case FLIP_UNDERLINE:
|
|
||||||
case FLIP_INVERSE:
|
|
||||||
case FLIP_CROSSED_OUT:
|
|
||||||
case FLIP_MONOSPACE:
|
|
||||||
void;
|
|
||||||
} items<>;
|
|
||||||
case BUFFER_CLEAR:
|
|
||||||
string buffer_name;
|
|
||||||
|
|
||||||
// Restriction: command_seq is strictly increasing, across both of these.
|
|
||||||
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<>;
|
|
||||||
} data;
|
|
||||||
} data;
|
|
||||||
};
|
|
||||||
3
xC.adoc
3
xC.adoc
@@ -77,7 +77,8 @@ their respective function names:
|
|||||||
The next key will be interpreted as a formatting mark to insert:
|
The next key will be interpreted as a formatting mark to insert:
|
||||||
*c* for colours (optionally followed by numbers for the foreground
|
*c* for colours (optionally followed by numbers for the foreground
|
||||||
and background), *i* for italics, *b* for bold text, *u* for underlined,
|
and background), *i* for italics, *b* for bold text, *u* for underlined,
|
||||||
*x* for struck-through, *v* for inverse text and *o* resets all formatting.
|
*s* for struck-through, *m* for monospace, *v* for inverse text,
|
||||||
|
and *o* resets all formatting.
|
||||||
*C-l*: *redraw-screen*::
|
*C-l*: *redraw-screen*::
|
||||||
Should there be any issues with the display, this will clear the terminal
|
Should there be any issues with the display, this will clear the terminal
|
||||||
screen and redraw all information.
|
screen and redraw all information.
|
||||||
|
|||||||
210
xC.lxdr
Normal file
210
xC.lxdr
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
// Backwards-compatible protocol version.
|
||||||
|
const VERSION = 1;
|
||||||
|
|
||||||
|
// From the frontend to the relay.
|
||||||
|
struct CommandMessage {
|
||||||
|
// The command sequence number will be repeated in responses
|
||||||
|
// in the respective fields.
|
||||||
|
u32 command_seq;
|
||||||
|
union CommandData switch (enum Command {
|
||||||
|
HELLO,
|
||||||
|
ACTIVE,
|
||||||
|
BUFFER_ACTIVATE,
|
||||||
|
BUFFER_INPUT,
|
||||||
|
BUFFER_TOGGLE_UNIMPORTANT,
|
||||||
|
PING_RESPONSE,
|
||||||
|
PING,
|
||||||
|
BUFFER_COMPLETE,
|
||||||
|
BUFFER_LOG,
|
||||||
|
} command) {
|
||||||
|
// If the version check succeeds, the client will receive
|
||||||
|
// an initial stream of SERVER_UPDATE, BUFFER_UPDATE, BUFFER_STATS,
|
||||||
|
// BUFFER_LINE, and finally a BUFFER_ACTIVATE message.
|
||||||
|
case HELLO:
|
||||||
|
u32 version;
|
||||||
|
case ACTIVE:
|
||||||
|
void;
|
||||||
|
case BUFFER_ACTIVATE:
|
||||||
|
string buffer_name;
|
||||||
|
case BUFFER_INPUT:
|
||||||
|
string buffer_name;
|
||||||
|
string text;
|
||||||
|
// 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 BUFFER_COMPLETE:
|
||||||
|
string buffer_name;
|
||||||
|
string text;
|
||||||
|
u32 position;
|
||||||
|
case BUFFER_LOG:
|
||||||
|
string buffer_name;
|
||||||
|
} data;
|
||||||
|
};
|
||||||
|
|
||||||
|
// From the relay to the frontend.
|
||||||
|
struct EventMessage {
|
||||||
|
u32 event_seq;
|
||||||
|
union EventData switch (enum Event {
|
||||||
|
PING,
|
||||||
|
BUFFER_LINE,
|
||||||
|
BUFFER_UPDATE,
|
||||||
|
BUFFER_STATS,
|
||||||
|
BUFFER_RENAME,
|
||||||
|
BUFFER_REMOVE,
|
||||||
|
BUFFER_ACTIVATE,
|
||||||
|
BUFFER_INPUT,
|
||||||
|
BUFFER_CLEAR,
|
||||||
|
SERVER_UPDATE,
|
||||||
|
SERVER_RENAME,
|
||||||
|
SERVER_REMOVE,
|
||||||
|
ERROR,
|
||||||
|
RESPONSE,
|
||||||
|
} event) {
|
||||||
|
case PING:
|
||||||
|
void;
|
||||||
|
|
||||||
|
case BUFFER_LINE:
|
||||||
|
string buffer_name;
|
||||||
|
// Whether the line should also be displayed in the active buffer.
|
||||||
|
bool leak_to_active;
|
||||||
|
bool is_unimportant;
|
||||||
|
bool is_highlight;
|
||||||
|
enum Rendition {
|
||||||
|
BARE,
|
||||||
|
INDENT,
|
||||||
|
STATUS,
|
||||||
|
ERROR,
|
||||||
|
JOIN,
|
||||||
|
PART,
|
||||||
|
ACTION,
|
||||||
|
} rendition;
|
||||||
|
// Unix timestamp in milliseconds.
|
||||||
|
u64 when;
|
||||||
|
// Broken-up text, with in-band formatting.
|
||||||
|
union ItemData switch (enum Item {
|
||||||
|
TEXT,
|
||||||
|
RESET,
|
||||||
|
FG_COLOR,
|
||||||
|
BG_COLOR,
|
||||||
|
FLIP_BOLD,
|
||||||
|
FLIP_ITALIC,
|
||||||
|
FLIP_UNDERLINE,
|
||||||
|
FLIP_INVERSE,
|
||||||
|
FLIP_CROSSED_OUT,
|
||||||
|
FLIP_MONOSPACE,
|
||||||
|
} kind) {
|
||||||
|
case TEXT:
|
||||||
|
string text;
|
||||||
|
case RESET:
|
||||||
|
void;
|
||||||
|
case FG_COLOR:
|
||||||
|
i16 color;
|
||||||
|
case BG_COLOR:
|
||||||
|
i16 color;
|
||||||
|
case FLIP_BOLD:
|
||||||
|
case FLIP_ITALIC:
|
||||||
|
case FLIP_UNDERLINE:
|
||||||
|
case FLIP_INVERSE:
|
||||||
|
case FLIP_CROSSED_OUT:
|
||||||
|
case FLIP_MONOSPACE:
|
||||||
|
void;
|
||||||
|
} items<>;
|
||||||
|
case BUFFER_UPDATE:
|
||||||
|
string buffer_name;
|
||||||
|
bool hide_unimportant;
|
||||||
|
union BufferContext switch (enum BufferKind {
|
||||||
|
GLOBAL,
|
||||||
|
SERVER,
|
||||||
|
CHANNEL,
|
||||||
|
PRIVATE_MESSAGE,
|
||||||
|
} kind) {
|
||||||
|
case GLOBAL:
|
||||||
|
void;
|
||||||
|
case SERVER:
|
||||||
|
string server_name;
|
||||||
|
case CHANNEL:
|
||||||
|
string server_name;
|
||||||
|
ItemData topic<>;
|
||||||
|
// This includes parameters, separated by spaces.
|
||||||
|
string modes;
|
||||||
|
case PRIVATE_MESSAGE:
|
||||||
|
string server_name;
|
||||||
|
} context;
|
||||||
|
case BUFFER_STATS:
|
||||||
|
string buffer_name;
|
||||||
|
// These are cumulative, even for lines flushed out from buffers.
|
||||||
|
// Updates to these values aren't broadcasted, thus handle:
|
||||||
|
// - BUFFER_LINE by bumping/setting them as appropriate,
|
||||||
|
// - BUFFER_ACTIVATE by clearing them for the previous buffer
|
||||||
|
// (this way, they can be used to mark unread messages).
|
||||||
|
u32 new_messages;
|
||||||
|
u32 new_unimportant_messages;
|
||||||
|
bool highlighted;
|
||||||
|
case BUFFER_RENAME:
|
||||||
|
string buffer_name;
|
||||||
|
string new;
|
||||||
|
case BUFFER_REMOVE:
|
||||||
|
string buffer_name;
|
||||||
|
case BUFFER_ACTIVATE:
|
||||||
|
string buffer_name;
|
||||||
|
case BUFFER_INPUT:
|
||||||
|
string buffer_name;
|
||||||
|
string text;
|
||||||
|
case BUFFER_CLEAR:
|
||||||
|
string buffer_name;
|
||||||
|
|
||||||
|
case SERVER_UPDATE:
|
||||||
|
string server_name;
|
||||||
|
union ServerData switch (enum ServerState {
|
||||||
|
DISCONNECTED,
|
||||||
|
CONNECTING,
|
||||||
|
CONNECTED,
|
||||||
|
REGISTERED,
|
||||||
|
DISCONNECTING,
|
||||||
|
} state) {
|
||||||
|
case DISCONNECTED:
|
||||||
|
case CONNECTING:
|
||||||
|
case CONNECTED:
|
||||||
|
void;
|
||||||
|
case REGISTERED:
|
||||||
|
string user;
|
||||||
|
string user_modes;
|
||||||
|
// Theoretically, we could also send user information in this state,
|
||||||
|
// but we'd have to duplicate both fields.
|
||||||
|
case DISCONNECTING:
|
||||||
|
void;
|
||||||
|
} data;
|
||||||
|
case SERVER_RENAME:
|
||||||
|
// Buffers aren't sent updates for in this circumstance,
|
||||||
|
// as that wouldn't be sufficiently atomic anyway.
|
||||||
|
string server_name;
|
||||||
|
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;
|
||||||
|
};
|
||||||
@@ -2,14 +2,14 @@
|
|||||||
BEGIN {
|
BEGIN {
|
||||||
# The message catalog is a by-product
|
# The message catalog is a by-product
|
||||||
msg = "xD.msg"
|
msg = "xD.msg"
|
||||||
print "$quote \"" > msg;
|
print "$quote \"" > msg
|
||||||
print "$set 1" > msg;
|
print "$set 1" > msg
|
||||||
}
|
}
|
||||||
|
|
||||||
/^[0-9]+ *IRC_(ERR|RPL)_[A-Z]+ *".*"$/ {
|
/^[0-9]+ *IRC_(ERR|RPL)_[A-Z]+ *".*"$/ {
|
||||||
match($0, /".*"/);
|
match($0, /".*"/)
|
||||||
ids[$1] = $2;
|
ids[$1] = $2
|
||||||
texts[$2] = substr($0, RSTART, RLENGTH);
|
texts[$2] = substr($0, RSTART, RLENGTH)
|
||||||
print $1 " " texts[$2] > msg
|
print $1 " " texts[$2] > msg
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
8
xD.c
8
xD.c
@@ -49,7 +49,7 @@ static struct simple_config_item g_config_table[] =
|
|||||||
{ "tls_key", NULL, "Server TLS private key (PEM)" },
|
{ "tls_key", NULL, "Server TLS private key (PEM)" },
|
||||||
{ "tls_ciphers", DEFAULT_CIPHERS, "OpenSSL cipher list" },
|
{ "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" },
|
{ "max_connections", "0", "Global connection limit" },
|
||||||
{ "ping_interval", "180", "Interval between PINGs (sec)" },
|
{ "ping_interval", "180", "Interval between PINGs (sec)" },
|
||||||
@@ -296,7 +296,7 @@ irc_is_valid_user_mask (const char *mask)
|
|||||||
static bool
|
static bool
|
||||||
irc_is_valid_fingerprint (const char *fp)
|
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) --------------------------------------------------
|
// --- Clients (equals users) --------------------------------------------------
|
||||||
@@ -1005,8 +1005,8 @@ client_get_ssl_cert_fingerprint (struct client *c)
|
|||||||
if (i2d_X509 (peer_cert, &p) < 0)
|
if (i2d_X509 (peer_cert, &p) < 0)
|
||||||
return NULL;
|
return NULL;
|
||||||
|
|
||||||
unsigned char hash[SHA_DIGEST_LENGTH];
|
unsigned char hash[SHA256_DIGEST_LENGTH];
|
||||||
SHA1 (cert, cert_len, hash);
|
SHA256 (cert, cert_len, hash);
|
||||||
|
|
||||||
struct str fingerprint = str_make ();
|
struct str fingerprint = str_make ();
|
||||||
for (size_t i = 0; i < sizeof hash; i++)
|
for (size_t i = 0; i < sizeof hash; i++)
|
||||||
|
|||||||
4
xF.svg
4
xF.svg
@@ -1,8 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
<svg version="1.1" width="48" height="48" viewBox="0 0 48 48"
|
<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="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg">
|
|
||||||
|
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient id="background" x1="0" y1="0" x2="1" y2="1">
|
<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
1
xK-version
Normal file
@@ -0,0 +1 @@
|
|||||||
|
2.0.0
|
||||||
45
xM/CMakeLists.txt
Normal file
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")
|
||||||
96
xM/gen-icon.swift
Normal file
96
xM/gen-icon.swift
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
// 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
|
||||||
|
//
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
1372
xM/main.swift
Normal file
1372
xM/main.swift
Normal file
File diff suppressed because it is too large
Load Diff
3
xN/.gitignore
vendored
Normal file
3
xN/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
/xN
|
||||||
|
/xN.1
|
||||||
|
/irc.go
|
||||||
21
xN/Makefile
Normal file
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
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
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 flag.NArg() < 1 {
|
||||||
|
flag.Usage()
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
if *version {
|
||||||
|
fmt.Printf("%s %s\n", projectName, projectVersion)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
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
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
xP/.gitignore
vendored
1
xP/.gitignore
vendored
@@ -1,3 +1,4 @@
|
|||||||
/xP
|
/xP
|
||||||
/proto.go
|
/proto.go
|
||||||
|
/public/proto.js
|
||||||
/public/mithril.js
|
/public/mithril.js
|
||||||
|
|||||||
15
xP/Makefile
15
xP/Makefile
@@ -1,13 +1,20 @@
|
|||||||
.POSIX:
|
.POSIX:
|
||||||
.SUFFIXES:
|
.SUFFIXES:
|
||||||
|
AWK = env LC_ALL=C awk
|
||||||
|
|
||||||
outputs = xP proto.go public/mithril.js
|
tools = ../liberty/tools
|
||||||
all: $(outputs)
|
outputs = xP proto.go public/proto.js public/mithril.js
|
||||||
|
all: $(outputs) public/ircfmt.woff2
|
||||||
|
|
||||||
xP: xP.go proto.go
|
xP: xP.go proto.go
|
||||||
go build -o $@
|
go build -o $@
|
||||||
proto.go: ../xC-gen-proto.awk ../xC-gen-proto-go.awk ../xC-proto
|
proto.go: $(tools)/lxdrgen.awk $(tools)/lxdrgen-go.awk ../xC.lxdr
|
||||||
awk -f ../xC-gen-proto.awk -f ../xC-gen-proto-go.awk ../xC-proto > $@
|
$(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:
|
public/mithril.js:
|
||||||
curl -Lo $@ https://unpkg.com/mithril/mithril.js
|
curl -Lo $@ https://unpkg.com/mithril/mithril.js
|
||||||
clean:
|
clean:
|
||||||
|
|||||||
89
xP/gen-ircfmt.awk
Normal file
89
xP/gen-ircfmt.awk
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
# gen-ircfmt.awk: generate a supplementary font for IRC formatting characters
|
||||||
|
#
|
||||||
|
# Copyright (c) 2022, Přemysl Eric Janouch <p@janouch.name>
|
||||||
|
# SPDX-License-Identifier: 0BSD
|
||||||
|
#
|
||||||
|
# Usage: awk -v Output=static/ircfmt.woff2 -f gen-ircfmt.awk
|
||||||
|
# Clean up SVG byproducts yourself.
|
||||||
|
|
||||||
|
BEGIN {
|
||||||
|
if (!Output) {
|
||||||
|
print "Error: you must specify the output filename"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function glyph(name, code, path, filename, actions, cmd) {
|
||||||
|
filename = Output "." name ".svg"
|
||||||
|
|
||||||
|
# Inkscape still does a terrible job at the stroke-to-path conversion.
|
||||||
|
actions = \
|
||||||
|
"select-by-id:group;" \
|
||||||
|
"selection-ungroup;" \
|
||||||
|
"select-clear;" \
|
||||||
|
"select-by-id:path;" \
|
||||||
|
"object-stroke-to-path;" \
|
||||||
|
"select-by-id:clip;" \
|
||||||
|
"path-intersection;" \
|
||||||
|
"select-all;" \
|
||||||
|
"path-combine;" \
|
||||||
|
"export-overwrite;" \
|
||||||
|
"export-filename:" filename ";" \
|
||||||
|
"export-do"
|
||||||
|
|
||||||
|
# These dimensions fit FontForge defaults, and happen to work well.
|
||||||
|
cmd = "inkscape --pipe --actions='" actions "'"
|
||||||
|
print "<?xml version='1.0' encoding='UTF-8' standalone='no'?>\n" \
|
||||||
|
"<svg version='1.1' xmlns='http://www.w3.org/2000/svg'\n" \
|
||||||
|
" width='1000' height='1000' viewBox='0 0 1000 1000'>\n" \
|
||||||
|
" <rect x='0' y='0' width='1000' height='1000' />\n" \
|
||||||
|
" <g id='group' transform='translate(200 200) scale(60, 60)'>\n" \
|
||||||
|
" <rect id='clip' x='0' y='0' width='10' height='10' />\n" \
|
||||||
|
" <path id='path' stroke-width='2' fill='none' stroke='#fff'\n" \
|
||||||
|
" d='" path "' />\n" \
|
||||||
|
" </g>\n" \
|
||||||
|
"</svg>\n" | cmd
|
||||||
|
close(cmd)
|
||||||
|
|
||||||
|
print "Select(0u" code ")\n" \
|
||||||
|
"Import('" filename "')" | FontForge
|
||||||
|
}
|
||||||
|
|
||||||
|
BEGIN {
|
||||||
|
FontForge = "fontforge -lang=ff -"
|
||||||
|
print "New()" | FontForge
|
||||||
|
|
||||||
|
# Designed a 10x10 raster, going for maximum simplicity.
|
||||||
|
glyph("B", "02", "m 6,5 c 0,0 2,0 2,2 0,2 -2,2 -2,2 h -3 v -8 h 2.5 c 0,0 2,0 2,2 0,2 -2,2 -2,2 h -2 Z")
|
||||||
|
glyph("C", "03", "m 7.6,7 A 3,4 0 0 1 4.25,8.875 3,4 0 0 1 2,5 3,4 0 0 1 4.25,1.125 3,4 0 0 1 7.6,3")
|
||||||
|
glyph("I", "1D", "m 3,9 h 4 m 0,-8 h -4 m 2,-1 v 10")
|
||||||
|
glyph("M", "11", "m 2,10 v -10 l 3,6 3,-6 v 10")
|
||||||
|
glyph("O", "0F", "m 1,9 l 8,-8 M 2,5 a 3,3 0 1 0 6,0 3,3 0 1 0 -6,0 z")
|
||||||
|
#glyph("R", "0F", "m 3,10 v -9 h 2 c 0,0 2.5,0 2.5,2.5 0,2.5 -2.5,2.5 -2.5,2.5 h -2 2.5 l 2.5,4.5")
|
||||||
|
glyph("S", "1E", "m 7.5,3 c 0,-1 -1,-2 -2.5,-2 -1.5,0 -2.5,1 -2.5,2 0,3 5,1 5,4 0,1 -1,2 -2.5,2 -1.5,0 -2.5,-1 -2.5,-2")
|
||||||
|
glyph("U", "1F", "m 2.5,0 v 6.5 c 0,1.5 1,2.5 2.5,2.5 1.5,0 2.5,-1 2.5,-2.5 v -6.5")
|
||||||
|
glyph("V", "16", "m 2,-1 3,11 3,-11")
|
||||||
|
|
||||||
|
# In practice, your typical browser font will overshoot its em box,
|
||||||
|
# so to make the display more cohesive, we need to do the same.
|
||||||
|
# Sadly, sf->use_typo_metrics can't be unset from FontForge script--
|
||||||
|
# this is necessary to prevent the caret from jumping upon the first
|
||||||
|
# inserted non-formatting character in xP's textarea.
|
||||||
|
# https://iamvdo.me/en/blog/css-font-metrics-line-height-and-vertical-align
|
||||||
|
print "SelectAll()\n" \
|
||||||
|
"Scale(115, 115, 0, 0)\n" \
|
||||||
|
"SetOS2Value('WinAscentIsOffset', 1)\n" \
|
||||||
|
"SetOS2Value('WinDescentIsOffset', 1)\n" \
|
||||||
|
"SetOS2Value('HHeadAscentIsOffset', 1)\n" \
|
||||||
|
"SetOS2Value('HHeadDescentIsOffset', 1)\n" \
|
||||||
|
"CorrectDirection()\n" \
|
||||||
|
"AutoWidth(100)\n" \
|
||||||
|
"AutoHint()\n" \
|
||||||
|
"AddExtrema()\n" \
|
||||||
|
"RoundToInt()\n" \
|
||||||
|
"SetFontNames('IRCFormatting-Regular'," \
|
||||||
|
" 'IRC Formatting', 'IRC Formatting Regular', 'Regular'," \
|
||||||
|
" 'Copyright (c) 2022, Premysl Eric Janouch')\n" \
|
||||||
|
"Generate('" Output "')\n" | FontForge
|
||||||
|
close(FontForge)
|
||||||
|
}
|
||||||
@@ -2,4 +2,9 @@ module janouch.name/xK/xP
|
|||||||
|
|
||||||
go 1.18
|
go 1.18
|
||||||
|
|
||||||
require golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d
|
require nhooyr.io/websocket v1.8.7
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/klauspost/compress v1.15.9 // indirect
|
||||||
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da // indirect
|
||||||
|
)
|
||||||
|
|||||||
64
xP/go.sum
64
xP/go.sum
@@ -1,2 +1,62 @@
|
|||||||
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d h1:20cMwl2fHAzkJMEA+8J4JgqBQcQGzbisXo31MIeenXI=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
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=
|
||||||
|
|||||||
BIN
xP/public/ircfmt.woff2
Normal file
BIN
xP/public/ircfmt.woff2
Normal file
Binary file not shown.
198
xP/public/xP.css
198
xP/public/xP.css
@@ -1,20 +1,100 @@
|
|||||||
|
@font-face {
|
||||||
|
src: url('ircfmt.woff2') format('woff2');
|
||||||
|
font-family: 'IRC Formatting';
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
font-family: sans-serif;
|
/* Firefox only renders C0 within the textarea, why? */
|
||||||
|
font-family: 'IRC Formatting', sans-serif;
|
||||||
|
font-size: clamp(0.5rem, 2vw, 1rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
.xP {
|
.xP {
|
||||||
height: 100vh;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
height: 100vh;
|
||||||
|
/* https://caniuse.com/viewport-unit-variants */
|
||||||
|
height: 100dvh;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.overlay {
|
||||||
|
padding: .6em .9em;
|
||||||
|
background: #eee;
|
||||||
|
border: 1px outset #eee;
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
text-align: center;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
.title, .status {
|
.title, .status {
|
||||||
background: #f8f8f8;
|
padding: .05em .3em;
|
||||||
border-bottom: 1px solid #ccc;
|
background: #eee;
|
||||||
padding: .05rem .3rem;
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: baseline;
|
||||||
|
column-gap: .3em;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
border-top: 3px solid #ccc;
|
||||||
|
border-bottom: 2px solid #888;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
/* To approximate right-aligned space-between. */
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
}
|
||||||
|
.title:before, .status:before {
|
||||||
|
content: " ";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 2px;
|
||||||
|
top: -2px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.title:after, .status:after {
|
||||||
|
content: " ";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 1px;
|
||||||
|
bottom: -1px;
|
||||||
|
background: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
margin-right: -.3em;
|
||||||
|
}
|
||||||
|
.indicator {
|
||||||
|
margin: 0 .3em;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
font: inherit;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
padding: 0 .3em;
|
||||||
|
}
|
||||||
|
button:focus {
|
||||||
|
border: 1px dotted #000;
|
||||||
|
}
|
||||||
|
button:hover {
|
||||||
|
border-left: 1px solid #fff;
|
||||||
|
border-top: 1px solid #fff;
|
||||||
|
border-right: 1px solid #888;
|
||||||
|
border-bottom: 1px solid #888;
|
||||||
|
}
|
||||||
|
button:hover:active {
|
||||||
|
border-left: 1px solid #888;
|
||||||
|
border-top: 1px solid #888;
|
||||||
|
border-right: 1px solid #fff;
|
||||||
|
border-bottom: 1px solid #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.middle {
|
.middle {
|
||||||
@@ -26,16 +106,27 @@ body {
|
|||||||
|
|
||||||
.list {
|
.list {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
border-right: 1px solid #ccc;
|
border-right: 2px solid #ccc;
|
||||||
min-width: 10rem;
|
min-width: 10em;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
.item {
|
.item {
|
||||||
padding: .05rem .3rem;
|
padding: .05em .3em;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
.item.active {
|
.item.highlighted {
|
||||||
|
color: #ff5f00;
|
||||||
|
}
|
||||||
|
.item.activity {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
.item:hover {
|
||||||
|
background: #f8f8f8;
|
||||||
|
}
|
||||||
|
.item.current {
|
||||||
|
font-style: italic;
|
||||||
|
background: #eee;
|
||||||
|
}
|
||||||
|
|
||||||
/* Only Firefox currently supports align-content: safe end, thus this. */
|
/* Only Firefox currently supports align-content: safe end, thus this. */
|
||||||
.buffer-container {
|
.buffer-container {
|
||||||
@@ -43,32 +134,59 @@ body {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
.filler {
|
.filler {
|
||||||
flex: auto;
|
flex: auto;
|
||||||
}
|
}
|
||||||
.buffer {
|
.buffer {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: max-content auto;
|
grid-template-columns: max-content minmax(0, 1fr);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
.log {
|
||||||
|
font-family: monospace;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.log, .content, .completions {
|
||||||
|
/* Note: https://bugs.chromium.org/p/chromium/issues/detail?id=1261435 */
|
||||||
|
white-space: break-spaces;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
.log, .buffer .content {
|
||||||
|
padding: .1em .3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaked {
|
||||||
|
opacity: 50%;
|
||||||
|
}
|
||||||
.date {
|
.date {
|
||||||
padding: .3rem;
|
padding: .3em;
|
||||||
grid-column: span 2;
|
grid-column: span 2;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
.unread {
|
||||||
|
grid-column: span 2;
|
||||||
|
border-top: 1px solid #ff5f00;
|
||||||
|
}
|
||||||
.time {
|
.time {
|
||||||
padding: .1rem .3rem;
|
padding: .1em .3em;
|
||||||
background: #f8f8f8;
|
background: #f8f8f8;
|
||||||
color: #bbb;
|
color: #bbb;
|
||||||
border-right: 1px solid #ccc;
|
border-right: 1px solid #ccc;
|
||||||
}
|
}
|
||||||
|
.time.hidden:after {
|
||||||
|
border-top: .2em dotted #ccc;
|
||||||
|
display: block;
|
||||||
|
width: 50%;
|
||||||
|
margin: 0 auto;
|
||||||
|
content: "";
|
||||||
|
}
|
||||||
.mark {
|
.mark {
|
||||||
padding-right: .3rem;
|
padding-right: .3em;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
min-width: 2rem;
|
min-width: 2em;
|
||||||
}
|
}
|
||||||
.mark.error {
|
.mark.error {
|
||||||
color: red;
|
color: red;
|
||||||
@@ -82,10 +200,6 @@ body {
|
|||||||
.mark.action {
|
.mark.action {
|
||||||
color: darkred;
|
color: darkred;
|
||||||
}
|
}
|
||||||
.content {
|
|
||||||
padding: .1rem .3rem;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
}
|
|
||||||
.content .b {
|
.content .b {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
@@ -102,11 +216,45 @@ body {
|
|||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status {
|
.completions {
|
||||||
border-top: 2px solid #fff;
|
position: absolute;
|
||||||
}
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: #fff;
|
||||||
|
padding: .05em .3em;
|
||||||
|
border-top: 1px solid #888;
|
||||||
|
|
||||||
textarea {
|
max-height: 50%;
|
||||||
padding: .05rem .3rem;
|
display: flex;
|
||||||
font-family: inherit;
|
flex-flow: column wrap;
|
||||||
|
column-gap: .6em;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
.input {
|
||||||
|
flex-shrink: 0;
|
||||||
|
border: 2px inset #eee;
|
||||||
|
overflow: hidden;
|
||||||
|
resize: vertical;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.input:focus-within {
|
||||||
|
border-color: #ff5f00;
|
||||||
|
}
|
||||||
|
.prompt {
|
||||||
|
padding: .05em .3em;
|
||||||
|
border-right: 1px solid #ccc;
|
||||||
|
background: #f8f8f8;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
textarea {
|
||||||
|
font: inherit;
|
||||||
|
padding: .05em .3em;
|
||||||
|
margin: 0;
|
||||||
|
border: 0;
|
||||||
|
flex-grow: 1;
|
||||||
|
resize: none;
|
||||||
|
}
|
||||||
|
textarea:focus {
|
||||||
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|||||||
1103
xP/public/xP.js
1103
xP/public/xP.js
File diff suppressed because it is too large
Load Diff
@@ -1,2 +0,0 @@
|
|||||||
{
|
|
||||||
}
|
|
||||||
243
xP/xP.go
243
xP/xP.go
@@ -4,9 +4,11 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"io"
|
"io"
|
||||||
@@ -14,28 +16,82 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"golang.org/x/net/websocket"
|
"nhooyr.io/websocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
debug = flag.Bool("debug", false, "enable debug output")
|
||||||
|
|
||||||
addressBind string
|
addressBind string
|
||||||
addressConnect string
|
addressConnect string
|
||||||
|
addressWS string
|
||||||
)
|
)
|
||||||
|
|
||||||
func clientToRelay(
|
// -----------------------------------------------------------------------------
|
||||||
ctx context.Context, ws *websocket.Conn, conn net.Conn) bool {
|
|
||||||
var j string
|
func relayReadFrame(r io.Reader) []byte {
|
||||||
if err := websocket.Message.Receive(ws, &j); err != nil {
|
var length uint32
|
||||||
log.Println("Command receive failed: " + err.Error())
|
if err := binary.Read(r, binary.BigEndian, &length); err != nil {
|
||||||
return false
|
log.Println("Event receive failed: " + err.Error())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
b := make([]byte, length)
|
||||||
|
if _, err := io.ReadFull(r, b); err != nil {
|
||||||
|
log.Println("Event receive failed: " + err.Error())
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("?> %s\n", j)
|
if *debug {
|
||||||
|
log.Printf("<? %v\n", b)
|
||||||
|
|
||||||
|
var m RelayEventMessage
|
||||||
|
if after, ok := m.ConsumeFrom(b); !ok {
|
||||||
|
log.Println("Event deserialization failed")
|
||||||
|
return nil
|
||||||
|
} else if len(after) != 0 {
|
||||||
|
log.Println("Event deserialization failed: trailing data")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
j, err := m.MarshalJSON()
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Event marshalling failed: " + err.Error())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("<- %s\n", j)
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func relayMakeReceiver(ctx context.Context, conn net.Conn) <-chan []byte {
|
||||||
|
// The usual event message rarely gets above 1 kilobyte,
|
||||||
|
// thus this is set to buffer up at most 1 megabyte or so.
|
||||||
|
p := make(chan []byte, 1000)
|
||||||
|
r := bufio.NewReaderSize(conn, 65536)
|
||||||
|
go func() {
|
||||||
|
defer close(p)
|
||||||
|
for {
|
||||||
|
j := relayReadFrame(r)
|
||||||
|
if j == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case p <- j:
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func relayWriteJSON(conn net.Conn, j []byte) bool {
|
||||||
var m RelayCommandMessage
|
var m RelayCommandMessage
|
||||||
if err := json.Unmarshal([]byte(j), &m); err != nil {
|
if err := json.Unmarshal(j, &m); err != nil {
|
||||||
log.Println("Command unmarshalling failed: " + err.Error())
|
log.Println("Command unmarshalling failed: " + err.Error())
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -51,50 +107,42 @@ func clientToRelay(
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if *debug {
|
||||||
log.Printf("-> %v\n", b)
|
log.Printf("-> %v\n", b)
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func relayToClient(
|
// -----------------------------------------------------------------------------
|
||||||
ctx context.Context, ws *websocket.Conn, conn net.Conn) bool {
|
|
||||||
var length uint32
|
|
||||||
if err := binary.Read(conn, binary.BigEndian, &length); err != nil {
|
|
||||||
log.Println("Event receive failed: " + err.Error())
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
b := make([]byte, length)
|
|
||||||
if _, err := io.ReadFull(conn, b); err != nil {
|
|
||||||
log.Println("Event receive failed: " + err.Error())
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("<? %v\n", b)
|
func clientReadJSON(ctx context.Context, ws *websocket.Conn) []byte {
|
||||||
|
t, j, err := ws.Read(ctx)
|
||||||
var m RelayEventMessage
|
|
||||||
if after, ok := m.ConsumeFrom(b); !ok {
|
|
||||||
log.Println("Event deserialization failed")
|
|
||||||
return false
|
|
||||||
} else if len(after) != 0 {
|
|
||||||
log.Println("Event deserialization failed: trailing data")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
j, err := json.Marshal(&m)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("Event marshalling failed: " + err.Error())
|
log.Println("Command receive failed: " + err.Error())
|
||||||
return false
|
return nil
|
||||||
}
|
}
|
||||||
if err := websocket.Message.Send(ws, string(j)); err != nil {
|
if t != websocket.MessageText {
|
||||||
|
log.Println(
|
||||||
|
"Command receive failed: " + "binary messages are not supported")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if *debug {
|
||||||
|
log.Printf("?> %s\n", j)
|
||||||
|
}
|
||||||
|
return j
|
||||||
|
}
|
||||||
|
|
||||||
|
func clientWriteBinary(ctx context.Context, ws *websocket.Conn, b []byte) bool {
|
||||||
|
if err := ws.Write(ctx, websocket.MessageBinary, b); err != nil {
|
||||||
log.Println("Event send failed: " + err.Error())
|
log.Println("Event send failed: " + err.Error())
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("<- %s\n", j)
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func errorToClient(ws *websocket.Conn, err error) bool {
|
func clientWriteError(ctx context.Context, ws *websocket.Conn, err error) bool {
|
||||||
j, err := json.Marshal(&RelayEventMessage{
|
b, ok := (&RelayEventMessage{
|
||||||
EventSeq: 0,
|
EventSeq: 0,
|
||||||
Data: RelayEventData{
|
Data: RelayEventData{
|
||||||
Interface: RelayEventDataError{
|
Interface: RelayEventDataError{
|
||||||
@@ -103,40 +151,96 @@ func errorToClient(ws *websocket.Conn, err error) bool {
|
|||||||
Error: err.Error(),
|
Error: err.Error(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
}).AppendTo(nil)
|
||||||
if err != nil {
|
if ok {
|
||||||
log.Println("Event marshalling failed: " + err.Error())
|
log.Println("Event serialization failed")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if err := websocket.Message.Send(ws, string(j)); err != nil {
|
return clientWriteBinary(ctx, ws, b)
|
||||||
log.Println("Event send failed: " + err.Error())
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleWebSocket(ws *websocket.Conn) {
|
func handleWS(w http.ResponseWriter, r *http.Request) {
|
||||||
|
opts := &websocket.AcceptOptions{
|
||||||
|
InsecureSkipVerify: true,
|
||||||
|
CompressionMode: websocket.CompressionContextTakeover,
|
||||||
|
// This is for the payload; set higher to avoid overhead.
|
||||||
|
CompressionThreshold: 64 << 10,
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppleWebKit can be broken with compression.
|
||||||
|
if agent := r.UserAgent(); strings.Contains(agent, " Version/") &&
|
||||||
|
(strings.HasPrefix(agent, "Mozilla/5.0 (Macintosh; ") ||
|
||||||
|
strings.HasPrefix(agent, "Mozilla/5.0 (iPhone; ")) {
|
||||||
|
opts.CompressionMode = websocket.CompressionDisabled
|
||||||
|
}
|
||||||
|
|
||||||
|
ws, err := websocket.Accept(w, r, opts)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Client rejected: " + err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer ws.Close(websocket.StatusGoingAway, "Goodbye")
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(r.Context())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
conn, err := net.Dial("tcp", addressConnect)
|
conn, err := net.Dial("tcp", addressConnect)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errorToClient(ws, err)
|
log.Println("Connection failed: " + err.Error())
|
||||||
|
clientWriteError(ctx, ws, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
// To decrease latencies, events are received and decoded in parallel
|
||||||
|
// to their sending, and we try to batch them together.
|
||||||
|
relayFrames := relayMakeReceiver(ctx, conn)
|
||||||
|
batchFrames := func() []byte {
|
||||||
|
batch, ok := <-relayFrames
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
Batch:
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case b, ok := <-relayFrames:
|
||||||
|
if !ok {
|
||||||
|
break Batch
|
||||||
|
}
|
||||||
|
batch = append(batch, b...)
|
||||||
|
default:
|
||||||
|
break Batch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return batch
|
||||||
|
}
|
||||||
|
|
||||||
// We don't need to intervene, so it's just two separate pipes so far.
|
// We don't need to intervene, so it's just two separate pipes so far.
|
||||||
ctx, cancel := context.WithCancel(ws.Request().Context())
|
|
||||||
go func() {
|
go func() {
|
||||||
for clientToRelay(ctx, ws, conn) {
|
defer cancel()
|
||||||
|
for {
|
||||||
|
j := clientReadJSON(ctx, ws)
|
||||||
|
if j == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
relayWriteJSON(conn, j)
|
||||||
}
|
}
|
||||||
cancel()
|
|
||||||
}()
|
}()
|
||||||
go func() {
|
go func() {
|
||||||
for relayToClient(ctx, ws, conn) {
|
defer cancel()
|
||||||
|
for {
|
||||||
|
b := batchFrames()
|
||||||
|
if b == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
clientWriteBinary(ctx, ws, b)
|
||||||
}
|
}
|
||||||
cancel()
|
|
||||||
}()
|
}()
|
||||||
<-ctx.Done()
|
<-ctx.Done()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
var staticHandler = http.FileServer(http.Dir("."))
|
var staticHandler = http.FileServer(http.Dir("."))
|
||||||
|
|
||||||
var page = template.Must(template.New("/").Parse(`<!DOCTYPE html>
|
var page = template.Must(template.New("/").Parse(`<!DOCTYPE html>
|
||||||
@@ -144,6 +248,7 @@ var page = template.Must(template.New("/").Parse(`<!DOCTYPE html>
|
|||||||
<head>
|
<head>
|
||||||
<title>xP</title>
|
<title>xP</title>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<link rel="stylesheet" href="xP.css" />
|
<link rel="stylesheet" href="xP.css" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -152,7 +257,7 @@ var page = template.Must(template.New("/").Parse(`<!DOCTYPE html>
|
|||||||
<script>
|
<script>
|
||||||
let proxy = '{{ . }}'
|
let proxy = '{{ . }}'
|
||||||
</script>
|
</script>
|
||||||
<script src="xP.js">
|
<script type="module" src="xP.js">
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>`))
|
</html>`))
|
||||||
@@ -163,20 +268,34 @@ func handleDefault(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
wsURI := fmt.Sprintf("ws://%s/ws", r.Host)
|
wsURI := addressWS
|
||||||
|
if wsURI == "" {
|
||||||
|
wsURI = fmt.Sprintf("ws://%s/ws", r.Host)
|
||||||
|
}
|
||||||
if err := page.Execute(w, wsURI); err != nil {
|
if err := page.Execute(w, wsURI); err != nil {
|
||||||
log.Println("Template execution failed: " + err.Error())
|
log.Println("Template execution failed: " + err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
if len(os.Args) != 3 {
|
flag.Usage = func() {
|
||||||
log.Fatalf("usage: %s BIND CONNECT\n", os.Args[0])
|
fmt.Fprintf(flag.CommandLine.Output(),
|
||||||
|
"Usage: %s [OPTION...] BIND CONNECT [WSURI]\n\n", os.Args[0])
|
||||||
|
flag.PrintDefaults()
|
||||||
}
|
}
|
||||||
|
|
||||||
addressBind, addressConnect = os.Args[1], os.Args[2]
|
flag.Parse()
|
||||||
|
if flag.NArg() < 2 || flag.NArg() > 3 {
|
||||||
|
flag.Usage()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
http.Handle("/ws", websocket.Handler(handleWebSocket))
|
addressBind, addressConnect = flag.Arg(0), flag.Arg(1)
|
||||||
|
if flag.NArg() > 2 {
|
||||||
|
addressWS = flag.Arg(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
http.Handle("/ws", http.HandlerFunc(handleWS))
|
||||||
http.Handle("/", http.HandlerFunc(handleDefault))
|
http.Handle("/", http.HandlerFunc(handleDefault))
|
||||||
|
|
||||||
s := &http.Server{
|
s := &http.Server{
|
||||||
@@ -185,5 +304,5 @@ func main() {
|
|||||||
WriteTimeout: 60 * time.Second,
|
WriteTimeout: 60 * time.Second,
|
||||||
MaxHeaderBytes: 32 << 10,
|
MaxHeaderBytes: 32 << 10,
|
||||||
}
|
}
|
||||||
log.Fatal(s.ListenAndServe())
|
log.Fatalln(s.ListenAndServe())
|
||||||
}
|
}
|
||||||
|
|||||||
3
xS/.gitignore
vendored
Normal file
3
xS/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
/xS
|
||||||
|
/xS-replies.go
|
||||||
|
/xS.1
|
||||||
18
xS/Makefile
Normal file
18
xS/Makefile
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
.POSIX:
|
||||||
|
.SUFFIXES:
|
||||||
|
AWK = env LC_ALL=C awk
|
||||||
|
|
||||||
|
outputs = xS xS-replies.go xS.1
|
||||||
|
all: $(outputs)
|
||||||
|
|
||||||
|
xS: xS.go ../xK-version xS-replies.go
|
||||||
|
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
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
|
||||||
|
}
|
||||||
20
xS/xS-gen-replies.awk
Executable file
20
xS/xS-gen-replies.awk
Executable file
@@ -0,0 +1,20 @@
|
|||||||
|
#!/usr/bin/awk -f
|
||||||
|
/^[0-9]+ *(ERR|RPL)_[A-Z]+ *".*"$/ {
|
||||||
|
match($0, /".*"/)
|
||||||
|
ids[$1] = $2
|
||||||
|
texts[$2] = substr($0, RSTART, RLENGTH)
|
||||||
|
}
|
||||||
|
|
||||||
|
END {
|
||||||
|
print "package main"
|
||||||
|
print ""
|
||||||
|
print "const ("
|
||||||
|
for (i in ids)
|
||||||
|
printf("\t%s = %s\n", ids[i], i)
|
||||||
|
print ")"
|
||||||
|
print ""
|
||||||
|
print "var defaultReplies = map[int]string{"
|
||||||
|
for (i in ids)
|
||||||
|
print "\t" ids[i] ": " texts[ids[i]] ","
|
||||||
|
print "}"
|
||||||
|
}
|
||||||
87
xS/xS-replies
Normal file
87
xS/xS-replies
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
1 RPL_WELCOME ":Welcome to the Internet Relay Network %s!%s@%s"
|
||||||
|
2 RPL_YOURHOST ":Your host is %s, running version %s"
|
||||||
|
3 RPL_CREATED ":This server was created %s"
|
||||||
|
4 RPL_MYINFO "%s %s %s %s"
|
||||||
|
5 RPL_ISUPPORT "%s :are supported by this server"
|
||||||
|
211 RPL_STATSLINKINFO "%s %d %d %d %d %d %d"
|
||||||
|
212 RPL_STATSCOMMANDS "%s %d %d %d"
|
||||||
|
219 RPL_ENDOFSTATS "%c :End of STATS report"
|
||||||
|
221 RPL_UMODEIS "+%s"
|
||||||
|
242 RPL_STATSUPTIME ":Server Up %d days %d:%02d:%02d"
|
||||||
|
251 RPL_LUSERCLIENT ":There are %d users and %d services on %d servers"
|
||||||
|
252 RPL_LUSEROP "%d :operator(s) online"
|
||||||
|
253 RPL_LUSERUNKNOWN "%d :unknown connection(s)"
|
||||||
|
254 RPL_LUSERCHANNELS "%d :channels formed"
|
||||||
|
255 RPL_LUSERME ":I have %d clients and %d servers"
|
||||||
|
301 RPL_AWAY "%s :%s"
|
||||||
|
302 RPL_USERHOST ":%s"
|
||||||
|
303 RPL_ISON ":%s"
|
||||||
|
305 RPL_UNAWAY ":You are no longer marked as being away"
|
||||||
|
306 RPL_NOWAWAY ":You have been marked as being away"
|
||||||
|
311 RPL_WHOISUSER "%s %s %s * :%s"
|
||||||
|
312 RPL_WHOISSERVER "%s %s :%s"
|
||||||
|
313 RPL_WHOISOPERATOR "%s :is an IRC operator"
|
||||||
|
314 RPL_WHOWASUSER "%s %s %s * :%s"
|
||||||
|
315 RPL_ENDOFWHO "%s :End of WHO list"
|
||||||
|
317 RPL_WHOISIDLE "%s %d :seconds idle"
|
||||||
|
318 RPL_ENDOFWHOIS "%s :End of WHOIS list"
|
||||||
|
319 RPL_WHOISCHANNELS "%s :%s"
|
||||||
|
322 RPL_LIST "%s %d :%s"
|
||||||
|
323 RPL_LISTEND ":End of LIST"
|
||||||
|
324 RPL_CHANNELMODEIS "%s +%s"
|
||||||
|
329 RPL_CREATIONTIME "%s %d"
|
||||||
|
331 RPL_NOTOPIC "%s :No topic is set"
|
||||||
|
332 RPL_TOPIC "%s :%s"
|
||||||
|
333 RPL_TOPICWHOTIME "%s %s %d"
|
||||||
|
341 RPL_INVITING "%s %s"
|
||||||
|
346 RPL_INVITELIST "%s %s"
|
||||||
|
347 RPL_ENDOFINVITELIST "%s :End of channel invite list"
|
||||||
|
348 RPL_EXCEPTLIST "%s %s"
|
||||||
|
349 RPL_ENDOFEXCEPTLIST "%s :End of channel exception list"
|
||||||
|
351 RPL_VERSION "%s.%d %s :%s"
|
||||||
|
352 RPL_WHOREPLY "%s %s %s %s %s %s :%d %s"
|
||||||
|
353 RPL_NAMREPLY "%c %s :%s"
|
||||||
|
364 RPL_LINKS "%s %s :%d %s"
|
||||||
|
365 RPL_ENDOFLINKS "%s :End of LINKS list"
|
||||||
|
366 RPL_ENDOFNAMES "%s :End of NAMES list"
|
||||||
|
367 RPL_BANLIST "%s %s"
|
||||||
|
368 RPL_ENDOFBANLIST "%s :End of channel ban list"
|
||||||
|
369 RPL_ENDOFWHOWAS "%s :End of WHOWAS"
|
||||||
|
372 RPL_MOTD ":- %s"
|
||||||
|
375 RPL_MOTDSTART ":- %s Message of the day - "
|
||||||
|
376 RPL_ENDOFMOTD ":End of MOTD command"
|
||||||
|
391 RPL_TIME "%s :%s"
|
||||||
|
401 ERR_NOSUCHNICK "%s :No such nick/channel"
|
||||||
|
402 ERR_NOSUCHSERVER "%s :No such server"
|
||||||
|
403 ERR_NOSUCHCHANNEL "%s :No such channel"
|
||||||
|
404 ERR_CANNOTSENDTOCHAN "%s :Cannot send to channel"
|
||||||
|
406 ERR_WASNOSUCHNICK "%s :There was no such nickname"
|
||||||
|
409 ERR_NOORIGIN ":No origin specified"
|
||||||
|
410 ERR_INVALIDCAPCMD "%s :%s"
|
||||||
|
411 ERR_NORECIPIENT ":No recipient given (%s)"
|
||||||
|
412 ERR_NOTEXTTOSEND ":No text to send"
|
||||||
|
421 ERR_UNKNOWNCOMMAND "%s: Unknown command"
|
||||||
|
422 ERR_NOMOTD ":MOTD File is missing"
|
||||||
|
423 ERR_NOADMININFO "%s :No administrative info available"
|
||||||
|
431 ERR_NONICKNAMEGIVEN ":No nickname given"
|
||||||
|
432 ERR_ERRONEOUSNICKNAME "%s :Erroneous nickname"
|
||||||
|
433 ERR_NICKNAMEINUSE "%s :Nickname is already in use"
|
||||||
|
441 ERR_USERNOTINCHANNEL "%s %s :They aren't on that channel"
|
||||||
|
442 ERR_NOTONCHANNEL "%s :You're not on that channel"
|
||||||
|
443 ERR_USERONCHANNEL "%s %s :is already on channel"
|
||||||
|
445 ERR_SUMMONDISABLED ":SUMMON has been disabled"
|
||||||
|
446 ERR_USERSDISABLED ":USERS has been disabled"
|
||||||
|
451 ERR_NOTREGISTERED ":You have not registered"
|
||||||
|
461 ERR_NEEDMOREPARAMS "%s :Not enough parameters"
|
||||||
|
462 ERR_ALREADYREGISTERED ":Unauthorized command (already registered)"
|
||||||
|
467 ERR_KEYSET "%s :Channel key already set"
|
||||||
|
471 ERR_CHANNELISFULL "%s :Cannot join channel (+l)"
|
||||||
|
472 ERR_UNKNOWNMODE "%c :is unknown mode char to me for %s"
|
||||||
|
473 ERR_INVITEONLYCHAN "%s :Cannot join channel (+i)"
|
||||||
|
474 ERR_BANNEDFROMCHAN "%s :Cannot join channel (+b)"
|
||||||
|
475 ERR_BADCHANNELKEY "%s :Cannot join channel (+k)"
|
||||||
|
476 ERR_BADCHANMASK "%s :Bad Channel Mask"
|
||||||
|
481 ERR_NOPRIVILEGES ":Permission Denied- You're not an IRC operator"
|
||||||
|
482 ERR_CHANOPRIVSNEEDED "%s :You're not channel operator"
|
||||||
|
501 ERR_UMODEUNKNOWNFLAG ":Unknown MODE flag"
|
||||||
|
502 ERR_USERSDONTMATCH ":Cannot change mode for other users"
|
||||||
57
xS/xS.adoc
Normal file
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)
|
||||||
168
xS/xS_test.go
Normal file
168
xS/xS_test.go
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
//
|
||||||
|
// Copyright (c) 2015 - 2018, 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 (
|
||||||
|
"crypto/tls"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"syscall"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSplitString(t *testing.T) {
|
||||||
|
var splitStringTests = []struct {
|
||||||
|
s, delims string
|
||||||
|
ignoreEmpty bool
|
||||||
|
result []string
|
||||||
|
}{
|
||||||
|
{",a,,bc", ",", false, []string{"", "a", "", "bc"}},
|
||||||
|
{",a,,bc", ",", true, []string{"a", "bc"}},
|
||||||
|
{"a,;bc,", ",;", false, []string{"a", "", "bc", ""}},
|
||||||
|
{"a,;bc,", ",;", true, []string{"a", "bc"}},
|
||||||
|
{"", ",", false, []string{""}},
|
||||||
|
{"", ",", true, nil},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, d := range splitStringTests {
|
||||||
|
got := splitString(d.s, d.delims, d.ignoreEmpty)
|
||||||
|
if !reflect.DeepEqual(got, d.result) {
|
||||||
|
t.Errorf("case %d: %v should be %v\n", i, got, d.result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func socketpair() (*os.File, *os.File, error) {
|
||||||
|
pair, err := syscall.Socketpair(syscall.AF_UNIX, syscall.SOCK_STREAM, 0)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// See go #24331, this makes 1.11 use the internal poller
|
||||||
|
// while there wasn't a way to achieve that before.
|
||||||
|
if err := syscall.SetNonblock(int(pair[0]), true); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
if err := syscall.SetNonblock(int(pair[1]), true); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
fa := os.NewFile(uintptr(pair[0]), "a")
|
||||||
|
if fa == nil {
|
||||||
|
return nil, nil, os.ErrInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
fb := os.NewFile(uintptr(pair[1]), "b")
|
||||||
|
if fb == nil {
|
||||||
|
fa.Close()
|
||||||
|
return nil, nil, os.ErrInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
return fa, fb, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDetectTLS(t *testing.T) {
|
||||||
|
detectTLSFromFunc := func(t *testing.T, writer func(net.Conn)) bool {
|
||||||
|
// net.Pipe doesn't use file descriptors, we need a socketpair.
|
||||||
|
sockA, sockB, err := socketpair()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer sockA.Close()
|
||||||
|
defer sockB.Close()
|
||||||
|
|
||||||
|
fcB, err := net.FileConn(sockB)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
go writer(fcB)
|
||||||
|
|
||||||
|
fcA, err := net.FileConn(sockA)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
sc, err := fcA.(syscall.Conn).SyscallConn()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return detectTLS(sc)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("SSL_2.0", func(t *testing.T) {
|
||||||
|
if !detectTLSFromFunc(t, func(fc net.Conn) {
|
||||||
|
// The obsolete, useless, unsupported SSL 2.0 record format.
|
||||||
|
_, _ = fc.Write([]byte{0x80, 0x01, 0x01})
|
||||||
|
}) {
|
||||||
|
t.Error("could not detect SSL")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("crypto_tls", func(t *testing.T) {
|
||||||
|
if !detectTLSFromFunc(t, func(fc net.Conn) {
|
||||||
|
conn := tls.Client(fc, &tls.Config{InsecureSkipVerify: true})
|
||||||
|
_ = conn.Handshake()
|
||||||
|
}) {
|
||||||
|
t.Error("could not detect TLS")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("text", func(t *testing.T) {
|
||||||
|
if detectTLSFromFunc(t, func(fc net.Conn) {
|
||||||
|
_, _ = fc.Write([]byte("ПРЕВЕД"))
|
||||||
|
}) {
|
||||||
|
t.Error("detected UTF-8 as TLS")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("EOF", func(t *testing.T) {
|
||||||
|
type connCloseWriter interface {
|
||||||
|
net.Conn
|
||||||
|
CloseWrite() error
|
||||||
|
}
|
||||||
|
if detectTLSFromFunc(t, func(fc net.Conn) {
|
||||||
|
_ = fc.(connCloseWriter).CloseWrite()
|
||||||
|
}) {
|
||||||
|
t.Error("detected EOF as TLS")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIRC(t *testing.T) {
|
||||||
|
msg := ircParseMessage(
|
||||||
|
`@first=a\:\s\r\n\\;2nd :srv hi there :good m8 :how are you?`)
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(msg.tags, map[string]string{
|
||||||
|
"first": "a; \r\n\\",
|
||||||
|
"2nd": "",
|
||||||
|
}) {
|
||||||
|
t.Error("tags parsed incorrectly")
|
||||||
|
}
|
||||||
|
|
||||||
|
if msg.nick != "srv" || msg.user != "" || msg.host != "" {
|
||||||
|
t.Error("server name parsed incorrectly")
|
||||||
|
}
|
||||||
|
if msg.command != "hi" {
|
||||||
|
t.Error("command name parsed incorrectly")
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(msg.params,
|
||||||
|
[]string{"there", "good m8 :how are you?"}) {
|
||||||
|
t.Error("params parsed incorrectly")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ircEqual("[fag]^", "{FAG}~") {
|
||||||
|
t.Error("string case comparison not according to RFC 2812")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: More tests.
|
||||||
|
}
|
||||||
11
xW/.clang-format
Normal file
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
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
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
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
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
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
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
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 |
Reference in New Issue
Block a user