38 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
25 changed files with 2619 additions and 319 deletions

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.

8
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
@@ -33,8 +35,12 @@
* 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 web + Win32 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.
'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
@@ -140,13 +145,23 @@ 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. In order to run it, make a shortcut for the executable and
include the relay address in its _Target_ field:
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

Submodule liberty updated: 62166f9679...f04cc2c61e

44
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.
@@ -616,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 ();
@@ -8317,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);
@@ -8345,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");
@@ -9439,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));
@@ -12937,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)
{
@@ -13002,7 +13017,6 @@ TRIVIAL_HANDLER (who, "WHO")
TRIVIAL_HANDLER (motd, "MOTD")
TRIVIAL_HANDLER (oper, "OPER")
TRIVIAL_HANDLER (stats, "STATS")
TRIVIAL_HANDLER (away, "AWAY")
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@@ -15543,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)))
@@ -15558,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);
}
@@ -15856,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)

View File

@@ -1 +1 @@
1.5.0
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)

View File

@@ -12,5 +12,7 @@ xS-replies.go: 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
}

126
xS/xS.go
View File

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

View File

@@ -18,60 +18,48 @@ 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})
# Icon generation utilities
# TODO: Shove this into liberty as a CMake module, similar to AddThreads,
# and remove the copies in the parent CMakeLists.txt as well as in tdv.
# Produce a beep sample
if (NOT ${CMAKE_VERSION} VERSION_LESS 3.18.0)
set (find_program_REQUIRE REQUIRED)
endif ()
function (icon_to_png name svg size output_dir output)
set (_dimensions ${size}x${size})
set (_png_path ${output_dir}/hicolor/${_dimensions}/apps)
set (_png ${_png_path}/${name}.png)
set (${output} ${_png} PARENT_SCOPE)
find_program (rsvg_convert_EXECUTABLE rsvg-convert ${find_program_REQUIRE})
add_custom_command (OUTPUT ${_png}
COMMAND ${CMAKE_COMMAND} -E make_directory ${_png_path}
COMMAND ${rsvg_convert_EXECUTABLE} --output=${_png}
--width=${size} --height=${size} ${svg}
DEPENDS ${svg}
COMMENT "Generating ${name} ${_dimensions} application icon" VERBATIM)
endfunction ()
function (icon_for_win32 pngs ico)
find_program (icotool_EXECUTABLE icotool ${find_program_REQUIRE})
add_custom_command (OUTPUT ${ico}
COMMAND ${icotool_EXECUTABLE} -c -o ${ico} ${pngs}
DEPENDS ${pngs}
COMMENT "Generating Windows program icon" VERBATIM)
endfunction ()
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 256)
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_png_list}" ${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})
set_property (SOURCE xW.rc
APPEND PROPERTY OBJECT_DEPENDS ${icon_ico_list} beep.wav)
# Build the main executable and link it
set (root "${PROJECT_SOURCE_DIR}/..")
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}
@@ -88,7 +76,7 @@ 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)
target_link_libraries (xW comctl32 ws2_32 winmm)
add_dependencies (xW xC-proto)
# At least with MinGW, this is a fully independent portable executable

View File

@@ -1,6 +1,14 @@
#ifndef CONFIG_H
#define CONFIG_H
#define PROGRAM_VERSION "${project_version}"
#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

View File

@@ -1,5 +1,6 @@
#define IDI_ICON 1
#define IDI_HIGHLIGHTED 2
#define IDR_BEEP 3
#define IDA_ACCELERATORS 10
// Named after input_add_functions() in xC.
@@ -10,3 +11,8 @@
#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

388
xW/xW.cpp
View File

