55 Commits

Author SHA1 Message Date
1639235a48 Start X11 and web frontends for xC
For this, we needed a wire protocol.  After surveying available options,
it was decided to implement an XDR-like protocol code generator
in portable AWK.  It now has two backends, per each of:

 - xF, the X11 frontend, is in C, and is meant to be the primary
   user interface in the future.

 - xP, the web frontend, relies on a protocol proxy written in Go,
   and is meant for use on-the-go (no pun intended).

They are very much work-in-progress proofs of concept right now,
and the relay protocol is certain to change.
2022-09-05 14:26:00 +02:00
2160d03794 xC: slightly clean up character encodings 2022-09-02 14:05:03 +02:00
36f8c7639f xC: clean up logging
Don't treat rendition as flags, separate the two.

Also treat join and part arrows as rendition.
2022-09-02 12:31:42 +02:00
74470f1aa4 CMakeLists.txt: improve dependencies of xD-replies 2022-09-02 12:25:37 +02:00
3af1765261 xC: make alias creation tolerant to prefixed names
Those would not work, so skip the first forward slash.

Note that liberty can save arbitrary alias names since 6e93119,
making the removed comment about checking outdated.
2022-08-29 15:22:11 +02:00
b454920c81 xC: deal with conflicts when renaming buffers 2022-08-29 15:05:02 +02:00
ef8f25d1dd xC: deal with any identifier conflicts
Invalid UTF-8 converted to UTF-8 may conflict with that
which was valid UTF-8 in the first place.
2022-08-29 14:41:23 +02:00
313a65180e xC: fix some corner cases around terminal handling 2022-08-29 14:05:33 +02:00
91db8e6e54 xC: use the correct way of resetting libedit
The only remaining major annoyance is incremental search
seemingly not giving back control.
2022-08-29 10:30:45 +02:00
dbe95fa298 xC: make libedit history switching more reliable 2022-08-29 09:20:56 +02:00
9d5e57a501 xC: improve libedit multiline input handling 2022-08-29 08:31:44 +02:00
4ed6693f57 xC: erase remaining mentions of a "backlog helper" 2022-08-29 08:22:09 +02:00
bea8d13227 xC: don't autosave when nothing changed 2022-08-29 08:22:09 +02:00
ecebeace0e Don't wrap xD-gen-replies in a shell script
AWK doesn't seem to be that friendly to shebangs, so let env,
also required for changing LC_ALL, locate it in PATH.
2022-08-29 06:07:49 +02:00
ca33adeeee Update README
Stop pretending that xD has a future.
2022-08-27 16:53:56 +02:00
b31e079256 Update README 2022-08-27 16:18:14 +02:00
57597bf8a2 xC: move TEXT_* constants where they belong 2022-08-27 15:06:28 +02:00
c0996fcbe7 xC: normalize BSD Editline's history behaviour
Now it's a realistically useful frontend.
2022-08-27 15:06:27 +02:00
03d8ea4c5a xC: general.save_on_quit -> general.autosave
Power outages and similar situations make the former unreliable,
so get rid of any false promise it might seem to give.
2022-08-27 09:15:38 +02:00
dc002a2db4 xC: revise configuration options
This commit constitutes a breaking change to old configurations.

All behaviour.* options have now become general.*, with the following
few renames as exceptions:

 - editor_command -> editor
 - backlog_helper -> pager
 - backlog_helper_strip_formatting -> pager_strip_formatting
2022-08-27 09:15:37 +02:00
a32916ffcf xC: label code sections better
Introduce tildes as a new sublevel of markers.
2022-08-27 09:15:37 +02:00
f7be510d26 xC: make fancy-prompt.lua alignment more reliable
And generally clean up that script.
2022-08-27 09:15:37 +02:00
83764d1e1b Fix xB.adoc parsing with current libasciidoc 2022-08-24 03:17:05 +02:00
a717782480 Build with AsciiDoc as well as Asciidoctor 2022-08-24 00:13:51 +02:00
c50c959f4d Bump copyright years 2022-08-17 18:27:52 +02:00
0dd7536b5a Update README 2022-08-15 15:49:59 +02:00
0750096827 xC: expand behaviour.editor_command examples 2022-08-14 20:27:30 +02:00
49d9980662 xC: improve backlog helper capabilities
Snippets now receive positional parameters in the form of the buffer's
name in the locale encoding, and a filename if applicable
(we keep passing stdin along with the filename, which happens to
work out well for less(1)).

The default value of the configuration option also no longer uses
the "long prompt", which used to unhelpfully tell position in terms
of lines, but rather sets its own prompt that counts pages,
and makes sure to indicate the source buffer.

The main motivation behind this change is to make the 'v' command
work in less(1).  LESSSECURE must be omitted from the snippet
for this to work.

Bump liberty to receive a config parser that allows for less
convoluted escaping.
2022-08-14 18:52:26 +02:00
2f7fbcdc5d CMakeLists.txt: fix a typo 2022-08-12 13:21:46 +02:00
ef0cbe9a59 Rename the project
It is about to see some extensions, obsoleting the number three.
2022-08-07 10:40:42 +02:00
2d8808d795 utm-filter.lua: mention the passing of fbclid 2022-07-18 17:59:28 +02:00
60d52ad479 xC, xD: add basic WALLOPS support 2022-02-04 22:48:54 +01:00
b358f53ec3 Bump version, update NEWS 2021-12-21 05:58:34 +01:00
2eb315f5c4 utm-filter.lua: add Facebook to the filter 2021-12-20 14:36:41 +01:00
851c2ee548 CMakeLists.txt: fix macOS build 2021-11-02 15:34:51 +01:00
f9848ed627 Update README 2021-10-31 05:16:57 +01:00
686a39df38 CMakeLists.txt: slightly modernize 2021-10-31 04:30:04 +01:00
9cea3fca91 Update NEWS 2021-10-30 14:25:13 +02:00
5165f76b7c xC: quote text coming from a bracketed paste
Not having this has caused me much annoyance over the years.
2021-10-30 09:27:32 +02:00
92ac13f3c6 xC: allow passing the cursor position to editors
Add a configuration option to set a custom editor command,
different from EDITOR or VISUAL--those remain as defaults.

Implement substitutions allowing to convey cursor information
to VIM and Emacs (the latter of which is fairly painful to cater to),
and put usage hints in the configuration option's description.

This should make the editing experience a bit more seamless
for users, even though the position is carried over in one way only.

No sophisticated quoting capabilities were deemed necessary,
it is a lot of code already.  The particular syntax is inspired
by .desktop files and systemd.

["/bin/sh", "-c", "vim +$2go \"$1\"", filename, position, line, column]
would be a slightly simpler but cryptic way of implementing this.
2021-10-30 09:02:35 +02:00
df4ca74580 xC: make libedit autocomplete less miserable
Omitting even this hack was a huge hit to overall usability.
2021-10-30 08:29:16 +02:00
9e297244a4 Update .gitignore 2021-10-30 03:37:22 +02:00
d32ba133c0 Add clang-format configuration, clean up 2021-10-30 02:55:19 +02:00
ce3976e1ec xC: normalize ^J behaviour to follow Readline
For some reason Editline inserts it verbatim,
but in a more broken manner than it has with ^V^J.
2021-10-28 08:49:01 +02:00
e5ed89646b xC: fix newer libedit (2021-08-29) 2021-10-28 08:23:52 +02:00
5e728f6d31 Bump version, update NEWS 2021-10-06 14:05:23 +02:00
766f68e070 Bump liberty 2021-10-06 13:52:59 +02:00
3dc5242d43 Bump liberty
Importing some minor unimportant fixes.
2021-09-26 08:55:46 +02:00
fd9d5db1d2 xD: bump the soft file descriptor limit
By default it's a mere thousand connections, which is unnecessarily
crippling our advertised ability to handle lots of them.

Thanks for the advice, Lennart.
2021-09-23 20:32:00 +02:00
cb480b4c71 xC: show orphan outcoming actions differently
It's hard to think of anything actually good here.

