xK/xW/xW.cpp

2026 lines
55 KiB
C++
Raw Normal View History

/*
* 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"
2023-07-15 23:34:37 +02:00
#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.
2023-07-26 01:46:59 +02:00
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
2023-07-29 01:59:38 +02:00
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)
{
2023-07-29 01:59:38 +02:00
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
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());
2023-07-29 01:59:38 +02:00
// 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;
2023-07-26 01:46:59 +02:00
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>
2023-07-26 14:57:35 +02:00
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
2023-07-26 01:46:59 +02:00
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);
2023-07-26 01:46:59 +02:00
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.)
2023-07-26 01:46:59 +02:00
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++;
}
2023-07-26 01:46:59 +02:00
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;
2023-07-29 01:59:38 +02:00
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;
2023-07-29 01:59:38 +02:00
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;
}
2023-07-29 01:59:38 +02:00
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 = {};
2023-07-29 01:59:38 +02:00
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({});
2023-07-29 01:59:38 +02:00
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();
2023-07-29 01:59:38 +02:00
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.
2023-07-29 01:59:38 +02:00
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;
2023-07-29 01:59:38 +02:00
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;
2023-07-26 01:46:59 +02:00
case WM_TIMECHANGE:
_tzset();
if (auto b = buffer_by_name(g.buffer_current))
refresh_buffer(*b);
return 0;
2023-07-26 14:57:35 +02:00
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);
2023-07-15 23:34:37 +02:00
wc.lpszClassName = TEXT(PROJECT_NAME);
if (!RegisterClassEx(&wc))
return 1;
2023-07-15 23:34:37 +02:00
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;
}
2023-07-29 01:59:38 +02:00
if (!(g.date_change_timer = CreateWaitableTimer(NULL, FALSE, NULL)) ||
!(g.flush_event = CreateEvent(NULL, FALSE, FALSE, NULL))) {
2023-07-26 01:46:59 +02:00
show_error_message(format_error_message(GetLastError()).c_str());
return 1;
}
while (process_messages(accelerators)) {
2023-07-29 01:59:38 +02:00
HANDLE handles[] = {g.date_change_timer, g.flush_event, g.socket_event};
DWORD count = 3 - !handles[2];
DWORD result = MsgWaitForMultipleObjects(
2023-07-26 01:46:59 +02:00
count, handles, FALSE, INFINITE, QS_ALLINPUT);
if (result == WAIT_FAILED) {
show_error_message(format_error_message(GetLastError()).c_str());
return 1;
}
2023-07-26 01:46:59 +02:00
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();
}
2023-07-29 01:59:38 +02:00
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();
2023-07-26 01:46:59 +02:00
CloseHandle(g.date_change_timer);
2023-07-29 01:59:38 +02:00
CloseHandle(g.flush_event);
return 0;
}