@@ -18,6 +18,7 @@
#include "xC-proto.cpp"
#include "xW-resources.h"
#include "config.h"
#include <winsock2.h>
#include <ws2tcpip.h>
@@ -99,6 +100,7 @@ struct {
HWND hwndInput; ///< edit: user input
HWND hwndLastFocused; ///< For Alt+Tab, e.g.
HANDLE date_change_timer; ///< Waitable timer for day changes
HICON hicon; ///< Normal program icon
HICON hiconHighlighted; ///< Highlighted program icon
@@ -111,10 +113,14 @@ struct {
// Networking:
std::wstring host; ///< Host as given by user
std::wstring port; ///< Port/service as given by user
addrinfoW *addresses; ///< GetAddrInfo() result
addrinfoW *addresses_iterator; ///< Currently processed address
SOCKET socket; ///< Relay socket
WSAEVENT event; ///< Relay socket event
WSAEVENT socket_event; ///< Relay socket event
HANDLE flush_event; ///< Write buffer has new data
std::vector<uint8_t> write_buffer; ///< Write buffer
std::vector<uint8_t> read_buffer; ///< Read buffer
@@ -151,6 +157,23 @@ format_error_message(int err)
return copy;
}
static std::wstring
window_get_text(HWND hWnd)
{
int length = GetWindowTextLength(hWnd);
std::wstring buffer(length, {});
GetWindowText(hWnd, buffer.data(), length + 1);
return buffer;
}
static void
beep()
{
if (!PlaySound(MAKEINTRESOURCE(IDR_BEEP),
GetModuleHandle(NULL), SND_ASYNC | SND_RESOURCE))
Beep(800, 100);
}
// --- Networking --------------------------------------------------------------
static bool
@@ -179,6 +202,8 @@ relay_try_read(std::wstring &error)
static bool
relay_try_write(std::wstring &error)
{
ResetEvent(g.flush_event);
auto &w = g.write_buffer;
int err = {};
while (!w.empty()) {
@@ -213,18 +238,10 @@ relay_send(Relay::CommandData *data, Callback callback = {})
g.write_buffer.insert(g.write_buffer.end(), prefix, prefix + sizeof len);
g.write_buffer.insert(g.write_buffer.end(), w.data.begin(), w.data.end());
// Call relay_try_write() separately.
}
static void
relay_send_now(Relay::CommandData *data, Callback callback = {})
{
relay_send(data, callback);
// TODO(p): Either tear down here, or run relay_try_write() from a timer.
std::wstring error;
if (!relay_try_write(error))
show_error_message(error.c_str());
// There doesn't seem to be a way to cause FD_WRITE without first
// unsuccessfully trying to send some data, but we don't want to
// handle any errors at this level.
SetEvent(g.flush_event);
}
// --- Buffers -----------------------------------------------------------------
@@ -243,7 +260,7 @@ buffer_activate(const std::wstring &name)
{
auto activate = new Relay::CommandData_BufferActivate();
activate->buffer_name = name;
relay_send_now(activate);
relay_send(activate);
}
static void
@@ -251,7 +268,7 @@ buffer_toggle_unimportant(const std::wstring &name)
{
auto toggle = new Relay::CommandData_BufferToggleUnimportant();
toggle->buffer_name = name;
relay_send_now(toggle);
relay_send(toggle);
}
// --- Current buffer ----------------------------------------------------------
@@ -297,7 +314,7 @@ buffer_toggle_log()
auto log = new Relay::CommandData_BufferLog();
log->buffer_name = g.buffer_current;
relay_send_now(log, [name = g.buffer_current](auto error, auto response) {
relay_send(log, [name = g.buffer_current](auto error, auto response) {
if (g.buffer_current != name)
return;
buffer_toggle_log(error,
@@ -401,8 +418,7 @@ refresh_status()
status += L"🡇 ";
status += g.buffer_current;
auto b = buffer_by_name(g.buffer_current);
if (b) {
if (auto b = buffer_by_name(g.buffer_current)) {
if (!b->modes.empty())
status += L"(+" + b->modes + L")";
if (b->hide_unimportant)
@@ -410,10 +426,7 @@ refresh_status()
}
// Buffer scrolling would cause a ton of flickering redraws.
int length = GetWindowTextLength(g.hwndStatus);
std::wstring buffer(length, {});
GetWindowText(g.hwndStatus, buffer.data(), length + 1);
if (buffer != status)
if (window_get_text(g.hwndStatus) != status)
SetWindowText(g.hwndStatus, status.c_str());
}
@@ -498,7 +511,7 @@ convert_item_formatting(Relay::ItemData *item, CHARFORMAT2 &cf, bool &inverse)
}
static std::vector<BufferLineItem>
convert_items(std::vector<std::unique_ptr<Relay::ItemData>> &items)
convert_items(const std::vector<std::unique_ptr<Relay::ItemData>> &items)
{
CHARFORMAT2 cf = default_charformat();
std::vector<BufferLineItem> result;
@@ -541,50 +554,91 @@ convert_buffer_line(Relay::EventData_BufferLine &line)
}
static void
buffer_print_line(std::vector<BufferLine>::const_iterator begin,
std::vector<BufferLine>::const_iterator line)
buffer_print_date_change(bool &sameline, const tm &last, const tm &current)
{
if (last.tm_year == current.tm_year &&
last.tm_mon == current.tm_mon &&
last.tm_mday == current.tm_mday)
return;
wchar_t buffer[64] = {};
wcsftime(buffer, sizeof buffer, &L"\n%x"[sameline], &current);
sameline = false;
CHARFORMAT2 cf = default_charformat();
cf.dwEffects |= CFE_BOLD;
richedit_replacesel(g.hwndBuffer, &cf, buffer);
}
static LONG
buffer_reset_selection()
{
CHARRANGE cr = {};
cr.cpMin = cr.cpMax = GetWindowTextLength(g.hwndBuffer);
SendMessage(g.hwndBuffer, EM_EXSETSEL, 0, (LPARAM) &cr);
return cr.cpMin;
}
static struct tm
buffer_localtime(time_t time)
{
// This isn't critical, so let it fail quietly.
struct tm result = {};
(void) localtime_s(&result, &time);
return result;
}
static void
buffer_print_and_watch_trailing_date_changes()
{
time_t current_unix = time(NULL);
tm current = buffer_localtime(current_unix);
auto b = buffer_by_name(g.buffer_current);
if (b && !b->lines.empty()) {
tm last = buffer_localtime(b->lines.back().when / 1000);
bool sameline = !buffer_reset_selection();
buffer_print_date_change(sameline, last, current);
}
current.tm_sec = current.tm_min = current.tm_hour = 0;
current.tm_mday++;
current.tm_isdst = -1;
const time_t midnight = mktime(&current);
if (midnight == (time_t) -1 || midnight < current_unix)
return;
// Note that after printing the first trailing update,
// follow-up updates may be duplicated if timer events arrive too early.
LARGE_INTEGER li = {};
li.QuadPart = (midnight - current_unix + 1) * -10000000LL;
SetWaitableTimer(g.date_change_timer, &li, 0, NULL, NULL, FALSE);
}
static void
buffer_print_line(std::vector<BufferLine>::const_iterator begin,
std::vector<BufferLine>::const_iterator line)
{
tm current = buffer_localtime(line->when / 1000);
tm last = buffer_localtime(
line == begin ? time(NULL) : (line - 1)->when / 1000);
// The Rich Edit control makes the window cursor transparent
// each time you add an independent newline character. Avoid that.
// (Sadly, this also makes Windows 7 end lines with a bogus space that
// has the CHARFORMAT2 of what we flush that newline together with.)
bool sameline = !cr.cpMin;
bool sameline = !buffer_reset_selection();
buffer_print_date_change(sameline, last, current);
time_t current_unix = line->when / 1000;
time_t last_unix = (line != begin)
? (line - 1)->when / 1000
: time(NULL);
wchar_t buffer[64] = {};
wcsftime(buffer, sizeof buffer, &L"\n%H:%M:%S"[sameline], &current);
tm current = {}, last = {};
(void) localtime_s(&current, &current_unix);
(void) localtime_s(&last, &last_unix);
if (last.tm_year != current.tm_year ||
last.tm_mon != current.tm_mon ||
last.tm_mday != current.tm_mday) {
wchar_t buffer[64] = {};
wcsftime(buffer, sizeof buffer, &L"\n%x\n"[sameline], &current);
sameline = true;
CHARFORMAT2 cf = default_charformat();
cf.dwEffects |= CFE_BOLD;
richedit_replacesel(g.hwndBuffer, &cf, buffer);
}
{
wchar_t buffer[64] = {};
wcsftime(buffer, sizeof buffer, &L"\n%H:%M:%S"[sameline], &current);
CHARFORMAT2 cf = default_charformat();
cf.dwEffects &= ~(CFE_AUTOCOLOR | CFE_AUTOBACKCOLOR);
cf.crTextColor = RGB(0xbb, 0xbb, 0xbb);
cf.crBackColor = RGB(0xf8, 0xf8, 0xf8);
richedit_replacesel(g.hwndBuffer, &cf, buffer);
cf = default_charformat();
richedit_replacesel(g.hwndBuffer, &cf, L" ");
}
CHARFORMAT2 cf = default_charformat();
cf.dwEffects &= ~(CFE_AUTOCOLOR | CFE_AUTOBACKCOLOR);
cf.crTextColor = RGB(0xbb, 0xbb, 0xbb);
cf.crBackColor = RGB(0xf8, 0xf8, 0xf8);
richedit_replacesel(g.hwndBuffer, &cf, buffer);
cf = default_charformat();
richedit_replacesel(g.hwndBuffer, &cf, L" ");
// Tabstops won't quite help us here, since we need it centred.
std::wstring prefix;
@@ -624,14 +678,12 @@ buffer_print_line(std::vector<BufferLine>::const_iterator begin,
if (!prefix.empty())
richedit_replacesel(g.hwndBuffer, &pcf, prefix.c_str());
std::wstring text;
for (const auto &it : line->items)
text += it.text;
CHARFORMAT2 format = default_charformat();
format.dwEffects &= ~CFE_AUTOCOLOR;
format.crTextColor = GetSysColor(COLOR_GRAYTEXT);
richedit_replacesel(g.hwndBuffer, &format, text.c_str());
for (auto it : line->items) {
it.format.dwEffects &= ~CFE_AUTOCOLOR;
it.format.crTextColor = GetSysColor(COLOR_GRAYTEXT);
it.format.dwEffects |= CFE_AUTOBACKCOLOR;
richedit_replacesel(g.hwndBuffer, &it.format, it.text.c_str());
}
} else {
if (!prefix.empty())
richedit_replacesel(g.hwndBuffer, &pcf, prefix.c_str());
@@ -643,10 +695,12 @@ buffer_print_line(std::vector<BufferLine>::const_iterator begin,
static void
buffer_print_separator()
{
bool sameline = !GetWindowTextLength(g.hwndBuffer);
CHARFORMAT2 format = default_charformat();
format.dwEffects &= ~CFE_AUTOCOLOR;
format.crTextColor = RGB(0xff, 0x5f, 0x00);
richedit_replacesel(g.hwndBuffer, &format, L"\n---");
richedit_replacesel(g.hwndBuffer, &format, &L"\n---"[sameline]);
}
static void
@@ -672,6 +726,7 @@ refresh_buffer(const Buffer &b)
i++;
}
buffer_print_and_watch_trailing_date_changes();
buffer_scroll_to_bottom();
SendMessage(g.hwndBuffer, WM_SETREDRAW, (WPARAM) TRUE, 0);
@@ -731,8 +786,7 @@ relay_process_buffer_line(Buffer &b, Relay::EventData_BufferLine &m)
if (line->is_highlight || (!visible && !line->is_unimportant &&
b.kind == Relay::BufferKind::PRIVATE_MESSAGE)) {
// TODO(p): Avoid the PC speaker, which is also unreliable.
Beep(800, 100);
beep();
if (!visible) {
b.highlighted = true;
@@ -766,8 +820,6 @@ relay_process_callbacks(uint32_t command_seq,
}
}
static std::wstring input_get_contents();
static void
relay_process_message(const Relay::EventMessage &m)
{
@@ -860,11 +912,15 @@ relay_process_message(const Relay::EventMessage &m)
if (!b)
break;
b->buffer_name = data.buffer_name;
b->buffer_name = data.new_;
refresh_buffer_list();
if (b->buffer_name == g.buffer_current)
if (data.buffer_name == g.buffer_current) {
g.buffer_current = data.new_;
refresh_status();
}
if (data.buffer_name == g.buffer_last)
g.buffer_last = data.new_;
break;
}
case Relay::Event::BUFFER_REMOVE:
@@ -896,7 +952,7 @@ relay_process_message(const Relay::EventMessage &m)
old->new_unimportant_messages = 0;
old->highlighted = false;
old->input = input_get_contents();
old->input = window_get_text(g.hwndInput);
SendMessage(g.hwndInput, EM_GETSEL,
(WPARAM) &old->input_start, (LPARAM) &old->input_end);
@@ -911,6 +967,7 @@ relay_process_message(const Relay::EventMessage &m)
b->highlighted = false;
SendMessage(g.hwndBufferList, LB_SETCURSEL, b - g.buffers.data(), 0);
refresh_icon();
refresh_topic(b->topic);
refresh_buffer(*b);
refresh_prompt();
@@ -1024,8 +1081,8 @@ relay_destroy_socket()
{
closesocket(g.socket);
g.socket = INVALID_SOCKET;
WSACloseEvent(g.event);
g.event = NULL;
WSACloseEvent(g.socket_event);
g.socket_event = NULL;
g.read_buffer.clear();
g.write_buffer.clear();
@@ -1046,8 +1103,8 @@ relay_connect_step(std::wstring& error)
return false;
}
g.event = WSACreateEvent();
if (WSAEventSelect(g.socket, g.event,
g.socket_event = WSACreateEvent();
if (WSAEventSelect(g.socket, g.socket_event,
FD_CONNECT | FD_READ | FD_WRITE | FD_CLOSE))
error = format_error_message(WSAGetLastError());
else if (!connect(g.socket, p->ai_addr, (int) p->ai_addrlen))
@@ -1117,7 +1174,7 @@ static bool
relay_process_socket_events(std::wstring &error)
{
WSANETWORKEVENTS wne = {};
if (WSAEnumNetworkEvents(g.socket, g.event, &wne)) {
if (WSAEnumNetworkEvents(g.socket, g.socket_event, &wne)) {
error = format_error_message(WSAGetLastError());
return false;
}
@@ -1137,15 +1194,6 @@ relay_process_socket_events(std::wstring &error)
// --- Input line --------------------------------------------------------------
static std::wstring
input_get_contents()
{
int length = GetWindowTextLength(g.hwndInput);
std::wstring buffer(length, {});
GetWindowText(g.hwndInput, buffer.data(), length + 1);
return buffer;
}
static void
input_set_contents(const std::wstring &input)
{
@@ -1163,7 +1211,7 @@ input_submit()
auto input = new Relay::CommandData_BufferInput();
input->buffer_name = b->buffer_name;
input->text = input_get_contents();
input->text = window_get_text(g.hwndInput);
// Buffer::history[Buffer::history.size()] is virtual,
// and is represented either by edit contents when it's currently
@@ -1172,7 +1220,7 @@ input_submit()
b->history_at = b->history.size();
input_set_contents({});
relay_send_now(input);
relay_send(input);
return true;
}
@@ -1187,7 +1235,7 @@ input_stamp()
{
DWORD start = {}, end = {};
SendMessage(g.hwndInput, EM_GETSEL, (WPARAM) &start, (LPARAM) &end);
return {start, end, input_get_contents()};
return {start, end, window_get_text(g.hwndInput)};
}
static void
@@ -1217,9 +1265,8 @@ input_complete(const InputStamp &state, const std::wstring &error,
SendMessage(g.hwndInput, EM_REPLACESEL, TRUE, (LPARAM) insert.c_str());
}
// TODO(p): Avoid the PC speaker, which is also unreliable.
if (response->completions.size() != 1)
Beep(800, 100);
beep();
// TODO(p): Show all completion options.
}
@@ -1240,7 +1287,7 @@ input_complete()
complete->buffer_name = g.buffer_current;
complete->text = state.input;
complete->position = utf8.length();
relay_send_now(complete, [state](auto error, auto response) {
relay_send(complete, [state](auto error, auto response) {
auto stamp = input_stamp();
if (std::make_tuple(stamp.start, stamp.end, stamp.input) !=
std::make_tuple(state.start, state.end, state.input))
@@ -1251,6 +1298,32 @@ input_complete()
return true;
}
static bool
input_up()
{
auto b = buffer_by_name(g.buffer_current);
if (!b || b->history_at < 1)
return false;
if (b->history_at == b->history.size())
b->input = window_get_text(g.hwndInput);
input_set_contents(b->history.at(--b->history_at));
return true;
}
static bool
input_down()
{
auto b = buffer_by_name(g.buffer_current);
if (!b || b->history_at >= b->history.size())
return false;
input_set_contents(++b->history_at == b->history.size()
? b->input
: b->history.at(b->history_at));
return true;
}
static boolean
input_wants(const MSG *message)
{
@@ -1282,34 +1355,18 @@ input_proc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam,
return lResult;
}
case WM_SYSCHAR:
{
auto b = buffer_by_name(g.buffer_current);
if (!b)
break;
// TODO(p): Emacs-style cursor movement shortcuts.
switch (wParam) {
case 'p':
{
if (b->history_at < 1)
break;
if (b->history_at == b->history.size())
b->input = input_get_contents();
input_set_contents(b->history.at(--b->history_at));
return 0;
}
if (input_up())
return 0;
break;
case 'n':
{
if (b->history_at >= b->history.size())
break;
input_set_contents(++b->history_at == b->history.size()
? b->input
: b->history.at(b->history_at));
return 0;
}
if (input_down())
return 0;
break;
}
break;
}
case WM_KEYDOWN:
{
HWND scrollable = IsWindowVisible(g.hwndBufferLog)
@@ -1317,6 +1374,14 @@ input_proc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam,
: g.hwndBuffer;
switch (wParam) {
case VK_UP:
if (input_up())
return 0;
break;
case VK_DOWN:
if (input_down())
return 0;
break;
case VK_PRIOR:
SendMessage(scrollable, EM_SCROLL, SB_PAGEUP, 0);
return 0;
@@ -1329,7 +1394,7 @@ input_proc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam,
case WM_CHAR:
{
// This could be implemented more precisely, but it will do.
relay_send_now(new Relay::CommandData_Active());
relay_send(new Relay::CommandData_Active());
switch (wParam) {
case VK_RETURN:
@@ -1369,7 +1434,7 @@ bufferlist_proc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam,
auto input = new Relay::CommandData_BufferInput();
input->buffer_name = g.buffer_current;
input->text = L"/buffer close " + g.buffers.at(index).buffer_name;
relay_send_now(input);
relay_send(input);
return 0;
}
case WM_NCDESTROY:
@@ -1579,6 +1644,26 @@ window_proc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
case WM_SIZE:
process_resize(LOWORD(lParam), HIWORD(lParam));
return 0;
case WM_TIMECHANGE:
_tzset();
if (auto b = buffer_by_name(g.buffer_current))
refresh_buffer(*b);
return 0;
case WM_SYSCOLORCHANGE:
// The topic would flicker with WS_EX_TRANSPARENT.
// The buffer only changed its text colour, not background.
SendMessage(g.hwndTopic,
EM_SETBKGNDCOLOR, 0, GetSysColor(COLOR_3DFACE));
SendMessage(g.hwndBuffer,
EM_SETBKGNDCOLOR, 1, 0);
// XXX: This is incomplete, we'd have to run convert_items() again;
// essentially only COLOR_GRAYTEXT is reloaded in here.
if (auto b = buffer_by_name(g.buffer_current))
refresh_buffer(*b);
// Pass it to all child windows, through DefWindowProc().
break;
case WM_ACTIVATE:
if (LOWORD(wParam) == WA_INACTIVE)
g.hwndLastFocused = GetFocus();
@@ -1635,10 +1720,33 @@ window_proc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
break;
}
}
case DM_GETDEFID:
case DM_SETDEFID:
break;
}
return DefWindowProc(hWnd, uMsg, wParam, lParam);
}
static INT_PTR CALLBACK
connect_proc(
HWND hDlg, UINT uMsg, WPARAM wParam, [[maybe_unused]] LPARAM lParam)
{
switch (uMsg) {
case WM_INITDIALOG:
return TRUE;
case WM_COMMAND:
switch (LOWORD(wParam)) {
case IDOK:
case IDCANCEL:
g.host = window_get_text(GetDlgItem(hDlg, IDC_HOST));
g.port = window_get_text(GetDlgItem(hDlg, IDC_PORT));
EndDialog(hDlg, LOWORD(wParam));
return TRUE;
}
}
return FALSE;
}
static void
get_font()
{
@@ -1744,11 +1852,12 @@ wWinMain(HINSTANCE hInstance, [[maybe_unused]] HINSTANCE hPrevInstance,
wc.hIcon = g.hicon;
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = GetSysColorBrush(COLOR_3DFACE);
wc.lpszClassName = L"xW";
wc.lpszClassName = TEXT(PROJECT_NAME);
if (!RegisterClassEx(&wc))
return 1;
g.hwndMain = CreateWindowEx(WS_EX_CONTROLPARENT, L"xW", L"xW",
g.hwndMain = CreateWindowEx(
WS_EX_CONTROLPARENT, wc.lpszClassName, TEXT(PROJECT_NAME),
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, 600, 400, NULL, NULL, hInstance, NULL);
@@ -1816,11 +1925,14 @@ wWinMain(HINSTANCE hInstance, [[maybe_unused]] HINSTANCE hPrevInstance,
int argc = 0;
LPWSTR *argv = CommandLineToArgvW(pCmdLine, &argc);
if (argc < 2) {
show_error_message(
L"You must pass the relay address and port on the command line.");
return 1;
if (argc >= 2) {
g.host = argv[0];
g.port = argv[1];
} else if (DialogBox(hInstance, MAKEINTRESOURCE(IDD_CONNECT),
g.hwndMain, connect_proc) != IDOK) {
return 0;
}
LocalFree(argv);
// We have a few suboptimal asynchronous options:
// a) WSAAsyncGetHostByName() requires us to distinguish hostnames
@@ -1831,8 +1943,7 @@ wWinMain(HINSTANCE hInstance, [[maybe_unused]] HINSTANCE hPrevInstance,
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_TCP;
err = GetAddrInfo(argv[0], argv[1], &hints, &g.addresses);
LocalFree(argv);
err = GetAddrInfo(g.host.c_str(), g.port.c_str(), &hints, &g.addresses);
if (err) {
show_error_message(format_error_message(err).c_str());
return 1;
@@ -1845,20 +1956,43 @@ wWinMain(HINSTANCE hInstance, [[maybe_unused]] HINSTANCE hPrevInstance,
return 1;
}
if (!(g.date_change_timer = CreateWaitableTimer(NULL, FALSE, NULL)) ||
!(g.flush_event = CreateEvent(NULL, FALSE, FALSE, NULL))) {
show_error_message(format_error_message(GetLastError()).c_str());
return 1;
}
while (process_messages(accelerators)) {
HANDLE handles[] = {g.date_change_timer, g.flush_event, g.socket_event};
DWORD count = 3 - !handles[2];
DWORD result = MsgWaitForMultipleObjects(
g.event != NULL, &g.event, FALSE, INFINITE, QS_ALLINPUT);
count, handles, FALSE, INFINITE, QS_ALLINPUT);
if (result == WAIT_FAILED) {
show_error_message(format_error_message(GetLastError()).c_str());
return 1;
}
if (g.event != NULL && result == WAIT_OBJECT_0 &&
!relay_process_socket_events(error)) {
if (result >= WAIT_OBJECT_0 + count)
continue;
auto signalled = handles[result];
if (signalled == g.date_change_timer) {
bool to_bottom = buffer_at_bottom();
buffer_print_and_watch_trailing_date_changes();
if (to_bottom)
buffer_scroll_to_bottom();
}
if (signalled == g.flush_event && !relay_try_write(error)) {
show_error_message(error.c_str());
return 1;
}
if (signalled == g.socket_event && !relay_process_socket_events(error)) {
show_error_message(error.c_str());
return 1;
}
}
FreeAddrInfo(g.addresses);
WSACleanup();
CloseHandle(g.date_change_timer);
CloseHandle(g.flush_event);
return 0;
}

View File

@@ -8,4 +8,18 @@
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>

View File

@@ -1,11 +1,18 @@
#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
@@ -16,8 +23,58 @@ BEGIN
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