49 Commits

Author SHA1 Message Date
73cc8f448a Bump version, update NEWS
All checks were successful
Alpine 3.20 Success
Arch Linux AUR Success
OpenBSD 7.5 Success
2024-07-28 07:29:15 +02:00
4565afe294 xC: expand a comment 2024-07-28 07:15:41 +02:00
3ad8c79de8 xC: handle multiline server commands properly
All checks were successful
Alpine 3.20 Success
Arch Linux AUR Success
OpenBSD 7.5 Success
Additional lines might have been passed to the server intact
as part of an argument, but we have /quote for that.
2024-07-28 04:10:30 +02:00
12fc3c228a xP: reset highlight state once reaching buffer end
All checks were successful
Alpine 3.20 Success
Arch Linux AUR Success
OpenBSD 7.5 Success
2024-07-28 03:44:37 +02:00
175533a5e9 xP: don't interrupt IME composition
All checks were successful
Alpine 3.20 Success
Arch Linux AUR Success
OpenBSD 7.5 Success
On Vivaldi/macOS, pressing Enter would send the input and still keep
editing it as it was.
2024-07-04 20:06:59 +02:00
a9b46141a9 xS/xN: add test targets
All checks were successful
Alpine 3.19 Success
Arch Linux AUR Success
OpenBSD 7.3 Success
2024-04-10 13:59:33 +02:00
c38cca3b92 Bump liberty
All checks were successful
Arch Linux AUR Success
Alpine 3.19 Success
2024-04-09 17:08:40 +02:00
aee7540faa Update README.adoc and xN usage output 2024-04-04 21:25:17 +02:00
53ba996ec9 Add a simple IRC notifier utility
All checks were successful
Arch Linux AUR Success
2024-04-03 15:56:33 +02:00
d450c6cc5f xP: do not send the Referrer header 2024-03-04 16:15:22 +01:00
f8ea1634c4 Bump liberty 2024-03-04 16:15:22 +01:00
ef257cd575 xP: avoid expensive updates/refreshes 2024-01-06 23:44:11 +01:00
69eccc7065 xP: don't let buffers grow indefinitely
Primarily for performance reasons.
2024-01-06 21:17:18 +01:00
13d2ff115b xM: improve the bundle icon a bit 2023-09-04 07:06:03 +02:00
9e4692bb09 xM: generate and use a bundle icon 2023-09-03 02:13:14 +02:00
1c4343058d Add a Cocoa frontend for xC
Some work remains to be done to get it to be even as good
as the Win32 frontend, but it's generally usable.
2023-09-01 01:12:51 +02:00
e5156cddbf xW: render leaked lines a bit more accurately
There is no need to reset all text attributes, just the colour.
2023-08-25 22:48:31 +02:00
34521e61c1 xP/xW: fix buffer rename handling
Maintaining string pointers to the current/last buffer
means that renames invalidate them.
2023-08-25 22:48:31 +02:00
c22dd67fc1 xC: send missing relay events for newly added servers 2023-08-25 22:48:27 +02:00
274d5f03e7 xC: give the /away command a proper handler
Multiple words should be passed to the server as a single argument.
2023-08-25 22:46:43 +02:00
2f19e5a733 xW: improve command sending 2023-07-29 02:15:24 +02:00
b9cdabca5d xC: fix relay handling of missing log files
Intermediate error messages would trash the prepared static buffer.
2023-07-28 04:30:45 +02:00
f60ca43156 xW: do not unnecessarily enter compatibility mode 2023-07-28 04:30:45 +02:00
afe4e61f08 xW: mark a footgun
These messages are used by IsDialogMessage(), and use the WM_USER range.
2023-07-27 23:08:16 +02:00
8d9d1c60ec xW: make Up/Down go through input history
The input field isn't multiline, so this doesn't pose an issue.
Otherwise, we'd have to check if we're on the top line first.
2023-07-27 16:35:54 +02:00
8c1464822b xW: don't delay sending out pongs 2023-07-27 16:19:32 +02:00
fcd1b8e011 xW: improve beeping
This adds yet another build dependency,
but it's better than the alternatives of handling it in code.
2023-07-27 16:06:41 +02:00
3d345987c3 xW: cleanup 2023-07-27 02:37:20 +02:00
3e37efd9cd xW: show a connect dialog when run without args 2023-07-27 01:28:52 +02:00
efb25b8aae xW: un-highlight the icon when activating buffers 2023-07-26 16:07:21 +02:00
e72793e315 xW: make newline before unread marker conditional 2023-07-26 16:07:20 +02:00
5a412ab6e2 xW: handle WM_SYSCOLORCHANGE 2023-07-26 16:07:20 +02:00
81bc578773 xW: add missing date change handling 2023-07-26 03:59:25 +02:00
100de5ac2d xC: fix Readline 6.3 compatibility 2023-07-24 07:59:22 +02:00
c157d3369f xP: make Page Up/Down in editor scroll the buffer
Just like in xW recently.  It is unlikely that the user would want
to use these keys to move the cursor.  Ctrl+Home/End still work,
as does holding Up/Down arrows.