This would be an exceptionally rare thing to do, anyway.
2021-09-05 02:51:05 +02:00
59cc423694 xC: abandon Freenode, embrace IRCnet
You're not fucking supposed to require a fucking registration
on fucking IRC networks.
2021-08-29 15:18:20 +02:00
9323089d66 xC: mIRC didn't invent all IRC formatting
So let's not confuse ourselves.
2021-08-29 12:12:52 +02:00
de7df1f60d xC: refactor parsing of IRC formatting 2021-08-29 12:06:53 +02:00
b082e82b62 xC: fix displaying IRC colours above 16
First, we indexed the colour array without a required offset.
Second, the data type was too small and overflowed negative.

Detected during a refactor, which this is a part of.
2021-08-28 18:25:03 +02:00
b8dbc70a9c xC: respect text formatting when autosplitting 2021-08-28 18:24:20 +02:00
31 changed files with 3776 additions and 576 deletions

32
.clang-format Normal file
View File

@@ -0,0 +1,32 @@
# clang-format is fairly limited, and these rules are approximate:
# - array initializers can get terribly mangled with clang-format 12.0,
# - sometimes it still aligns with space characters,
# - struct name NL { NL ... NL } NL name; is unachievable.
BasedOnStyle: GNU
ColumnLimit: 80
IndentWidth: 4
TabWidth: 4
UseTab: ForContinuationAndIndentation
BreakBeforeBraces: Allman
SpaceAfterCStyleCast: true
AlignAfterOpenBracket: DontAlign
AlignOperands: DontAlign
AlignConsecutiveMacros: Consecutive
AllowAllArgumentsOnNextLine: false
AllowAllParametersOfDeclarationOnNextLine: false
IndentGotoLabels: false
# IncludeCategories has some potential, but it may also break the build.
# Note that the documentation says the value should be "Never".
SortIncludes: false
# This is a compromise, it generally works out aesthetically better.
BinPackArguments: false
# Unfortunately, this can't be told to align to column 40 or so.
SpacesBeforeTrailingComments: 2
# liberty-specific macro body wrappers.
MacroBlockBegin: "BLOCK_START"
MacroBlockEnd: "BLOCK_END"
ForEachMacros: ["LIST_FOR_EACH"]

10
.gitignore vendored
View File

@@ -3,7 +3,9 @@
# Qt Creator files
/CMakeLists.txt.user*
/uirc3.config
/uirc3.files
/uirc3.creator*
/uirc3.includes
/xK.config
/xK.files
/xK.creator*
/xK.includes
/xK.cflags
/xK.cxxflags

View File

