These messages are used by IsDialogMessage(), and use the WM_USER range.
		
			
				
	
	
		
			1997 lines
		
	
	
		
			54 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			1997 lines
		
	
	
		
			54 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
/*
 | 
						|
 * 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 "config.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.
 | 
						|
	HANDLE date_change_timer;           ///< Waitable timer for day changes
 | 
						|
 | 
						|
	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:
 | 
						|
 | 
						|
	std::wstring host;                  ///< Host as given by user
 | 
						|
	std::wstring port;                  ///< Port/service as given by user
 | 
						|
 | 
						|
	addrinfoW *addresses;               ///< GetAddrInfo() result
 | 
						|
	addrinfoW *addresses_iterator;      ///< Currently processed address
 | 
						|
	SOCKET socket;                      ///< Relay socket
 | 
						|
	WSAEVENT event;                     ///< Relay socket event
 | 
						|
	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;
 | 
						|
}
 | 
						|
 | 
						|
static std::wstring
 | 
						|
window_get_text(HWND hWnd)
 | 
						|
{
 | 
						|
	int length = GetWindowTextLength(hWnd);
 | 
						|
	std::wstring buffer(length, {});
 | 
						|
	GetWindowText(hWnd, buffer.data(), length + 1);
 | 
						|
	return buffer;
 | 
						|
}
 | 
						|
 | 
						|
static void
 | 
						|
beep()
 | 
						|
{
 | 
						|
	if (!PlaySound(MAKEINTRESOURCE(IDR_BEEP),
 | 
						|
			GetModuleHandle(NULL), SND_ASYNC | SND_RESOURCE))
 | 
						|
		Beep(800, 100);
 | 
						|
}
 | 
						|
 | 
						|
// --- Networking --------------------------------------------------------------
 | 
						|
 | 
						|
static bool
 | 
						|
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;
 | 
						|
	if (auto b = buffer_by_name(g.buffer_current)) {
 | 
						|
		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.
 | 
						|
	if (window_get_text(g.hwndStatus) != 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(const 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_date_change(bool &sameline, const tm &last, const tm ¤t)
 | 
						|
{
 | 
						|
	if (last.tm_year == current.tm_year &&
 | 
						|
		last.tm_mon == current.tm_mon &&
 | 
						|
		last.tm_mday == current.tm_mday)
 | 
						|
		return;
 | 
						|
 | 
						|
	wchar_t buffer[64] = {};
 | 
						|
	wcsftime(buffer, sizeof buffer, &L"\n%x"[sameline], ¤t);
 | 
						|
	sameline = false;
 | 
						|
 | 
						|
	CHARFORMAT2 cf = default_charformat();
 | 
						|
	cf.dwEffects |= CFE_BOLD;
 | 
						|
	richedit_replacesel(g.hwndBuffer, &cf, buffer);
 | 
						|
}
 | 
						|
 | 
						|
static LONG
 | 
						|
buffer_reset_selection()
 | 
						|
{
 | 
						|
	CHARRANGE cr = {};
 | 
						|
	cr.cpMin = cr.cpMax = GetWindowTextLength(g.hwndBuffer);
 | 
						|
	SendMessage(g.hwndBuffer, EM_EXSETSEL, 0, (LPARAM) &cr);
 | 
						|
	return cr.cpMin;
 | 
						|
}
 | 
						|
 | 
						|
static struct tm
 | 
						|
buffer_localtime(time_t time)
 | 
						|
{
 | 
						|
	// This isn't critical, so let it fail quietly.
 | 
						|
	struct tm result = {};
 | 
						|
	(void) localtime_s(&result, &time);
 | 
						|
	return result;
 | 
						|
}
 | 
						|
 | 
						|
static void
 | 
						|
buffer_print_and_watch_trailing_date_changes()
 | 
						|
{
 | 
						|
	time_t current_unix = time(NULL);
 | 
						|
	tm current = buffer_localtime(current_unix);
 | 
						|
	auto b = buffer_by_name(g.buffer_current);
 | 
						|
	if (b && !b->lines.empty()) {
 | 
						|
		tm last = buffer_localtime(b->lines.back().when / 1000);
 | 
						|
		bool sameline = !buffer_reset_selection();
 | 
						|
		buffer_print_date_change(sameline, last, current);
 | 
						|
	}
 | 
						|
 | 
						|
	current.tm_sec = current.tm_min = current.tm_hour = 0;
 | 
						|
	current.tm_mday++;
 | 
						|
	current.tm_isdst = -1;
 | 
						|
	const time_t midnight = mktime(¤t);
 | 
						|
	if (midnight == (time_t) -1 || midnight < current_unix)
 | 
						|
		return;
 | 
						|
 | 
						|
	// Note that after printing the first trailing update,
 | 
						|
	// follow-up updates may be duplicated if timer events arrive too early.
 | 
						|
	LARGE_INTEGER li = {};
 | 
						|
	li.QuadPart = (midnight - current_unix + 1) * -10000000LL;
 | 
						|
	SetWaitableTimer(g.date_change_timer, &li, 0, NULL, NULL, FALSE);
 | 
						|
}
 | 
						|
 | 
						|
static void
 | 
						|
buffer_print_line(std::vector<BufferLine>::const_iterator begin,
 | 
						|
	std::vector<BufferLine>::const_iterator line)
 | 
						|
{
 | 
						|
	tm current = buffer_localtime(line->when / 1000);
 | 
						|
	tm last = buffer_localtime(
 | 
						|
		line == begin ? time(NULL) : (line - 1)->when / 1000);
 | 
						|
 | 
						|
	// The Rich Edit control makes the window cursor transparent
 | 
						|
	// each time you add an independent newline character. Avoid that.
 | 
						|
	// (Sadly, this also makes Windows 7 end lines with a bogus space that
 | 
						|
	// has the CHARFORMAT2 of what we flush that newline together with.)
 | 
						|
	bool sameline = !buffer_reset_selection();
 | 
						|
	buffer_print_date_change(sameline, last, current);
 | 
						|
 | 
						|
	wchar_t buffer[64] = {};
 | 
						|
	wcsftime(buffer, sizeof buffer, &L"\n%H:%M:%S"[sameline], ¤t);
 | 
						|
 | 
						|
	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()
 | 
						|
{
 | 
						|
	bool sameline = !GetWindowTextLength(g.hwndBuffer);
 | 
						|
 | 
						|
	CHARFORMAT2 format = default_charformat();
 | 
						|
	format.dwEffects &= ~CFE_AUTOCOLOR;
 | 
						|
	format.crTextColor = RGB(0xff, 0x5f, 0x00);
 | 
						|
	richedit_replacesel(g.hwndBuffer, &format, &L"\n---"[sameline]);
 | 
						|
}
 | 
						|
 | 
						|
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_print_and_watch_trailing_date_changes();
 | 
						|
	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)) {
 | 
						|
		beep();
 | 
						|
 | 
						|
		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 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_now(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 = window_get_text(g.hwndInput);
 | 
						|
			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_icon();
 | 
						|
		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 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 = window_get_text(g.hwndInput);
 | 
						|
 | 
						|
	// 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, window_get_text(g.hwndInput)};
 | 
						|
}
 | 
						|
 | 
						|
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());
 | 
						|
	}
 | 
						|
 | 
						|
	if (response->completions.size() != 1)
 | 
						|
		beep();
 | 
						|
 | 
						|
	// 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 bool
 | 
						|
input_up()
 | 
						|
{
 | 
						|
	auto b = buffer_by_name(g.buffer_current);
 | 
						|
	if (!b || b->history_at < 1)
 | 
						|
		return false;
 | 
						|
 | 
						|
	if (b->history_at == b->history.size())
 | 
						|
		b->input = window_get_text(g.hwndInput);
 | 
						|
	input_set_contents(b->history.at(--b->history_at));
 | 
						|
	return true;
 | 
						|
}
 | 
						|
 | 
						|
static bool
 | 
						|
input_down()
 | 
						|
{
 | 
						|
	auto b = buffer_by_name(g.buffer_current);
 | 
						|
	if (!b || b->history_at >= b->history.size())
 | 
						|
		return false;
 | 
						|
 | 
						|
	input_set_contents(++b->history_at == b->history.size()
 | 
						|
		? b->input
 | 
						|
		: b->history.at(b->history_at));
 | 
						|
	return true;
 | 
						|
}
 | 
						|
 | 
						|
static boolean
 | 
						|
input_wants(const MSG *message)
 | 
						|
{
 | 
						|
	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:
 | 
						|
		// TODO(p): Emacs-style cursor movement shortcuts.
 | 
						|
		switch (wParam) {
 | 
						|
		case 'p':
 | 
						|
			if (input_up())
 | 
						|
				return 0;
 | 
						|
			break;
 | 
						|
		case 'n':
 | 
						|
			if (input_down())
 | 
						|
				return 0;
 | 
						|
			break;
 | 
						|
		}
 | 
						|
		break;
 | 
						|
	case WM_KEYDOWN:
 | 
						|
	{
 | 
						|
		HWND scrollable = IsWindowVisible(g.hwndBufferLog)
 | 
						|
			? g.hwndBufferLog
 | 
						|
			: g.hwndBuffer;
 | 
						|
 | 
						|
		switch (wParam) {
 | 
						|
		case VK_UP:
 | 
						|
			if (input_up())
 | 
						|
				return 0;
 | 
						|
			break;
 | 
						|
		case VK_DOWN:
 | 
						|
			if (input_down())
 | 
						|
				return 0;
 | 
						|
			break;
 | 
						|
		case VK_PRIOR:
 | 
						|
			SendMessage(scrollable, EM_SCROLL, SB_PAGEUP, 0);
 | 
						|
			return 0;
 | 
						|
		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_TIMECHANGE:
 | 
						|
		_tzset();
 | 
						|
		if (auto b = buffer_by_name(g.buffer_current))
 | 
						|
			refresh_buffer(*b);
 | 
						|
		return 0;
 | 
						|
	case WM_SYSCOLORCHANGE:
 | 
						|
		// The topic would flicker with WS_EX_TRANSPARENT.
 | 
						|
		// The buffer only changed its text colour, not background.
 | 
						|
		SendMessage(g.hwndTopic,
 | 
						|
			EM_SETBKGNDCOLOR, 0, GetSysColor(COLOR_3DFACE));
 | 
						|
		SendMessage(g.hwndBuffer,
 | 
						|
			EM_SETBKGNDCOLOR, 1, 0);
 | 
						|
 | 
						|
		// XXX: This is incomplete, we'd have to run convert_items() again;
 | 
						|
		// essentially only COLOR_GRAYTEXT is reloaded in here.
 | 
						|
		if (auto b = buffer_by_name(g.buffer_current))
 | 
						|
			refresh_buffer(*b);
 | 
						|
 | 
						|
		// Pass it to all child windows, through DefWindowProc().
 | 
						|
		break;
 | 
						|
	case WM_ACTIVATE:
 | 
						|
		if (LOWORD(wParam) == WA_INACTIVE)
 | 
						|
			g.hwndLastFocused = GetFocus();
 | 
						|
		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;
 | 
						|
		}
 | 
						|
		}
 | 
						|
	case DM_GETDEFID:
 | 
						|
	case DM_SETDEFID:
 | 
						|
		break;
 | 
						|
	}
 | 
						|
	return DefWindowProc(hWnd, uMsg, wParam, lParam);
 | 
						|
}
 | 
						|
 | 
						|
static INT_PTR CALLBACK
 | 
						|
connect_proc(
 | 
						|
	HWND hDlg, UINT uMsg, WPARAM wParam, [[maybe_unused]] LPARAM lParam)
 | 
						|
{
 | 
						|
	switch (uMsg) {
 | 
						|
	case WM_INITDIALOG:
 | 
						|
		return TRUE;
 | 
						|
	case WM_COMMAND:
 | 
						|
		switch (LOWORD(wParam)) {
 | 
						|
		case IDOK:
 | 
						|
		case IDCANCEL:
 | 
						|
			g.host = window_get_text(GetDlgItem(hDlg, IDC_HOST));
 | 
						|
			g.port = window_get_text(GetDlgItem(hDlg, IDC_PORT));
 | 
						|
			EndDialog(hDlg, LOWORD(wParam));
 | 
						|
			return TRUE;
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return FALSE;
 | 
						|
}
 | 
						|
 | 
						|
static void
 | 
						|
get_font()
 | 
						|
{
 | 
						|
	// 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 = TEXT(PROJECT_NAME);
 | 
						|
	if (!RegisterClassEx(&wc))
 | 
						|
		return 1;
 | 
						|
 | 
						|
	g.hwndMain = CreateWindowEx(
 | 
						|
		WS_EX_CONTROLPARENT, wc.lpszClassName, TEXT(PROJECT_NAME),
 | 
						|
		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) {
 | 
						|
		g.host = argv[0];
 | 
						|
		g.port = argv[1];
 | 
						|
	} else if (DialogBox(hInstance, MAKEINTRESOURCE(IDD_CONNECT),
 | 
						|
			g.hwndMain, connect_proc) != IDOK) {
 | 
						|
		return 0;
 | 
						|
	}
 | 
						|
	LocalFree(argv);
 | 
						|
 | 
						|
	// We have a few suboptimal asynchronous options:
 | 
						|
	// a) WSAAsyncGetHostByName() requires us to distinguish hostnames
 | 
						|
	//    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(g.host.c_str(), g.port.c_str(), &hints, &g.addresses);
 | 
						|
	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;
 | 
						|
	}
 | 
						|
 | 
						|
	g.date_change_timer = CreateWaitableTimer(NULL, FALSE, NULL);
 | 
						|
	if (!g.date_change_timer) {
 | 
						|
		show_error_message(format_error_message(GetLastError()).c_str());
 | 
						|
		return 1;
 | 
						|
	}
 | 
						|
 | 
						|
	while (process_messages(accelerators)) {
 | 
						|
		HANDLE handles[] = {g.date_change_timer, g.event};
 | 
						|
		DWORD count = 2 - !handles[1];
 | 
						|
		DWORD result = MsgWaitForMultipleObjects(
 | 
						|
			count, handles, FALSE, INFINITE, QS_ALLINPUT);
 | 
						|
		if (result == WAIT_FAILED) {
 | 
						|
			show_error_message(format_error_message(GetLastError()).c_str());
 | 
						|
			return 1;
 | 
						|
		}
 | 
						|
		if (result >= WAIT_OBJECT_0 + count)
 | 
						|
			continue;
 | 
						|
 | 
						|
		auto signalled = handles[result];
 | 
						|
		if (signalled == g.date_change_timer) {
 | 
						|
			bool to_bottom = buffer_at_bottom();
 | 
						|
			buffer_print_and_watch_trailing_date_changes();
 | 
						|
			if (to_bottom)
 | 
						|
				buffer_scroll_to_bottom();
 | 
						|
		}
 | 
						|
		if (signalled == g.event && !relay_process_socket_events(error)) {
 | 
						|
			show_error_message(error.c_str());
 | 
						|
			return 1;
 | 
						|
		}
 | 
						|
	}
 | 
						|
	FreeAddrInfo(g.addresses);
 | 
						|
	WSACleanup();
 | 
						|
	CloseHandle(g.date_change_timer);
 | 
						|
	return 0;
 | 
						|
}
 |