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
|
# Ubuntu 18.04 LTS and OpenBSD 6.4
|
||||||
cmake_minimum_required (VERSION 3.10)
|
cmake_minimum_required (VERSION 3.10)
|
||||||
project (xK VERSION 1.5.0
|
|
||||||
|
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)
|
DESCRIPTION "IRC daemon, bot, TUI client and its web frontend" LANGUAGES C)
|
||||||
|
|
||||||
# Options
|
# 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")
|
set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wextra -Wno-unused-function")
|
||||||
endif ()
|
endif ()
|
||||||
|
|
||||||
# Version
|
|
||||||
set (project_version "${PROJECT_VERSION}")
|
|
||||||
|
|
||||||
# Try to append commit ID if it follows a version tag. It might be nicer if
|
# Try to append commit ID if it follows a version tag. It might be nicer if
|
||||||
# we could also detect dirty worktrees but that's very hard to get right.
|
# we could also detect dirty worktrees but that's very hard to get right.
|
||||||
# If we didn't need this for CPack, we could use add_custom_command to generate
|
# If we didn't need this for CPack, we could use add_custom_command to generate
|
||||||
|
|||||||
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
|
Permission to use, copy, modify, and/or distribute this software for any
|
||||||
purpose with or without fee is hereby granted.
|
purpose with or without fee is hereby granted.
|
||||||
|
|||||||
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
|
* xD: now using SHA-256 for client certificate fingerprints
|
||||||
|
|
||||||
@@ -16,6 +16,8 @@
|
|||||||
|
|
||||||
* xC: replaced behaviour.save_on_quit with general.autosave
|
* xC: replaced behaviour.save_on_quit with general.autosave
|
||||||
|
|
||||||
|
* xC: the server *.command configuration option now supports multiple lines
|
||||||
|
|
||||||
* xC: improved pager integration capabilities
|
* xC: improved pager integration capabilities
|
||||||
|
|
||||||
* xC: unsolicited JOINs will no longer automatically activate the buffer
|
* xC: unsolicited JOINs will no longer automatically activate the buffer
|
||||||
@@ -31,8 +33,14 @@
|
|||||||
|
|
||||||
* Added a web frontend for xC called xP
|
* 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 Go port of xD called xS
|
||||||
|
|
||||||
|
* Added a simple notifier called xN
|
||||||
|
|
||||||
|
|
||||||
1.5.0 (2021-12-21) "The Show Must Go On"
|
1.5.0 (2021-12-21) "The Show Must Go On"
|
||||||
|
|
||||||
|
|||||||
42
README.adoc
42
README.adoc
@@ -1,9 +1,10 @@
|
|||||||
xK
|
xK
|
||||||
==
|
==
|
||||||
|
|
||||||
'xK' (chat kit) is an IRC software suite consisting of a daemon, bot, terminal
|
'xK' (chat kit) is an IRC software suite consisting of a daemon, bot, notifier,
|
||||||
client, and a web frontend for the client. It's all you're ever going to
|
terminal client, and web/Windows/macOS frontends for the client. It's all
|
||||||
need for chatting, so long as you can make do with slightly minimalist software.
|
you're ever going to need for chatting, so long as you can make do with slightly
|
||||||
|
minimalist software.
|
||||||
|
|
||||||
They're all lean on dependencies, and offer a maximally permissive licence.
|
They're all lean on dependencies, and offer a maximally permissive licence.
|
||||||
|
|
||||||
@@ -55,6 +56,10 @@ https://github.com/kiwiirc/webircgateway[].
|
|||||||
Any further development, such as P10 or TS6 linking for IRC services,
|
Any further development, such as P10 or TS6 linking for IRC services,
|
||||||
or plugin support for arbitrary bridges, will happen here.
|
or plugin support for arbitrary bridges, will happen here.
|
||||||
|
|
||||||
|
xN
|
||||||
|
--
|
||||||
|
The IRC notifier, should you ever need to send automated messages from a script.
|
||||||
|
|
||||||
xB
|
xB
|
||||||
--
|
--
|
||||||
The IRC bot. While originally intended to be a simple rewrite of my old GNU AWK
|
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
|
Packages
|
||||||
--------
|
--------
|
||||||
Regular releases are sporadic. git master should be stable enough. You can get
|
Regular releases are sporadic. git master should be stable enough.
|
||||||
a package with the latest development version from Archlinux's AUR.
|
You can get a package with the latest development version using Arch Linux's
|
||||||
|
https://aur.archlinux.org/packages/xk-git[AUR],
|
||||||
|
or as a https://git.janouch.name/p/nixexprs[Nix derivation].
|
||||||
|
|
||||||
Building
|
Building
|
||||||
--------
|
--------
|
||||||
@@ -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
|
and some form of HTTP authentication. Pass the external URL of the WebSocket
|
||||||
endpoint as the third command line argument in this case.
|
endpoint as the third command line argument in this case.
|
||||||
|
|
||||||
|
xW
|
||||||
|
~~
|
||||||
|
The Win32 frontend is a separate CMake subproject that should be compiled
|
||||||
|
using MinGW-w64. To avoid having to specify the relay address each time you
|
||||||
|
run it, create a shortcut for the executable and include the address in its
|
||||||
|
_Target_ field:
|
||||||
|
|
||||||
|
C:\...\xW.exe 127.0.0.1 9000
|
||||||
|
|
||||||
|
It works reasonably well starting with Windows 7.
|
||||||
|
|
||||||
|
xM
|
||||||
|
~~
|
||||||
|
The Cocoa frontend is a separate CMake subproject that requires Xcode to build.
|
||||||
|
It is currently not that usable. The relay address can either be passed on
|
||||||
|
the command line, or preset in the _defaults_ database:
|
||||||
|
|
||||||
|
$ defaults write name.janouch.xM relayHost 127.0.0.1
|
||||||
|
$ defaults write name.janouch.xM relayPort 9000
|
||||||
|
|
||||||
Client Certificates
|
Client Certificates
|
||||||
-------------------
|
-------------------
|
||||||
'xC' will use the SASL EXTERNAL method to authenticate using the TLS client
|
'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
|
certificate specified by the respective server's `tls_cert` option if you add
|
||||||
`sasl` to the `capabilities` option and the server supports this.
|
`sasl` to the `capabilities` option and the server supports this.
|
||||||
|
|
||||||
'xD' uses SHA-256 fingerprints of TLS client certificates to authenticate users.
|
'xD' and 'xS' use SHA-256 fingerprints of TLS client certificates
|
||||||
To get the fingerprint from a certificate file in the required form, use:
|
to authenticate users. To get the fingerprint from a certificate file
|
||||||
|
in the required form, use:
|
||||||
|
|
||||||
$ openssl x509 -in public.pem -outform DER | sha256sum
|
$ 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
|
#!/usr/bin/expect -f
|
||||||
# Very basic end-to-end testing for CI
|
# Very basic end-to-end testing for CI
|
||||||
|
set tempdir [exec mktemp -d]
|
||||||
|
set ::env(XDG_CONFIG_HOME) $tempdir
|
||||||
|
|
||||||
# Run the daemon to test against
|
# Run the daemon to test against
|
||||||
system ./xD --write-default-cfg
|
system ./xD --write-default-cfg
|
||||||
@@ -27,7 +29,7 @@ expect "Option changed"
|
|||||||
send "/disconnect\n"
|
send "/disconnect\n"
|
||||||
expect "]"
|
expect "]"
|
||||||
send "/connect\n"
|
send "/connect\n"
|
||||||
expect "Connection established"
|
expect "Welcome to"
|
||||||
|
|
||||||
# Try some chatting
|
# Try some chatting
|
||||||
send "/join #test\n"
|
send "/join #test\n"
|
||||||
|
|||||||
12
test-static
12
test-static
@@ -1,8 +1,16 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# We don't use printf's percent notation with our custom logging mechanism,
|
# We don't use printf's percent notation with our custom logging mechanism,
|
||||||
# so the compiler cannot check it for us like it usually does
|
# so the compiler cannot check it for us like it usually does.
|
||||||
|
#
|
||||||
|
# In clang-query terms, the string we're interested in can be found through:
|
||||||
|
# set traversal IgnoreUnlessSpelledInSource
|
||||||
|
# set output dump
|
||||||
|
# match callExpr(callee(functionDecl(
|
||||||
|
# hasName("log_full"))),
|
||||||
|
# hasArgument(5, stringLiteral().bind("format")))
|
||||||
|
# However, the tool is too restricted to be useful in a shell script.
|
||||||
perl -n0777 - "$(dirname "$0")"/xC.c <<-'END'
|
perl -n0777 - "$(dirname "$0")"/xC.c <<-'END'
|
||||||
while (/\blog_[^ ]+\s*\([^"()]*"[^"]*%[^%][^"]*"/gm) {
|
while (/\blog_[^ ]+\s*\([^"()]*"[^"]*%\w[^"]*"/gm) {
|
||||||
my ($p, $m) = ($`, $&);
|
my ($p, $m) = ($`, $&);
|
||||||
printf "$ARGV:%d: suspicious log format string: %s...\n",
|
printf "$ARGV:%d: suspicious log format string: %s...\n",
|
||||||
(1 + $p =~ tr/\n//), ($m =~ s/\s+/ /rg);
|
(1 + $p =~ tr/\n//), ($m =~ s/\s+/ /rg);
|
||||||
|
|||||||
4
xB.c
4
xB.c
@@ -1009,7 +1009,7 @@ is_valid_plugin_name (const char *name)
|
|||||||
if (!*name)
|
if (!*name)
|
||||||
return false;
|
return false;
|
||||||
for (const char *p = name; *p; p++)
|
for (const char *p = name; *p; p++)
|
||||||
if (!isgraph (*p) || *p == '/')
|
if (!isgraph ((uint8_t) *p) || *p == '/')
|
||||||
return false;
|
return false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1213,7 +1213,7 @@ parse_bot_command (const char *s, const char *command, const char **following)
|
|||||||
s += command_len;
|
s += command_len;
|
||||||
|
|
||||||
// Expect a word boundary, so that we don't respond to invalid things
|
// Expect a word boundary, so that we don't respond to invalid things
|
||||||
if (isalnum (*s))
|
if (isalnum ((uint8_t) *s))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
// Ignore any initial spaces; the rest is the command's argument
|
// Ignore any initial spaces; the rest is the command's argument
|
||||||
|
|||||||
53
xC.c
53
xC.c
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* xC.c: a terminal-based IRC client
|
* 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
|
* Permission to use, copy, modify, and/or distribute this software for any
|
||||||
* purpose with or without fee is hereby granted.
|
* purpose with or without fee is hereby granted.
|
||||||
@@ -50,6 +50,7 @@ enum
|
|||||||
|
|
||||||
// fmemopen
|
// fmemopen
|
||||||
#define _POSIX_C_SOURCE 200809L
|
#define _POSIX_C_SOURCE 200809L
|
||||||
|
#define _XOPEN_SOURCE 700
|
||||||
|
|
||||||
#include "common.c"
|
#include "common.c"
|
||||||
#include "xD-replies.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.
|
// or temporarily switch histories.
|
||||||
if (!buffer->history)
|
if (!buffer->history)
|
||||||
{
|
{
|
||||||
bool at_end = history_offset == history_length;
|
bool at_end = where_history () == history_length;
|
||||||
add_history (line);
|
add_history (line);
|
||||||
if (at_end)
|
if (at_end)
|
||||||
next_history ();
|
next_history ();
|
||||||
@@ -8316,6 +8317,8 @@ irc_try_parse_welcome_for_userhost (struct server *s, const char *m)
|
|||||||
strv_free (&v);
|
strv_free (&v);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void process_input
|
||||||
|
(struct app_context *, struct buffer *, const char *);
|
||||||
static bool process_input_line
|
static bool process_input_line
|
||||||
(struct app_context *, struct buffer *, const char *, int);
|
(struct app_context *, struct buffer *, const char *, int);
|
||||||
static void on_autoaway_timer (struct app_context *ctx);
|
static void on_autoaway_timer (struct app_context *ctx);
|
||||||
@@ -8344,7 +8347,7 @@ irc_on_registered (struct server *s, const char *nickname)
|
|||||||
if (command)
|
if (command)
|
||||||
{
|
{
|
||||||
log_server_debug (s, "Executing \"#s\"", 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");
|
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);
|
str_map_set (&ctx->servers, s->name, s);
|
||||||
s->config = subtree;
|
s->config = subtree;
|
||||||
|
|
||||||
|
relay_prepare_server_update (ctx, s);
|
||||||
|
relay_broadcast (ctx);
|
||||||
|
|
||||||
// Add a buffer and activate it
|
// Add a buffer and activate it
|
||||||
struct buffer *buffer = s->buffer = buffer_new (ctx->input,
|
struct buffer *buffer = s->buffer = buffer_new (ctx->input,
|
||||||
BUFFER_SERVER, irc_make_buffer_name (s, NULL));
|
BUFFER_SERVER, irc_make_buffer_name (s, NULL));
|
||||||
@@ -12936,6 +12942,16 @@ handle_command_kill (struct handler_args *a)
|
|||||||
return true;
|
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
|
static bool
|
||||||
handle_command_nick (struct handler_args *a)
|
handle_command_nick (struct handler_args *a)
|
||||||
{
|
{
|
||||||
@@ -13001,7 +13017,6 @@ TRIVIAL_HANDLER (who, "WHO")
|
|||||||
TRIVIAL_HANDLER (motd, "MOTD")
|
TRIVIAL_HANDLER (motd, "MOTD")
|
||||||
TRIVIAL_HANDLER (oper, "OPER")
|
TRIVIAL_HANDLER (oper, "OPER")
|
||||||
TRIVIAL_HANDLER (stats, "STATS")
|
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)
|
switch (m->data.command)
|
||||||
{
|
{
|
||||||
case RELAY_COMMAND_BUFFER_COMPLETE:
|
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:
|
case RELAY_COMMAND_BUFFER_ACTIVATE:
|
||||||
return m->data.buffer_activate.buffer_name.str;
|
return m->data.buffer_activate.buffer_name.str;
|
||||||
case RELAY_COMMAND_BUFFER_INPUT:
|
case RELAY_COMMAND_BUFFER_INPUT:
|
||||||
@@ -15542,14 +15557,13 @@ static void
|
|||||||
client_process_buffer_log
|
client_process_buffer_log
|
||||||
(struct client *c, uint32_t seq, struct buffer *buffer)
|
(struct client *c, uint32_t seq, struct buffer *buffer)
|
||||||
{
|
{
|
||||||
struct relay_event_data_response *e = relay_prepare_response (c->ctx, seq);
|
// XXX: We log failures to the global buffer,
|
||||||
e->data.command = RELAY_COMMAND_BUFFER_LOG;
|
// so the client just receives nothing if there is no log file.
|
||||||
|
struct str log = str_make ();
|
||||||
char *path = buffer_get_log_path (buffer);
|
char *path = buffer_get_log_path (buffer);
|
||||||
FILE *fp = open_log_path (c->ctx, buffer, path);
|
FILE *fp = open_log_path (c->ctx, buffer, path);
|
||||||
if (fp)
|
if (fp)
|
||||||
{
|
{
|
||||||
struct str log = str_make ();
|
|
||||||
char buf[BUFSIZ];
|
char buf[BUFSIZ];
|
||||||
size_t len;
|
size_t len;
|
||||||
while ((len = fread (buf, 1, sizeof buf, fp)))
|
while ((len = fread (buf, 1, sizeof buf, fp)))
|
||||||
@@ -15557,17 +15571,15 @@ client_process_buffer_log
|
|||||||
if (ferror (fp))
|
if (ferror (fp))
|
||||||
log_global_error (c->ctx, "Failed to read `#l': #l",
|
log_global_error (c->ctx, "Failed to read `#l': #l",
|
||||||
path, strerror (errno));
|
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);
|
fclose (fp);
|
||||||
}
|
}
|
||||||
|
|
||||||
// XXX: We log failures to the global buffer,
|
|
||||||
// so the client just receives nothing if there is no log file.
|
|
||||||
|
|
||||||
free (path);
|
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);
|
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.
|
// 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);
|
int fd = relay_listen_with_context (ctx, result, e);
|
||||||
freeaddrinfo (result);
|
freeaddrinfo (result);
|
||||||
if (fd == -1)
|
if (fd == -1)
|
||||||
@@ -16090,7 +16103,11 @@ main (int argc, char *argv[])
|
|||||||
|
|
||||||
// The following part is a bit brittle because of interdependencies
|
// The following part is a bit brittle because of interdependencies
|
||||||
init_colors (&ctx);
|
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);
|
init_global_buffer (&ctx);
|
||||||
show_logo (&ctx);
|
show_logo (&ctx);
|
||||||
setup_signal_handlers ();
|
setup_signal_handlers ();
|
||||||
|
|||||||
4
xF.svg
4
xF.svg
@@ -1,8 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
<svg version="1.1" width="48" height="48" viewBox="0 0 48 48"
|
<svg version="1.1" width="48" height="48" viewBox="0 0 48 48"
|
||||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg">
|
|
||||||
|
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient id="background" x1="0" y1="0" x2="1" y2="1">
|
<linearGradient id="background" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.2 KiB |
1
xK-version
Normal file
1
xK-version
Normal file
@@ -0,0 +1 @@
|
|||||||
|
2.0.0
|
||||||
45
xM/CMakeLists.txt
Normal file
45
xM/CMakeLists.txt
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# Swift language support
|
||||||
|
cmake_minimum_required (VERSION 3.15)
|
||||||
|
|
||||||
|
file (READ ../xK-version project_version)
|
||||||
|
configure_file (../xK-version xK-version.tag COPYONLY)
|
||||||
|
string (STRIP "${project_version}" project_version)
|
||||||
|
|
||||||
|
# There were two issues when building this from the main CMakeLists.txt:
|
||||||
|
# a) renaming main.swift to xM.swift requires removing top-level statements,
|
||||||
|
# b) there is a "redefinition of module 'FFI'" error.
|
||||||
|
project (xM VERSION "${project_version}"
|
||||||
|
DESCRIPTION "Cocoa frontend for xC" LANGUAGES Swift)
|
||||||
|
|
||||||
|
set (root "${PROJECT_SOURCE_DIR}/..")
|
||||||
|
add_custom_command (OUTPUT xC-proto.swift
|
||||||
|
COMMAND env LC_ALL=C awk
|
||||||
|
-f ${root}/liberty/tools/lxdrgen.awk
|
||||||
|
-f ${root}/liberty/tools/lxdrgen-swift.awk
|
||||||
|
-v PrefixCamel=Relay
|
||||||
|
${root}/xC.lxdr > xC-proto.swift
|
||||||
|
DEPENDS
|
||||||
|
${root}/liberty/tools/lxdrgen.awk
|
||||||
|
${root}/liberty/tools/lxdrgen-swift.awk
|
||||||
|
${root}/xC.lxdr
|
||||||
|
COMMENT "Generating xC relay protocol code" VERBATIM)
|
||||||
|
|
||||||
|
set (MACOSX_BUNDLE_GUI_IDENTIFIER name.janouch.${PROJECT_NAME})
|
||||||
|
set (MACOSX_BUNDLE_ICON_FILE xM.icns)
|
||||||
|
|
||||||
|
# Avoid including binary files in the repository by generating icons in code.
|
||||||
|
# sips(1) + Javascript + iconutil(1) could probably also be used.
|
||||||
|
find_program (SWIFT_EXECUTABLE swift REQUIRED)
|
||||||
|
|
||||||
|
set (icon "${PROJECT_BINARY_DIR}/${MACOSX_BUNDLE_ICON_FILE}")
|
||||||
|
add_custom_command (OUTPUT "${icon}"
|
||||||
|
COMMAND ${SWIFT_EXECUTABLE} "${PROJECT_SOURCE_DIR}/gen-icon.swift" "${icon}"
|
||||||
|
DEPENDS gen-icon.swift
|
||||||
|
COMMENT "Generating xM application icon" VERBATIM)
|
||||||
|
set_source_files_properties ("${icon}" PROPERTIES
|
||||||
|
MACOSX_PACKAGE_LOCATION Resources)
|
||||||
|
|
||||||
|
# Other requirements: macOS 10.14 for Network, and macOS 11 for Logger.
|
||||||
|
set (CMAKE_Swift_LANGUAGE_VERSION 5)
|
||||||
|
add_executable (xM MACOSX_BUNDLE
|
||||||
|
main.swift "${icon}" "${PROJECT_BINARY_DIR}/xC-proto.swift")
|
||||||
96
xM/gen-icon.swift
Normal file
96
xM/gen-icon.swift
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
// gen-icon.swift: generate a program icon for xM in the Apple icon format
|
||||||
|
//
|
||||||
|
// Copyright (c) 2023, Přemysl Eric Janouch <p@janouch.name>
|
||||||
|
// SPDX-License-Identifier: 0BSD
|
||||||
|
//
|
||||||
|
// NSGraphicsContext mostly just weirdly wraps over Quartz,
|
||||||
|
// so we do it all in Quartz directly.
|
||||||
|
import CoreGraphics
|
||||||
|
import Foundation
|
||||||
|
import ImageIO
|
||||||
|
import UniformTypeIdentifiers
|
||||||
|
|
||||||
|
// Apple uses something that's close to a "quintic superellipse" in their icons,
|
||||||
|
// but doesn't quite match. Either way, it looks better than rounded rectangles.
|
||||||
|
func addSquircle(context: CGContext, bounds: CGRect) {
|
||||||
|
context.move(to: CGPoint(x: bounds.maxX, y: bounds.midY))
|
||||||
|
for theta in stride(from: 0.0, to: .pi * 2, by: .pi / 1e4) {
|
||||||
|
let x = pow(abs(cos(theta)), 2 / 5.0) * bounds.width / 2
|
||||||
|
* CGFloat(signOf: cos(theta), magnitudeOf: 1) + bounds.midX
|
||||||
|
let y = pow(abs(sin(theta)), 2 / 5.0) * bounds.height / 2
|
||||||
|
* CGFloat(signOf: sin(theta), magnitudeOf: 1) + bounds.midY
|
||||||
|
context.addLine(to: CGPoint(x: x, y: y))
|
||||||
|
}
|
||||||
|
context.closePath()
|
||||||
|
}
|
||||||
|
|
||||||
|
func drawIcon(scale: CGFloat) -> CGImage? {
|
||||||
|
let size = CGSizeMake(1024, 1024)
|
||||||
|
|
||||||
|
let colorspace = CGColorSpaceCreateDeviceRGB()
|
||||||
|
let context = CGContext(data: nil,
|
||||||
|
width: Int(size.width * scale), height: Int(size.height * scale),
|
||||||
|
bitsPerComponent: 8, bytesPerRow: 0, space: colorspace,
|
||||||
|
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)!
|
||||||
|
context.scaleBy(x: scale, y: scale)
|
||||||
|
|
||||||
|
let bounds = CGRectMake(100, 100, size.width - 200, size.height - 200)
|
||||||
|
addSquircle(context: context, bounds: bounds)
|
||||||
|
let squircle = context.path!
|
||||||
|
|
||||||
|
// Gradients don't draw shadows, so draw it separately.
|
||||||
|
context.saveGState()
|
||||||
|
context.setShadow(offset: CGSizeMake(0, -12).applying(context.ctm),
|
||||||
|
blur: 28 * scale, color: CGColor(gray: 0, alpha: 0.5))
|
||||||
|
context.setFillColor(CGColor(red: 1, green: 0x55p-8, blue: 0, alpha: 1))
|
||||||
|
context.fillPath()
|
||||||
|
context.restoreGState()
|
||||||
|
|
||||||
|
context.saveGState()
|
||||||
|
context.addPath(squircle)
|
||||||
|
context.clip()
|
||||||
|
context.drawLinearGradient(
|
||||||
|
CGGradient(colorsSpace: colorspace, colors: [
|
||||||
|
CGColor(red: 1, green: 0x00p-8, blue: 0, alpha: 1),
|
||||||
|
CGColor(red: 1, green: 0xaap-8, blue: 0, alpha: 1)
|
||||||
|
] as CFArray, locations: [0, 1])!,
|
||||||
|
start: CGPointMake(0, 100), end: CGPointMake(0, size.height - 100),
|
||||||
|
options: CGGradientDrawingOptions(rawValue: 0))
|
||||||
|
context.restoreGState()
|
||||||
|
|
||||||
|
context.move(to: CGPoint(x: size.width * 0.30, y: size.height * 0.30))
|
||||||
|
context.addLine(to: CGPoint(x: size.width * 0.30, y: size.height * 0.70))
|
||||||
|
context.addLine(to: CGPoint(x: size.width * 0.575, y: size.height * 0.425))
|
||||||
|
context.move(to: CGPoint(x: size.width * 0.70, y: size.height * 0.30))
|
||||||
|
context.addLine(to: CGPoint(x: size.width * 0.70, y: size.height * 0.70))
|
||||||
|
context.addLine(to: CGPoint(x: size.width * 0.425, y: size.height * 0.425))
|
||||||
|
context.setLineWidth(80)
|
||||||
|
context.setLineCap(.round)
|
||||||
|
context.setLineJoin(.round)
|
||||||
|
context.setStrokeColor(CGColor.white)
|
||||||
|
context.strokePath()
|
||||||
|
return context.makeImage()
|
||||||
|
}
|
||||||
|
|
||||||
|
if CommandLine.arguments.count != 2 {
|
||||||
|
print("Usage: \(CommandLine.arguments.first!) OUTPUT.icns")
|
||||||
|
exit(EXIT_FAILURE)
|
||||||
|
}
|
||||||
|
|
||||||
|
let filename = CommandLine.arguments[1]
|
||||||
|
|
||||||
|
let macOSSizes: Array<CGFloat> = [16, 32, 128, 256, 512]
|
||||||
|
let icns = CGImageDestinationCreateWithURL(
|
||||||
|
URL(fileURLWithPath: filename) as CFURL,
|
||||||
|
UTType.icns.identifier as CFString, macOSSizes.count * 2, nil)!
|
||||||
|
for size in macOSSizes {
|
||||||
|
CGImageDestinationAddImage(icns, drawIcon(scale: size / 1024.0)!, nil)
|
||||||
|
CGImageDestinationAddImage(icns, drawIcon(scale: size / 1024.0 * 2)!, [
|
||||||
|
kCGImagePropertyDPIWidth: 144,
|
||||||
|
kCGImagePropertyDPIHeight: 144,
|
||||||
|
] as CFDictionary)
|
||||||
|
}
|
||||||
|
if !CGImageDestinationFinalize(icns) {
|
||||||
|
print("ICNS finalization failed.")
|
||||||
|
exit(EXIT_FAILURE)
|
||||||
|
}
|
||||||
1372
xM/main.swift
Normal file
1372
xM/main.swift
Normal file
File diff suppressed because it is too large
Load Diff
3
xN/.gitignore
vendored
Normal file
3
xN/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
/xN
|
||||||
|
/xN.1
|
||||||
|
/irc.go
|
||||||
21
xN/Makefile
Normal file
21
xN/Makefile
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
.POSIX:
|
||||||
|
.SUFFIXES:
|
||||||
|
AWK = env LC_ALL=C awk
|
||||||
|
|
||||||
|
outputs = irc.go xN xN.1
|
||||||
|
all: $(outputs)
|
||||||
|
|
||||||
|
# If we want to keep module dependencies separate, we don't have many options.
|
||||||
|
# Symlinking seems to work good enough.
|
||||||
|
irc.go: ../xS/irc.go
|
||||||
|
ln -sf ../xS/irc.go $@
|
||||||
|
|
||||||
|
xN: xN.go ../xK-version irc.go
|
||||||
|
go build -ldflags "-X 'main.projectVersion=$$(cat ../xK-version)'" -o $@
|
||||||
|
xN.1: ../xK-version ../liberty/tools/asciiman.awk xN.adoc
|
||||||
|
env "asciidoc-release-version=$$(cat ../xK-version)" \
|
||||||
|
$(AWK) -f ../liberty/tools/asciiman.awk xN.adoc > $@
|
||||||
|
test: all
|
||||||
|
go test
|
||||||
|
clean:
|
||||||
|
rm -f $(outputs)
|
||||||
86
xN/xN.adoc
Normal file
86
xN/xN.adoc
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
xN(1)
|
||||||
|
=====
|
||||||
|
:doctype: manpage
|
||||||
|
:manmanual: xK Manual
|
||||||
|
:mansource: xK {release-version}
|
||||||
|
|
||||||
|
Name
|
||||||
|
----
|
||||||
|
xN - IRC notifier
|
||||||
|
|
||||||
|
Synopsis
|
||||||
|
--------
|
||||||
|
*xN* [_OPTION_]... IRC-URL...
|
||||||
|
|
||||||
|
Description
|
||||||
|
-----------
|
||||||
|
*xN* is a simple IRC notifier, sending the text it receives on its standard
|
||||||
|
input to all IRC targets specified by its command line arguments.
|
||||||
|
|
||||||
|
The input text is forced to validate as UTF-8, and it is _not_ split
|
||||||
|
automatically to comply with the maximum IRC message length.
|
||||||
|
Thus, make sure to make the lines short, or they will be trimmed by the servers.
|
||||||
|
|
||||||
|
*xN* does not attempt to appease flood detectors.
|
||||||
|
|
||||||
|
Options
|
||||||
|
-------
|
||||||
|
*-debug*::
|
||||||
|
Print incoming IRC traffic, which may help in debugging any issues.
|
||||||
|
|
||||||
|
*-version*::
|
||||||
|
Output version information and exit.
|
||||||
|
|
||||||
|
URL format
|
||||||
|
----------
|
||||||
|
*xN* accepts URLs describing IRC users and channels, roughly as specified by
|
||||||
|
the Internet Draft _draft-butcher-irc-url-04.txt_. Note, however, that close
|
||||||
|
to no validation is done on these, and you should not pass URLs from untrusted
|
||||||
|
sources, so as to avoid command or parameter injection.
|
||||||
|
|
||||||
|
Any provided username will be propagated to the nickname, username,
|
||||||
|
and realname. The default value for these is the name of your system user.
|
||||||
|
|
||||||
|
As an extension, you may use the following options:
|
||||||
|
|
||||||
|
*skipjoin*::
|
||||||
|
Do not JOIN channels before sending messages to them.
|
||||||
|
This requires channels to allow external messages
|
||||||
|
(which are disabled by channel mode *+n*).
|
||||||
|
*usenotice*::
|
||||||
|
Send a NOTICE rather than a PRIVMSG, which distinguishes automated messages,
|
||||||
|
and is more appropriate for bots.
|
||||||
|
|
||||||
|
Examples
|
||||||
|
--------
|
||||||
|
$ uptime | xN 'irc://uptime@localhost/%23watch?skipjoin&usenotice'
|
||||||
|
|
||||||
|
Send *uptime*(1) information as an external notice to channel *#watch*
|
||||||
|
on the local server, using the standard port *6667*.
|
||||||
|
|
||||||
|
$ fortune -s | xN ircs://ohayou@irc.libera.chat/john,isuser
|
||||||
|
|
||||||
|
Greet user *john* with a fortune for this day. In compliance with _RFC 7194_,
|
||||||
|
the default TLS port is assumed to be *6697*.
|
||||||
|
|
||||||
|
$ xN 'ircs://agent:Password123@irc.cia.gov:1337/#hq?key=123456' <<EOF
|
||||||
|
The red fox trots quietly at midnight.
|
||||||
|
EOF
|
||||||
|
|
||||||
|
Connect over TLS to *irc.cia.gov* on port *1337*, use *Password123*
|
||||||
|
as the server password, register as user *agent*,
|
||||||
|
join channel *#hq* using the channel key *123456*,
|
||||||
|
and send a very secret message.
|
||||||
|
|
||||||
|
Reporting bugs
|
||||||
|
--------------
|
||||||
|
Use https://git.janouch.name/p/xK to report bugs, request features,
|
||||||
|
or submit pull requests.
|
||||||
|
|
||||||
|
See also
|
||||||
|
--------
|
||||||
|
_Uniform Resource Locator Schemes for Internet Relay Chat Entities_,
|
||||||
|
https://datatracker.ietf.org/doc/html/draft-butcher-irc-url-04[].
|
||||||
|
|
||||||
|
_Default Port for Internet Relay Chat (IRC) via TLS/SSL_,
|
||||||
|
https://datatracker.ietf.org/doc/html/rfc7194[].
|
||||||
279
xN/xN.go
Normal file
279
xN/xN.go
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
//
|
||||||
|
// Copyright (c) 2024, Přemysl Eric Janouch <p@janouch.name>
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
|
||||||
|
// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
|
||||||
|
// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
|
||||||
|
// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
//
|
||||||
|
|
||||||
|
// xN is a simple IRC notifier.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"os/user"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const projectName = "xN"
|
||||||
|
|
||||||
|
var projectVersion = "?"
|
||||||
|
|
||||||
|
var debugMode = false
|
||||||
|
|
||||||
|
type parameters struct {
|
||||||
|
conn net.Conn // underlying network connection
|
||||||
|
username string // nickname + username + realname
|
||||||
|
password string // server password
|
||||||
|
target string // where to send text
|
||||||
|
isuser bool // the target is a user rather than a channel
|
||||||
|
chankey string // channel key
|
||||||
|
skipjoin bool // whether to send external messages to channels
|
||||||
|
usenotice bool // whether to use NOTICE rather than PRIVMSG
|
||||||
|
message []string // lines of the message to send
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *parameters) send(command string, args ...string) error {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
buf.WriteString(command)
|
||||||
|
for i, arg := range args {
|
||||||
|
buf.WriteRune(' ')
|
||||||
|
if i+1 == len(args) {
|
||||||
|
buf.WriteRune(':')
|
||||||
|
}
|
||||||
|
buf.WriteString(arg)
|
||||||
|
}
|
||||||
|
buf.WriteString("\r\n")
|
||||||
|
_, err := p.conn.Write(buf.Bytes())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
rplWELCOME = "001"
|
||||||
|
errNICKNAMEINUSE = "433"
|
||||||
|
)
|
||||||
|
|
||||||
|
func notify(p *parameters) error {
|
||||||
|
// The intro should comfortably fit in the TCP send buffer whole.
|
||||||
|
if p.password != "" {
|
||||||
|
p.send("PASS", p.password)
|
||||||
|
}
|
||||||
|
p.send("USER", p.username, "0", "*", p.username)
|
||||||
|
p.send("NICK", p.username)
|
||||||
|
|
||||||
|
scanner, nickCounter, issue := bufio.NewScanner(p.conn), 1, ""
|
||||||
|
for scanner.Scan() {
|
||||||
|
if debugMode {
|
||||||
|
log.Println(scanner.Text())
|
||||||
|
}
|
||||||
|
|
||||||
|
m := ircParseMessage(scanner.Text())
|
||||||
|
switch m.command {
|
||||||
|
case "PING":
|
||||||
|
p.send("PONG", m.params...)
|
||||||
|
case rplWELCOME:
|
||||||
|
if !p.isuser && !p.skipjoin {
|
||||||
|
if p.chankey != "" {
|
||||||
|
p.send("JOIN", p.target, p.chankey)
|
||||||
|
} else {
|
||||||
|
p.send("JOIN", p.target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, line := range p.message {
|
||||||
|
if p.usenotice {
|
||||||
|
p.send("NOTICE", p.target, line)
|
||||||
|
} else {
|
||||||
|
p.send("PRIVMSG", p.target, line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.send("QUIT")
|
||||||
|
case errNICKNAMEINUSE:
|
||||||
|
p.send("NICK", fmt.Sprintf("%s%d", p.username, nickCounter))
|
||||||
|
nickCounter++
|
||||||
|
default:
|
||||||
|
// Prevent hanging on unsuccessful registrations.
|
||||||
|
numeric, _ := strconv.Atoi(m.command)
|
||||||
|
if numeric >= 400 && numeric <= 599 {
|
||||||
|
if len(m.params) > 1 {
|
||||||
|
issue = strings.Join(m.params[1:], " ")
|
||||||
|
} else {
|
||||||
|
issue = strings.Join(m.params, " ")
|
||||||
|
}
|
||||||
|
p.send("QUIT")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if issue != "" {
|
||||||
|
return errors.New(issue)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parse(rawURL string, text []byte) (
|
||||||
|
p parameters, connect func() (net.Conn, error), err error) {
|
||||||
|
u, err := url.Parse(rawURL)
|
||||||
|
if err != nil {
|
||||||
|
return p, nil, err
|
||||||
|
} else if !u.IsAbs() || u.Opaque != "" {
|
||||||
|
return p, nil, errors.New("need an absolute URL")
|
||||||
|
} else if u.Path == "/" && u.Fragment != "" {
|
||||||
|
// Try to handle the common but degenerate case.
|
||||||
|
fragment := "%23" + u.Fragment
|
||||||
|
u.Fragment, u.RawFragment = "", ""
|
||||||
|
if u, err = url.Parse(u.String() + fragment); err != nil {
|
||||||
|
return p, nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Figure out registration details.
|
||||||
|
p.username = projectName
|
||||||
|
if u, _ := user.Current(); u != nil {
|
||||||
|
p.username = u.Username
|
||||||
|
}
|
||||||
|
if u.User.Username() != "" {
|
||||||
|
p.username = u.User.Username()
|
||||||
|
}
|
||||||
|
|
||||||
|
p.password, _ = u.User.Password()
|
||||||
|
|
||||||
|
// Figure out the target, which for our intents must accept messages.
|
||||||
|
path, _ := strings.CutPrefix(u.Path, "/")
|
||||||
|
elements := strings.Split(path, ",")
|
||||||
|
if path == "" || elements[0] == "" {
|
||||||
|
return p, nil, errors.New("unspecified entity")
|
||||||
|
}
|
||||||
|
|
||||||
|
// The last entity type wins.
|
||||||
|
p.target, p.isuser = elements[0], false
|
||||||
|
for _, typ := range elements[1:] {
|
||||||
|
switch typ {
|
||||||
|
case "isuser":
|
||||||
|
p.isuser = true
|
||||||
|
case "ischannel":
|
||||||
|
p.isuser = false
|
||||||
|
case "isserver":
|
||||||
|
// We do not support network names, and this is the default.
|
||||||
|
default:
|
||||||
|
return p, nil, errors.New("unsupported type: " + typ)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if p.isuser {
|
||||||
|
if i := strings.IndexAny(p.target, "!@"); i != -1 {
|
||||||
|
p.target = p.target[:i]
|
||||||
|
}
|
||||||
|
} else if !strings.HasPrefix(p.target, "#") {
|
||||||
|
// TODO(p): We should consult RPL_ISUPPORT rather than guess,
|
||||||
|
// though other prefixes are rare.
|
||||||
|
p.target = "#" + p.target
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note that the draft RFC wants these to be case-insensitive.
|
||||||
|
p.chankey = u.Query().Get("key")
|
||||||
|
// Being able to skip channel join is our own requirement and invention,
|
||||||
|
// as are notices (names taken from Travis CI configuration).
|
||||||
|
p.skipjoin = u.Query().Has("skipjoin")
|
||||||
|
p.usenotice = u.Query().Has("usenotice")
|
||||||
|
|
||||||
|
// Ensure valid LF-separated UTF-8, and split it at lines.
|
||||||
|
sanitized := strings.ReplaceAll(string([]rune(string(text))), "\r", "\n")
|
||||||
|
for _, line := range strings.Split(sanitized, "\n") {
|
||||||
|
if line != "" {
|
||||||
|
p.message = append(p.message, line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hostname, port := u.Hostname(), u.Port()
|
||||||
|
switch u.Scheme {
|
||||||
|
case "irc":
|
||||||
|
if port == "" {
|
||||||
|
port = "6667"
|
||||||
|
}
|
||||||
|
connect = func() (net.Conn, error) {
|
||||||
|
return net.Dial("tcp", net.JoinHostPort(hostname, port))
|
||||||
|
}
|
||||||
|
case "ircs":
|
||||||
|
if port == "" {
|
||||||
|
port = "6697"
|
||||||
|
}
|
||||||
|
connect = func() (net.Conn, error) {
|
||||||
|
return tls.Dial("tcp", net.JoinHostPort(hostname, port), nil)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
err = errors.New("unsupported scheme: " + u.Scheme)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// notifyByURL sends the given text to the IRC server specified by a URL.
|
||||||
|
// See draft-butcher-irc-url-04.txt for the URL scheme specification
|
||||||
|
// this function loosely follows.
|
||||||
|
func notifyByURL(rawURL string, text []byte) error {
|
||||||
|
p, connect, err := parse(rawURL, text)
|
||||||
|
if p.conn, err = connect(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer p.conn.Close()
|
||||||
|
return notify(&p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.BoolVar(&debugMode, "debug", false, "run in verbose debug mode")
|
||||||
|
version := flag.Bool("version", false, "show version and exit")
|
||||||
|
|
||||||
|
flag.Usage = func() {
|
||||||
|
f := flag.CommandLine.Output()
|
||||||
|
fmt.Fprintf(f, "Usage: %s [OPTION]... URL...\n", os.Args[0])
|
||||||
|
flag.PrintDefaults()
|
||||||
|
}
|
||||||
|
flag.Parse()
|
||||||
|
if flag.NArg() < 1 {
|
||||||
|
flag.Usage()
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
if *version {
|
||||||
|
fmt.Printf("%s %s\n", projectName, projectVersion)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
text, err := io.ReadAll(os.Stdin)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
status := 0
|
||||||
|
for _, rawURL := range flag.Args() {
|
||||||
|
if err := notifyByURL(rawURL, text); err != nil {
|
||||||
|
status = 1
|
||||||
|
|
||||||
|
var ue *url.Error
|
||||||
|
if errors.As(err, &ue) {
|
||||||
|
log.Println(err)
|
||||||
|
} else {
|
||||||
|
log.Printf("notify %q: %s\n", rawURL, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
os.Exit(status)
|
||||||
|
}
|
||||||
98
xN/xN_test.go
Normal file
98
xN/xN_test.go
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
//
|
||||||
|
// Copyright (c) 2024, Přemysl Eric Janouch <p@janouch.name>
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
|
||||||
|
// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
|
||||||
|
// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
|
||||||
|
// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
//
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestParseURL(t *testing.T) {
|
||||||
|
for _, rawURL := range []string{
|
||||||
|
`irc:server/channel`,
|
||||||
|
`irc://server/channel,isnetwork`,
|
||||||
|
`ircs://ssl.ircnet.io`,
|
||||||
|
`ircs://ssl.ircnet.io/`,
|
||||||
|
`http://server/path`,
|
||||||
|
} {
|
||||||
|
if _, _, err := parse(rawURL, nil); err == nil {
|
||||||
|
t.Errorf("%q should not parse\n", rawURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range []struct {
|
||||||
|
rawURL string
|
||||||
|
p parameters
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
rawURL: `irc://uptime@localhost/%23watch?skipjoin&usenotice`,
|
||||||
|
p: parameters{
|
||||||
|
username: "uptime",
|
||||||
|
target: "#watch",
|
||||||
|
skipjoin: true,
|
||||||
|
usenotice: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rawURL: `ircs://ohayou@irc.libera.chat/john,isuser,isserver`,
|
||||||
|
p: parameters{
|
||||||
|
username: "ohayou",
|
||||||
|
target: "john",
|
||||||
|
isuser: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rawURL: `ircs://agent:Password123@irc.cia.gov:1337/#hq?key=123456`,
|
||||||
|
p: parameters{
|
||||||
|
username: "agent",
|
||||||
|
password: "Password123",
|
||||||
|
target: "#hq",
|
||||||
|
chankey: "123456",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
p, _, err := parse(test.rawURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%q should parse, got: %s\n", test.rawURL, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if p.username != test.p.username {
|
||||||
|
t.Errorf("%q: username: %v ≠ %v\n",
|
||||||
|
test.rawURL, p.username, test.p.username)
|
||||||
|
}
|
||||||
|
if p.password != test.p.password {
|
||||||
|
t.Errorf("%q: password: %v ≠ %v\n",
|
||||||
|
test.rawURL, p.password, test.p.password)
|
||||||
|
}
|
||||||
|
if p.target != test.p.target {
|
||||||
|
t.Errorf("%q: target: %v ≠ %v\n",
|
||||||
|
test.rawURL, p.target, test.p.target)
|
||||||
|
}
|
||||||
|
if p.isuser != test.p.isuser {
|
||||||
|
t.Errorf("%q: isuser: %v ≠ %v\n",
|
||||||
|
test.rawURL, p.isuser, test.p.isuser)
|
||||||
|
}
|
||||||
|
if p.chankey != test.p.chankey {
|
||||||
|
t.Errorf("%q: chankey: %v ≠ %v\n",
|
||||||
|
test.rawURL, p.chankey, test.p.chankey)
|
||||||
|
}
|
||||||
|
if p.skipjoin != test.p.skipjoin {
|
||||||
|
t.Errorf("%q: skipjoin: %v ≠ %v\n",
|
||||||
|
test.rawURL, p.skipjoin, test.p.skipjoin)
|
||||||
|
}
|
||||||
|
if p.usenotice != test.p.usenotice {
|
||||||
|
t.Errorf("%q: usenotice: %v ≠ %v\n",
|
||||||
|
test.rawURL, p.usenotice, test.p.usenotice)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -197,6 +197,14 @@ function bufferResetStats(b) {
|
|||||||
b.highlighted = false
|
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) {
|
function bufferActivate(name) {
|
||||||
rpc.send({command: 'BufferActivate', bufferName: name})
|
rpc.send({command: 'BufferActivate', bufferName: name})
|
||||||
}
|
}
|
||||||
@@ -294,6 +302,13 @@ rpcEventHandlers.set(Relay.Event.BufferLine, e => {
|
|||||||
b.newMessages++
|
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) {
|
if (e.leakToActive) {
|
||||||
let bc = buffers.get(bufferCurrent)
|
let bc = buffers.get(bufferCurrent)
|
||||||
bc.lines.push({...line, leaked: true})
|
bc.lines.push({...line, leaked: true})
|
||||||
@@ -336,7 +351,7 @@ rpcEventHandlers.set(Relay.Event.BufferStats, e => {
|
|||||||
if (b === undefined)
|
if (b === undefined)
|
||||||
return
|
return
|
||||||
|
|
||||||
b.newMessages = e.newMessages,
|
b.newMessages = e.newMessages
|
||||||
b.newUnimportantMessages = e.newUnimportantMessages
|
b.newUnimportantMessages = e.newUnimportantMessages
|
||||||
b.highlighted = e.highlighted
|
b.highlighted = e.highlighted
|
||||||
})
|
})
|
||||||
@@ -344,6 +359,11 @@ rpcEventHandlers.set(Relay.Event.BufferStats, e => {
|
|||||||
rpcEventHandlers.set(Relay.Event.BufferRename, e => {
|
rpcEventHandlers.set(Relay.Event.BufferRename, e => {
|
||||||
buffers.set(e.new, buffers.get(e.bufferName))
|
buffers.set(e.new, buffers.get(e.bufferName))
|
||||||
buffers.delete(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 => {
|
rpcEventHandlers.set(Relay.Event.BufferRemove, e => {
|
||||||
@@ -354,8 +374,16 @@ rpcEventHandlers.set(Relay.Event.BufferRemove, e => {
|
|||||||
|
|
||||||
rpcEventHandlers.set(Relay.Event.BufferActivate, e => {
|
rpcEventHandlers.set(Relay.Event.BufferActivate, e => {
|
||||||
let old = buffers.get(bufferCurrent)
|
let old = buffers.get(bufferCurrent)
|
||||||
if (old !== undefined)
|
if (old !== undefined) {
|
||||||
bufferResetStats(old)
|
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
|
bufferLast = bufferCurrent
|
||||||
let b = buffers.get(e.bufferName)
|
let b = buffers.get(e.bufferName)
|
||||||
@@ -501,7 +529,8 @@ let Content = {
|
|||||||
while ((match = re.exec(text)) !== null) {
|
while ((match = re.exec(text)) !== null) {
|
||||||
if (end < match.index)
|
if (end < match.index)
|
||||||
a.push(m('span', attrs, text.substring(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
|
end = re.lastIndex
|
||||||
}
|
}
|
||||||
if (end < text.length)
|
if (end < text.length)
|
||||||
@@ -664,6 +693,12 @@ let Buffer = {
|
|||||||
const dom = event.target
|
const dom = event.target
|
||||||
bufferAutoscroll =
|
bufferAutoscroll =
|
||||||
dom.scrollTop + dom.clientHeight + 1 >= dom.scrollHeight
|
dom.scrollTop + dom.clientHeight + 1 >= dom.scrollHeight
|
||||||
|
|
||||||
|
let b = buffers.get(bufferCurrent)
|
||||||
|
if (b !== undefined && b.highlighted && !bufferAutoscroll) {
|
||||||
|
b.highlighted = false
|
||||||
|
m.redraw()
|
||||||
|
}
|
||||||
}}, lines)
|
}}, lines)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -679,7 +714,8 @@ let Log = {
|
|||||||
while ((match = re.exec(text)) !== null) {
|
while ((match = re.exec(text)) !== null) {
|
||||||
if (end < match.index)
|
if (end < match.index)
|
||||||
a.push(text.substring(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
|
end = re.lastIndex
|
||||||
}
|
}
|
||||||
if (end < text.length)
|
if (end < text.length)
|
||||||
@@ -967,7 +1003,7 @@ let Input = {
|
|||||||
rpc.send({command: 'Active'})
|
rpc.send({command: 'Active'})
|
||||||
|
|
||||||
let b = buffers.get(bufferCurrent)
|
let b = buffers.get(bufferCurrent)
|
||||||
if (b === undefined)
|
if (b === undefined || event.isComposing)
|
||||||
return
|
return
|
||||||
|
|
||||||
let textarea = event.currentTarget
|
let textarea = event.currentTarget
|
||||||
@@ -1013,10 +1049,23 @@ let Input = {
|
|||||||
} else if (!event.altKey && !event.ctrlKey && !event.metaKey &&
|
} else if (!event.altKey && !event.ctrlKey && !event.metaKey &&
|
||||||
!event.shiftKey) {
|
!event.shiftKey) {
|
||||||
handled = true
|
handled = true
|
||||||
switch (event.keyCode) {
|
switch (event.key) {
|
||||||
case 9: success = Input.complete(b, textarea); break
|
case 'PageUp':
|
||||||
case 13: success = Input.submit(b, textarea); break
|
Array.from(document.getElementsByClassName('buffer'))
|
||||||
default: handled = false
|
.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)
|
if (!success)
|
||||||
|
|||||||
1
xS/.gitignore
vendored
1
xS/.gitignore
vendored
@@ -1,2 +1,3 @@
|
|||||||
/xS
|
/xS
|
||||||
/xS-replies.go
|
/xS-replies.go
|
||||||
|
/xS.1
|
||||||
|
|||||||
15
xS/Makefile
15
xS/Makefile
@@ -2,16 +2,17 @@
|
|||||||
.SUFFIXES:
|
.SUFFIXES:
|
||||||
AWK = env LC_ALL=C awk
|
AWK = env LC_ALL=C awk
|
||||||
|
|
||||||
outputs = xS xS-version.go xS-replies.go
|
outputs = xS xS-replies.go xS.1
|
||||||
all: $(outputs)
|
all: $(outputs)
|
||||||
|
|
||||||
xS: xS.go xS-version.go xS-replies.go
|
xS: xS.go ../xK-version xS-replies.go
|
||||||
go build -o $@
|
go build -ldflags "-X 'main.projectVersion=$$(cat ../xK-version)'" -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-replies.go: xS-gen-replies.awk xS-replies
|
xS-replies.go: xS-gen-replies.awk xS-replies
|
||||||
$(AWK) -f 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:
|
clean:
|
||||||
rm -f $(outputs)
|
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"
|
const projectName = "xS"
|
||||||
|
|
||||||
|
var projectVersion = "?"
|
||||||
|
|
||||||
var debugMode = false
|
var debugMode = false
|
||||||
|
|
||||||
// --- Logging -----------------------------------------------------------------
|
// --- Logging -----------------------------------------------------------------
|
||||||
@@ -454,132 +456,6 @@ func (fd *floodDetector) check() bool {
|
|||||||
return count <= fd.limit
|
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 ----------------------------------------------------
|
// --- IRC token validation ----------------------------------------------------
|
||||||
|
|
||||||
// Everything as per RFC 2812
|
// 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