Also stop using the deprecated and somewhat cryptic keyCode.
2023-07-23 00:20:32 +02:00
8b5ea67aff xW: fix Clang build 2023-07-21 12:37:01 +02:00
6f02af814f xW: store the largest program icon in PNG format
This shaves off about half a megabyte.
2023-07-16 08:35:39 +02:00
90859107c8 xW: set version information 2023-07-15 23:35:46 +02:00
0219dbd026 Add a Win32 frontend for xC
This has been more of an exercise.  The performance of Msftedit.dll
is rather abysmal, and its interface isn't the most accomodating.

That said, the frontend is quite usable, at least on Windows 10+.
2023-07-15 17:00:21 +02:00
1da4699a7a Cleanup 2023-07-09 09:35:16 +02:00
9e993c50e6 xC: don't crash with unknown terminals
It would be possible to avoid using cur_term fields in this case,
but the program would likely be of little use anyway.
2023-07-07 10:43:15 +02:00
b3e9218b23 Fix Cygwin build warnings 2023-07-07 09:53:20 +02:00
bc8867eb22 Fix the integration test
IRCv3 capabilities broke it a bit.

Also change it so that it doesn't destroy existing configuration.
2023-07-05 00:16:55 +02:00
ec33adba35 Update README.adoc 2023-07-04 23:50:01 +02:00
6f596f1dcb Move project version to file, add xS manual page
So far Go applications remain independent to handle Nix's inability
to easily combine them with the CMake part.

There is also no "install" target, because any packagers will want to
adjust installation paths manually, and there is no configure step.
2023-07-04 23:26:05 +02:00
abcff46dc4 xC: fix an OpenBSD build warning
sys/cdefs.h makes _XOPEN_SOURCE cause _POSIX_C_SOURCE to be overriden.
2023-07-04 06:31:02 +02:00
8d9ce92758 README.adoc: update package information 2023-07-01 22:02:10 +02:00
4bb9449e47 Fix the static analysis test
Adjust its query so that it doesn't cause a particular false positive.
2023-06-16 19:45:12 +02:00
50f70f93bb xC: fix a harmless copy-paste error 2023-06-13 09:02:20 +02:00
36 changed files with 4644 additions and 196 deletions

View File

@@ -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

View File

@@ -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
View File

@@ -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"

View File

@@ -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

Submodule liberty updated: 0f3ed14575...f04cc2c61e

4
test
View File

@@ -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"

View File

@@ -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
View File

@@ -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
View File

@@ -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
View File

@@ -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
View File

@@ -0,0 +1 @@
2.0.0

45
xM/CMakeLists.txt Normal file
View 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
View 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

File diff suppressed because it is too large Load Diff

3
xN/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
/xN
/xN.1
/irc.go

21
xN/Makefile Normal file
View 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)

3
xN/go.mod Normal file
View File

@@ -0,0 +1,3 @@
module janouch.name/xK/xN
go 1.19

86
xN/xN.adoc Normal file
View 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
View 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
View 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)
}
}
}

View File

@@ -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
View File

@@ -1,2 +1,3 @@
/xS
/xS-replies.go
/xS.1

View File

@@ -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
View 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
}

View File

@@ -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
View 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
View File

@@ -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
View 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
View 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
View 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
View 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
View 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

1998
xW/xW.cpp Normal file

File diff suppressed because it is too large Load Diff

25
xW/xW.manifest Normal file
View 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
View 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
View 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