@@ -1,15 +1,21 @@
cmake_minimum_required (VERSION 3.0)
project (uirc3 VERSION 1.3.0 LANGUAGES C)
# Ubuntu 18.04 LTS and OpenBSD 6.4
cmake_minimum_required (VERSION 3.10)
project (xK VERSION 1.5.0
DESCRIPTION "IRC daemon, bot, TUI client and X11/web frontends" LANGUAGES C)
# Options
option (WANT_READLINE "Use GNU Readline for the UI (better)" ON)
option (WANT_LIBEDIT "Use BSD libedit for the UI" OFF)
option (WANT_XF "Build xF" OFF)
# Moar warnings
set (CMAKE_C_STANDARD 99)
set (CMAKE_C_STANDARD_REQUIRED ON)
set (CMAKE_C_EXTENSIONS OFF)
if ("${CMAKE_C_COMPILER_ID}" MATCHES "GNU" OR CMAKE_COMPILER_IS_GNUCC)
# -Wunused-function is pretty annoying here, as everything is static
set (wdisabled "-Wno-unused-function")
set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -std=c99 -Wall -Wextra ${wdisabled}")
set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wextra -Wno-unused-function")
endif ()
# Version
@@ -57,6 +63,8 @@ if ("${CMAKE_SYSTEM_NAME}" MATCHES "BSD")
# Need this for SIGWINCH in FreeBSD and OpenBSD respectively;
# our POSIX version macros make it undefined
add_definitions (-D__BSD_VISIBLE=1 -D_BSD_SOURCE=1)
elseif (APPLE)
add_definitions (-D_DARWIN_C_SOURCE)
endif ()
# -lrt is only for glibc < 2.17
@@ -112,10 +120,16 @@ endif ()
if ((WANT_READLINE AND WANT_LIBEDIT) OR (NOT WANT_READLINE AND NOT WANT_LIBEDIT))
message (SEND_ERROR "You have to choose either GNU Readline or libedit")
elseif (WANT_READLINE)
pkg_check_modules (readline readline)
# OpenBSD's default readline is too old
if ("${CMAKE_SYSTEM_NAME}" MATCHES "OpenBSD")
include_directories (${OPENBSD_LOCALBASE}/include/ereadline)
list (APPEND xC_libraries ereadline)
elseif (readline_FOUND)
list (APPEND xC_libraries ${readline_LIBRARIES})
include_directories (${readline_INCLUDE_DIRS})
link_directories (${readline_LIBRARY_DIRS})
else ()
list (APPEND xC_libraries readline)
endif ()
@@ -131,28 +145,55 @@ set (HAVE_EDITLINE "${WANT_LIBEDIT}")
set (HAVE_LUA "${WITH_LUA}")
include (GNUInstallDirs)
configure_file (${PROJECT_SOURCE_DIR}/config.h.in ${PROJECT_BINARY_DIR}/config.h)
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})
# Generate IRC replies--we need a custom target because of the multiple outputs
add_custom_command (OUTPUT xD-replies.c xD.msg
COMMAND ${PROJECT_SOURCE_DIR}/xD-gen-replies.sh
> xD-replies.c < ${PROJECT_SOURCE_DIR}/xD-replies
DEPENDS ${PROJECT_SOURCE_DIR}/xD-replies
COMMAND env LC_ALL=C awk
-f ${PROJECT_SOURCE_DIR}/xD-gen-replies.awk
${PROJECT_SOURCE_DIR}/xD-replies > xD-replies.c
DEPENDS
${PROJECT_SOURCE_DIR}/xD-gen-replies.awk
${PROJECT_SOURCE_DIR}/xD-replies
COMMENT "Generating files from the list of server numerics")
add_custom_target (replies DEPENDS ${PROJECT_BINARY_DIR}/xD-replies.c)
add_custom_command (OUTPUT xC-proto.c
COMMAND env LC_ALL=C awk
-f ${PROJECT_SOURCE_DIR}/xC-gen-proto.awk
-f ${PROJECT_SOURCE_DIR}/xC-gen-proto-c.awk
${PROJECT_SOURCE_DIR}/xC-proto > xC-proto.c
DEPENDS
${PROJECT_SOURCE_DIR}/xC-gen-proto.awk
${PROJECT_SOURCE_DIR}/xC-gen-proto-c.awk
${PROJECT_SOURCE_DIR}/xC-proto
COMMENT "Generating xC relay protocol code")
add_custom_target (xC-proto DEPENDS ${PROJECT_BINARY_DIR}/xC-proto.c)
# Build
foreach (name xB xC xD)
add_executable (${name} ${name}.c ${PROJECT_BINARY_DIR}/config.h)
add_executable (${name} ${name}.c ${project_config})
target_link_libraries (${name} ${project_libraries})
add_threads (${name})
endforeach ()
add_dependencies (xD replies)
add_dependencies (xC replies)
add_dependencies (xC replies xC-proto)
target_link_libraries (xC ${xC_libraries})
if (WANT_XF)
pkg_check_modules (x11 REQUIRED x11 xrender xft fontconfig)
include_directories (${x11_INCLUDE_DIRS})
link_directories (${x11_LIBRARY_DIRS})
add_executable (xF xF.c ${project_config})
add_dependencies (xF xC-proto)
target_link_libraries (xF ${x11_LIBRARIES} ${project_libraries})
add_threads (xF)
endif ()
# Tests
include (CTest)
if (BUILD_TESTING)
@@ -184,7 +225,7 @@ add_custom_target (clang-tidy
# Installation
install (TARGETS xB xC xD DESTINATION ${CMAKE_INSTALL_BINDIR})
install (FILES LICENSE DESTINATION ${CMAKE_INSTALL_DOCDIR})
# XXX: our defaults for XDG_DATA_DIRS expect /usr/local/shore or /usr/share
# XXX: our defaults for XDG_DATA_DIRS expect /usr/local/share or /usr/share
install (DIRECTORY plugins/xB/
DESTINATION ${CMAKE_INSTALL_DATADIR}/xB/plugins USE_SOURCE_PERMISSIONS)
install (DIRECTORY plugins/xC/
@@ -192,20 +233,31 @@ install (DIRECTORY plugins/xC/
# Generate documentation from text markup
find_program (ASCIIDOCTOR_EXECUTABLE asciidoctor)
if (NOT ASCIIDOCTOR_EXECUTABLE)
message (FATAL_ERROR "asciidoctor not found")
find_program (A2X_EXECUTABLE a2x)
if (NOT ASCIIDOCTOR_EXECUTABLE AND NOT A2X_EXECUTABLE)
message (FATAL_ERROR "Neither asciidoctor nor a2x were found")
endif ()
foreach (page xB xC xD)
set (page_output "${PROJECT_BINARY_DIR}/${page}.1")
list (APPEND project_MAN_PAGES "${page_output}")
add_custom_command (OUTPUT ${page_output}
COMMAND ${ASCIIDOCTOR_EXECUTABLE} -b manpage
-a release-version=${project_version}
"${PROJECT_SOURCE_DIR}/${page}.adoc"
-o "${page_output}"
DEPENDS ${page}.adoc
COMMENT "Generating man page for ${page}" VERBATIM)
if (ASCIIDOCTOR_EXECUTABLE)
add_custom_command (OUTPUT ${page_output}
COMMAND ${ASCIIDOCTOR_EXECUTABLE} -b manpage
-a release-version=${project_version}
-o "${page_output}"
"${PROJECT_SOURCE_DIR}/${page}.adoc"
DEPENDS ${page}.adoc
COMMENT "Generating man page for ${page}" VERBATIM)
elseif (A2X_EXECUTABLE)
add_custom_command (OUTPUT ${page_output}
COMMAND ${A2X_EXECUTABLE} --doctype manpage --format manpage
-a release-version=${project_version}
-D "${PROJECT_BINARY_DIR}"
"${PROJECT_SOURCE_DIR}/${page}.adoc"
DEPENDS ${page}.adoc
COMMENT "Generating man page for ${page}" VERBATIM)
endif ()
endforeach ()
add_custom_target (docs ALL DEPENDS ${project_MAN_PAGES})
@@ -217,7 +269,6 @@ foreach (page ${project_MAN_PAGES})
endforeach ()
# CPack
set (CPACK_PACKAGE_DESCRIPTION_SUMMARY "Unreasonable IRC client, daemon and bot")
set (CPACK_PACKAGE_VERSION "${project_version_safe}")
set (CPACK_PACKAGE_VENDOR "Premysl Eric Janouch")
set (CPACK_PACKAGE_CONTACT "Přemysl Eric Janouch <p@janouch.name>")

View File

@@ -1,4 +1,4 @@
Copyright (c) 2014 - 2021, Přemysl Eric Janouch <p@janouch.name>
Copyright (c) 2014 - 2022, 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.

52
NEWS
View File

@@ -1,3 +1,55 @@
Unreleased
* xD: implemented WALLOPS, choosing to make it target even non-operators
* xC: made it show WALLOPS messages, as PRIVMSG for the server buffer
* xC: all behaviour.* configuration options have been renamed to general.*,
with the exception of editor_command/editor, backlog_helper/pager,
and backlog_helper_strip_formatting/pager_strip_formatting
* xC: replaced behaviour.save_on_quit with general.autosave
* xC: improved pager integration capabilities
* xC: normalized editline's history behaviour, making it a viable frontend
* xC: various bugfixes
* xC: added a relay interface, enabled through the general.relay_bind option
* Added an experimental X11 frontend for xC called xF
* Added an experimental web frontend for xC called xP
1.5.0 (2021-12-21) "The Show Must Go On"
* xC: made it possible to pass the cursor position to external editors,
in particular VIM and Emacs
* xC: started quoting text coming from bracketed pastes,
to minimize the risk of trying to execute filesystem paths as commands
* xC: fixed to work with post-2021-08-29 editline
* xC: extended editline's autocomplete to show all options
* utm-filter.lua: added Facebook's tracking parameter to the filter
1.4.0 (2021-10-06) "Call Me Scruffy Scruffington"
* xC: made message autosplitting respect text formatting
* xC: fixed displaying IRC colours above 16
* xC: offer IRCnet as an IRC network to connect to,
rather than the lunatic new Freenode
* xD: started bumping the soft limit on file descriptors to the hard one
1.3.0 (2021-08-07) "New World Order"
* xC: made nick autocompletion offer recent speakers first

View File

@@ -1,73 +1,66 @@
uirc3
=====
:compact-option:
xK
==
The unreasonable IRC trinity. This project consists of an IRC client, daemon,
and bot. It's all you're ever going to need for chatting, as long as you can
make do with minimalist software.
'xK' (chat kit) is an IRC software suite consisting of a daemon, bot, terminal
client, and X11/web frontends for the client. It's all you're ever going to
need for chatting, so long as you can make do with slightly minimalist software.
All of them have these potentially interesting properties:
- IPv6 support
- TLS support, including client certificates
- lean on dependencies (with the exception of 'xC')
- compact and arguably easy to hack on
- very permissive license
They're all lean on dependencies, and offer a maximally permissive licence.
xC
--
The IRC client. It is largely defined by being built on top of GNU Readline
that has been hacked to death. Its interface should feel somewhat familiar for
weechat or irssi users.
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
should feel somewhat familiar for weechat or irssi users.
image::xC.png[align="center"]
This is the largest application within the project. It has most of the stuff
you'd expect of an IRC client, such as being able to set up multiple servers,
a powerful configuration system, integrated help, text formatting, CTCP queries,
automatic splitting of overlong messages, autocomplete, logging to file,
auto-away, command aliases and basic support for Lua scripting.
It has most features you'd expect of an IRC client, such as being multiserver,
a powerful configuration system, integrated help, text formatting, automatic
message splitting, multiline editing, bracketed paste support, word wrapping
that doesn't break links, autocomplete, logging, CTCP queries, auto-away,
command aliases, SOCKS proxying, SASL EXTERNAL authentication using TLS client
certificates, a remote relay interface, or basic support for Lua scripting.
As a unique bonus, you can launch a full text editor from within.
xF
--
The X11 frontend for 'xC', making use of its networked relay interface.
It's currently in development, and is hidden behind a CMake option.
xP
--
The web frontend for 'xC', making use of its networked relay interface.
It's currently rather elementary, and can be built from within its directory.
xD
--
The IRC daemon. It is designed to be used as a regular user application rather
than a system-wide daemon. If all you want is a decent, minimal IRCd for
testing purposes or a small network of respectful users (or bots), this one will
do it just fine.
The IRC daemon. It is designed for use as a regular user application rather
than a system-wide daemon, and follows the XDG Base Directory Specification.
If all you want is a decent, minimal IRCd for testing purposes or a small
network of respectful users (or bots), this one will do it just fine.
Notable features:
It autodetects TLS on incoming connections (I'm still wondering why everyone
doesn't have this), authenticates operators via TLS client certificate
fingerprints, and supports a number of IRCv3 capabilities.
- TLS autodetection (why doesn't everyone have this?), using secure defaults
- IRCop authentication via TLS client certificates
- epoll/kqueue support; this means that it should be able to handle quite
a number of concurrent user connections
- partial IRCv3 support
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
(which also means no services).
Not supported:
- server linking (which also means no services); I consider existing protocols
for this purpose ugly and tricky to implement correctly; I've also found no
use for this feature yet
- online changes to configuration; the configuration system from 'xC' could
be used to implement this feature if needed
- limits of almost any kind, just connections and mode `+l`
This program has been
https://git.janouch.name/p/haven/src/branch/master/hid[ported to Go],
and development continues over there.
This program has been https://git.janouch.name/p/haven/src/branch/master/hid[
ported to Go] in a different project, and development continues over there.
xB
--
The IRC bot. It builds upon the concept of my other VitaminA IRC bot. The main
characteristic of these two bots is that they run plugins as coprocesses, which
allows for enhanced reliability and programming language freedom.
The IRC bot. While originally intended to be a simple rewrite of my old GNU AWK
bot in C, it fairly quickly became a playground, and it eventually got me into
writing the rest of this package.
While originally intended to be a simple rewrite of the original AWK bot in C,
it fairly quickly became a playground, and it eventually got me into writing
the rest of the package.
It survives crashes, server disconnects and timeouts, and also has native SOCKS
support (even though socksify can add that easily to any program).
Its main characteristic is that it runs plugins as coprocesses, allowing for
enhanced reliability and programming language freedom. Moreover, it recovers
from any crashes, and offers native SOCKS support (even though socksify can add
that easily to any program).
Packages
--------
@@ -76,18 +69,17 @@ a package with the latest development version from Archlinux's AUR.
Building
--------
Build dependencies: CMake, pkg-config, asciidoctor, awk, liberty (included) +
Runtime dependencies: openssl +
Additionally for 'xC': curses, libffi, lua >= 5.3 (optional),
readline >= 6.0 or libedit >= 2013-07-12
Build-only dependencies:
CMake, pkg-config, asciidoctor or asciidoc, awk, liberty (included) +
Common runtime dependencies: openssl +
Additionally for 'xC': curses, libffi, +
readline >= 6.0 or libedit >= 2013-07-12, lua >= 5.3 (optional) +
Additionally for 'xF': x11, xft
Avoid libedit if you can, in general it works but at the moment history is
acting up and I have no clue about fixing it.
$ git clone --recursive https://git.janouch.name/p/uirc3.git
$ mkdir uirc3/build
$ cd uirc3/build
$ cmake .. -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Debug \
$ git clone --recursive https://git.janouch.name/p/xK.git
$ mkdir xK/build
$ cd xK/build
$ cmake .. -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=RelWithDebInfo \
-DWANT_READLINE=ON -DWANT_LIBEDIT=OFF -DWANT_LUA=ON
$ make
@@ -95,10 +87,10 @@ To install the application, you can do either the usual:
# make install
Or you can try telling CMake to make a package for you. For Debian it is:
Or you can try telling CMake to make a package for you:
$ cpack -G DEB
# dpkg -i uirc3-*.deb
$ cpack -G DEB # also supported: RPM, FreeBSD
# dpkg -i xK-*.deb
Usage
-----
@@ -156,14 +148,18 @@ Beware that you can easily break the program if you're not careful.
How do I make xC look like the screenshot?
------------------------------------------
First of all, you must build it with Lua support. With the defaults, 'xC'
doesn't look too fancy because I don't want to depend on Lua or 256-colour
terminals. In addition to that, I appear to be one of the few people who use
black on white terminals.
With the defaults, 'xC' doesn't look too fancy because I don't want to have
a hard dependency on either Lua for the bundled script that provides an easily
adjustable enhanced prompt, or on 256-colour terminals. Moreover, it's nearly
impossible to come up with a colour theme that would work well with both
black-on-white and white-on-black terminals, or anything wild in between.
/set behaviour.date_change_line = "%a %e %b %Y"
/set behaviour.plugin_autoload += "fancy-prompt.lua"
/set behaviour.backlog_helper = "LESSSECURE=1 less -R +Gb1d -Ps'Backlog ?ltlines %lt-%lb?L/%L. .?e(END):?pB%pB\\%..'"
Assuming that your build supports Lua plugins, and that you have a decent,
properly set-up terminal emulator, it suffices to run:
/set general.pager = Press Tab here and change +Gb to +Gb1d
/set general.date_change_line = "%a %e %b %Y"
/set general.plugin_autoload += "fancy-prompt.lua"
/set attributes.userhost = "\x1b[38;5;109m"
/set attributes.join = "\x1b[38;5;108m"
/set attributes.part = "\x1b[38;5;138m"
@@ -184,7 +180,7 @@ configurations accordingly, but I consider it rather messy and unnecessary.
Contributing and Support
------------------------
Use https://git.janouch.name/p/uirc3 to report any bugs, request features,
Use https://git.janouch.name/p/xK to report any bugs, request features,
or submit pull requests. `git send-email` is tolerated. If you want to discuss
the project, feel free to join me at ircs://irc.janouch.name, channel #dev.

View File

@@ -1,7 +1,7 @@
/*
* common.c: common functionality
*
* Copyright (c) 2014 - 2020, Přemysl Eric Janouch <p@janouch.name>
* Copyright (c) 2014 - 2022, 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.
@@ -22,11 +22,11 @@
#define LIBERTY_WANT_PROTO_IRC
#ifdef WANT_SYSLOG_LOGGING
#define print_fatal_data ((void *) LOG_ERR)
#define print_error_data ((void *) LOG_ERR)
#define print_warning_data ((void *) LOG_WARNING)
#define print_status_data ((void *) LOG_INFO)
#define print_debug_data ((void *) LOG_DEBUG)
#define print_fatal_data ((void *) LOG_ERR)
#define print_error_data ((void *) LOG_ERR)
#define print_warning_data ((void *) LOG_WARNING)
#define print_status_data ((void *) LOG_INFO)
#define print_debug_data ((void *) LOG_DEBUG)
#endif // WANT_SYSLOG_LOGGING
#include "liberty/liberty.c"
@@ -48,6 +48,66 @@ init_openssl (void)
#endif
}
static char *
gai_reconstruct_address (struct addrinfo *ai)
{
char host[NI_MAXHOST] = {}, port[NI_MAXSERV] = {};
int err = getnameinfo (ai->ai_addr, ai->ai_addrlen,
host, sizeof host, port, sizeof port,
NI_NUMERICHOST | NI_NUMERICSERV);
if (err)
{
print_debug ("%s: %s", "getnameinfo", gai_strerror (err));
return xstrdup ("?");
}
return format_host_port_pair (host, port);
}
static bool
accept_error_is_transient (int err)
{
// OS kernels may return a wide range of unforeseeable errors.
// Assuming that they're either transient or caused by
// a connection that we've just extracted from the queue.
switch (err)
{
case EBADF:
case EINVAL:
case ENOTSOCK:
case EOPNOTSUPP:
return false;
default:
return true;
}
}
/// Destructively tokenize an address into a host part, and a port part.
/// The port is only overwritten if that part is found, allowing for defaults.
static const char *
tokenize_host_port (char *address, const char **port)
{
// Unwrap IPv6 addresses in format_host_port_pair() format.
char *rbracket = strchr (address, ']');
if (*address == '[' && rbracket)
{
if (rbracket[1] == ':')
{
*port = rbracket + 2;
return *rbracket = 0, address + 1;
}
if (!rbracket[1])
return *rbracket = 0, address + 1;
}
char *colon = strchr (address, ':');
if (colon)
{
*port = colon + 1;
return *colon = 0, address;
}
return address;
}
// --- To be moved to liberty --------------------------------------------------
// FIXME: in xssl_get_error() we rely on error reasons never being NULL (i.e.,
@@ -74,6 +134,15 @@ xerr_describe_error (void)
return reason;
}
static struct str
str_from_cstr (const char *cstr)
{
struct str self;
self.alloc = (self.len = strlen (cstr)) + 1;
self.str = memcpy (xmalloc (self.alloc), cstr, self.alloc);
return self;
}
static ssize_t
strv_find (const struct strv *v, const char *s)
{
@@ -87,15 +156,15 @@ static time_t
unixtime_msec (long *msec)
{
#ifdef _POSIX_TIMERS
struct timespec tp;
hard_assert (clock_gettime (CLOCK_REALTIME, &tp) != -1);
*msec = tp.tv_nsec / 1000000;
struct timespec tp;
hard_assert (clock_gettime (CLOCK_REALTIME, &tp) != -1);
*msec = tp.tv_nsec / 1000000;
#else // ! _POSIX_TIMERS
struct timeval tp;
hard_assert (gettimeofday (&tp, NULL) != -1);
*msec = tp.tv_usec / 1000;
struct timeval tp;
hard_assert (gettimeofday (&tp, NULL) != -1);
*msec = tp.tv_usec / 1000;
#endif // ! _POSIX_TIMERS
return tp.tv_sec;
return tp.tv_sec;
}
// --- Logging -----------------------------------------------------------------

Submodule liberty updated: 9639777814...f545be725d

View File

@@ -1,7 +1,7 @@
--
-- fancy-prompt.lua: the fancy multiline prompt you probably want
--
-- Copyright (c) 2016, Přemysl Eric Janouch <p@janouch.name>
-- Copyright (c) 2016 - 2022, 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.
@@ -40,7 +40,7 @@ xC.hook_prompt (function (hook)
if buffer == current then
current_n = i
elseif buffer.new_messages_count ~= buffer.new_unimportant_count then
if active ~= "" then active = active .. "," end
active = active .. ","
if buffer.highlighted then
active = active .. "!"
bg_color = "224"
@@ -48,7 +48,6 @@ xC.hook_prompt (function (hook)
active = active .. i
end
end
if active ~= "" then active = "(" .. active .. ")" end
local x = current_n .. ":" .. current.name
if chan and chan.users_len ~= 0 then
local params = ""
@@ -56,25 +55,34 @@ xC.hook_prompt (function (hook)
params = params .. " +" .. mode .. " " .. param
end
local modes = chan.no_param_modes .. params:sub (3)
if modes ~= "" then x = x .. "(+" .. modes .. ")" end
if modes ~= "" then
x = x .. "(+" .. modes .. ")"
end
x = x .. "{" .. chan.users_len .. "}"
end
if current.hide_unimportant then x = x .. "<H>" end
local lines, cols = xC.get_screen_size ()
x = x .. " " .. active .. string.rep (" ", cols)
if current.hide_unimportant then
x = x .. "<H>"
end
if active ~= "" then
x = x .. " (" .. active:sub (2) .. ")"
end
-- Readline 7.0.003 seems to be broken and completely corrupts the prompt.
-- However 8.0.004 seems to be fine with these, as is libedit 20191231-3.1.
--x = x:gsub("[\128-\255]", "?")
-- Cut off extra characters and apply formatting, including the hack.
-- FIXME: this doesn't count with full-width or zero-width characters.
-- We might want to export wcwidth() above term_from_utf8 somehow.
local overflow = utf8.offset (x, cols - 1)
if overflow then x = x:sub (1, overflow) end
-- Align to the terminal's width and apply formatting, including the hack.
local lines, cols = xC.get_screen_size ()
local trailing, width = " ", xC.measure (x)
while cols > 0 and width >= cols do
x = x:sub (1, utf8.offset (x, -1) - 1)
trailing, width = ">", xC.measure (x)
end
x = "\x01\x1b[0;4;1;38;5;16m\x1b[48;5;" .. bg_color .. "m\x02" ..
x .. "\x01\x1b[0;4;1;7;38;5;" .. bg_color .. "m\x02 \x01\x1b[0;1m\x02"
x .. string.rep (" ", cols - width - 1) ..
"\x01\x1b[0;4;1;7;38;5;" .. bg_color .. "m\x02" ..
trailing .. "\x01\x1b[0;1m\x02"
local user_prefix = function (chan, user)
for i, chan_user in ipairs (chan.users) do

View File

@@ -1,7 +1,7 @@
--
-- utm-filter.lua: filter out Google Analytics bullshit from URLs
-- utm-filter.lua: filter out Google Analytics bullshit etc. from URLs
--
-- Copyright (c) 2015, Přemysl Eric Janouch <p@janouch.name>
-- Copyright (c) 2015 - 2021, 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.
@@ -19,6 +19,10 @@
local banned = {
gclid = 1,
-- Alas, Facebook no longer uses this parameter, see:
-- https://news.ycombinator.com/item?id=32117489
fbclid = 1,
utm_source = 1,
utm_medium = 1,
utm_term = 1,

12
xB.adoc
View File

@@ -1,8 +1,8 @@
xB(1)
=====
:doctype: manpage
:manmanual: uirc3 Manual
:mansource: uirc3 {release-version}
:manmanual: xK Manual
:mansource: xK {release-version}
Name
----
@@ -60,14 +60,14 @@ using the IRC protocol. (Caveat: the standard C library doesn't automatically
flush FILE streams for pipes on newlines.) A special *XB* command is introduced
for RPC, with the following subcommands:
*XB get_config* _key_::
*XB get_config* __key__::
Request the value of the given configuration option. If no such option
exists, the value will be empty. The response will be delivered in
the following format:
+
```
....
XB :value
```
....
+
This is particularly useful for retrieving the *prefix* string.
@@ -100,5 +100,5 @@ _/usr/share/xB/plugins/_::
Reporting bugs
--------------
Use https://git.janouch.name/p/uirc3 to report bugs, request features,
Use https://git.janouch.name/p/xK to report bugs, request features,
or submit pull requests.

325
xC-gen-proto-c.awk Normal file
View File

@@ -0,0 +1,325 @@
# 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]
}

447
xC-gen-proto-go.awk Normal file
View File

@@ -0,0 +1,447 @@
# 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 ""
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 Normal file
View File

@@ -0,0 +1,303 @@
# 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(";"))
}
}

120
xC-proto Normal file
View File

@@ -0,0 +1,120 @@
// 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,
} rendition;
// Unix timestamp in seconds.
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;
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;
};

19
xC.adoc
View File

@@ -1,8 +1,8 @@
xC(1)
=====
:doctype: manpage
:manmanual: uirc3 Manual
:mansource: uirc3 {release-version}
:manmanual: xK Manual
:mansource: xK {release-version}
Name
----
@@ -25,9 +25,9 @@ Options
other formatting marks to ANSI codes retrieved from the *terminfo*(5)
database:
+
```
....
printf '\x02bold\x02\n' | xC -f
```
....
+
This feature may be used to preview server MOTD files.
@@ -62,10 +62,10 @@ their respective function names:
*M-a*: *goto-activity*::
Go to the first following buffer with unseen activity.
*PageUp*: *display-backlog*::
Show the in-memory backlog for this buffer in the backlog helper,
Show the in-memory backlog for this buffer using *general.pager*,
which is almost certainly the *less*(1) program.
*M-h*: *display-full-log*::
Show the log file for this buffer in the backlog helper.
Show the log file for this buffer using *general.pager*.
*M-H*: *toggle-unimportant*::
Hide all join, part and quit messages, as well as all channel mode changes
that only relate to user channel modes. Intended to reduce noise in
@@ -105,7 +105,7 @@ _~/.config/xC/xC.conf_::
as the */set* command, to make changes in it.
_~/.local/share/xC/logs/_::
When enabled by *behaviour.logging*, log files are stored here.
When enabled by *general.logging*, log files are stored here.
_~/.local/share/xC/plugins/_::
_/usr/local/share/xC/plugins/_::
@@ -114,12 +114,11 @@ _/usr/share/xC/plugins/_::
Bugs
----
The editline (libedit) frontend is more of a proof of concept that mostly seems
to work but exhibits bugs that are not our fault.
The editline (libedit) frontend may exhibit some unexpected behaviour.
Reporting bugs
--------------
Use https://git.janouch.name/p/uirc3 to report bugs, request features,
Use https://git.janouch.name/p/xK to report bugs, request features,
or submit pull requests.
See also

1789
xC.c

File diff suppressed because it is too large Load Diff

29
xD-gen-replies.awk Executable file
View File

@@ -0,0 +1,29 @@
#!/usr/bin/awk -f
BEGIN {
# The message catalog is a by-product
msg = "xD.msg"
print "$quote \"" > msg;
print "$set 1" > msg;
}
/^[0-9]+ *IRC_(ERR|RPL)_[A-Z]+ *".*"$/ {
match($0, /".*"/);
ids[$1] = $2;
texts[$2] = substr($0, RSTART, RLENGTH);
print $1 " " texts[$2] > msg
}
END {
printf("enum\n{")
for (i in ids) {
if (seen_first)
printf(",")
seen_first = 1
printf("\n\t%s = %s", ids[i], i)
}
print "\n};\n"
print "static const char *g_default_replies[] =\n{"
for (i in ids)
print "\t[" ids[i] "] = " texts[ids[i]] ","
print "};"
}

View File

@@ -1,28 +0,0 @@
#!/bin/sh
LC_ALL=C exec awk '
BEGIN {
# The message catalog is a by-product
msg = "xD.msg"
print "$quote \"" > msg;
print "$set 1" > msg;
}
/^[0-9]+ *IRC_(ERR|RPL)_[A-Z]+ *".*"$/ {
match($0, /".*"/);
ids[$1] = $2;
texts[$2] = substr($0, RSTART, RLENGTH);
print $1 " " texts[$2] > msg
}
END {
printf("enum\n{")
for (i in ids) {
if (seen_first)
printf(",")
seen_first = 1
printf("\n\t%s = %s", ids[i], i)
}
print "\n};\n"
print "static const char *g_default_replies[] =\n{"
for (i in ids)
print "\t[" ids[i] "] = " texts[ids[i]] ","
print "};"
}'

View File

@@ -1,8 +1,8 @@
xD(1)
=====
:doctype: manpage
:manmanual: uirc3 Manual
:mansource: uirc3 {release-version}
:manmanual: xK Manual
:mansource: xK {release-version}
Name
----
@@ -49,5 +49,5 @@ _/etc/xdg/xD/xD.conf_::
Reporting bugs
--------------
Use https://git.janouch.name/p/uirc3 to report bugs, request features,
Use https://git.janouch.name/p/xK to report bugs, request features,
or submit pull requests.

83
xD.c
View File

@@ -1,7 +1,7 @@
/*
* xD.c: an IRC daemon
*
* Copyright (c) 2014 - 2020, Přemysl Eric Janouch <p@janouch.name>
* Copyright (c) 2014 - 2022, 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.
@@ -22,7 +22,9 @@
#define WANT_SYSLOG_LOGGING
#include "common.c"
#include "xD-replies.c"
#include <nl_types.h>
#include <sys/resource.h>
enum { PIPE_READ, PIPE_WRITE };
@@ -851,8 +853,6 @@ client_send_str (struct client *c, const struct str *s)
str_append_data (&c->write_buffer, s->str,
MIN (s->len, IRC_MAX_MESSAGE_LENGTH));
str_append (&c->write_buffer, "\r\n");
// XXX: we might want to move this elsewhere, so that it doesn't get called
// as often; it's going to cause a lot of syscalls with epoll.
client_update_poller (c, NULL);
// Technically we haven't sent it yet but that's a minor detail
@@ -2930,6 +2930,29 @@ irc_handle_links (const struct irc_message *msg, struct client *c)
irc_send_reply (c, IRC_RPL_ENDOFLINKS, mask);
}
static void
irc_handle_wallops (const struct irc_message *msg, struct client *c)
{
if (msg->params.len < 1)
RETURN_WITH_REPLY (c, IRC_ERR_NEEDMOREPARAMS, msg->command);
if (!(c->mode & IRC_USER_MODE_OPERATOR))
RETURN_WITH_REPLY (c, IRC_ERR_NOPRIVILEGES);
const char *message = msg->params.vector[0];
// Our interpretation: anonymize the sender,
// and target all users who want to receive these messages
struct str_map_iter iter = str_map_iter_make (&c->ctx->users);
struct client *target;
while ((target = str_map_iter_next (&iter)))
{
if (target != c && !(target->mode & IRC_USER_MODE_RX_WALLOPS))
continue;
client_send (target, ":%s WALLOPS :%s", c->ctx->server_name, message);
}
}
static void
irc_handle_kill (const struct irc_message *msg, struct client *c)
{
@@ -2992,6 +3015,7 @@ irc_register_handlers (struct server_context *ctx)
{ "ADMIN", true, irc_handle_admin, 0, 0 },
{ "STATS", true, irc_handle_stats, 0, 0 },
{ "LINKS", true, irc_handle_links, 0, 0 },
{ "WALLOPS", true, irc_handle_wallops, 0, 0 },
{ "MODE", true, irc_handle_mode, 0, 0 },
{ "PRIVMSG", true, irc_handle_privmsg, 0, 0 },
@@ -3069,6 +3093,7 @@ irc_try_read (struct client *c)
{
buf->str[buf->len += n_read] = '\0';
// TODO: discard characters above the 512 character limit
// FIXME: we should probably discard the data if closing_link
irc_process_buffer (buf, irc_process_message, c);
continue;
}
@@ -3110,6 +3135,7 @@ irc_try_read_tls (struct client *c)
case SSL_ERROR_NONE:
buf->str[buf->len += n_read] = '\0';
// TODO: discard characters above the 512 character limit
// FIXME: we should probably discard the data if closing_link
irc_process_buffer (buf, irc_process_message, c);
continue;
case SSL_ERROR_ZERO_RETURN:
@@ -3395,16 +3421,10 @@ irc_try_fetch_client (struct server_context *ctx, int listen_fd)
if (errno == EINTR)
return true;
if (errno == EBADF
|| errno == EINVAL
|| errno == ENOTSOCK
|| errno == EOPNOTSUPP)
if (accept_error_is_transient (errno))
print_warning ("%s: %s", "accept", strerror (errno));
else
print_fatal ("%s: %s", "accept", strerror (errno));
// OS kernels may return a wide range of unforeseeable errors.
// Assuming that they're either transient or caused by
// a connection that we've just extracted from the queue.
print_warning ("%s: %s", "accept", strerror (errno));
return true;
}
@@ -3788,10 +3808,9 @@ irc_lock_pid_file (struct server_context *ctx, struct error **e)
}
static int
irc_listen (struct addrinfo *gai_iter)
irc_listen (struct addrinfo *ai)
{
int fd = socket (gai_iter->ai_family,
gai_iter->ai_socktype, gai_iter->ai_protocol);
int fd = socket (ai->ai_family, ai->ai_socktype, ai->ai_protocol);
if (fd == -1)
return -1;
set_cloexec (fd);
@@ -3805,21 +3824,13 @@ irc_listen (struct addrinfo *gai_iter)
#if defined SOL_IPV6 && defined IPV6_V6ONLY
// Make NULL always bind to both IPv4 and IPv6, irrespectively of the order
// of results; only INADDR6_ANY seems to be affected by this
if (gai_iter->ai_family == AF_INET6)
if (ai->ai_family == AF_INET6)
soft_assert (setsockopt (fd, SOL_IPV6, IPV6_V6ONLY,
&yes, sizeof yes) != -1);
#endif
char host[NI_MAXHOST], port[NI_MAXSERV];
host[0] = port[0] = '\0';
int err = getnameinfo (gai_iter->ai_addr, gai_iter->ai_addrlen,
host, sizeof host, port, sizeof port,
NI_NUMERICHOST | NI_NUMERICSERV);
if (err)
print_debug ("%s: %s", "getnameinfo", gai_strerror (err));
char *address = format_host_port_pair (host, port);
if (bind (fd, gai_iter->ai_addr, gai_iter->ai_addrlen))
char *address = gai_reconstruct_address (ai);
if (bind (fd, ai->ai_addr, ai->ai_addrlen))
print_error ("bind to %s failed: %s", address, strerror (errno));
else if (listen (fd, 16 /* arbitrary number */))
print_error ("listen on %s failed: %s", address, strerror (errno));
@@ -3839,12 +3850,12 @@ static void
irc_listen_resolve (struct server_context *ctx,
const char *host, const char *port, struct addrinfo *gai_hints)
{
struct addrinfo *gai_result, *gai_iter;
struct addrinfo *gai_result = NULL, *gai_iter = NULL;
int err = getaddrinfo (host, port, gai_hints, &gai_result);
if (err)
{
char *address = format_host_port_pair (host, port);
print_error ("bind to %s failed: %s: %s",
print_error ("binding to %s failed: %s: %s",
address, "getaddrinfo", gai_strerror (err));
free (address);
return;
@@ -3984,6 +3995,21 @@ daemonize (struct server_context *ctx)
poller_post_fork (&ctx->poller);
}
static void
setup_limits (void)
{
struct rlimit limit;
if (getrlimit (RLIMIT_NOFILE, &limit))
{
print_warning ("%s: %s", "getrlimit", strerror (errno));
return;
}
limit.rlim_cur = limit.rlim_max;
if (setrlimit (RLIMIT_NOFILE, &limit))
print_warning ("%s: %s", "setrlimit", strerror (errno));
}
int
main (int argc, char *argv[])
{
@@ -4030,6 +4056,7 @@ main (int argc, char *argv[])
print_status (PROGRAM_NAME " " PROGRAM_VERSION " starting");
setup_signal_handlers ();
setup_limits ();
init_openssl ();
struct server_context ctx;

172
xF.c Normal file
View File

@@ -0,0 +1,172 @@
/*
* xF.c: a toothless IRC client frontend
*
* Copyright (c) 2022, 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.
*
*/
#include "config.h"
#define PROGRAM_NAME "xF"
#include "common.c"
#include "xC-proto.c"
#include <X11/Xatom.h>
#include <X11/Xlib.h>
#include <X11/keysym.h>
#include <X11/XKBlib.h>
#include <X11/Xft/Xft.h>
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static struct
{
bool polling;
struct connector connector;
int socket;
}
g;
static void
on_connector_connecting (void *user_data, const char *address)
{
(void) user_data;
print_status ("connecting to %s...", address);
}
static void
on_connector_error (void *user_data, const char *error)
{
(void) user_data;
print_status ("connection failed: %s", error);
}
static void
on_connector_failure (void *user_data)
{
(void) user_data;
exit_fatal ("giving up");
}
static void
on_connector_connected (void *user_data, int socket, const char *hostname)
{
(void) user_data;
(void) hostname;
g.polling = false;
g.socket = socket;
}
static void
protocol_test (const char *host, const char *port)
{
struct poller poller = {};
poller_init (&poller);
connector_init (&g.connector, &poller);
g.connector.on_connecting = on_connector_connecting;
g.connector.on_error = on_connector_error;
g.connector.on_connected = on_connector_connected;
g.connector.on_failure = on_connector_failure;
connector_add_target (&g.connector, host, port);
g.polling = true;
while (g.polling)
poller_run (&poller);
connector_free (&g.connector);
struct str s = str_make ();
str_pack_u32 (&s, 0);
struct relay_command_message m = {};
m.data.hello.command = RELAY_COMMAND_HELLO;
m.data.hello.version = RELAY_VERSION;
if (!relay_command_message_serialize (&m, &s))
exit_fatal ("serialization failed");
uint32_t len = htonl (s.len - sizeof len);
memcpy (s.str, &len, sizeof len);
if (errno = 0, write (g.socket, s.str, s.len) != (ssize_t) s.len)
exit_fatal ("short send or error: %s", strerror (errno));
char buf[1 << 20] = "";
while (errno = 0, read (g.socket, &len, sizeof len) == sizeof len)
{
len = ntohl (len);
if (errno = 0, read (g.socket, buf, MIN (len, sizeof buf)) != len)
exit_fatal ("short read or error: %s", strerror (errno));
struct msg_unpacker r = msg_unpacker_make (buf, len);
struct relay_event_message m = {};
if (!relay_event_message_deserialize (&m, &r))
exit_fatal ("deserialization failed");
if (msg_unpacker_get_available (&r))
exit_fatal ("trailing data");
printf ("event: %d\n", m.data.event);
relay_event_message_free (&m);
}
exit_fatal ("short read or error: %s", strerror (errno));
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
int
main (int argc, char *argv[])
{
static const struct opt opts[] =
{
{ 'h', "help", NULL, 0, "display this help and exit" },
{ 'V', "version", NULL, 0, "output version information and exit" },
{ 0, NULL, NULL, 0, NULL }
};
struct opt_handler oh = opt_handler_make (argc, argv, opts,
"HOST:PORT", "X11 frontend for xC.");
int c;
while ((c = opt_handler_get (&oh)) != -1)
switch (c)
{
case 'h':
opt_handler_usage (&oh, stdout);
exit (EXIT_SUCCESS);
case 'V':
printf (PROGRAM_NAME " " PROGRAM_VERSION "\n");
exit (EXIT_SUCCESS);
default:
print_error ("wrong options");
opt_handler_usage (&oh, stderr);
exit (EXIT_FAILURE);
}
argc -= optind;
argv += optind;
if (argc != 1)
{
opt_handler_usage (&oh, stderr);
exit (EXIT_FAILURE);
}
opt_handler_free (&oh);
char *address = xstrdup (argv[0]);
const char *port = NULL, *host = tokenize_host_port (address, &port);
if (!port)
exit_fatal ("missing port number/service name");
// TODO: Actually implement an X11-based user interface.
protocol_test (host, port);
return 0;
}

36
xF.svg Normal file
View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg version="1.1" width="48" height="48" viewBox="0 0 48 48"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="background" x1="0" y1="0" x2="1" y2="1">
<stop stop-color="#808080" offset="0" />
<stop stop-color="#000000" offset="1" />
</linearGradient>
<!-- librsvg screws up the filter's orientation in a weird way
otherwise a larger blur value would look better -->
<filter id="shadow" color-interpolation-filters="sRGB">
<feOffset dy="0.5" />
<feGaussianBlur stdDeviation="0.5" />
<feComposite in2="SourceGraphic" operator="in" />
</filter>
<clipPath id="clip">
<rect x="-7" y="-10" width="14" height="20" />
</clipPath>
</defs>
<circle cx="24" cy="24" r="20"
fill="url(#background)" stroke="#404040" stroke-width="2" />
<g transform="rotate(-45 24 24)" filter="url(#shadow)">
<path d="m 12,25 h 24 v 11 h -5 v -8 h -4.5 v 6 h -5 v -6 h -9.5 z"
fill="#ffffff" />
<g stroke-width="4" transform="translate(24, 16)" clip-path="url(#clip)"
stroke="#ffffff">
<line x1="-8" x2="8" y1="-5" y2="5" />
<line x1="-8" x2="8" y1="5" y2="-5" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

3
xP/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
/xP
/proto.go
/public/mithril.js

14
xP/Makefile Normal file
View File

@@ -0,0 +1,14 @@
.POSIX:
.SUFFIXES:
outputs = xP proto.go public/mithril.js
all: $(outputs)
xP: xP.go proto.go
go build -o $@
proto.go: ../xC-gen-proto.awk ../xC-gen-proto-go.awk ../xC-proto
awk -f ../xC-gen-proto.awk -f ../xC-gen-proto-go.awk ../xC-proto > $@
public/mithril.js:
curl -Lo $@ https://unpkg.com/mithril/mithril.js
clean:
rm -f $(outputs)

5
xP/go.mod Normal file
View File

@@ -0,0 +1,5 @@
module janouch.name/xK
go 1.18
require golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d

2
xP/go.sum Normal file
View File

@@ -0,0 +1,2 @@
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d h1:20cMwl2fHAzkJMEA+8J4JgqBQcQGzbisXo31MIeenXI=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=

109
xP/public/xP.css Normal file
View File

@@ -0,0 +1,109 @@
body {
margin: 0;
padding: 0;
font-family: sans-serif;
}
.xP {
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
}
.title, .status {
background: #f8f8f8;
border-bottom: 1px solid #ccc;
padding: .05rem .3rem;
}
.middle {
flex: auto;
display: flex;
flex-direction: row;
overflow: hidden;
}
.list {
overflow-y: auto;
border-right: 1px solid #ccc;
min-width: 10rem;
}
.item {
padding: .05rem .3rem;
cursor: default;
}
.item.active {
font-weight: bold;
}
/* Only Firefox currently supports align-content: safe end, thus this. */
.buffer-container {
flex: auto;
display: flex;
flex-direction: column;
overflow: hidden;
}
.filler {
flex: auto;
}
.buffer {
display: grid;
grid-template-columns: max-content auto;
overflow-y: auto;
}
.date {
padding: .3rem;
grid-column: span 2;
font-weight: bold;
}
.time {
padding: .1rem .3rem;
background: #f8f8f8;
color: #bbb;
border-right: 1px solid #ccc;
}
.mark {
padding-right: .3rem;
text-align: center;
display: inline-block;
min-width: 2rem;
}
.mark.error {
color: red;
}
.mark.join {
color: green;
}
.mark.part {
color: red;
}
.content {
padding: .1rem .3rem;
white-space: pre-wrap;
}
.content span.b {
font-weight: bold;
}
.content span.i {
font-style: italic;
}
.content span.u {
text-decoration: underline;
}
.content span.s {
text-decoration: line-through;
}
.content span.m {
font-family: monospace;
}
.status {
border-top: 2px solid #fff;
}
textarea {
padding: .05rem .3rem;
font-family: inherit;
}

188
xP/public/xP.js Normal file
View File

@@ -0,0 +1,188 @@
// TODO: Probably reset state on disconnect, and indicate to user.
let socket = new WebSocket(proxy)
let commandSeq = 0
function send(command) {
socket.send(JSON.stringify({commandSeq: ++commandSeq, data: command}))
}
socket.onopen = function(event) {
send({command: 'Hello', version: 1})
}
let buffers = new Map()
let bufferCurrent = undefined
socket.onmessage = function(event) {
console.log(event.data)
let e = JSON.parse(event.data).data
switch (e.event) {
case 'BufferUpdate':
{
let b = buffers.get(e.bufferName)
if (b === undefined) {
b = {lines: []}
buffers.set(e.bufferName, b)
}
// TODO: Update any buffer properties.
break
}
case 'BufferRename':
buffers.set(e.new, buffers.get(e.bufferName))
buffers.delete(e.bufferName)
break
case 'BufferRemove':
buffers.delete(e.bufferName)
break
case 'BufferActivate':
bufferCurrent = e.bufferName
// TODO: Somehow scroll to the end of it immediately.
// TODO: Focus the textarea.
break
case 'BufferLine':
{
let b = buffers.get(e.bufferName)
if (b !== undefined)
b.lines.push({when: e.when, rendition: e.rendition, items: e.items})
break
}
case 'BufferClear':
{
let b = buffers.get(e.bufferName)
if (b !== undefined)
b.lines.length = 0
break
}
}
m.redraw()
}
let BufferList = {
view: vnode => {
let items = []
buffers.forEach((b, name) => {
let attrs = {
onclick: e => {
send({command: 'BufferActivate', bufferName: name})
},
}
if (name == bufferCurrent)
attrs.class = 'active'
items.push(m('.item', attrs, name))
})
return m('.list', {}, items)
},
}
let Content = {
view: vnode => {
let line = vnode.children[0]
let content = []
switch (line.rendition) {
case 'Indent': content.push(m('span.mark', {}, '')); break
case 'Status': content.push(m('span.mark', {}, '')); break
case 'Error': content.push(m('span.mark.error', {}, '⚠')); break
case 'Join': content.push(m('span.mark.join', {}, '→')); break
case 'Part': content.push(m('span.mark.part', {}, '←')); break
}
let classes = new Set()
let flip = c => {
if (classes.has(c))
classes.delete(c)
else
classes.add(c)
}
line.items.forEach(item => {
// TODO: Colours.
switch (item.kind) {
case 'Text':
// TODO: Detect and transform links.
content.push(m('span', {
class: Array.from(classes.keys()).join(' '),
}, item.text))
break
case 'Reset':
classes.clear()
break
case 'FlipBold': flip('b'); break
case 'FlipItalic': flip('i'); break
case 'FlipUnderline': flip('u'); break
case 'FlipInverse': flip('i'); break
case 'FlipCrossedOut': flip('s'); break
case 'FlipMonospace': flip('m'); break
}
})
return m('.content', {}, content)
},
}
let Buffer = {
view: vnode => {
let lines = []
let b = buffers.get(bufferCurrent)
if (b === undefined)
return
let lastDateMark = undefined
b.lines.forEach(line => {
let date = new Date(line.when * 1000)
let dateMark = date.toLocaleDateString()
if (dateMark !== lastDateMark) {
lines.push(m('.date', {}, dateMark))
lastDateMark = dateMark
}
lines.push(m('.time', {}, date.toLocaleTimeString()))
lines.push(m(Content, {}, line))
})
return m('.buffer-container', {}, [
m('.filler'),
m('.buffer', {}, lines),
])
},
}
// TODO: This should be remembered across buffer switches,
// and we'll probably have to intercept /all/ key presses.
let Input = {
view: vnode => {
return m('textarea', {
rows: 1,
onkeydown: e => {
// TODO: And perhaps on other actions, too.
send({command: 'Active'})
if (e.keyCode !== 13)
return
send({
command: 'BufferInput',
bufferName: bufferCurrent,
text: e.currentTarget.value,
})
e.preventDefault()
e.currentTarget.value = ''
},
})
},
}
let Main = {
view: vnode => {
return m('.xP', {}, [
m('.title', {}, "xP"),
m('.middle', {}, [m(BufferList), m(Buffer)]),
m('.status', {}, bufferCurrent),
m(Input),
])
},
}
// TODO: Buffer names should work as routes.
window.addEventListener('load', () => {
m.route(document.body, '/', {
'/': Main,
})
})

2
xP/xP.example.json Normal file
View File

@@ -0,0 +1,2 @@
{
}

186
xP/xP.go Normal file
View File

@@ -0,0 +1,186 @@
package main
import (
"context"
"encoding/binary"
"encoding/json"
"fmt"
"html/template"
"io"
"log"
"net"
"net/http"
"os"
"time"
"golang.org/x/net/websocket"
)
var (
addressBind string
addressConnect string
)
func clientToRelay(
ctx context.Context, ws *websocket.Conn, conn net.Conn) bool {
var j string
if err := websocket.Message.Receive(ws, &j); err != nil {
log.Println("Command receive failed: " + err.Error())
return false
}
log.Printf("?> %s\n", j)
var m RelayCommandMessage
if err := json.Unmarshal([]byte(j), &m); err != nil {
log.Println("Command unmarshalling failed: " + err.Error())
return false
}
b, ok := m.AppendTo(make([]byte, 4))
if !ok {
log.Println("Command serialization failed")
return false
}
binary.BigEndian.PutUint32(b[:4], uint32(len(b)-4))
if _, err := conn.Write(b); err != nil {
log.Println("Command send failed: " + err.Error())
return false
}
log.Printf("-> %v\n", b)
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)
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 {
log.Println("Event marshalling failed: " + err.Error())
return false
}
if err := websocket.Message.Send(ws, string(j)); err != nil {
log.Println("Event send failed: " + err.Error())
return false
}
log.Printf("<- %s\n", j)
return true
}
func errorToClient(ws *websocket.Conn, err error) bool {
j, err := json.Marshal(&RelayEventMessage{
EventSeq: 0,
Data: RelayEventData{
Interface: RelayEventDataError{
Event: RelayEventError,
CommandSeq: 0,
Error: err.Error(),
},
},
})
if err != nil {
log.Println("Event marshalling failed: " + err.Error())
return false
}
if err := websocket.Message.Send(ws, string(j)); err != nil {
log.Println("Event send failed: " + err.Error())
return false
}
return true
}
func handleWebSocket(ws *websocket.Conn) {
conn, err := net.Dial("tcp", addressConnect)
if err != nil {
errorToClient(ws, err)
return
}
// We don't need to intervene, so it's just two separate pipes so far.
ctx, cancel := context.WithCancel(ws.Request().Context())
go func() {
for clientToRelay(ctx, ws, conn) {
}
cancel()
}()
go func() {
for relayToClient(ctx, ws, conn) {
}
cancel()
}()
<-ctx.Done()
}
var staticHandler = http.FileServer(http.Dir("."))
var page = template.Must(template.New("/").Parse(`<!DOCTYPE html>
<html>
<head>
<title>xP</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="xP.css" />
</head>
<body>
<script src="mithril.js">
</script>
<script>
let proxy = '{{ . }}'
</script>
<script src="xP.js">
</script>
</body>
</html>`))
func handleDefault(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
staticHandler.ServeHTTP(w, r)
return
}
wsURI := fmt.Sprintf("ws://%s/ws", r.Host)
if err := page.Execute(w, wsURI); err != nil {
log.Println("Template execution failed: " + err.Error())
}
}
func main() {
if len(os.Args) != 3 {
log.Fatalf("usage: %s BIND CONNECT\n", os.Args[0])
}
addressBind, addressConnect = os.Args[1], os.Args[2]
http.Handle("/ws", websocket.Handler(handleWebSocket))
http.Handle("/", http.HandlerFunc(handleDefault))
s := &http.Server{
Addr: addressBind,
ReadTimeout: 60 * time.Second,
WriteTimeout: 60 * time.Second,
MaxHeaderBytes: 32 << 10,
}
log.Fatal(s.ListenAndServe())
}