Compare commits
	
		
			49 Commits
		
	
	
		
			3f9a365d36
			...
			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
	
				 | 
					
					
						
@@ -1,6 +1,11 @@
 | 
			
		||||
# Ubuntu 18.04 LTS and OpenBSD 6.4
 | 
			
		||||
cmake_minimum_required (VERSION 3.10)
 | 
			
		||||
project (xK VERSION 1.5.0
 | 
			
		||||
 | 
			
		||||
file (READ xK-version project_version)
 | 
			
		||||
configure_file (xK-version xK-version.tag COPYONLY)
 | 
			
		||||
string (STRIP "${project_version}" project_version)
 | 
			
		||||
 | 
			
		||||
project (xK VERSION "${project_version}"
 | 
			
		||||
	DESCRIPTION "IRC daemon, bot, TUI client and its web frontend" LANGUAGES C)
 | 
			
		||||
 | 
			
		||||
# Options
 | 
			
		||||
@@ -18,9 +23,6 @@ if ("${CMAKE_C_COMPILER_ID}" MATCHES "GNU" OR CMAKE_COMPILER_IS_GNUCC)
 | 
			
		||||
	set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wextra -Wno-unused-function")
 | 
			
		||||
endif ()
 | 
			
		||||
 | 
			
		||||
# Version
 | 
			
		||||
set (project_version "${PROJECT_VERSION}")
 | 
			
		||||
 | 
			
		||||
# Try to append commit ID if it follows a version tag.  It might be nicer if
 | 
			
		||||
# we could also detect dirty worktrees but that's very hard to get right.
 | 
			
		||||
# If we didn't need this for CPack, we could use add_custom_command to generate
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								LICENSE
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								LICENSE
									
									
									
									
									
								
							@@ -1,4 +1,4 @@
 | 
			
		||||
Copyright (c) 2014 - 2023, 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
 | 
			
		||||
purpose with or without fee is hereby granted.
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										10
									
								
								NEWS
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								NEWS
									
									
									
									
									
								
							@@ -1,4 +1,4 @@
 | 
			
		||||
2.0.0 (Unreleased)
 | 
			
		||||
2.0.0 (2024-07-28) "Perfect Is the Enemy of Good"
 | 
			
		||||
 | 
			
		||||
 * xD: now using SHA-256 for client certificate fingerprints
 | 
			
		||||
 | 
			
		||||
@@ -16,6 +16,8 @@
 | 
			
		||||
 | 
			
		||||
 * 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: unsolicited JOINs will no longer automatically activate the buffer
 | 
			
		||||
@@ -31,8 +33,14 @@
 | 
			
		||||
 | 
			
		||||
 * Added a web frontend for xC called xP
 | 
			
		||||
 | 
			
		||||
 * Added a Win32 frontend for xC called xW
 | 
			
		||||
 | 
			
		||||
 * Added a Cocoa frontend for xC called xM
 | 
			
		||||
 | 
			
		||||
 * Added a Go port of xD called xS
 | 
			
		||||
 | 
			
		||||
 * Added a simple notifier called xN
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
1.5.0 (2021-12-21) "The Show Must Go On"
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										42
									
								
								README.adoc
									
									
									
									
									
								
							
							
						
						
									
										42
									
								
								README.adoc
									
									
									
									
									
								
							@@ -1,9 +1,10 @@
 | 
			
		||||
xK
 | 
			
		||||
==
 | 
			
		||||
 | 
			
		||||
'xK' (chat kit) is an IRC software suite consisting of a daemon, bot, terminal
 | 
			
		||||
client, and a web frontend for the client.  It's all you're ever going to
 | 
			
		||||
need for chatting, so long as you can make do with slightly minimalist software.
 | 
			
		||||
'xK' (chat kit) is an IRC software suite consisting of a daemon, bot, notifier,
 | 
			
		||||
terminal client, and web/Windows/macOS frontends for the client.  It's all
 | 
			
		||||
you're ever going to need for chatting, so long as you can make do with slightly
 | 
			
		||||
minimalist software.
 | 
			
		||||
 | 
			
		||||
They're all lean on dependencies, and offer a maximally permissive licence.
 | 
			
		||||
 | 
			
		||||
@@ -55,6 +56,10 @@ 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
 | 
			
		||||
--
 | 
			
		||||
The IRC bot.  While originally intended to be a simple rewrite of my old GNU AWK
 | 
			
		||||
@@ -68,8 +73,10 @@ that easily to any program).
 | 
			
		||||
 | 
			
		||||
