summaryrefslogtreecommitdiff
path: root/xW
diff options
context:
space:
mode:
Diffstat (limited to 'xW')
-rw-r--r--xW/.clang-format11
-rw-r--r--xW/CMakeLists.txt97
-rw-r--r--xW/config.h.in6
-rw-r--r--xW/xW-highlighted.svg24
-rw-r--r--xW/xW-resources.h12
-rw-r--r--xW/xW.cpp1864
-rw-r--r--xW/xW.manifest11
-rw-r--r--xW/xW.rc23
-rw-r--r--xW/xW.svg24
9 files changed, 2072 insertions, 0 deletions
diff --git a/xW/.clang-format b/xW/.clang-format
new file mode 100644
index 0000000..025ef20
--- /dev/null
+++ b/xW/.clang-format
@@ -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
diff --git a/xW/CMakeLists.txt b/xW/CMakeLists.txt
new file mode 100644
index 0000000..eb374b9
--- /dev/null
+++ b/xW/CMakeLists.txt
@@ -0,0 +1,97 @@
+# 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>")
+
+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.
+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 ()
+
+# Rasterize SVG icons
+set (icon_ico_list)
+foreach (icon xW xW-highlighted)
+ set (icon_png_list)
+ foreach (icon_size 16 32 48 256)
+ 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 ()
+ set (icon_ico ${PROJECT_BINARY_DIR}/${icon}.ico)
+ icon_for_win32 ("${icon_png_list}" ${icon_ico})
+ list (APPEND icon_ico_list ${icon_ico})
+endforeach ()
+
+set_property (SOURCE xW.rc APPEND PROPERTY OBJECT_DEPENDS ${icon_ico_list})
+
+# 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}
+ -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)
+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)
diff --git a/xW/config.h.in b/xW/config.h.in
new file mode 100644
index 0000000..2bfe8fc
--- /dev/null
+++ b/xW/config.h.in
@@ -0,0 +1,6 @@
+#ifndef CONFIG_H
+#define CONFIG_H
+
+#define PROGRAM_VERSION "${project_version}"
+
+#endif // ! CONFIG_H
diff --git a/xW/xW-highlighted.svg b/xW/xW-highlighted.svg
new file mode 100644
index 0000000..cdd8105
--- /dev/null
+++ b/xW/xW-highlighted.svg
@@ -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>
diff --git a/xW/xW-resources.h b/xW/xW-resources.h
new file mode 100644
index 0000000..caa37ad
--- /dev/null
+++ b/xW/xW-resources.h
@@ -0,0 +1,12 @@
+#define IDI_ICON 1
+#define IDI_HIGHLIGHTED 2
+#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
diff --git a/xW/xW.cpp b/xW/xW.cpp
new file mode 100644
index 0000000..bbd208a
--- /dev/null
+++ b/xW/xW.cpp
@@ -0,0 +1,1864 @@
+/*
+ * xW.cpp: Win32 frontend for xC
+ *
+ * Copyright (c) 2023, Přemysl Eric Janouch <p@janouch.name>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+ * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
+ * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+ * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ *
+ */
+
+#include "xC-proto.cpp"
+#include "xW-resources.h"
+
+#include <winsock2.h>
+#include <ws2tcpip.h>
+
+#define WIN32_LEAN_AND_MEAN
+#include <windows.h>
+#include <commctrl.h>
+#include <richedit.h>
+#undef ERROR
+#undef REGISTERED
+
+#include <algorithm>
+#include <clocale>
+#include <ctime>
+#include <functional>
+#include <map>
+#include <string>
+
+struct Server {
+ Relay::ServerState state = {};
+ std::wstring user;
+ std::wstring user_modes;
+};
+
+struct BufferLineItem {
+ CHARFORMAT2 format = {};
+ std::wstring text;
+};
+
+struct BufferLine {
+ /// Leaked from another buffer, but temporarily staying in another one.
+ bool leaked = {};
+
+ bool is_unimportant = {};
+ bool is_highlight = {};
+ Relay::Rendition rendition = {};
+ uint64_t when = {};
+ std::vector<BufferLineItem> items;
+};
+
+struct Buffer {
+ std::wstring buffer_name;
+ bool hide_unimportant = {};
+ Relay::BufferKind kind = {};
+ std::wstring server_name;
+ std::vector<BufferLine> lines;
+
+ // Channel:
+
+ std::vector<BufferLineItem> topic;
+ std::wstring modes;
+
+ // Stats:
+
+ uint32_t new_messages = {};
+ uint32_t new_unimportant_messages = {};
+ bool highlighted = {};
+
+ // Input:
+
+ std::wstring input;
+ DWORD input_start = {};
+ DWORD input_end = {};
+ std::vector<std::wstring> history;
+ size_t history_at = {};
+};
+
+using Callback = std::function<
+ void(std::wstring error, const Relay::ResponseData *response)>;
+
+struct {
+ HWND hwndMain; ///< Main program window
+ HWND hwndTopic; ///< static: channel topic
+ HWND hwndBufferList; ///< listbox: buffer list
+ HWND hwndBuffer; ///< richedit: buffer backlog
+ HWND hwndBufferLog; ///< edit: buffer log
+ HWND hwndPrompt; ///< static: user name, etc.
+ HWND hwndStatus; ///< static: buffer name, etc.
+ HWND hwndInput; ///< edit: user input
+
+ HWND hwndLastFocused; ///< For Alt+Tab, e.g.
+
+ HICON hicon; ///< Normal program icon
+ HICON hiconHighlighted; ///< Highlighted program icon
+
+ HFONT hfont; ///< Normal variant of the UI font
+ HFONT hfontBold; ///< Bold variant of the UI font
+
+ LOGFONT fontlog; ///< UI font characteristics
+ LONG font_height; ///< UI font height in pixels
+
+ // Networking:
+
+ addrinfoW *addresses; ///< GetAddrInfo() result
+ addrinfoW *addresses_iterator; ///< Currently processed address
+ SOCKET socket; ///< Relay socket
+ WSAEVENT event; ///< Relay socket event
+ std::vector<uint8_t> write_buffer; ///< Write buffer
+ std::vector<uint8_t> read_buffer; ///< Read buffer
+
+ // Relay protocol:
+
+ uint32_t command_seq; ///< Outgoing message counter
+
+ std::map<uint32_t, Callback> command_callbacks;
+
+ std::vector<Buffer> buffers; ///< List of all buffers
+ std::wstring buffer_current; ///< Current buffer name or ""
+ std::wstring buffer_last; ///< Previous buffer name or ""
+
+ std::map<std::wstring, Server> servers;
+} g;
+
+static void
+show_error_message(const wchar_t *message)
+{
+ MessageBox(g.hwndMain, message, NULL, MB_ICONERROR | MB_OK | MB_APPLMODAL);
+}
+
+static std::wstring
+format_error_message(int err)
+{
+ wchar_t *message = NULL;
+ if (!FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER |
+ FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
+ NULL, err, 0, (LPWSTR) &message, 0, NULL))
+ return std::to_wstring(err);
+
+ std::wstring copy = message;
+ LocalFree(message);
+ return copy;
+}
+
+// --- Networking --------------------------------------------------------------
+
+static bool
+relay_try_read(std::wstring &error)
+{
+ auto &r = g.read_buffer;
+ char buffer[8192] = {};
+ int err = {};
+ while (true) {
+ int n = recv(g.socket, buffer, sizeof buffer, 0);
+ if (!n) {
+ error = L"Server closed the connection.";
+ return false;
+ } else if (n != SOCKET_ERROR) {
+ r.insert(r.end(), buffer, buffer + n);
+ } else if ((err = WSAGetLastError()) != WSAEWOULDBLOCK) {
+ error = format_error_message(err);
+ return false;
+ } else {
+ break;
+ }
+ }
+ return true;
+}
+
+static bool
+relay_try_write(std::wstring &error)
+{
+ auto &w = g.write_buffer;
+ int err = {};
+ while (!w.empty()) {
+ int n = send(g.socket,
+ reinterpret_cast<const char *>(w.data()), w.size(), 0);
+ if (n != SOCKET_ERROR) {
+ w.erase(w.begin(), w.begin() + n);
+ } else if ((err = WSAGetLastError()) != WSAEWOULDBLOCK) {
+ error = format_error_message(err);
+ return false;
+ } else {
+ break;
+ }
+ }
+ return true;
+}
+
+static void
+relay_send(Relay::CommandData *data, Callback callback = {})
+{
+ Relay::CommandMessage m = {};
+ m.command_seq = ++g.command_seq;
+ m.data.reset(data);
+ LibertyXDR::Writer w;
+ m.serialize(w);
+
+ if (callback)
+ g.command_callbacks[m.command_seq] = std::move(callback);
+
+ uint32_t len = htonl(w.data.size());
+ uint8_t *prefix = reinterpret_cast<uint8_t *>(&len);
+ 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());
+}
+
+// --- Buffers -----------------------------------------------------------------
+
+static Buffer *
+buffer_by_name(const std::wstring &name)
+{
+ for (auto &b : g.buffers)
+ if (b.buffer_name == name)
+ return &b;
+ return nullptr;
+}
+
+static void
+buffer_activate(const std::wstring &name)
+{
+ auto activate = new Relay::CommandData_BufferActivate();
+ activate->buffer_name = name;
+ relay_send_now(activate);
+}
+
+static void
+buffer_toggle_unimportant(const std::wstring &name)
+{
+ auto toggle = new Relay::CommandData_BufferToggleUnimportant();
+ toggle->buffer_name = name;
+ relay_send_now(toggle);
+}
+
+// --- Current buffer ----------------------------------------------------------
+
+static void
+buffer_toggle_log(
+ const std::wstring &error, const Relay::ResponseData_BufferLog *response)
+{
+ if (!response) {
+ show_error_message(error.c_str());
+ return;
+ }
+
+ std::wstring log;
+ if (!LibertyXDR::utf8_to_wstring(
+ response->log.data(), response->log.size(), log)) {
+ show_error_message(L"Invalid encoding.");
+ return;
+ }
+
+ std::wstring filtered;
+ for (auto wch : log) {
+ if (wch == L'\n')
+ filtered += L"\r\n";
+ else
+ filtered += wch;
+ }
+
+ SetWindowText(g.hwndBufferLog, filtered.c_str());
+ ShowWindow(g.hwndBuffer, SW_HIDE);
+ ShowWindow(g.hwndBufferLog, SW_SHOW);
+}
+
+static void
+buffer_toggle_log()
+{
+ if (IsWindowVisible(g.hwndBufferLog)) {
+ ShowWindow(g.hwndBufferLog, SW_HIDE);
+ ShowWindow(g.hwndBuffer, SW_SHOW);
+ SetWindowText(g.hwndBufferLog, L"");
+ return;
+ }
+
+ auto log = new Relay::CommandData_BufferLog();
+ log->buffer_name = g.buffer_current;
+ relay_send_now(log, [name = g.buffer_current](auto error, auto response) {
+ if (g.buffer_current != name)
+ return;
+ buffer_toggle_log(error,
+ dynamic_cast<const Relay::ResponseData_BufferLog *>(response));
+ });
+}
+
+static bool
+buffer_at_bottom()
+{
+ // It is created with this style, and should retain it indefinitely,
+ // however this check works. It is necessary because when richedit
+ // hides its scrollbar, it does not bother resetting its values.
+ if (!(GetWindowLong(g.hwndBuffer, GWL_STYLE) & WS_VSCROLL))
+ return true;
+
+ SCROLLINFO si = {};
+ si.cbSize = sizeof si;
+ si.fMask = SIF_ALL;
+ GetScrollInfo(g.hwndBuffer, SB_VERT, &si);
+ return si.nPos + (int) si.nPage >= si.nMax;
+}
+
+static void
+buffer_scroll_to_bottom()
+{
+ SendMessage(g.hwndBuffer, EM_SCROLL, SB_BOTTOM, 0);
+}
+
+// --- UI state refresh --------------------------------------------------------
+
+static void
+refresh_icon()
+{
+ HICON icon = g.hicon;
+ for (const auto &b : g.buffers)
+ if (b.highlighted)
+ icon = g.hiconHighlighted;
+
+ SendMessage(g.hwndMain, WM_SETICON, ICON_SMALL, (LPARAM) icon);
+ SendMessage(g.hwndMain, WM_SETICON, ICON_BIG, (LPARAM) icon);
+}
+
+static void
+richedit_replacesel(HWND hWnd, const CHARFORMAT2 *cf, const wchar_t *text)
+{
+ SendMessage(hWnd, EM_SETCHARFORMAT, SCF_SELECTION, (LPARAM) cf);
+ SendMessage(hWnd, EM_REPLACESEL, FALSE, (LPARAM) text);
+}
+
+static void
+refresh_topic(const std::vector<BufferLineItem> &topic)
+{
+ SetWindowText(g.hwndTopic, L"");
+ for (const auto &it : topic)
+ richedit_replacesel(g.hwndTopic, &it.format, it.text.c_str());
+}
+
+static void
+refresh_buffer_list()
+{
+ InvalidateRect(g.hwndBufferList, NULL, TRUE);
+}
+
+static std::wstring
+server_state_to_string(Relay::ServerState state)
+{
+ switch (state) {
+ case Relay::ServerState::DISCONNECTED: return L"disconnected";
+ case Relay::ServerState::CONNECTING: return L"connecting";
+ case Relay::ServerState::CONNECTED: return L"connected";
+ case Relay::ServerState::REGISTERED: return L"registered";
+ case Relay::ServerState::DISCONNECTING: return L"disconnecting";
+ }
+ return {};
+}
+
+static void
+refresh_prompt()
+{
+ std::wstring prompt;
+ auto b = buffer_by_name(g.buffer_current);
+ if (!b) {
+ prompt = L"Synchronizing...";
+ } else if (auto server = g.servers.find(b->server_name);
+ server != g.servers.end()) {
+ prompt = server->second.user;
+ if (!server->second.user_modes.empty())
+ prompt += L"(" + server->second.user_modes + L")";
+ if (prompt.empty())
+ prompt = L"(" + server_state_to_string(server->second.state) + L")";
+ }
+ SetWindowText(g.hwndPrompt, prompt.c_str());
+}
+
+static void
+refresh_status()
+{
+ std::wstring status;
+ if (!buffer_at_bottom())
+ status += L"🡇 ";
+
+ status += g.buffer_current;
+ auto b = buffer_by_name(g.buffer_current);
+ if (b) {
+ if (!b->modes.empty())
+ status += L"(+" + b->modes + L")";
+ if (b->hide_unimportant)
+ status += L"<H>";
+ }
+
+ // 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)
+ SetWindowText(g.hwndStatus, status.c_str());
+}
+
+// --- Rich Edit formatting ----------------------------------------------------
+
+static COLORREF
+convert_color(int16_t color)
+{
+ static const uint16_t base16[] = {
+ 0x000, 0x800, 0x080, 0x880, 0x008, 0x808, 0x088, 0xccc,
+ 0x888, 0xf00, 0x0f0, 0xff0, 0x00f, 0xf0f, 0x0ff, 0xfff,
+ };
+ if (color < 16) {
+ uint8_t r = 0xf & (base16[color] >> 8);
+ uint8_t g = 0xf & (base16[color] >> 4);
+ uint8_t b = 0xf & (base16[color]);
+ return RGB(r * 0x11, g * 0x11, b * 0x11);
+ }
+ if (color >= 216) {
+ uint8_t g = 8 + (color - 216) * 10;
+ return RGB(g, g, g);
+ }
+
+ uint8_t i = color - 16, r = i / 36 >> 0, g = (i / 6 >> 0) % 6, b = i % 6;
+ return RGB(
+ !r ? 0 : 55 + 40 * r,
+ !g ? 0 : 55 + 40 * g,
+ !b ? 0 : 55 + 40 * b);
+}
+
+static CHARFORMAT2
+default_charformat()
+{
+ // Everything we leave out will be kept as it was.
+ // So, e.g., there is no way to "unset" a monospace font.
+ CHARFORMAT2 reset = {};
+ reset.cbSize = sizeof reset;
+ reset.dwMask = CFM_BOLD | CFM_ITALIC | CFM_UNDERLINE | CFM_STRIKEOUT |
+ CFM_COLOR | CFM_BACKCOLOR | CFM_FACE | CFM_LINK;
+ reset.dwEffects = CFE_AUTOCOLOR | CFE_AUTOBACKCOLOR;
+ lstrcpyn(reset.szFaceName, g.fontlog.lfFaceName, sizeof reset.szFaceName);
+ return reset;
+}
+
+static void
+convert_item_formatting(Relay::ItemData *item, CHARFORMAT2 &cf, bool &inverse)
+{
+ if (dynamic_cast<Relay::ItemData_Reset *>(item)) {
+ cf = default_charformat();
+ inverse = false;
+ } else if (dynamic_cast<Relay::ItemData_FlipBold *>(item)) {
+ cf.dwEffects ^= CFE_BOLD;
+ } else if (dynamic_cast<Relay::ItemData_FlipItalic *>(item)) {
+ cf.dwEffects ^= CFE_ITALIC;
+ } else if (dynamic_cast<Relay::ItemData_FlipUnderline *>(item)) {
+ cf.dwEffects ^= CFE_UNDERLINE;
+ } else if (dynamic_cast<Relay::ItemData_FlipCrossedOut *>(item)) {
+ cf.dwEffects ^= CFE_STRIKEOUT;
+ } else if (dynamic_cast<Relay::ItemData_FlipInverse *>(item)) {
+ inverse = !inverse;
+ } else if (dynamic_cast<Relay::ItemData_FlipMonospace *>(item)) {
+ auto reset = default_charformat();
+ const auto face = !lstrcmp(cf.szFaceName, reset.szFaceName)
+ ? L"Courier New"
+ : reset.szFaceName;
+ lstrcpyn(cf.szFaceName, face, sizeof cf.szFaceName);
+ } else if (auto data = dynamic_cast<Relay::ItemData_FgColor *>(item)) {
+ if (data->color < 0) {
+ cf.dwEffects |= CFE_AUTOCOLOR;
+ } else {
+ cf.dwEffects &= ~CFE_AUTOCOLOR;
+ cf.crTextColor = convert_color(data->color);
+ }
+ } else if (auto data = dynamic_cast<Relay::ItemData_BgColor *>(item)) {
+ if (data->color < 0) {
+ cf.dwEffects |= CFE_AUTOBACKCOLOR;
+ } else {
+ cf.dwEffects &= ~CFE_AUTOBACKCOLOR;
+ cf.crBackColor = convert_color(data->color);
+ }
+ }
+}
+
+static std::vector<BufferLineItem>
+convert_items(std::vector<std::unique_ptr<Relay::ItemData>> &items)
+{
+ CHARFORMAT2 cf = default_charformat();
+ std::vector<BufferLineItem> result;
+ bool inverse = false;
+ for (const auto &it : items) {
+ auto text = dynamic_cast<Relay::ItemData_Text *>(it.get());
+ if (!text) {
+ convert_item_formatting(it.get(), cf, inverse);
+ continue;
+ }
+
+ BufferLineItem item = {};
+ item.format = cf;
+ item.text = text->text;
+ if (inverse) {
+ std::swap(item.format.crTextColor, item.format.crBackColor);
+ item.format.dwEffects &= ~(CFE_AUTOCOLOR | CFE_AUTOBACKCOLOR);
+ if (cf.dwEffects & CFE_AUTOCOLOR)
+ item.format.crBackColor = GetSysColor(COLOR_WINDOWTEXT);
+ if (cf.dwEffects & CFE_AUTOBACKCOLOR)
+ item.format.crTextColor = GetSysColor(COLOR_WINDOW);
+ }
+ result.push_back(std::move(item));
+ }
+ return result;
+}
+
+// --- Buffer output -----------------------------------------------------------
+
+static BufferLine
+convert_buffer_line(Relay::EventData_BufferLine &line)
+{
+ BufferLine self = {};
+ self.items = convert_items(line.items);
+ self.is_unimportant = line.is_unimportant;
+ self.is_highlight = line.is_highlight;
+ self.rendition = line.rendition;
+ self.when = line.when;
+ return self;
+}
+
+static void
+buffer_print_line(std::vector<BufferLine>::const_iterator begin,
+ std::vector<BufferLine>::const_iterator line)
+{
+ CHARRANGE cr = {};
+ cr.cpMin = cr.cpMax = GetWindowTextLength(g.hwndBuffer);
+ SendMessage(g.hwndBuffer, EM_EXSETSEL, 0, (LPARAM) &cr);
+
+ // 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;
+
+ time_t current_unix = line->when / 1000;
+ time_t last_unix = (line != begin)
+ ? (line - 1)->when / 1000
+ : time(NULL);
+
+ 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" ");
+ }
+
+ // Tabstops won't quite help us here, since we need it centred.
+ std::wstring prefix;
+ CHARFORMAT2 pcf = default_charformat();
+ lstrcpyn(pcf.szFaceName, L"Courier New", sizeof pcf.szFaceName);
+ // This looks better, but it may trigger a repaint bug in richedit.
+#if 1
+ pcf.dwEffects |= CFE_BOLD;
+#endif
+ switch (line->rendition) {
+ break; case Relay::Rendition::BARE:
+ break; case Relay::Rendition::INDENT:
+ prefix = L" ";
+ break; case Relay::Rendition::STATUS:
+ prefix = L" - ";
+ break; case Relay::Rendition::ERROR:
+ prefix = L"=!= ";
+ pcf.dwEffects &= ~CFE_AUTOCOLOR;
+ pcf.crTextColor = RGB(0xff, 0, 0);
+ break; case Relay::Rendition::JOIN:
+ prefix = L"——> ";
+ pcf.dwEffects &= ~CFE_AUTOCOLOR;
+ pcf.crTextColor = RGB(0, 0x88, 0);
+ break; case Relay::Rendition::PART:
+ prefix = L"<—— ";
+ pcf.dwEffects &= ~CFE_AUTOCOLOR;
+ pcf.crTextColor = RGB(0x88, 0, 0);
+ break; case Relay::Rendition::ACTION:
+ prefix = L" * ";
+ pcf.dwEffects &= ~CFE_AUTOCOLOR;
+ pcf.crTextColor = RGB(0x88, 0, 0);
+ }
+
+ if (line->leaked) {
+ pcf.dwEffects &= ~CFE_AUTOCOLOR;
+ pcf.crTextColor = GetSysColor(COLOR_GRAYTEXT);
+ 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());
+ } else {
+ if (!prefix.empty())
+ richedit_replacesel(g.hwndBuffer, &pcf, prefix.c_str());
+ for (const auto &it : line->items)
+ richedit_replacesel(g.hwndBuffer, &it.format, it.text.c_str());
+ }
+}
+
+static void
+buffer_print_separator()
+{
+ CHARFORMAT2 format = default_charformat();
+ format.dwEffects &= ~CFE_AUTOCOLOR;
+ format.crTextColor = RGB(0xff, 0x5f, 0x00);
+ richedit_replacesel(g.hwndBuffer, &format, L"\n---");
+}
+
+static void
+refresh_buffer(const Buffer &b)
+{
+ HCURSOR oldCursor = SetCursor(LoadCursor(NULL, IDC_WAIT));
+ SendMessage(g.hwndBuffer, WM_SETREDRAW, (WPARAM) FALSE, 0);
+ SetWindowText(g.hwndBuffer, L"");
+
+ // PFM_OFFSET could also be used, but the result isn't very nice.
+ //
+ // PFM_BORDER is not implemented, at most we can try to construct
+ // an OLE object the width of the screen and see how it clips
+ // (this is a lot of code).
+ size_t i = 0, mark_before = b.lines.size() -
+ b.new_messages - b.new_unimportant_messages;
+ for (auto line = b.lines.begin(); line != b.lines.end(); ++line) {
+ if (i == mark_before)
+ buffer_print_separator();
+ if (!line->is_unimportant || !b.hide_unimportant)
+ buffer_print_line(b.lines.begin(), line);
+
+ i++;
+ }
+
+ buffer_scroll_to_bottom();
+
+ SendMessage(g.hwndBuffer, WM_SETREDRAW, (WPARAM) TRUE, 0);
+ InvalidateRect(g.hwndBuffer, NULL, TRUE);
+ SetCursor(oldCursor);
+}
+
+// --- Event processing --------------------------------------------------------
+
+static void
+relay_process_buffer_line(Buffer &b, Relay::EventData_BufferLine &m)
+{
+ // Initial sync: skip all other processing, let highlights be.
+ auto bc = buffer_by_name(g.buffer_current);
+ if (!bc) {
+ b.lines.push_back(convert_buffer_line(m));
+ return;
+ }
+
+ // Retained mode is complicated.
+ bool display = (!m.is_unimportant || !bc->hide_unimportant) &&
+ (b.buffer_name == g.buffer_current || m.leak_to_active);
+ bool to_bottom = display &&
+ buffer_at_bottom();
+ bool visible = display &&
+ to_bottom &&
+ !IsIconic(g.hwndMain) &&
+ !IsWindowVisible(g.hwndBufferLog);
+ bool separate = display &&
+ !visible && !bc->new_messages && !bc->new_unimportant_messages;
+
+ auto line = b.lines.insert(b.lines.end(), convert_buffer_line(m));
+ if (!(visible || m.leak_to_active) ||
+ b.new_messages || b.new_unimportant_messages) {
+ if (line->is_unimportant || m.leak_to_active)
+ b.new_unimportant_messages++;
+ else
+ b.new_messages++;
+ }
+
+ if (m.leak_to_active) {
+ auto line = bc->lines.insert(bc->lines.end(), convert_buffer_line(m));
+ line->leaked = true;
+ if (!visible || bc->new_messages || bc->new_unimportant_messages) {
+ if (line->is_unimportant)
+ bc->new_unimportant_messages++;
+ else
+ bc->new_messages++;
+ }
+ }
+ if (separate)
+ buffer_print_separator();
+ if (display)
+ buffer_print_line(bc->lines.begin(), bc->lines.end() - 1);
+ if (to_bottom)
+ buffer_scroll_to_bottom();
+
+ 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);
+
+ if (!visible) {
+ b.highlighted = true;
+ refresh_icon();
+ }
+ }
+
+ refresh_buffer_list();
+}
+
+static void
+relay_process_callbacks(uint32_t command_seq,
+ const std::wstring& error, const Relay::ResponseData *response)
+{
+ auto &callbacks = g.command_callbacks;
+ auto handler = callbacks.find(command_seq);
+ if (handler == callbacks.end()) {
+ // TODO(p): Warn about an unawaited response.
+ } else {
+ if (handler->second)
+ handler->second(error, response);
+ callbacks.erase(handler);
+ }
+
+ // We don't particularly care about wraparound issues.
+ while (!callbacks.empty() && callbacks.begin()->first <= command_seq) {
+ auto front = callbacks.begin();
+ if (front->second)
+ front->second(L"No response", nullptr);
+ callbacks.erase(front);
+ }
+}
+
+static std::wstring input_get_contents();
+
+static void
+relay_process_message(const Relay::EventMessage &m)
+{
+ switch (m.data->event) {
+ case Relay::Event::ERROR:
+ {
+ auto data = dynamic_cast<Relay::EventData_Error *>(m.data.get());
+ relay_process_callbacks(data->command_seq, data->error, nullptr);
+ break;
+ }
+ case Relay::Event::RESPONSE:
+ {
+ auto data = dynamic_cast<Relay::EventData_Response *>(m.data.get());
+ relay_process_callbacks(data->command_seq, {}, data->data.get());
+ break;
+ }
+
+ case Relay::Event::PING:
+ {
+ auto pong = new Relay::CommandData_PingResponse();
+ pong->event_seq = m.event_seq;
+ relay_send(pong);
+ break;
+ }
+
+ case Relay::Event::BUFFER_LINE:
+ {
+ auto &data = dynamic_cast<Relay::EventData_BufferLine &>(*m.data);
+ auto b = buffer_by_name(data.buffer_name);
+ if (!b)
+ break;
+
+ relay_process_buffer_line(*b, data);
+ break;
+ }
+ case Relay::Event::BUFFER_UPDATE:
+ {
+ auto &data = dynamic_cast<Relay::EventData_BufferUpdate &>(*m.data);
+ auto b = buffer_by_name(data.buffer_name);
+ if (!b) {
+ b = &*g.buffers.insert(g.buffers.end(), Buffer());
+ b->buffer_name = data.buffer_name;
+ SendMessage(g.hwndBufferList, LB_ADDSTRING, 0, 0);
+ }
+
+ bool hiding_toggled = b->hide_unimportant != data.hide_unimportant;
+ b->hide_unimportant = data.hide_unimportant;
+ b->kind = data.context->kind;
+ b->server_name.clear();
+ if (auto context = dynamic_cast<Relay::BufferContext_Server *>(
+ data.context.get()))
+ b->server_name = context->server_name;
+ if (auto context = dynamic_cast<Relay::BufferContext_Channel *>(
+ data.context.get())) {
+ b->server_name = context->server_name;
+ b->modes = context->modes;
+ b->topic = convert_items(context->topic);
+ }
+ if (auto context = dynamic_cast<Relay::BufferContext_PrivateMessage *>(
+ data.context.get()))
+ b->server_name = context->server_name;
+
+ if (b->buffer_name == g.buffer_current) {
+ refresh_topic(b->topic);
+ refresh_status();
+
+ if (hiding_toggled)
+ refresh_buffer(*b);
+ }
+ break;
+ }
+ case Relay::Event::BUFFER_STATS:
+ {
+ auto &data = dynamic_cast<Relay::EventData_BufferStats &>(*m.data);
+ auto b = buffer_by_name(data.buffer_name);
+ if (!b)
+ break;
+
+ b->new_messages = data.new_messages;
+ b->new_unimportant_messages = data.new_unimportant_messages;
+ b->highlighted = data.highlighted;
+
+ refresh_icon();
+ break;
+ }
+ case Relay::Event::BUFFER_RENAME:
+ {
+ auto &data = dynamic_cast<Relay::EventData_BufferRename &>(*m.data);
+ auto b = buffer_by_name(data.buffer_name);
+ if (!b)
+ break;
+
+ b->buffer_name = data.buffer_name;
+
+ refresh_buffer_list();
+ if (b->buffer_name == g.buffer_current)
+ refresh_status();
+ break;
+ }
+ case Relay::Event::BUFFER_REMOVE:
+ {
+ auto &data = dynamic_cast<Relay::EventData_BufferRemove &>(*m.data);
+ auto b = buffer_by_name(data.buffer_name);
+ if (!b)
+ break;
+
+ int index = b - g.buffers.data();
+ SendMessage(g.hwndBufferList, LB_DELETESTRING, index, 0);
+ g.buffers.erase(g.buffers.begin() + index);
+
+ refresh_icon();
+ break;
+ }
+ case Relay::Event::BUFFER_ACTIVATE:
+ {
+ auto &data = dynamic_cast<Relay::EventData_BufferActivate &>(*m.data);
+ Buffer *old = buffer_by_name(g.buffer_current);
+ g.buffer_last = g.buffer_current;
+ g.buffer_current = data.buffer_name;
+ auto b = buffer_by_name(data.buffer_name);
+ if (!b)
+ break;
+
+ if (old) {
+ old->new_messages = 0;
+ old->new_unimportant_messages = 0;
+ old->highlighted = false;
+
+ old->input = input_get_contents();
+ SendMessage(g.hwndInput, EM_GETSEL,
+ (WPARAM) &old->input_start, (LPARAM) &old->input_end);
+
+ // Note that we effectively overwrite the newest line
+ // with the current textarea contents, and jump there.
+ old->history_at = old->history.size();
+ }
+
+ if (IsWindowVisible(g.hwndBufferLog))
+ buffer_toggle_log();
+ if (!IsIconic(g.hwndMain))
+ b->highlighted = false;
+ SendMessage(g.hwndBufferList, LB_SETCURSEL, b - g.buffers.data(), 0);
+
+ refresh_topic(b->topic);
+ refresh_buffer(*b);
+ refresh_prompt();
+ refresh_status();
+
+ SetWindowText(g.hwndInput, b->input.c_str());
+ SendMessage(g.hwndInput, EM_SETSEL,
+ (WPARAM) b->input_start, (LPARAM) b->input_end);
+ SetFocus(g.hwndInput);
+ break;
+ }
+ case Relay::Event::BUFFER_INPUT:
+ {
+ auto &data = dynamic_cast<Relay::EventData_BufferInput &>(*m.data);
+ auto b = buffer_by_name(data.buffer_name);
+ if (!b)
+ break;
+
+ if (b->history_at == b->history.size())
+ b->history_at++;
+ b->history.push_back(data.text);
+ break;
+ }
+ case Relay::Event::BUFFER_CLEAR:
+ {
+ auto &data = dynamic_cast<Relay::EventData_BufferClear &>(*m.data);
+ auto b = buffer_by_name(data.buffer_name);
+ if (!b)
+ break;
+
+ b->lines.clear();
+ if (b->buffer_name == g.buffer_current)
+ refresh_buffer(*b);
+ break;
+ }
+
+ case Relay::Event::SERVER_UPDATE:
+ {
+ auto &data = dynamic_cast<Relay::EventData_ServerUpdate &>(*m.data);
+ if (!g.servers.count(data.server_name))
+ g.servers.emplace(data.server_name, Server());
+
+ auto &server = g.servers.at(data.server_name);
+ server.state = data.data->state;
+
+ server.user.clear();
+ server.user_modes.clear();
+ if (auto registered = dynamic_cast<Relay::ServerData_Registered *>(
+ data.data.get())) {
+ server.user = registered->user;
+ server.user_modes = registered->user_modes;
+ }
+
+ refresh_prompt();
+ break;
+ }
+ case Relay::Event::SERVER_RENAME:
+ {
+ auto &data = dynamic_cast<Relay::EventData_ServerRename &>(*m.data);
+ g.servers.insert_or_assign(data.new_, g.servers.at(data.server_name));
+ g.servers.erase(data.server_name);
+ break;
+ }
+ case Relay::Event::SERVER_REMOVE:
+ {
+ auto &data = dynamic_cast<Relay::EventData_ServerRemove &>(*m.data);
+ g.servers.erase(data.server_name);
+ break;
+ }
+ }
+}
+
+// --- Networking --------------------------------------------------------------
+
+static bool
+relay_process_buffer(std::wstring &error)
+{
+ auto &b = g.read_buffer;
+ size_t offset = 0;
+ while (true) {
+ LibertyXDR::Reader r;
+ r.data = b.data() + offset;
+ r.length = b.size() - offset;
+
+ uint32_t frame_len = 0;
+ if (!r.read(frame_len))
+ break;
+
+ r.length = std::min<size_t>(r.length, frame_len);
+ if (r.length < frame_len)
+ break;
+
+ Relay::EventMessage m = {};
+ if (!m.deserialize(r) || r.length) {
+ error = L"Deserialization failed.";
+ return false;
+ }
+
+ relay_process_message(m);
+ offset += sizeof frame_len + frame_len;
+ }
+
+ b.erase(b.begin(), b.begin() + offset);
+ return true;
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static void
+relay_destroy_socket()
+{
+ closesocket(g.socket);
+ g.socket = INVALID_SOCKET;
+ WSACloseEvent(g.event);
+ g.event = NULL;
+
+ g.read_buffer.clear();
+ g.write_buffer.clear();
+}
+
+static bool
+relay_connect_step(std::wstring& error)
+{
+ addrinfoW *&p = g.addresses_iterator;
+ while (p) {
+ g.socket = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
+ if (g.socket != INVALID_SOCKET)
+ break;
+ p = p->ai_next;
+ }
+ if (!p) {
+ error = L"Failed to create a socket.";
+ return false;
+ }
+
+ g.event = WSACreateEvent();
+ if (WSAEventSelect(g.socket, g.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))
+ error = L"Connection succeeded unexpectedly early.";
+ else if (int err = WSAGetLastError(); err != WSAEWOULDBLOCK)
+ error = format_error_message(err);
+ else
+ return true;
+
+ relay_destroy_socket();
+ return false;
+}
+
+static bool
+relay_process_connect_event(int err, std::wstring &error)
+{
+ addrinfoW *&p = g.addresses_iterator;
+ if (err) {
+ relay_destroy_socket();
+ if (!(p = p->ai_next)) {
+ error = L"Connection failed.";
+ return false;
+ }
+ return relay_connect_step(error);
+ }
+
+ g.read_buffer.clear();
+ g.write_buffer.clear();
+
+ auto hello = new Relay::CommandData_Hello();
+ hello->version = Relay::VERSION;
+ relay_send(hello);
+ // The message will be flushed at the upcoming FD_WRITE notification.
+ return true;
+}
+
+static bool
+relay_process_socket_event(int event, int err, std::wstring &error)
+{
+ if (err) {
+ relay_destroy_socket();
+ error = format_error_message(err);
+ return false;
+ }
+
+ switch (event) {
+ case FD_READ:
+ if (!relay_try_read(error) ||
+ !relay_process_buffer(error) ||
+ !relay_try_write(error))
+ return false;
+ break;
+ case FD_WRITE:
+ if (!relay_try_write(error))
+ return false;
+ break;
+ case FD_CLOSE:
+ // Handling this seems excessive, since we also get EOF while reading.
+ // But we may not receive an FD_READ notification for it.
+ error = L"Connection closed.";
+ return false;
+ }
+ return true;
+}
+
+static bool
+relay_process_socket_events(std::wstring &error)
+{
+ WSANETWORKEVENTS wne = {};
+ if (WSAEnumNetworkEvents(g.socket, g.event, &wne)) {
+ error = format_error_message(WSAGetLastError());
+ return false;
+ }
+
+ // TODO(p): Offer reconnecting.
+ // TODO(p): Consider disabling UI controls while disconnected.
+ if (wne.lNetworkEvents & FD_CONNECT &&
+ !relay_process_connect_event(wne.iErrorCode[FD_CONNECT_BIT], error))
+ return false;
+
+ for (auto bit : {FD_READ_BIT, FD_WRITE_BIT, FD_CLOSE_BIT})
+ if ((wne.lNetworkEvents & (1 << bit)) &&
+ !relay_process_socket_event((1 << bit), wne.iErrorCode[bit], error))
+ return false;
+ return true;
+}
+
+// --- 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)
+{
+ SetWindowText(g.hwndInput, input.c_str());
+ if (input.size())
+ SendMessage(g.hwndInput, EM_SETSEL, input.size(), input.size());
+}
+
+static bool
+input_submit()
+{
+ auto b = buffer_by_name(g.buffer_current);
+ if (!b)
+ return false;
+
+ auto input = new Relay::CommandData_BufferInput();
+ input->buffer_name = b->buffer_name;
+ input->text = input_get_contents();
+
+ // Buffer::history[Buffer::history.size()] is virtual,
+ // and is represented either by edit contents when it's currently
+ // being edited, or by Buffer::input in all other cases.
+ b->history.push_back(input->text);
+ b->history_at = b->history.size();
+ input_set_contents({});
+
+ relay_send_now(input);
+ return true;
+}
+
+struct InputStamp {
+ DWORD start = {};
+ DWORD end = {};
+ std::wstring input;
+};
+
+static InputStamp
+input_stamp()
+{
+ DWORD start = {}, end = {};
+ SendMessage(g.hwndInput, EM_GETSEL, (WPARAM) &start, (LPARAM) &end);
+ return {start, end, input_get_contents()};
+}
+
+static void
+input_complete(const InputStamp &state, const std::wstring &error,
+ const Relay::ResponseData_BufferComplete *response)
+{
+ if (!response) {
+ show_error_message(error.c_str());
+ return;
+ }
+
+ std::string utf8;
+ if (!LibertyXDR::wstring_to_utf8(state.input.substr(0, state.start), utf8))
+ return;
+ std::wstring preceding;
+ if (!LibertyXDR::utf8_to_wstring(
+ reinterpret_cast<const uint8_t *>(utf8.c_str()), response->start,
+ preceding))
+ return;
+
+ if (response->completions.size() > 0) {
+ auto insert = response->completions.at(0);
+ if (response->completions.size() == 1)
+ insert += L" ";
+
+ SendMessage(g.hwndInput, EM_SETSEL, preceding.length(), state.end);
+ 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);
+
+ // TODO(p): Show all completion options.
+}
+
+static bool
+input_complete()
+{
+ // TODO(p): Also add an increasing counter to the stamp.
+ auto state = input_stamp();
+ if (state.start != state.end)
+ return false;
+
+ std::string utf8;
+ if (!LibertyXDR::wstring_to_utf8(state.input.substr(0, state.start), utf8))
+ return false;
+
+ auto complete = new Relay::CommandData_BufferComplete();
+ complete->buffer_name = g.buffer_current;
+ complete->text = state.input;
+ complete->position = utf8.length();
+ relay_send_now(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))
+ return;
+ input_complete(stamp, error,
+ dynamic_cast<const Relay::ResponseData_BufferComplete *>(response));
+ });
+ return true;
+}
+
+static boolean
+input_wants(const MSG *message)
+{
+ switch (message->message) {
+ case WM_KEYDOWN:
+ // Shift-Tab can go to the dialog manager.
+ return message->wParam == VK_RETURN ||
+ (message->wParam == VK_TAB && !(GetKeyState(VK_SHIFT) & 0x8000));
+ case WM_SYSCHAR:
+ switch (message->wParam) {
+ case 'p': return true;
+ case 'n': return true;
+ }
+ }
+ return false;
+}
+
+static LRESULT CALLBACK
+input_proc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam,
+ UINT_PTR uIdSubclass, [[maybe_unused]] DWORD_PTR dwRefData)
+{
+ switch (uMsg) {
+ case WM_GETDLGCODE:
+ {
+ // https://devblogs.microsoft.com/oldnewthing/20070627-00/?p=26243
+ LRESULT lResult = DefSubclassProc(hWnd, uMsg, wParam, lParam);
+ if (lParam && input_wants((MSG *) lParam))
+ lResult |= DLGC_WANTMESSAGE;
+ 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;
+ }
+ 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;
+ }
+ }
+ break;
+ }
+ case WM_KEYDOWN:
+ {
+ HWND scrollable = IsWindowVisible(g.hwndBufferLog)
+ ? g.hwndBufferLog
+ : g.hwndBuffer;
+
+ switch (wParam) {
+ case VK_PRIOR:
+ SendMessage(scrollable, EM_SCROLL, SB_PAGEUP, 0);
+ return 0;
+ case VK_NEXT:
+ SendMessage(scrollable, EM_SCROLL, SB_PAGEDOWN, 0);
+ return 0;
+ }
+ break;
+ }
+ case WM_CHAR:
+ {
+ // This could be implemented more precisely, but it will do.
+ relay_send_now(new Relay::CommandData_Active());
+
+ switch (wParam) {
+ case VK_RETURN:
+ if (!input_submit())
+ break;
+ return 0;
+ case VK_TAB:
+ if (!input_complete())
+ break;
+ return 0;
+ }
+ break;
+ }
+ case WM_NCDESTROY:
+ // https://devblogs.microsoft.com/oldnewthing/20031111-00/?p=41883
+ RemoveWindowSubclass(hWnd, input_proc, uIdSubclass);
+ break;
+ }
+ return DefSubclassProc(hWnd, uMsg, wParam, lParam);
+}
+
+// --- General UI --------------------------------------------------------------
+
+static LRESULT CALLBACK
+bufferlist_proc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam,
+ UINT_PTR uIdSubclass, [[maybe_unused]] DWORD_PTR dwRefData)
+{
+ switch (uMsg) {
+ case WM_MBUTTONUP:
+ {
+ POINT p = {LOWORD(lParam), HIWORD(lParam)};
+ ClientToScreen(hWnd, &p);
+ int index = LBItemFromPt(hWnd, p, FALSE);
+ if (wParam || index < 0 || (size_t) index > g.buffers.size())
+ break;
+
+ 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);
+ return 0;
+ }
+ case WM_NCDESTROY:
+ // https://devblogs.microsoft.com/oldnewthing/20031111-00/?p=41883
+ RemoveWindowSubclass(hWnd, bufferlist_proc, uIdSubclass);
+ break;
+ }
+ return DefSubclassProc(hWnd, uMsg, wParam, lParam);
+}
+
+static LRESULT CALLBACK
+richedit_proc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam,
+ UINT_PTR uIdSubclass, [[maybe_unused]] DWORD_PTR dwRefData)
+{
+ switch (uMsg) {
+ case WM_GETDLGCODE:
+ {
+ // https://devblogs.microsoft.com/oldnewthing/20070627-00/?p=26243
+ LRESULT lResult = DefSubclassProc(hWnd, uMsg, wParam, lParam);
+ lResult &= ~DLGC_WANTTAB;
+ if (lParam &&
+ ((MSG *) lParam)->message == WM_KEYDOWN &&
+ ((MSG *) lParam)->wParam == VK_TAB)
+ lResult &= ~DLGC_WANTMESSAGE;
+ return lResult;
+ }
+ case WM_VSCROLL:
+ {
+ // Dragging the scrollbar doesn't result in EN_VSCROLL.
+ LRESULT lResult = DefSubclassProc(hWnd, uMsg, wParam, lParam);
+ refresh_status();
+ return lResult;
+ }
+ case WM_NCDESTROY:
+ // https://devblogs.microsoft.com/oldnewthing/20031111-00/?p=41883
+ RemoveWindowSubclass(hWnd, richedit_proc, uIdSubclass);
+ break;
+ }
+ return DefSubclassProc(hWnd, uMsg, wParam, lParam);
+}
+
+static LRESULT CALLBACK
+log_proc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam,
+ UINT_PTR uIdSubclass, [[maybe_unused]] DWORD_PTR dwRefData)
+{
+ switch (uMsg) {
+ case WM_GETDLGCODE:
+ {
+ // https://devblogs.microsoft.com/oldnewthing/20070627-00/?p=26243
+ // https://devblogs.microsoft.com/oldnewthing/20031114-00/?p=41823
+ LRESULT lResult = DefSubclassProc(hWnd, uMsg, wParam, lParam);
+ lResult &= ~(DLGC_WANTTAB | DLGC_HASSETSEL);
+ if (lParam &&
+ ((MSG *) lParam)->message == WM_KEYDOWN &&
+ ((MSG *) lParam)->wParam == VK_TAB)
+ lResult &= ~DLGC_WANTMESSAGE;
+ return lResult;
+ }
+ case WM_NCDESTROY:
+ // https://devblogs.microsoft.com/oldnewthing/20031111-00/?p=41883
+ RemoveWindowSubclass(hWnd, log_proc, uIdSubclass);
+ break;
+ }
+ return DefSubclassProc(hWnd, uMsg, wParam, lParam);
+}
+
+static void
+process_resize(UINT w, UINT h)
+{
+ // Font height, control height (accounts for padding and borders)
+ int fh = g.font_height, ch = fh + 10;
+
+ int top = 0;
+ MoveWindow(g.hwndTopic, 5, 5, w - 10, fh, FALSE);
+ top += ch;
+
+ int bottom = 0;
+ MoveWindow(g.hwndInput, 3, h - ch - 3, w - 6, ch, FALSE);
+ bottom += ch + 3;
+ MoveWindow(g.hwndPrompt, 5, h - bottom - fh - 5, w / 2 - 5, fh, FALSE);
+ MoveWindow(g.hwndStatus, w / 2, h - bottom - fh - 5, w / 2 - 5, fh, FALSE);
+ bottom += ch;
+
+ bool to_bottom = buffer_at_bottom();
+ MoveWindow(g.hwndBufferList, 3, top, 150, h - top - bottom, FALSE);
+ MoveWindow(g.hwndBuffer, 156, top, w - 159, h - top - bottom, FALSE);
+ MoveWindow(g.hwndBufferLog, 156, top, w - 159, h - top - bottom, FALSE);
+ if (to_bottom)
+ buffer_scroll_to_bottom();
+
+ InvalidateRect(g.hwndMain, NULL, TRUE);
+}
+
+static void
+process_bufferlist_drawitem(PDRAWITEMSTRUCT dis)
+{
+ // Just always redraw it entirely, disregarding dis->itemAction.
+ COLORREF foreground = GetSysColor(dis->itemState & ODS_SELECTED
+ ? COLOR_HIGHLIGHTTEXT : COLOR_WINDOWTEXT);
+ COLORREF background = GetSysColor(dis->itemState & ODS_SELECTED
+ ? COLOR_HIGHLIGHT : COLOR_WINDOW);
+ if (dis->itemState & ODS_DISABLED)
+ foreground = GetSysColor(COLOR_GRAYTEXT);
+
+ HFONT oldFont = NULL;
+ std::wstring text;
+ if (dis->itemID != (UINT) -1 && dis->itemID < g.buffers.size()) {
+ const Buffer& b = g.buffers.at(dis->itemID);
+ text = b.buffer_name;
+ if (b.buffer_name != g.buffer_current && b.new_messages) {
+ text += L" (" + std::to_wstring(b.new_messages) + L")";
+ oldFont = (HFONT) SelectObject(dis->hDC, g.hfontBold);
+ }
+ if (b.highlighted)
+ foreground = RGB(0xff, 0x5f, 0x00);
+ }
+
+ COLORREF oldForeground = SetTextColor(dis->hDC, foreground);
+ COLORREF oldBackground = SetBkColor(dis->hDC, background);
+
+ // Old Windows hardcoded two pixels, and so will we.
+ ExtTextOut(dis->hDC, dis->rcItem.left + 2, dis->rcItem.top,
+ ETO_CLIPPED | ETO_OPAQUE, &dis->rcItem,
+ text.c_str(), text.length(), NULL);
+ if (oldFont)
+ SelectObject(dis->hDC, oldFont);
+
+ SetTextColor(dis->hDC, oldForeground);
+ SetBkColor(dis->hDC, oldBackground);
+
+ if (dis->itemState & ODS_FOCUS)
+ DrawFocusRect(dis->hDC, &dis->rcItem);
+}
+
+static void
+process_bufferlist_notification(WORD code)
+{
+ if (code == LBN_SELCHANGE) {
+ auto i = (size_t) SendMessage(g.hwndBufferList, LB_GETCURSEL, 0, 0);
+ if (i < g.buffers.size())
+ buffer_activate(g.buffers.at(i).buffer_name);
+ }
+}
+
+static void
+process_accelerator(WORD id)
+{
+ // Buffer indexes rotated to start after the current buffer.
+ std::vector<size_t> rotated(g.buffers.size());
+ size_t start = 0;
+ for (auto it = g.buffers.begin(); it != g.buffers.end(); ++it)
+ if (it->buffer_name == g.buffer_current) {
+ start = it - g.buffers.begin();
+ break;
+ }
+ for (auto &index : rotated)
+ index = ++start % g.buffers.size();
+
+ auto b = buffer_by_name(g.buffer_current);
+ switch (id) {
+ case ID_PREVIOUS_BUFFER:
+ if (rotated.size() > 0) {
+ size_t i = (rotated.back() ? rotated.back() : g.buffers.size()) - 1;
+ buffer_activate(g.buffers[i].buffer_name);
+ }
+ return;
+ case ID_NEXT_BUFFER:
+ if (rotated.size() > 0)
+ buffer_activate(g.buffers[rotated.front()].buffer_name);
+ return;
+ case ID_SWITCH_BUFFER:
+ if (!g.buffer_last.empty())
+ buffer_activate(g.buffer_last);
+ return;
+ case ID_GOTO_HIGHLIGHT:
+ for (auto i : rotated)
+ if (g.buffers[i].highlighted) {
+ buffer_activate(g.buffers[i].buffer_name);
+ break;
+ }
+ return;
+ case ID_GOTO_ACTIVITY:
+ for (auto i : rotated)
+ if (g.buffers[i].new_messages) {
+ buffer_activate(g.buffers[i].buffer_name);
+ break;
+ }
+ return;
+ case ID_TOGGLE_UNIMPORTANT:
+ if (b)
+ buffer_toggle_unimportant(b->buffer_name);
+ return;
+ case ID_DISPLAY_FULL_LOG:
+ if (b)
+ buffer_toggle_log();
+ return;
+ }
+}
+
+static LRESULT CALLBACK
+window_proc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
+{
+ switch (uMsg) {
+ case WM_DESTROY:
+ PostQuitMessage(0);
+ return 0;
+ case WM_SIZE:
+ process_resize(LOWORD(lParam), HIWORD(lParam));
+ return 0;
+ case WM_ACTIVATE:
+ if (LOWORD(wParam) == WA_INACTIVE)
+ g.hwndLastFocused = GetFocus();
+ else if (g.hwndLastFocused)
+ SetFocus(g.hwndLastFocused);
+ return 0;
+ case WM_MEASUREITEM:
+ {
+ auto mis = (PMEASUREITEMSTRUCT) lParam;
+ mis->itemHeight = g.font_height;
+ return TRUE;
+ }
+ case WM_DRAWITEM:
+ {
+ auto dis = (PDRAWITEMSTRUCT) lParam;
+ if (dis->hwndItem == g.hwndBufferList)
+ process_bufferlist_drawitem(dis);
+ return TRUE;
+ }
+ case WM_SYSCOMMAND:
+ {
+ auto b = buffer_by_name(g.buffer_current);
+ if (b && wParam == SC_RESTORE) {
+ b->highlighted = false;
+ refresh_icon();
+ }
+ // Here we absolutely must pass to DefWindowProc().
+ break;
+ }
+ case WM_COMMAND:
+ if (!lParam)
+ process_accelerator(LOWORD(wParam));
+ else if (lParam == (LPARAM) g.hwndBufferList)
+ process_bufferlist_notification(HIWORD(wParam));
+ else if (lParam == (LPARAM) g.hwndBuffer &&
+ HIWORD(wParam) == EN_VSCROLL)
+ refresh_status();
+ return 0;
+ case WM_NOTIFY:
+ switch (((LPNMHDR) lParam)->code) {
+ case EN_LINK:
+ {
+ auto link = (ENLINK *) lParam;
+ if (link->msg == WM_LBUTTONUP) {
+ TEXTRANGE tr = {};
+ tr.chrg = link->chrg;
+ tr.lpstrText = new wchar_t[tr.chrg.cpMax - tr.chrg.cpMin + 1]();
+ SendMessage(
+ link->nmhdr.hwndFrom, EM_GETTEXTRANGE, 0, (LPARAM) &tr);
+ ShellExecute(
+ NULL, L"open", tr.lpstrText, NULL, NULL, SW_SHOWNORMAL);
+ delete[] tr.lpstrText;
+ }
+ break;
+ }
+ }
+ }
+ return DefWindowProc(hWnd, uMsg, wParam, lParam);
+}
+
+static void
+get_font()
+{
+ // To enable the "Make text bigger" scaling functionality.
+ NONCLIENTMETRICS ncm = {};
+ ncm.cbSize = sizeof ncm;
+ if (SystemParametersInfo(SPI_GETNONCLIENTMETRICS, sizeof ncm, &ncm, 0)) {
+ g.hfont = CreateFontIndirect(&ncm.lfMessageFont);
+
+ LOGFONT bold = g.fontlog = ncm.lfMessageFont;
+ bold.lfWeight = FW_BOLD;
+ g.hfontBold = CreateFontIndirect(&bold);
+ }
+
+ if (!g.hfont)
+ g.hfont = g.hfontBold = (HFONT) GetStockObject(DEFAULT_GUI_FONT);
+
+ // There doesn't seem to be a better way than through a drawing context.
+ HDC hdc = GetDC(NULL);
+ HFONT oldFont = (HFONT) SelectObject(hdc, g.hfont);
+ TEXTMETRIC tm = {};
+ GetTextMetrics(hdc, &tm);
+ SelectObject(hdc, oldFont);
+ ReleaseDC(NULL, hdc);
+
+ g.font_height = tm.tmHeight;
+}
+
+static BOOL CALLBACK
+set_font(HWND child, LPARAM font)
+{
+ SendMessage(child, WM_SETFONT, font, (LPARAM) TRUE);
+ return TRUE;
+}
+
+static bool
+process_messages(HACCEL accelerators)
+{
+ MSG message = {};
+ while (PeekMessage(&message, NULL, 0, 0, TRUE)) {
+ if (message.message == WM_QUIT)
+ return false;
+ if (TranslateAccelerator(g.hwndMain, accelerators, &message))
+ continue;
+
+ // https://devblogs.microsoft.com/oldnewthing/20031021-00/?p=42083
+ if (IsDialogMessage(g.hwndMain, &message))
+ continue;
+
+ TranslateMessage(&message);
+ DispatchMessage(&message);
+ }
+ return true;
+}
+
+int WINAPI
+wWinMain(HINSTANCE hInstance, [[maybe_unused]] HINSTANCE hPrevInstance,
+ PWSTR pCmdLine, int nCmdShow)
+{
+ setlocale(LC_ALL, "");
+
+ WSADATA wd = {};
+ int err = {};
+ if ((err = WSAStartup(MAKEWORD(2, 2), &wd))) {
+ show_error_message(format_error_message(err).c_str());
+ return 1;
+ }
+
+ INITCOMMONCONTROLSEX icc = {};
+ icc.dwICC = 0;
+ icc.dwSize = sizeof icc;
+ (void) InitCommonControlsEx(&icc);
+
+ // TODO(p): The control doesn't seem to support visual styles at all,
+ // try to figure out how to immitate it with WM_NCPAINT,
+ // GetThemeBackgroundContentRect(), DrawThemeBackground(), etc.,
+ // and remember to handle the case of visual styles being disabled,
+ // perhaps by using DrawEdge() → InflateRect(-CXBORDER, -CYBORDER).
+ // This is by no means simple.
+ //
+ // Example implementation: https://web.archive.org/web/20210707175627
+ // /http://www.codeguru.com/cpp/w-d/dislog/miscellaneous/article.php/c8729
+ // /XP-Theme-Support-for-Rich-Edit-and-Custom-Controls.htm
+ if (!LoadLibrary(L"Msftedit.dll")) {
+ show_error_message(format_error_message(GetLastError()).c_str());
+ return 1;
+ }
+
+ // WINE calls WM_MEASUREITEM as soon as the listbox is created.
+ // TODO(p): Watch for WM_SETTINGCHANGE/SPI_SETNONCLIENTMETRICS,
+ // reset all fonts in all widgets, and the topic background colour.
+ get_font();
+
+ g.hicon =
+ LoadIcon(hInstance, MAKEINTRESOURCE(IDI_ICON));
+ g.hiconHighlighted =
+ LoadIcon(hInstance, MAKEINTRESOURCE(IDI_HIGHLIGHTED));
+
+ WNDCLASSEX wc = {};
+ wc.cbSize = sizeof wc;
+ wc.lpfnWndProc = window_proc;
+ wc.hInstance = hInstance;
+ wc.hIcon = g.hicon;
+ wc.hCursor = LoadCursor(NULL, IDC_ARROW);
+ wc.hbrBackground = GetSysColorBrush(COLOR_3DFACE);
+ wc.lpszClassName = L"xW";
+ if (!RegisterClassEx(&wc))
+ return 1;
+
+ g.hwndMain = CreateWindowEx(WS_EX_CONTROLPARENT, L"xW", L"xW",
+ WS_OVERLAPPEDWINDOW,
+ CW_USEDEFAULT, CW_USEDEFAULT, 600, 400, NULL, NULL, hInstance, NULL);
+
+ // We're lucky to not need much user user interface,
+ // because Win32 is in many aspects quite difficult to work with.
+ HMENU id = 0;
+ g.hwndTopic = CreateWindowEx(0, MSFTEDIT_CLASS, L"",
+ WS_VISIBLE | WS_CHILD | WS_TABSTOP |
+ ES_AUTOHSCROLL | ES_READONLY,
+ 0, 0, 100, 20, g.hwndMain, ++id, hInstance, NULL);
+ g.hwndBufferList = CreateWindowEx(WS_EX_CLIENTEDGE, WC_LISTBOX, L"",
+ WS_VISIBLE | WS_CHILD | WS_TABSTOP | WS_VSCROLL |
+ LBS_NOINTEGRALHEIGHT | LBS_NOTIFY | LBS_NODATA | LBS_OWNERDRAWFIXED,
+ 0, 20, 50, 40, g.hwndMain, ++id, hInstance, NULL);
+ g.hwndBuffer = CreateWindowEx(WS_EX_CLIENTEDGE, MSFTEDIT_CLASS, L"",
+ WS_VISIBLE | WS_CHILD | WS_TABSTOP | WS_VSCROLL |
+ ES_MULTILINE | ES_READONLY | ES_SAVESEL,
+ 50, 20, 50, 40, g.hwndMain, ++id, hInstance, NULL);
+ g.hwndBufferLog = CreateWindowEx(WS_EX_CLIENTEDGE, WC_EDIT, L"",
+ WS_CHILD | WS_TABSTOP | WS_VSCROLL |
+ ES_MULTILINE | ES_READONLY,
+ 50, 20, 50, 40, g.hwndMain, ++id, hInstance, NULL);
+ g.hwndPrompt = CreateWindowEx(0, WC_STATIC, L"Connecting...",
+ WS_VISIBLE | WS_CHILD,
+ 0, 60, 50, 20, g.hwndMain, ++id, hInstance, NULL);
+ g.hwndStatus = CreateWindowEx(0, WC_STATIC, L"",
+ WS_VISIBLE | WS_CHILD | ES_RIGHT,
+ 50, 60, 50, 20, g.hwndMain, ++id, hInstance, NULL);
+ g.hwndInput = CreateWindowEx(WS_EX_CLIENTEDGE, WC_EDIT, L"",
+ WS_VISIBLE | WS_CHILD | WS_TABSTOP |
+ ES_AUTOHSCROLL,
+ 0, 80, 100, 20, g.hwndMain, ++id, hInstance, NULL);
+
+ SendMessage(g.hwndTopic, EM_SETBKGNDCOLOR, 0, GetSysColor(COLOR_3DFACE));
+ // The 1 probably means AURL_ENABLEEA only in later versions.
+ SendMessage(g.hwndTopic, EM_AUTOURLDETECT, 1, 0);
+ SendMessage(g.hwndTopic, EM_SETEVENTMASK, 0, ENM_LINK);
+ SendMessage(g.hwndTopic, EM_SETUNDOLIMIT, 0, 0);
+ SetWindowSubclass(g.hwndTopic, richedit_proc, 0, 0);
+ SendMessage(g.hwndBuffer, EM_AUTOURLDETECT, 1, 0);
+ SendMessage(g.hwndBuffer, EM_SETEVENTMASK, 0, ENM_LINK | ENM_SCROLL);
+ SendMessage(g.hwndBuffer, EM_SETUNDOLIMIT, 0, 0);
+ SetWindowSubclass(g.hwndBufferList, bufferlist_proc, 0, 0);
+ SetWindowSubclass(g.hwndBuffer, richedit_proc, 0, 0);
+ SetWindowSubclass(g.hwndBufferLog, log_proc, 0, 0);
+
+ RECT client_rect = {};
+ if (GetClientRect(g.hwndMain, &client_rect)) {
+ process_resize(client_rect.right - client_rect.left,
+ client_rect.bottom - client_rect.top);
+ }
+
+ EnumChildWindows(g.hwndMain, (WNDENUMPROC) set_font, (LPARAM) g.hfont);
+ SendMessage(g.hwndPrompt, WM_SETFONT, (WPARAM) g.hfontBold, (LPARAM) TRUE);
+ SetFocus(g.hwndInput);
+ SetWindowSubclass(g.hwndInput, input_proc, 0, 0);
+ ShowWindow(g.hwndMain, nCmdShow);
+
+ HACCEL accelerators =
+ LoadAccelerators(hInstance, MAKEINTRESOURCE(IDA_ACCELERATORS));
+ if (!accelerators) {
+ show_error_message(format_error_message(GetLastError()).c_str());
+ return 1;
+ }
+
+ 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;
+ }
+
+ // We have a few suboptimal asynchronous options:
+ // a) WSAAsyncGetHostByName() requires us to distinguish hostnames
+ // from IP literals manually,
+ // b) GetAddrInfoEx() only supports asynchronous operation since Windows 8,
+ // c) run this from a thread.
+ addrinfoW hints = {};
+ 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);
+ if (err) {
+ show_error_message(format_error_message(err).c_str());
+ return 1;
+ }
+
+ std::wstring error;
+ g.addresses_iterator = g.addresses;
+ if (!relay_connect_step(error)) {
+ show_error_message(error.c_str());
+ return 1;
+ }
+
+ while (process_messages(accelerators)) {
+ DWORD result = MsgWaitForMultipleObjects(
+ g.event != NULL, &g.event, 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)) {
+ show_error_message(error.c_str());
+ return 1;
+ }
+ }
+ FreeAddrInfo(g.addresses);
+ WSACleanup();
+ return 0;
+}
diff --git a/xW/xW.manifest b/xW/xW.manifest
new file mode 100644
index 0000000..68bb0f4
--- /dev/null
+++ b/xW/xW.manifest
@@ -0,0 +1,11 @@
+<?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>
+</assembly>
diff --git a/xW/xW.rc b/xW/xW.rc
new file mode 100644
index 0000000..858b104
--- /dev/null
+++ b/xW/xW.rc
@@ -0,0 +1,23 @@
+#include <windows.h>
+#include "xW-resources.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"
+
+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
+ "!", ID_GOTO_HIGHLIGHT, ALT
+ "a", ID_GOTO_ACTIVITY, ALT
+ "H", ID_TOGGLE_UNIMPORTANT, ALT
+ "h", ID_DISPLAY_FULL_LOG, ALT
+END
diff --git a/xW/xW.svg b/xW/xW.svg
new file mode 100644
index 0000000..d68c63e
--- /dev/null
+++ b/xW/xW.svg
@@ -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>