summaryrefslogtreecommitdiff
path: root/xW/xW.cpp
diff options
context:
space:
mode:
authorPřemysl Eric Janouch <p@janouch.name>2023-07-09 07:06:19 +0200
committerPřemysl Eric Janouch <p@janouch.name>2023-07-15 17:00:21 +0200
commit0219dbd02615498f4a931e33ef2337e0bbf9b526 (patch)
treeb80e489614dcec83b8600534b5701b742d524ee4 /xW/xW.cpp
parent1da4699a7afb0085c498196bf41fef6b69168a6f (diff)
downloadxK-0219dbd02615498f4a931e33ef2337e0bbf9b526.tar.gz
xK-0219dbd02615498f4a931e33ef2337e0bbf9b526.tar.xz
xK-0219dbd02615498f4a931e33ef2337e0bbf9b526.zip
Add a Win32 frontend for xC
This has been more of an exercise. The performance of Msftedit.dll is rather abysmal, and its interface isn't the most accomodating. That said, the frontend is quite usable, at least on Windows 10+.
Diffstat (limited to 'xW/xW.cpp')
-rw-r--r--xW/xW.cpp1864
1 files changed, 1864 insertions, 0 deletions
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;
+}