Packages
 | 
			
		||||
--------
 | 
			
		||||
Regular releases are sporadic.  git master should be stable enough.  You can get
 | 
			
		||||
a package with the latest development version from Archlinux's AUR.
 | 
			
		||||
Regular releases are sporadic.  git master should be stable enough.
 | 
			
		||||
You can get a package with the latest development version using Arch Linux's
 | 
			
		||||
https://aur.archlinux.org/packages/xk-git[AUR],
 | 
			
		||||
or as a https://git.janouch.name/p/nixexprs[Nix derivation].
 | 
			
		||||
 | 
			
		||||
Building
 | 
			
		||||
--------
 | 
			
		||||
@@ -135,14 +142,35 @@ 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
 | 
			
		||||
-------------------
 | 
			
		||||
'xC' will use the SASL EXTERNAL method to authenticate using the TLS client
 | 
			
		||||
certificate specified by the respective server's `tls_cert` option if you add
 | 
			
		||||
`sasl` to the `capabilities` option and the server supports this.
 | 
			
		||||
 | 
			
		||||
'xD' uses SHA-256 fingerprints of TLS client certificates to authenticate users.
 | 
			
		||||
To get the fingerprint from a certificate file in the required form, use:
 | 
			
		||||
'xD' and 'xS' use SHA-256 fingerprints of TLS client certificates
 | 
			
		||||
to authenticate users.  To get the fingerprint from a certificate file
 | 
			
		||||
in the required form, use:
 | 
			
		||||
 | 
			
		||||
 $ openssl x509 -in public.pem -outform DER | sha256sum
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								liberty
									
									
									
									
									
								
							
							
								
								
								
								
								
							
						
						
									
										2
									
								
								liberty
									
									
									
									
									
								
							 Submodule liberty updated: 0f3ed14575...f04cc2c61e
									
								
							
							
								
								
									
										4
									
								
								test
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								test
									
									
									
									
									
								
							@@ -1,5 +1,7 @@
 | 
			
		||||
#!/usr/bin/expect -f
 | 
			
		||||
# 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
 | 
			
		||||
system ./xD --write-default-cfg
 | 
			
		||||
@@ -27,7 +29,7 @@ expect "Option changed"
 | 
			
		||||
send "/disconnect\n"
 | 
			
		||||
expect "]"
 | 
			
		||||
send "/connect\n"
 | 
			
		||||
expect "Connection established"
 | 
			
		||||
expect "Welcome to"
 | 
			
		||||
 | 
			
		||||
# Try some chatting
 | 
			
		||||
send "/join #test\n"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										12
									
								
								test-static
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								test-static
									
									
									
									
									
								
							@@ -1,8 +1,16 @@
 | 
			
		||||
#!/bin/sh
 | 
			
		||||
# We don't use printf's percent notation with our custom logging mechanism,
 | 
			
		||||
# so the compiler cannot check it for us like it usually does
 | 
			
		||||
# so the compiler cannot check it for us like it usually does.
 | 
			
		||||
#
 | 
			
		||||
# In clang-query terms, the string we're interested in can be found through:
 | 
			
		||||
#   set traversal IgnoreUnlessSpelledInSource
 | 
			
		||||
#   set output dump
 | 
			
		||||
#   match callExpr(callee(functionDecl(
 | 
			
		||||
#     hasName("log_full"))),
 | 
			
		||||
#     hasArgument(5, stringLiteral().bind("format")))
 | 
			
		||||
# However, the tool is too restricted to be useful in a shell script.
 | 
			
		||||
