xK/xW/xW.cpp
Přemysl Eric Janouch 7ba17a0161
All checks were successful
Alpine 3.21 Success
Arch Linux AUR Success
OpenBSD 7.6 Success
Make the relay acknowledge all received commands
To that effect, bump liberty and the xC relay protocol version.
Relay events have been reordered to improve forward compatibility.

Also prevent use-after-free when serialization fails.

xP now slightly throttles activity notifications,
and indicates when there are unacknowledged commands.
2025-05-10 12:08:51 +02:00

2036 lines
55 KiB
C++

/*
* xW.cpp: Win32 frontend for xC
*
* Copyright (c) 2023 - 2024, Přemysl Eric Janouch <p@janouch.name>
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
* SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
* OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
* CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*
*/
#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 socket_event; ///< Relay socket event
HANDLE flush_event; ///< Write buffer has new data
std::vector<uint8_t> write_buffer; ///< Write buffer
std::vector<uint8_t> read_buffer; ///< Read buffer
// 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)
{
ResetEvent(g.flush_event);
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
on_relay_generic_response(
std::wstring error, const Relay::ResponseData *response)
{
if (!response)
show_error_message(error.c_str());
}
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);
else
g.command_callbacks[m.command_seq] = on_relay_generic_response;
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());
// There doesn't seem to be a way to cause FD_WRITE without first
// unsuccessfully trying to send some data, but we don't want to
// handle any errors at this level.
SetEvent(g.flush_event);
}
// --- Buffers -----------------------------------------------------------------
static Buffer *
buffer_by_name(const std::wstring &name)
{
for (auto &b : g.buffers)
if (b.buffer_name == name)
return &b;
return nullptr;
}
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;
// XXX: This may not change the taskbar icon.
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());
}
static void
recheck_highlighted()
{
// Corresponds to the logic toggling the bool on.
auto b = buffer_by_name(g.buffer_current);
if (b && b->highlighted && buffer_at_bottom() &&
!IsIconic(g.hwndMain) && !IsWindowVisible(g.hwndBufferLog)) {
b->highlighted = false;
refresh_icon();
refresh_buffer_list();
}
}
// --- Buffer actions ----------------------------------------------------------
static void
buffer_activate(const std::wstring &name)
{
auto activate = new Relay::CommandData_BufferActivate();
activate->buffer_name = name;
relay_send(activate);
}
static void
buffer_toggle_unimportant(const std::wstring &name)
{
auto toggle = new Relay::CommandData_BufferToggleUnimportant();
toggle->buffer_name = name;
relay_send(toggle);
}
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"");
recheck_highlighted();
return;
}
auto log = new Relay::CommandData_BufferLog();
log->buffer_name = g.buffer_current;
relay_send(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));
});
}
// --- 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 &current)
{
if (last.tm_year == current.tm_year &&
last.tm_mon == current.tm_mon &&
last.tm_mday == current.tm_mday)
return;
wchar_t buffer[64] = {};
wcsftime(buffer, sizeof buffer, &L"\n%x"[sameline], &current);
sameline = false;
CHARFORMAT2 cf = default_charformat();
cf.dwEffects |= CFE_BOLD;
richedit_replacesel(g.hwndBuffer, &cf, buffer);
}
static LONG
buffer_reset_selection()
{
CHARRANGE cr = {};
cr.cpMin = cr.cpMax = GetWindowTextLength(g.hwndBuffer);
SendMessage(g.hwndBuffer, EM_EXSETSEL, 0, (LPARAM) &cr);
return cr.cpMin;
}
static struct tm
buffer_localtime(time_t time)
{
// This isn't critical, so let it fail quietly.
struct tm result = {};
(void) localtime_s(&result, &time);
return result;
}
static void
buffer_print_and_watch_trailing_date_changes()
{
time_t current_unix = time(NULL);
tm current = buffer_localtime(current_unix);
auto b = buffer_by_name(g.buffer_current);
if (b && !b->lines.empty()) {
tm last = buffer_localtime(b->lines.back().when / 1000);
bool sameline = !buffer_reset_selection();
buffer_print_date_change(sameline, last, current);
}
current.tm_sec = current.tm_min = current.tm_hour = 0;
current.tm_mday++;
current.tm_isdst = -1;
const time_t midnight = mktime(&current);
if (midnight == (time_t) -1 || midnight < current_unix)
return;
// Note that after printing the first trailing update,
// follow-up updates may be duplicated if timer events arrive too early.
LARGE_INTEGER li = {};
li.QuadPart = (midnight - current_unix + 1) * -10000000LL;
SetWaitableTimer(g.date_change_timer, &li, 0, NULL, NULL, FALSE);
}
static void
buffer_print_line(std::vector<BufferLine>::const_iterator begin,
std::vector<BufferLine>::const_iterator line)
{
tm current = buffer_localtime(line->when / 1000);
tm last = buffer_localtime(
line == begin ? time(NULL) : (line - 1)->when / 1000);
// The Rich Edit control makes the window cursor transparent
// each time you add an independent newline character. Avoid that.
// (Sadly, this also makes Windows 7 end lines with a bogus space that
// has the CHARFORMAT2 of what we flush that newline together with.)
bool sameline = !buffer_reset_selection();
buffer_print_date_change(sameline, last, current);
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());
for (auto it : line->items) {
it.format.dwEffects &= ~CFE_AUTOCOLOR;
it.format.crTextColor = GetSysColor(COLOR_GRAYTEXT);
it.format.dwEffects |= CFE_AUTOBACKCOLOR;
richedit_replacesel(g.hwndBuffer, &it.format, it.text.c_str());
}
} else {
if (!prefix.empty())
richedit_replacesel(g.hwndBuffer, &pcf, prefix.c_str());
for (const auto &it : line->items)
richedit_replacesel(g.hwndBuffer, &it.format, it.text.c_str());
}
}
static void
buffer_print_separator()
{
bool sameline = !buffer_reset_selection();
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();
// We will get a scroll event, so no need to recheck_highlighted() here.
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);
// XXX: It would be great if it didn't autoscroll when focused.
bool to_bottom = display &&
(buffer_at_bottom() || GetFocus() == g.hwndBuffer);
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(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.new_;
if (data.buffer_name == g.buffer_current) {
g.buffer_current = data.new_;
refresh_status();
}
refresh_buffer_list();
if (data.buffer_name == g.buffer_last)
g.buffer_last = data.new_;
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.socket_event);
g.socket_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.socket_event = WSACreateEvent();
if (WSAEventSelect(g.socket, g.socket_event,
FD_CONNECT | FD_READ | FD_WRITE | FD_CLOSE))
error = format_error_message(WSAGetLastError());
else if (!connect(g.socket, p->ai_addr, (int) p->ai_addrlen))
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.socket_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(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(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(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(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);
recheck_highlighted();
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();
} else {
recheck_highlighted();
refresh_status();
}
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:
{
// We're not deiconified yet, so duplicate recheck_highlighted().
auto b = buffer_by_name(g.buffer_current);
if (wParam == SC_RESTORE && b && b->highlighted && buffer_at_bottom() &&
!IsWindowVisible(g.hwndBufferLog)) {
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) {
recheck_highlighted();
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;
}
if (!(g.date_change_timer = CreateWaitableTimer(NULL, FALSE, NULL)) ||
!(g.flush_event = CreateEvent(NULL, FALSE, FALSE, NULL))) {
show_error_message(format_error_message(GetLastError()).c_str());
return 1;
}
while (process_messages(accelerators)) {
HANDLE handles[] = {g.date_change_timer, g.flush_event, g.socket_event};
DWORD count = 3 - !handles[2];
DWORD result = MsgWaitForMultipleObjects(
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.flush_event && !relay_try_write(error)) {
show_error_message(error.c_str());
return 1;
}
if (signalled == g.socket_event && !relay_process_socket_events(error)) {
show_error_message(error.c_str());
return 1;
}
}
FreeAddrInfo(g.addresses);
WSACleanup();
CloseHandle(g.date_change_timer);
CloseHandle(g.flush_event);
return 0;
}