perl -n0777 - "$(dirname "$0")"/xC.c <<-'END'
 | 
			
		||||
	while (/\blog_[^ ]+\s*\([^"()]*"[^"]*%[^%][^"]*"/gm) {
 | 
			
		||||
	while (/\blog_[^ ]+\s*\([^"()]*"[^"]*%\w[^"]*"/gm) {
 | 
			
		||||
		my ($p, $m) = ($`, $&);
 | 
			
		||||
		printf "$ARGV:%d: suspicious log format string: %s...\n",
 | 
			
		||||
			(1 + $p =~ tr/\n//), ($m =~ s/\s+/ /rg);
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										4
									
								
								xB.c
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								xB.c
									
									
									
									
									
								
							@@ -1009,7 +1009,7 @@ is_valid_plugin_name (const char *name)
 | 
			
		||||
	if (!*name)
 | 
			
		||||
		return false;
 | 
			
		||||
	for (const char *p = name; *p; p++)
 | 
			
		||||
		if (!isgraph (*p) || *p == '/')
 | 
			
		||||
		if (!isgraph ((uint8_t) *p) || *p == '/')
 | 
			
		||||
			return false;
 | 
			
		||||
	return true;
 | 
			
		||||
}
 | 
			
		||||
@@ -1213,7 +1213,7 @@ parse_bot_command (const char *s, const char *command, const char **following)
 | 
			
		||||
	s += command_len;
 | 
			
		||||
 | 
			
		||||
	// Expect a word boundary, so that we don't respond to invalid things
 | 
			
		||||
	if (isalnum (*s))
 | 
			
		||||
	if (isalnum ((uint8_t) *s))
 | 
			
		||||
		return false;
 | 
			
		||||
 | 
			
		||||
	// Ignore any initial spaces; the rest is the command's argument
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										53
									
								
								xC.c
									
									
									
									
									
								
							
							
						
						
									
										53
									
								
								xC.c
									
									
									
									
									
								
							@@ -1,7 +1,7 @@
 | 
			
		||||
/*
 | 
			
		||||
 * xC.c: a terminal-based IRC client
 | 
			
		||||
 *
 | 
			
		||||
 * Copyright (c) 2015 - 2022, Přemysl Eric Janouch <p@janouch.name>
 | 
			
		||||
 * Copyright (c) 2015 - 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.
 | 
			
		||||
@@ -50,6 +50,7 @@ enum
 | 
			
		||||
 | 
			
		||||
// fmemopen
 | 
			
		||||
#define _POSIX_C_SOURCE 200809L
 | 
			
		||||
#define _XOPEN_SOURCE 700
 | 
			
		||||
 | 
			
		||||
#include "common.c"
 | 
			
		||||
#include "xD-replies.c"
 | 
			
		||||
@@ -615,7 +616,7 @@ input_rl_buffer_add_history (void *input, input_buffer_t input_buffer,
 | 
			
		||||
	// or temporarily switch histories.
 | 
			
		||||
	if (!buffer->history)
 | 
			
		||||
	{
 | 
			
		||||
		bool at_end = history_offset == history_length;
 | 
			
		||||
		bool at_end = where_history () == history_length;
 | 
			
		||||
		add_history (line);
 | 
			
		||||
		if (at_end)
 | 
			
		||||
			next_history ();
 | 
			
		||||
@@ -8316,6 +8317,8 @@ irc_try_parse_welcome_for_userhost (struct server *s, const char *m)
 | 
			
		||||
	strv_free (&v);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void process_input
 | 
			
		||||
	(struct app_context *, struct buffer *, const char *);
 | 
			
		||||
static bool process_input_line
 | 
			
		||||
	(struct app_context *, struct buffer *, const char *, int);
 | 
			
		||||
static void on_autoaway_timer (struct app_context *ctx);
 | 
			
		||||
@@ -8344,7 +8347,7 @@ irc_on_registered (struct server *s, const char *nickname)
 | 
			
		||||
	if (command)
 | 
			
		||||
	{
 | 
			
		||||
		log_server_debug (s, "Executing \"#s\"", command);
 | 
			
		||||
		(void) process_input_line (s->ctx, s->buffer, command, 0);
 | 
			
		||||
		(void) process_input (s->ctx, s->buffer, command);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	int64_t command_delay = get_config_integer (s->config, "command_delay");
 | 
			
		||||
@@ -9438,6 +9441,9 @@ server_add (struct app_context *ctx,
 | 
			
		||||
	str_map_set (&ctx->servers, s->name, s);
 | 
			
		||||
	s->config = subtree;
 | 
			
		||||
 | 
			
		||||
	relay_prepare_server_update (ctx, s);
 | 
			
		||||
	relay_broadcast (ctx);
 | 
			
		||||
 | 
			
		||||
	// Add a buffer and activate it
 | 
			
		||||
	struct buffer *buffer = s->buffer = buffer_new (ctx->input,
 | 
			
		||||
		BUFFER_SERVER, irc_make_buffer_name (s, NULL));
 | 
			
		||||
@@ -12936,6 +12942,16 @@ handle_command_kill (struct handler_args *a)
 | 
			
		||||
	return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static bool
 | 
			
		||||
handle_command_away (struct handler_args *a)
 | 
			
		||||
{
 | 
			
		||||
	if (*a->arguments)
 | 
			
		||||
		irc_send (a->s, "AWAY :%s", a->arguments);
 | 
			
		||||
	else
 | 
			
		||||
		irc_send (a->s, "AWAY");
 | 
			
		||||
	return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static bool
 | 
			
		||||
handle_command_nick (struct handler_args *a)
 | 
			
		||||
{
 | 
			
		||||
@@ -13001,7 +13017,6 @@ TRIVIAL_HANDLER (who,   "WHO")
 | 
			
		||||
TRIVIAL_HANDLER (motd,  "MOTD")
 | 
			
		||||
TRIVIAL_HANDLER (oper,  "OPER")
 | 
			
		||||
TRIVIAL_HANDLER (stats, "STATS")
 | 
			
		||||
TRIVIAL_HANDLER (away,  "AWAY")
 | 
			
		||||
 | 
			
		||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | 
			
		||||
 | 
			
		||||
@@ -15470,7 +15485,7 @@ client_message_buffer_name (const struct relay_command_message *m)
 | 
			
		||||
	switch (m->data.command)
 | 
			
		||||
	{
 | 
			
		||||
	case RELAY_COMMAND_BUFFER_COMPLETE:
 | 
			
		||||
		return m->data.buffer_input.buffer_name.str;
 | 
			
		||||
		return m->data.buffer_complete.buffer_name.str;
 | 
			
		||||
	case RELAY_COMMAND_BUFFER_ACTIVATE:
 | 
			
		||||
		return m->data.buffer_activate.buffer_name.str;
 | 
			
		||||
	case RELAY_COMMAND_BUFFER_INPUT:
 | 
			
		||||
@@ -15542,14 +15557,13 @@ static void
 | 
			
		||||
client_process_buffer_log
 | 
			
		||||
	(struct client *c, uint32_t seq, struct buffer *buffer)
 | 
			
		||||
{
 | 
			
		||||
	struct relay_event_data_response *e = relay_prepare_response (c->ctx, seq);
 | 
			
		||||
	e->data.command = RELAY_COMMAND_BUFFER_LOG;
 | 
			
		||||
 | 
			
		||||
	// XXX: We log failures to the global buffer,
 | 
			
		||||
	//   so the client just receives nothing if there is no log file.
 | 
			
		||||
	struct str log = str_make ();
 | 
			
		||||
	char *path = buffer_get_log_path (buffer);
 | 
			
		||||
	FILE *fp = open_log_path (c->ctx, buffer, path);
 | 
			
		||||
	if (fp)
 | 
			
		||||
	{
 | 
			
		||||
		struct str log = str_make ();
 | 
			
		||||
		char buf[BUFSIZ];
 | 
			
		||||
		size_t len;
 | 
			
		||||
		while ((len = fread (buf, 1, sizeof buf, fp)))
 | 
			
		||||
@@ -15557,17 +15571,15 @@ client_process_buffer_log
 | 
			
		||||
		if (ferror (fp))
 | 
			
		||||
			log_global_error (c->ctx, "Failed to read `#l': #l",
 | 
			
		||||
				path, strerror (errno));
 | 
			
		||||
 | 
			
		||||
		// On overflow, it will later fail serialization.
 | 
			
		||||
		e->data.buffer_log.log_len = MIN (UINT32_MAX, log.len);
 | 
			
		||||
		e->data.buffer_log.log = (uint8_t *) str_steal (&log);
 | 
			
		||||
		fclose (fp);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// XXX: We log failures to the global buffer,
 | 
			
		||||
	//   so the client just receives nothing if there is no log file.
 | 
			
		||||
 | 
			
		||||
	free (path);
 | 
			
		||||
 | 
			
		||||
	struct relay_event_data_response *e = relay_prepare_response (c->ctx, seq);
 | 
			
		||||
	e->data.command = RELAY_COMMAND_BUFFER_LOG;
 | 
			
		||||
	// On overflow, it will later fail serialization (frame will be too long).
 | 
			
		||||
	e->data.buffer_log.log_len = MIN (UINT32_MAX, log.len);
 | 
			
		||||
	e->data.buffer_log.log = (uint8_t *) str_steal (&log);
 | 
			
		||||
	relay_send (c);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -15855,6 +15867,7 @@ relay_start (struct app_context *ctx, char *address, struct error **e)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Just try the first one, disregarding IPv4/IPv6 ordering.
 | 
			
		||||
	// Use 0.0.0.0 or [::] to request either one specifically.
 | 
			
		||||
	int fd = relay_listen_with_context (ctx, result, e);
 | 
			
		||||
	freeaddrinfo (result);
 | 
			
		||||
	if (fd == -1)
 | 
			
		||||
@@ -16090,7 +16103,11 @@ main (int argc, char *argv[])
 | 
			
		||||
 | 
			
		||||
	// The following part is a bit brittle because of interdependencies
 | 
			
		||||
	init_colors (&ctx);
 | 
			
		||||
	if (format_mode)  format_input_and_die (&ctx);
 | 
			
		||||
	if (format_mode)
 | 
			
		||||
		format_input_and_die (&ctx);
 | 
			
		||||
	if (!cur_term)
 | 
			
		||||
		exit_fatal ("terminal initialization failed");
 | 
			
		||||
 | 
			
		||||
	init_global_buffer (&ctx);
 | 
			
		||||
	show_logo (&ctx);
 | 
			
		||||
	setup_signal_handlers ();
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										4
									
								
								xF.svg
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								xF.svg
									
									
									
									
									
								
							@@ -1,8 +1,6 @@
 | 
			
		||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
 | 
			
		||||
<svg version="1.1" width="48" height="48" viewBox="0 0 48 48"
 | 
			
		||||
   xmlns:xlink="http://www.w3.org/1999/xlink"
 | 
			
		||||
   xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
   xmlns:svg="http://www.w3.org/2000/svg">
 | 
			
		||||
   xmlns="http://www.w3.org/2000/svg">
 | 
			
		||||
 | 
			
		||||
  <defs>
 | 
			
		||||
    <linearGradient id="background" x1="0" y1="0" x2="1" y2="1">
 | 
			
		||||
 
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.2 KiB  | 
							
								
								
									
										1
									
								
								xK-version
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										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)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -197,6 +197,14 @@ function bufferResetStats(b) {
 | 
			
		||||
	b.highlighted = false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function bufferPopExcessLines(b) {
 | 
			
		||||
	// Let "new" messages be, if only because pulling the log file
 | 
			
		||||
	// is much more problematic in the web browser than in xC.
 | 
			
		||||
	// TODO: Make the limit configurable, or extract general.backlog_limit.
 | 
			
		||||
	const old = b.lines.length - b.newMessages - b.newUnimportantMessages
 | 
			
		||||
	b.lines.splice(0, old - 1000)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function bufferActivate(name) {
 | 
			
		||||
	rpc.send({command: 'BufferActivate', bufferName: name})
 | 
			
		||||
}
 | 
			
		||||
@@ -294,6 +302,13 @@ rpcEventHandlers.set(Relay.Event.BufferLine, e => {
 | 
			
		||||
			b.newMessages++
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// XXX: In its unkeyed diff algorithm, Mithril.js can only efficiently
 | 
			
		||||
	// deal with common prefixes, i.e., indefinitely growing buffers.
 | 
			
		||||
	// But we don't want to key all children of Buffer,
 | 
			
		||||
	// so only trim buffers while they are, or once they become invisible.
 | 
			
		||||
	if (e.bufferName != bufferCurrent)
 | 
			
		||||
		bufferPopExcessLines(b)
 | 
			
		||||
 | 
			
		||||
	if (e.leakToActive) {
 | 
			
		||||
		let bc = buffers.get(bufferCurrent)
 | 
			
		||||
		bc.lines.push({...line, leaked: true})
 | 
			
		||||
@@ -336,7 +351,7 @@ rpcEventHandlers.set(Relay.Event.BufferStats, e => {
 | 
			
		||||
	if (b === undefined)
 | 
			
		||||
		return
 | 
			
		||||
 | 
			
		||||
	b.newMessages = e.newMessages,
 | 
			
		||||
	b.newMessages = e.newMessages
 | 
			
		||||
	b.newUnimportantMessages = e.newUnimportantMessages
 | 
			
		||||
	b.highlighted = e.highlighted
 | 
			
		||||
})
 | 
			
		||||
@@ -344,6 +359,11 @@ rpcEventHandlers.set(Relay.Event.BufferStats, e => {
 | 
			
		||||
rpcEventHandlers.set(Relay.Event.BufferRename, e => {
 | 
			
		||||
	buffers.set(e.new, buffers.get(e.bufferName))
 | 
			
		||||
	buffers.delete(e.bufferName)
 | 
			
		||||
 | 
			
		||||
	if (e.bufferName === bufferCurrent)
 | 
			
		||||
		bufferCurrent = e.new
 | 
			
		||||
	if (e.bufferName === bufferLast)
 | 
			
		||||
		bufferLast = e.new
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
rpcEventHandlers.set(Relay.Event.BufferRemove, e => {
 | 
			
		||||
@@ -354,8 +374,16 @@ rpcEventHandlers.set(Relay.Event.BufferRemove, e => {
 | 
			
		||||
 | 
			
		||||
rpcEventHandlers.set(Relay.Event.BufferActivate, e => {
 | 
			
		||||
	let old = buffers.get(bufferCurrent)
 | 
			
		||||
	if (old !== undefined)
 | 
			
		||||
	if (old !== undefined) {
 | 
			
		||||
		bufferResetStats(old)
 | 
			
		||||
		bufferPopExcessLines(old)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Initial sync: trim all buffers to our limit, just for consistency.
 | 
			
		||||
	if (bufferCurrent === undefined) {
 | 
			
		||||
		for (let b of buffers.values())
 | 
			
		||||
			bufferPopExcessLines(b)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	bufferLast = bufferCurrent
 | 
			
		||||
	let b = buffers.get(e.bufferName)
 | 
			
		||||
@@ -501,7 +529,8 @@ let Content = {
 | 
			
		||||
		while ((match = re.exec(text)) !== null) {
 | 
			
		||||
			if (end < match.index)
 | 
			
		||||
				a.push(m('span', attrs, text.substring(end, match.index)))
 | 
			
		||||
			a.push(m('a[target=_blank]', {href: match[0], ...attrs}, match[0]))
 | 
			
		||||
			a.push(m('a[target=_blank][rel=noreferrer]',
 | 
			
		||||
				{href: match[0], ...attrs}, match[0]))
 | 
			
		||||
			end = re.lastIndex
 | 
			
		||||
		}
 | 
			
		||||
		if (end < text.length)
 | 
			
		||||
@@ -664,6 +693,12 @@ let Buffer = {
 | 
			
		||||
			const dom = event.target
 | 
			
		||||
			bufferAutoscroll =
 | 
			
		||||
				dom.scrollTop + dom.clientHeight + 1 >= dom.scrollHeight
 | 
			
		||||
 | 
			
		||||
			let b = buffers.get(bufferCurrent)
 | 
			
		||||
			if (b !== undefined && b.highlighted && !bufferAutoscroll) {
 | 
			
		||||
				b.highlighted = false
 | 
			
		||||
				m.redraw()
 | 
			
		||||
			}
 | 
			
		||||
		}}, lines)
 | 
			
		||||
	},
 | 
			
		||||
}
 | 
			
		||||
@@ -679,7 +714,8 @@ let Log = {
 | 
			
		||||
		while ((match = re.exec(text)) !== null) {
 | 
			
		||||
			if (end < match.index)
 | 
			
		||||
				a.push(text.substring(end, match.index))
 | 
			
		||||
			a.push(m('a[target=_blank]', {href: match[0]}, match[0]))
 | 
			
		||||
			a.push(m('a[target=_blank][rel=noreferrer]',
 | 
			
		||||
				{href: match[0]}, match[0]))
 | 
			
		||||
			end = re.lastIndex
 | 
			
		||||
		}
 | 
			
		||||
		if (end < text.length)
 | 
			
		||||
@@ -967,7 +1003,7 @@ let Input = {
 | 
			
		||||
		rpc.send({command: 'Active'})
 | 
			
		||||
 | 
			
		||||
		let b = buffers.get(bufferCurrent)
 | 
			
		||||
		if (b === undefined)
 | 
			
		||||
		if (b === undefined || event.isComposing)
 | 
			
		||||
			return
 | 
			
		||||
 | 
			
		||||
		let textarea = event.currentTarget
 | 
			
		||||
@@ -1013,10 +1049,23 @@ let Input = {
 | 
			
		||||
		} else if (!event.altKey && !event.ctrlKey && !event.metaKey &&
 | 
			
		||||
				!event.shiftKey) {
 | 
			
		||||
			handled = true
 | 
			
		||||
			switch (event.keyCode) {
 | 
			
		||||
			case 9:   success = Input.complete(b, textarea); break
 | 
			
		||||
			case 13:  success = Input.submit(b, textarea);   break
 | 
			
		||||
			default:  handled = false
 | 
			
		||||
			switch (event.key) {
 | 
			
		||||
			case 'PageUp':
 | 
			
		||||
				Array.from(document.getElementsByClassName('buffer'))
 | 
			
		||||
					.forEach(b => b.scrollBy(0, -b.clientHeight))
 | 
			
		||||
				break
 | 
			
		||||
			case 'PageDown':
 | 
			
		||||
				Array.from(document.getElementsByClassName('buffer'))
 | 
			
		||||
					.forEach(b => b.scrollBy(0, +b.clientHeight))
 | 
			
		||||
				break
 | 
			
		||||
			case 'Tab':
 | 
			
		||||
				success = Input.complete(b, textarea);
 | 
			
		||||
				break
 | 
			
		||||
			case 'Enter':
 | 
			
		||||
				success = Input.submit(b, textarea);
 | 
			
		||||
				break
 | 
			
		||||
			default:
 | 
			
		||||
				handled = false
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		if (!success)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								xS/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								xS/.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -1,2 +1,3 @@
 | 
			
		||||
/xS
 | 
			
		||||
/xS-replies.go
 | 
			
		||||
/xS.1
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										15
									
								
								xS/Makefile
									
									
									
									
									
								
							
							
						
						
									
										15
									
								
								xS/Makefile
									
									
									
									
									
								
							@@ -2,16 +2,17 @@
 | 
			
		||||
.SUFFIXES:
 | 
			
		||||
AWK = env LC_ALL=C awk
 | 
			
		||||
 | 
			
		||||
outputs = xS xS-version.go xS-replies.go
 | 
			
		||||
outputs = xS xS-replies.go xS.1
 | 
			
		||||
all: $(outputs)
 | 
			
		||||
 | 
			
		||||
xS: xS.go xS-version.go xS-replies.go
 | 
			
		||||
	go build -o $@
 | 
			
		||||
xS-version.go: ../liberty/tools/cmake-parser.awk \
 | 
			
		||||
	xS-gen-version.awk ../CMakeLists.txt
 | 
			
		||||
	$(AWK) -f ../liberty/tools/cmake-parser.awk \
 | 
			
		||||
		-f xS-gen-version.awk ../CMakeLists.txt > $@
 | 
			
		||||
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
 | 
			
		||||
}
 | 
			
		||||
@@ -1,14 +0,0 @@
 | 
			
		||||
# xS-gen-version.awk: extract version information from the CMake script
 | 
			
		||||
#
 | 
			
		||||
# Copyright (c) 2022, Přemysl Eric Janouch <p@janouch.name>
 | 
			
		||||
# SPDX-License-Identifier: 0BSD
 | 
			
		||||
 | 
			
		||||
Command == "project" {
 | 
			
		||||
	for (i = 2; i in Args; i++)
 | 
			
		||||
		if (Args[i] == "VERSION") {
 | 
			
		||||
			print "package main"
 | 
			
		||||
			print ""
 | 
			
		||||
			print "const projectVersion = `" Args[++i] "`"
 | 
			
		||||
			exit
 | 
			
		||||
		}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										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)
 | 
			
		||||
							
								
								
									
										128
									
								
								xS/xS.go
									
									
									
									
									
								
							
							
						
						
									
										128
									
								
								xS/xS.go
									
									
									
									
									
								
							@@ -42,6 +42,8 @@ import (
 | 
			
		||||
 | 
			
		||||
const projectName = "xS"
 | 
			
		||||
 | 
			
		||||
var projectVersion = "?"
 | 
			
		||||
 | 
			
		||||
var debugMode = false
 | 
			
		||||
 | 
			
		||||
// --- Logging -----------------------------------------------------------------
 | 
			
		||||
@@ -454,132 +456,6 @@ func (fd *floodDetector) check() bool {
 | 
			
		||||
	return count <= fd.limit
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// --- IRC protocol ------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
func ircToLower(c byte) byte {
 | 
			
		||||
	switch c {
 | 
			
		||||
	case '[':
 | 
			
		||||
		return '{'
 | 
			
		||||
	case ']':
 | 
			
		||||
		return '}'
 | 
			
		||||
	case '\\':
 | 
			
		||||
		return '|'
 | 
			
		||||
	case '~':
 | 
			
		||||
		return '^'
 | 
			
		||||
	}
 | 
			
		||||
	if c >= 'A' && c <= 'Z' {
 | 
			
		||||
		return c + ('a' - 'A')
 | 
			
		||||
	}
 | 
			
		||||
	return c
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ircToUpper(c byte) byte {
 | 
			
		||||
	switch c {
 | 
			
		||||
	case '{':
 | 
			
		||||
		return '['
 | 
			
		||||
	case '}':
 | 
			
		||||
		return ']'
 | 
			
		||||
	case '|':
 | 
			
		||||
		return '\\'
 | 
			
		||||
	case '^':
 | 
			
		||||
		return '~'
 | 
			
		||||
	}
 | 
			
		||||
	if c >= 'a' && c <= 'z' {
 | 
			
		||||
		return c - ('a' - 'A')
 | 
			
		||||
	}
 | 
			
		||||
	return c
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Convert identifier to a canonical form for case-insensitive comparisons.
 | 
			
		||||
// ircToUpper is used so that statically initialized maps can be in uppercase.
 | 
			
		||||
func ircToCanon(ident string) string {
 | 
			
		||||
	var canon []byte
 | 
			
		||||
	for _, c := range []byte(ident) {
 | 
			
		||||
		canon = append(canon, ircToUpper(c))
 | 
			
		||||
	}
 | 
			
		||||
	return string(canon)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ircEqual(s1, s2 string) bool {
 | 
			
		||||
	return ircToCanon(s1) == ircToCanon(s2)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ircFnmatch(pattern string, s string) bool {
 | 
			
		||||
	pattern, s = ircToCanon(pattern), ircToCanon(s)
 | 
			
		||||
	// FIXME: This should not support [] ranges and handle '/' specially.
 | 
			
		||||
	// We could translate the pattern to a regular expression.
 | 
			
		||||
	matched, _ := filepath.Match(pattern, s)
 | 
			
		||||
	return matched
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var reMsg = regexp.MustCompile(
 | 
			
		||||
	`^(?:@([^ ]*) +)?(?::([^! ]*)(?:!([^@]*)@([^ ]*))? +)?([^ ]+)(.*)?$`)
 | 
			
		||||
var reArgs = regexp.MustCompile(`:.*| [^: ][^ ]*`)
 | 
			
		||||
 | 
			
		||||
type message struct {
 | 
			
		||||
	tags    map[string]string // IRC 3.2 message tags
 | 
			
		||||
	nick    string            // optional nickname
 | 
			
		||||
	user    string            // optional username
 | 
			
		||||
	host    string            // optional hostname or IP address
 | 
			
		||||
	command string            // command name
 | 
			
		||||
	params  []string          // arguments
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ircUnescapeMessageTag(value string) string {
 | 
			
		||||
	var buf []byte
 | 
			
		||||
	escape := false
 | 
			
		||||
	for i := 0; i < len(value); i++ {
 | 
			
		||||
		if escape {
 | 
			
		||||
			switch value[i] {
 | 
			
		||||
			case ':':
 | 
			
		||||
				buf = append(buf, ';')
 | 
			
		||||
			case 's':
 | 
			
		||||
				buf = append(buf, ' ')
 | 
			
		||||
			case 'r':
 | 
			
		||||
				buf = append(buf, '\r')
 | 
			
		||||
			case 'n':
 | 
			
		||||
				buf = append(buf, '\n')
 | 
			
		||||
			default:
 | 
			
		||||
				buf = append(buf, value[i])
 | 
			
		||||
			}
 | 
			
		||||
			escape = false
 | 
			
		||||
		} else if value[i] == '\\' {
 | 
			
		||||
			escape = true
 | 
			
		||||
		} else {
 | 
			
		||||
			buf = append(buf, value[i])
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return string(buf)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ircParseMessageTags(tags string, out map[string]string) {
 | 
			
		||||
	for _, tag := range splitString(tags, ";", true /* ignoreEmpty */) {
 | 
			
		||||
		if equal := strings.IndexByte(tag, '='); equal < 0 {
 | 
			
		||||
			out[tag] = ""
 | 
			
		||||
		} else {
 | 
			
		||||
			out[tag[:equal]] = ircUnescapeMessageTag(tag[equal+1:])
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ircParseMessage(line string) *message {
 | 
			
		||||
	m := reMsg.FindStringSubmatch(line)
 | 
			
		||||
	if m == nil {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	msg := message{nil, m[2], m[3], m[4], m[5], nil}
 | 
			
		||||
	if m[1] != "" {
 | 
			
		||||
		msg.tags = make(map[string]string)
 | 
			
		||||
		ircParseMessageTags(m[1], msg.tags)
 | 
			
		||||
	}
 | 
			
		||||
	for _, x := range reArgs.FindAllString(m[6], -1) {
 | 
			
		||||
		msg.params = append(msg.params, x[1:])
 | 
			
		||||
	}
 | 
			
		||||
	return &msg
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// --- IRC token validation ----------------------------------------------------
 | 
			
		||||
 | 
			
		||||
// Everything as per RFC 2812
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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