/* * xW.cpp: Win32 frontend for xC * * Copyright (c) 2023 - 2024, Přemysl Eric Janouch * * 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 #include #define WIN32_LEAN_AND_MEAN #include #include #include #undef ERROR #undef REGISTERED #include #include #include #include #include #include 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 items; }; struct Buffer { std::wstring buffer_name; bool hide_unimportant = {}; Relay::BufferKind kind = {}; std::wstring server_name; std::vector lines; // Channel: std::vector 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 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 write_buffer; ///< Write buffer std::vector read_buffer; ///< Read buffer // Relay protocol: uint32_t command_seq; ///< Outgoing message counter std::map command_callbacks; std::vector buffers; ///< List of all buffers std::wstring buffer_current; ///< Current buffer name or "" std::wstring buffer_last; ///< Previous buffer name or "" std::map 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(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(&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 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); } // --- Current buffer ---------------------------------------------------------- static void buffer_toggle_log( const std::wstring &error, const Relay::ResponseData_BufferLog *response) { if (!response) { show_error_message(error.c_str()); return; } std::wstring log; if (!LibertyXDR::utf8_to_wstring( response->log.data(), response->log.size(), log)) { show_error_message(L"Invalid encoding."); return; } std::wstring filtered; for (auto wch : log) { if (wch == L'\n') filtered += L"\r\n"; else filtered += wch; } SetWindowText(g.hwndBufferLog, filtered.c_str()); ShowWindow(g.hwndBuffer, SW_HIDE); ShowWindow(g.hwndBufferLog, SW_SHOW); } static void buffer_toggle_log() { if (IsWindowVisible(g.hwndBufferLog)) { ShowWindow(g.hwndBufferLog, SW_HIDE); ShowWindow(g.hwndBuffer, SW_SHOW); SetWindowText(g.hwndBufferLog, L""); return; } auto log = new Relay::CommandData_BufferLog(); log->buffer_name = g.buffer_current; relay_send(log, [name = g.buffer_current](auto error, auto response) { if (g.buffer_current != name) return; buffer_toggle_log(error, dynamic_cast(response)); }); } static bool buffer_at_bottom() { // It is created with this style, and should retain it indefinitely, // however this check works. It is necessary because when richedit // hides its scrollbar, it does not bother resetting its values. if (!(GetWindowLong(g.hwndBuffer, GWL_STYLE) & WS_VSCROLL)) return true; SCROLLINFO si = {}; si.cbSize = sizeof si; si.fMask = SIF_ALL; GetScrollInfo(g.hwndBuffer, SB_VERT, &si); return si.nPos + (int) si.nPage >= si.nMax; } static void buffer_scroll_to_bottom() { SendMessage(g.hwndBuffer, EM_SCROLL, SB_BOTTOM, 0); } // --- UI state refresh -------------------------------------------------------- static void refresh_icon() { HICON icon = g.hicon; for (const auto &b : g.buffers) if (b.highlighted) icon = g.hiconHighlighted; SendMessage(g.hwndMain, WM_SETICON, ICON_SMALL, (LPARAM) icon); SendMessage(g.hwndMain, WM_SETICON, ICON_BIG, (LPARAM) icon); } static void richedit_replacesel(HWND hWnd, const CHARFORMAT2 *cf, const wchar_t *text) { SendMessage(hWnd, EM_SETCHARFORMAT, SCF_SELECTION, (LPARAM) cf); SendMessage(hWnd, EM_REPLACESEL, FALSE, (LPARAM) text); } static void refresh_topic(const std::vector &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""; } // Buffer scrolling would cause a ton of flickering redraws. if (window_get_text(g.hwndStatus) != status) SetWindowText(g.hwndStatus, status.c_str()); } // --- Rich Edit formatting ---------------------------------------------------- static COLORREF convert_color(int16_t color) { static const uint16_t base16[] = { 0x000, 0x800, 0x080, 0x880, 0x008, 0x808, 0x088, 0xccc, 0x888, 0xf00, 0x0f0, 0xff0, 0x00f, 0xf0f, 0x0ff, 0xfff, }; if (color < 16) { uint8_t r = 0xf & (base16[color] >> 8); uint8_t g = 0xf & (base16[color] >> 4); uint8_t b = 0xf & (base16[color]); return RGB(r * 0x11, g * 0x11, b * 0x11); } if (color >= 216) { uint8_t g = 8 + (color - 216) * 10; return RGB(g, g, g); } uint8_t i = color - 16, r = i / 36 >> 0, g = (i / 6 >> 0) % 6, b = i % 6; return RGB( !r ? 0 : 55 + 40 * r, !g ? 0 : 55 + 40 * g, !b ? 0 : 55 + 40 * b); } static CHARFORMAT2 default_charformat() { // Everything we leave out will be kept as it was. // So, e.g., there is no way to "unset" a monospace font. CHARFORMAT2 reset = {}; reset.cbSize = sizeof reset; reset.dwMask = CFM_BOLD | CFM_ITALIC | CFM_UNDERLINE | CFM_STRIKEOUT | CFM_COLOR | CFM_BACKCOLOR | CFM_FACE | CFM_LINK; reset.dwEffects = CFE_AUTOCOLOR | CFE_AUTOBACKCOLOR; lstrcpyn(reset.szFaceName, g.fontlog.lfFaceName, sizeof reset.szFaceName); return reset; } static void convert_item_formatting(Relay::ItemData *item, CHARFORMAT2 &cf, bool &inverse) { if (dynamic_cast(item)) { cf = default_charformat(); inverse = false; } else if (dynamic_cast(item)) { cf.dwEffects ^= CFE_BOLD; } else if (dynamic_cast(item)) { cf.dwEffects ^= CFE_ITALIC; } else if (dynamic_cast(item)) { cf.dwEffects ^= CFE_UNDERLINE; } else if (dynamic_cast(item)) { cf.dwEffects ^= CFE_STRIKEOUT; } else if (dynamic_cast(item)) { inverse = !inverse; } else if (dynamic_cast(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(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(item)) { if (data->color < 0) { cf.dwEffects |= CFE_AUTOBACKCOLOR; } else { cf.dwEffects &= ~CFE_AUTOBACKCOLOR; cf.crBackColor = convert_color(data->color); } } } static std::vector convert_items(const std::vector> &items) { CHARFORMAT2 cf = default_charformat(); std::vector result; bool inverse = false; for (const auto &it : items) { auto text = dynamic_cast(it.get()); if (!text) { convert_item_formatting(it.get(), cf, inverse); continue; } BufferLineItem item = {}; item.format = cf; item.text = text->text; if (inverse) { std::swap(item.format.crTextColor, item.format.crBackColor); item.format.dwEffects &= ~(CFE_AUTOCOLOR | CFE_AUTOBACKCOLOR); if (cf.dwEffects & CFE_AUTOCOLOR) item.format.crBackColor = GetSysColor(COLOR_WINDOWTEXT); if (cf.dwEffects & CFE_AUTOBACKCOLOR) item.format.crTextColor = GetSysColor(COLOR_WINDOW); } result.push_back(std::move(item)); } return result; } // --- Buffer output ----------------------------------------------------------- static BufferLine convert_buffer_line(Relay::EventData_BufferLine &line) { BufferLine self = {}; self.items = convert_items(line.items); self.is_unimportant = line.is_unimportant; self.is_highlight = line.is_highlight; self.rendition = line.rendition; self.when = line.when; return self; } static void buffer_print_date_change(bool &sameline, const tm &last, const tm ¤t) { if (last.tm_year == current.tm_year && last.tm_mon == current.tm_mon && last.tm_mday == current.tm_mday) return; wchar_t buffer[64] = {}; wcsftime(buffer, sizeof buffer, &L"\n%x"[sameline], ¤t); sameline = false; CHARFORMAT2 cf = default_charformat(); cf.dwEffects |= CFE_BOLD; richedit_replacesel(g.hwndBuffer, &cf, buffer); } static LONG buffer_reset_selection() { CHARRANGE cr = {}; cr.cpMin = cr.cpMax = GetWindowTextLength(g.hwndBuffer); SendMessage(g.hwndBuffer, EM_EXSETSEL, 0, (LPARAM) &cr); return cr.cpMin; } static struct tm buffer_localtime(time_t time) { // This isn't critical, so let it fail quietly. struct tm result = {}; (void) localtime_s(&result, &time); return result; } static void buffer_print_and_watch_trailing_date_changes() { time_t current_unix = time(NULL); tm current = buffer_localtime(current_unix); auto b = buffer_by_name(g.buffer_current); if (b && !b->lines.empty()) { tm last = buffer_localtime(b->lines.back().when / 1000); bool sameline = !buffer_reset_selection(); buffer_print_date_change(sameline, last, current); } current.tm_sec = current.tm_min = current.tm_hour = 0; current.tm_mday++; current.tm_isdst = -1; const time_t midnight = mktime(¤t); if (midnight == (time_t) -1 || midnight < current_unix) return; // Note that after printing the first trailing update, // follow-up updates may be duplicated if timer events arrive too early. LARGE_INTEGER li = {}; li.QuadPart = (midnight - current_unix + 1) * -10000000LL; SetWaitableTimer(g.date_change_timer, &li, 0, NULL, NULL, FALSE); } static void buffer_print_line(std::vector::const_iterator begin, std::vector::const_iterator line) { tm current = buffer_localtime(line->when / 1000); tm last = buffer_localtime( line == begin ? time(NULL) : (line - 1)->when / 1000); // The Rich Edit control makes the window cursor transparent // each time you add an independent newline character. Avoid that. // (Sadly, this also makes Windows 7 end lines with a bogus space that // has the CHARFORMAT2 of what we flush that newline together with.) bool sameline = !buffer_reset_selection(); buffer_print_date_change(sameline, last, current); wchar_t buffer[64] = {}; wcsftime(buffer, sizeof buffer, &L"\n%H:%M:%S"[sameline], ¤t); CHARFORMAT2 cf = default_charformat(); cf.dwEffects &= ~(CFE_AUTOCOLOR | CFE_AUTOBACKCOLOR); cf.crTextColor = RGB(0xbb, 0xbb, 0xbb); cf.crBackColor = RGB(0xf8, 0xf8, 0xf8); richedit_replacesel(g.hwndBuffer, &cf, buffer); cf = default_charformat(); richedit_replacesel(g.hwndBuffer, &cf, L" "); // Tabstops won't quite help us here, since we need it centred. std::wstring prefix; CHARFORMAT2 pcf = default_charformat(); lstrcpyn(pcf.szFaceName, L"Courier New", sizeof pcf.szFaceName); // This looks better, but it may trigger a repaint bug in richedit. #if 1 pcf.dwEffects |= CFE_BOLD; #endif switch (line->rendition) { break; case Relay::Rendition::BARE: break; case Relay::Rendition::INDENT: prefix = L" "; break; case Relay::Rendition::STATUS: prefix = L" - "; break; case Relay::Rendition::ERROR: prefix = L"=!= "; pcf.dwEffects &= ~CFE_AUTOCOLOR; pcf.crTextColor = RGB(0xff, 0, 0); break; case Relay::Rendition::JOIN: prefix = L"——> "; pcf.dwEffects &= ~CFE_AUTOCOLOR; pcf.crTextColor = RGB(0, 0x88, 0); break; case Relay::Rendition::PART: prefix = L"<—— "; pcf.dwEffects &= ~CFE_AUTOCOLOR; pcf.crTextColor = RGB(0x88, 0, 0); break; case Relay::Rendition::ACTION: prefix = L" * "; pcf.dwEffects &= ~CFE_AUTOCOLOR; pcf.crTextColor = RGB(0x88, 0, 0); } if (line->leaked) { pcf.dwEffects &= ~CFE_AUTOCOLOR; pcf.crTextColor = GetSysColor(COLOR_GRAYTEXT); if (!prefix.empty()) richedit_replacesel(g.hwndBuffer, &pcf, prefix.c_str()); 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(); SendMessage(g.hwndBuffer, WM_SETREDRAW, (WPARAM) TRUE, 0); InvalidateRect(g.hwndBuffer, NULL, TRUE); SetCursor(oldCursor); } // --- Event processing -------------------------------------------------------- static void relay_process_buffer_line(Buffer &b, Relay::EventData_BufferLine &m) { // Initial sync: skip all other processing, let highlights be. auto bc = buffer_by_name(g.buffer_current); if (!bc) { b.lines.push_back(convert_buffer_line(m)); return; } // Retained mode is complicated. bool display = (!m.is_unimportant || !bc->hide_unimportant) && (b.buffer_name == g.buffer_current || m.leak_to_active); bool to_bottom = display && buffer_at_bottom(); bool visible = display && to_bottom && !IsIconic(g.hwndMain) && !IsWindowVisible(g.hwndBufferLog); bool separate = display && !visible && !bc->new_messages && !bc->new_unimportant_messages; auto line = b.lines.insert(b.lines.end(), convert_buffer_line(m)); if (!(visible || m.leak_to_active) || b.new_messages || b.new_unimportant_messages) { if (line->is_unimportant || m.leak_to_active) b.new_unimportant_messages++; else b.new_messages++; } if (m.leak_to_active) { auto line = bc->lines.insert(bc->lines.end(), convert_buffer_line(m)); line->leaked = true; if (!visible || bc->new_messages || bc->new_unimportant_messages) { if (line->is_unimportant) bc->new_unimportant_messages++; else bc->new_messages++; } } if (separate) buffer_print_separator(); if (display) buffer_print_line(bc->lines.begin(), bc->lines.end() - 1); if (to_bottom) buffer_scroll_to_bottom(); if (line->is_highlight || (!visible && !line->is_unimportant && b.kind == Relay::BufferKind::PRIVATE_MESSAGE)) { beep(); if (!visible) { b.highlighted = true; refresh_icon(); } } refresh_buffer_list(); } static void relay_process_callbacks(uint32_t command_seq, const std::wstring& error, const Relay::ResponseData *response) { auto &callbacks = g.command_callbacks; auto handler = callbacks.find(command_seq); if (handler == callbacks.end()) { // TODO(p): Warn about an unawaited response. } else { if (handler->second) handler->second(error, response); callbacks.erase(handler); } // We don't particularly care about wraparound issues. while (!callbacks.empty() && callbacks.begin()->first <= command_seq) { auto front = callbacks.begin(); if (front->second) front->second(L"No response", nullptr); callbacks.erase(front); } } static void relay_process_message(const Relay::EventMessage &m) { switch (m.data->event) { case Relay::Event::ERROR: { auto data = dynamic_cast(m.data.get()); relay_process_callbacks(data->command_seq, data->error, nullptr); break; } case Relay::Event::RESPONSE: { auto data = dynamic_cast(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(*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(*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( data.context.get())) b->server_name = context->server_name; if (auto context = dynamic_cast( data.context.get())) { b->server_name = context->server_name; b->modes = context->modes; b->topic = convert_items(context->topic); } if (auto context = dynamic_cast( 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(*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(*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(*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(*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(*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(*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(*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( 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(*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(*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(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(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(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); refresh_status(); return lResult; } case WM_NCDESTROY: // https://devblogs.microsoft.com/oldnewthing/20031111-00/?p=41883 RemoveWindowSubclass(hWnd, richedit_proc, uIdSubclass); break; } return DefSubclassProc(hWnd, uMsg, wParam, lParam); } static LRESULT CALLBACK log_proc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, [[maybe_unused]] DWORD_PTR dwRefData) { switch (uMsg) { case WM_GETDLGCODE: { // https://devblogs.microsoft.com/oldnewthing/20070627-00/?p=26243 // https://devblogs.microsoft.com/oldnewthing/20031114-00/?p=41823 LRESULT lResult = DefSubclassProc(hWnd, uMsg, wParam, lParam); lResult &= ~(DLGC_WANTTAB | DLGC_HASSETSEL); if (lParam && ((MSG *) lParam)->message == WM_KEYDOWN && ((MSG *) lParam)->wParam == VK_TAB) lResult &= ~DLGC_WANTMESSAGE; return lResult; } case WM_NCDESTROY: // https://devblogs.microsoft.com/oldnewthing/20031111-00/?p=41883 RemoveWindowSubclass(hWnd, log_proc, uIdSubclass); break; } return DefSubclassProc(hWnd, uMsg, wParam, lParam); } static void process_resize(UINT w, UINT h) { // Font height, control height (accounts for padding and borders) int fh = g.font_height, ch = fh + 10; int top = 0; MoveWindow(g.hwndTopic, 5, 5, w - 10, fh, FALSE); top += ch; int bottom = 0; MoveWindow(g.hwndInput, 3, h - ch - 3, w - 6, ch, FALSE); bottom += ch + 3; MoveWindow(g.hwndPrompt, 5, h - bottom - fh - 5, w / 2 - 5, fh, FALSE); MoveWindow(g.hwndStatus, w / 2, h - bottom - fh - 5, w / 2 - 5, fh, FALSE); bottom += ch; bool to_bottom = buffer_at_bottom(); MoveWindow(g.hwndBufferList, 3, top, 150, h - top - bottom, FALSE); MoveWindow(g.hwndBuffer, 156, top, w - 159, h - top - bottom, FALSE); MoveWindow(g.hwndBufferLog, 156, top, w - 159, h - top - bottom, FALSE); if (to_bottom) buffer_scroll_to_bottom(); InvalidateRect(g.hwndMain, NULL, TRUE); } static void process_bufferlist_drawitem(PDRAWITEMSTRUCT dis) { // Just always redraw it entirely, disregarding dis->itemAction. COLORREF foreground = GetSysColor(dis->itemState & ODS_SELECTED ? COLOR_HIGHLIGHTTEXT : COLOR_WINDOWTEXT); COLORREF background = GetSysColor(dis->itemState & ODS_SELECTED ? COLOR_HIGHLIGHT : COLOR_WINDOW); if (dis->itemState & ODS_DISABLED) foreground = GetSysColor(COLOR_GRAYTEXT); HFONT oldFont = NULL; std::wstring text; if (dis->itemID != (UINT) -1 && dis->itemID < g.buffers.size()) { const Buffer& b = g.buffers.at(dis->itemID); text = b.buffer_name; if (b.buffer_name != g.buffer_current && b.new_messages) { text += L" (" + std::to_wstring(b.new_messages) + L")"; oldFont = (HFONT) SelectObject(dis->hDC, g.hfontBold); } if (b.highlighted) foreground = RGB(0xff, 0x5f, 0x00); } COLORREF oldForeground = SetTextColor(dis->hDC, foreground); COLORREF oldBackground = SetBkColor(dis->hDC, background); // Old Windows hardcoded two pixels, and so will we. ExtTextOut(dis->hDC, dis->rcItem.left + 2, dis->rcItem.top, ETO_CLIPPED | ETO_OPAQUE, &dis->rcItem, text.c_str(), text.length(), NULL); if (oldFont) SelectObject(dis->hDC, oldFont); SetTextColor(dis->hDC, oldForeground); SetBkColor(dis->hDC, oldBackground); if (dis->itemState & ODS_FOCUS) DrawFocusRect(dis->hDC, &dis->rcItem); } static void process_bufferlist_notification(WORD code) { if (code == LBN_SELCHANGE) { auto i = (size_t) SendMessage(g.hwndBufferList, LB_GETCURSEL, 0, 0); if (i < g.buffers.size()) buffer_activate(g.buffers.at(i).buffer_name); } } static void process_accelerator(WORD id) { // Buffer indexes rotated to start after the current buffer. std::vector rotated(g.buffers.size()); size_t start = 0; for (auto it = g.buffers.begin(); it != g.buffers.end(); ++it) if (it->buffer_name == g.buffer_current) { start = it - g.buffers.begin(); break; } for (auto &index : rotated) index = ++start % g.buffers.size(); auto b = buffer_by_name(g.buffer_current); switch (id) { case ID_PREVIOUS_BUFFER: if (rotated.size() > 0) { size_t i = (rotated.back() ? rotated.back() : g.buffers.size()) - 1; buffer_activate(g.buffers[i].buffer_name); } return; case ID_NEXT_BUFFER: if (rotated.size() > 0) buffer_activate(g.buffers[rotated.front()].buffer_name); return; case ID_SWITCH_BUFFER: if (!g.buffer_last.empty()) buffer_activate(g.buffer_last); return; case ID_GOTO_HIGHLIGHT: for (auto i : rotated) if (g.buffers[i].highlighted) { buffer_activate(g.buffers[i].buffer_name); break; } return; case ID_GOTO_ACTIVITY: for (auto i : rotated) if (g.buffers[i].new_messages) { buffer_activate(g.buffers[i].buffer_name); break; } return; case ID_TOGGLE_UNIMPORTANT: if (b) buffer_toggle_unimportant(b->buffer_name); return; case ID_DISPLAY_FULL_LOG: if (b) buffer_toggle_log(); return; } } static LRESULT CALLBACK window_proc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { switch (uMsg) { case WM_DESTROY: PostQuitMessage(0); return 0; case WM_SIZE: process_resize(LOWORD(lParam), HIWORD(lParam)); return 0; case WM_TIMECHANGE: _tzset(); if (auto b = buffer_by_name(g.buffer_current)) refresh_buffer(*b); return 0; case WM_SYSCOLORCHANGE: // The topic would flicker with WS_EX_TRANSPARENT. // The buffer only changed its text colour, not background. SendMessage(g.hwndTopic, EM_SETBKGNDCOLOR, 0, GetSysColor(COLOR_3DFACE)); SendMessage(g.hwndBuffer, EM_SETBKGNDCOLOR, 1, 0); // XXX: This is incomplete, we'd have to run convert_items() again; // essentially only COLOR_GRAYTEXT is reloaded in here. if (auto b = buffer_by_name(g.buffer_current)) refresh_buffer(*b); // Pass it to all child windows, through DefWindowProc(). break; case WM_ACTIVATE: if (LOWORD(wParam) == WA_INACTIVE) g.hwndLastFocused = GetFocus(); else if (g.hwndLastFocused) SetFocus(g.hwndLastFocused); return 0; case WM_MEASUREITEM: { auto mis = (PMEASUREITEMSTRUCT) lParam; mis->itemHeight = g.font_height; return TRUE; } case WM_DRAWITEM: { auto dis = (PDRAWITEMSTRUCT) lParam; if (dis->hwndItem == g.hwndBufferList) process_bufferlist_drawitem(dis); return TRUE; } case WM_SYSCOMMAND: { auto b = buffer_by_name(g.buffer_current); if (b && wParam == SC_RESTORE) { b->highlighted = false; refresh_icon(); } // Here we absolutely must pass to DefWindowProc(). break; } case WM_COMMAND: if (!lParam) process_accelerator(LOWORD(wParam)); else if (lParam == (LPARAM) g.hwndBufferList) process_bufferlist_notification(HIWORD(wParam)); else if (lParam == (LPARAM) g.hwndBuffer && HIWORD(wParam) == EN_VSCROLL) refresh_status(); return 0; case WM_NOTIFY: switch (((LPNMHDR) lParam)->code) { case EN_LINK: { auto link = (ENLINK *) lParam; if (link->msg == WM_LBUTTONUP) { TEXTRANGE tr = {}; tr.chrg = link->chrg; tr.lpstrText = new wchar_t[tr.chrg.cpMax - tr.chrg.cpMin + 1](); SendMessage( link->nmhdr.hwndFrom, EM_GETTEXTRANGE, 0, (LPARAM) &tr); ShellExecute( NULL, L"open", tr.lpstrText, NULL, NULL, SW_SHOWNORMAL); delete[] tr.lpstrText; } break; } } case DM_GETDEFID: case DM_SETDEFID: break; } return DefWindowProc(hWnd, uMsg, wParam, lParam); } static INT_PTR CALLBACK connect_proc( HWND hDlg, UINT uMsg, WPARAM wParam, [[maybe_unused]] LPARAM lParam) { switch (uMsg) { case WM_INITDIALOG: return TRUE; case WM_COMMAND: switch (LOWORD(wParam)) { case IDOK: case IDCANCEL: g.host = window_get_text(GetDlgItem(hDlg, IDC_HOST)); g.port = window_get_text(GetDlgItem(hDlg, IDC_PORT)); EndDialog(hDlg, LOWORD(wParam)); return TRUE; } } return FALSE; } static void get_font() { // To enable the "Make text bigger" scaling functionality. NONCLIENTMETRICS ncm = {}; ncm.cbSize = sizeof ncm; if (SystemParametersInfo(SPI_GETNONCLIENTMETRICS, sizeof ncm, &ncm, 0)) { g.hfont = CreateFontIndirect(&ncm.lfMessageFont); LOGFONT bold = g.fontlog = ncm.lfMessageFont; bold.lfWeight = FW_BOLD; g.hfontBold = CreateFontIndirect(&bold); } if (!g.hfont) g.hfont = g.hfontBold = (HFONT) GetStockObject(DEFAULT_GUI_FONT); // There doesn't seem to be a better way than through a drawing context. HDC hdc = GetDC(NULL); HFONT oldFont = (HFONT) SelectObject(hdc, g.hfont); TEXTMETRIC tm = {}; GetTextMetrics(hdc, &tm); SelectObject(hdc, oldFont); ReleaseDC(NULL, hdc); g.font_height = tm.tmHeight; } static BOOL CALLBACK set_font(HWND child, LPARAM font) { SendMessage(child, WM_SETFONT, font, (LPARAM) TRUE); return TRUE; } static bool process_messages(HACCEL accelerators) { MSG message = {}; while (PeekMessage(&message, NULL, 0, 0, TRUE)) { if (message.message == WM_QUIT) return false; if (TranslateAccelerator(g.hwndMain, accelerators, &message)) continue; // https://devblogs.microsoft.com/oldnewthing/20031021-00/?p=42083 if (IsDialogMessage(g.hwndMain, &message)) continue; TranslateMessage(&message); DispatchMessage(&message); } return true; } int WINAPI wWinMain(HINSTANCE hInstance, [[maybe_unused]] HINSTANCE hPrevInstance, PWSTR pCmdLine, int nCmdShow) { setlocale(LC_ALL, ""); WSADATA wd = {}; int err = {}; if ((err = WSAStartup(MAKEWORD(2, 2), &wd))) { show_error_message(format_error_message(err).c_str()); return 1; } INITCOMMONCONTROLSEX icc = {}; icc.dwICC = 0; icc.dwSize = sizeof icc; (void) InitCommonControlsEx(&icc); // TODO(p): The control doesn't seem to support visual styles at all, // try to figure out how to immitate it with WM_NCPAINT, // GetThemeBackgroundContentRect(), DrawThemeBackground(), etc., // and remember to handle the case of visual styles being disabled, // perhaps by using DrawEdge() → InflateRect(-CXBORDER, -CYBORDER). // This is by no means simple. // // Example implementation: https://web.archive.org/web/20210707175627 // /http://www.codeguru.com/cpp/w-d/dislog/miscellaneous/article.php/c8729 // /XP-Theme-Support-for-Rich-Edit-and-Custom-Controls.htm if (!LoadLibrary(L"Msftedit.dll")) { show_error_message(format_error_message(GetLastError()).c_str()); return 1; } // WINE calls WM_MEASUREITEM as soon as the listbox is created. // TODO(p): Watch for WM_SETTINGCHANGE/SPI_SETNONCLIENTMETRICS, // reset all fonts in all widgets, and the topic background colour. get_font(); g.hicon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_ICON)); g.hiconHighlighted = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_HIGHLIGHTED)); WNDCLASSEX wc = {}; wc.cbSize = sizeof wc; wc.lpfnWndProc = window_proc; wc.hInstance = hInstance; wc.hIcon = g.hicon; wc.hCursor = LoadCursor(NULL, IDC_ARROW); wc.hbrBackground = GetSysColorBrush(COLOR_3DFACE); wc.lpszClassName = TEXT(PROJECT_NAME); if (!RegisterClassEx(&wc)) return 1; g.hwndMain = CreateWindowEx( WS_EX_CONTROLPARENT, wc.lpszClassName, TEXT(PROJECT_NAME), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 600, 400, NULL, NULL, hInstance, NULL); // We're lucky to not need much user user interface, // because Win32 is in many aspects quite difficult to work with. HMENU id = 0; g.hwndTopic = CreateWindowEx(0, MSFTEDIT_CLASS, L"", WS_VISIBLE | WS_CHILD | WS_TABSTOP | ES_AUTOHSCROLL | ES_READONLY, 0, 0, 100, 20, g.hwndMain, ++id, hInstance, NULL); g.hwndBufferList = CreateWindowEx(WS_EX_CLIENTEDGE, WC_LISTBOX, L"", WS_VISIBLE | WS_CHILD | WS_TABSTOP | WS_VSCROLL | LBS_NOINTEGRALHEIGHT | LBS_NOTIFY | LBS_NODATA | LBS_OWNERDRAWFIXED, 0, 20, 50, 40, g.hwndMain, ++id, hInstance, NULL); g.hwndBuffer = CreateWindowEx(WS_EX_CLIENTEDGE, MSFTEDIT_CLASS, L"", WS_VISIBLE | WS_CHILD | WS_TABSTOP | WS_VSCROLL | ES_MULTILINE | ES_READONLY | ES_SAVESEL, 50, 20, 50, 40, g.hwndMain, ++id, hInstance, NULL); g.hwndBufferLog = CreateWindowEx(WS_EX_CLIENTEDGE, WC_EDIT, L"", WS_CHILD | WS_TABSTOP | WS_VSCROLL | ES_MULTILINE | ES_READONLY, 50, 20, 50, 40, g.hwndMain, ++id, hInstance, NULL); g.hwndPrompt = CreateWindowEx(0, WC_STATIC, L"Connecting...", WS_VISIBLE | WS_CHILD, 0, 60, 50, 20, g.hwndMain, ++id, hInstance, NULL); g.hwndStatus = CreateWindowEx(0, WC_STATIC, L"", WS_VISIBLE | WS_CHILD | ES_RIGHT, 50, 60, 50, 20, g.hwndMain, ++id, hInstance, NULL); g.hwndInput = CreateWindowEx(WS_EX_CLIENTEDGE, WC_EDIT, L"", WS_VISIBLE | WS_CHILD | WS_TABSTOP | ES_AUTOHSCROLL, 0, 80, 100, 20, g.hwndMain, ++id, hInstance, NULL); SendMessage(g.hwndTopic, EM_SETBKGNDCOLOR, 0, GetSysColor(COLOR_3DFACE)); // The 1 probably means AURL_ENABLEEA only in later versions. SendMessage(g.hwndTopic, EM_AUTOURLDETECT, 1, 0); SendMessage(g.hwndTopic, EM_SETEVENTMASK, 0, ENM_LINK); SendMessage(g.hwndTopic, EM_SETUNDOLIMIT, 0, 0); SetWindowSubclass(g.hwndTopic, richedit_proc, 0, 0); SendMessage(g.hwndBuffer, EM_AUTOURLDETECT, 1, 0); SendMessage(g.hwndBuffer, EM_SETEVENTMASK, 0, ENM_LINK | ENM_SCROLL); SendMessage(g.hwndBuffer, EM_SETUNDOLIMIT, 0, 0); SetWindowSubclass(g.hwndBufferList, bufferlist_proc, 0, 0); SetWindowSubclass(g.hwndBuffer, richedit_proc, 0, 0); SetWindowSubclass(g.hwndBufferLog, log_proc, 0, 0); RECT client_rect = {}; if (GetClientRect(g.hwndMain, &client_rect)) { process_resize(client_rect.right - client_rect.left, client_rect.bottom - client_rect.top); } EnumChildWindows(g.hwndMain, (WNDENUMPROC) set_font, (LPARAM) g.hfont); SendMessage(g.hwndPrompt, WM_SETFONT, (WPARAM) g.hfontBold, (LPARAM) TRUE); SetFocus(g.hwndInput); SetWindowSubclass(g.hwndInput, input_proc, 0, 0); ShowWindow(g.hwndMain, nCmdShow); HACCEL accelerators = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDA_ACCELERATORS)); if (!accelerators) { show_error_message(format_error_message(GetLastError()).c_str()); return 1; } int argc = 0; LPWSTR *argv = CommandLineToArgvW(pCmdLine, &argc); if (argc >= 2) { g.host = argv[0]; g.port = argv[1]; } else if (DialogBox(hInstance, MAKEINTRESOURCE(IDD_CONNECT), g.hwndMain, connect_proc) != IDOK) { return 0; } LocalFree(argv); // We have a few suboptimal asynchronous options: // a) WSAAsyncGetHostByName() requires us to distinguish hostnames // from IP literals manually, // b) GetAddrInfoEx() only supports asynchronous operation since Windows 8, // c) run this from a thread. addrinfoW hints = {}; hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; hints.ai_protocol = IPPROTO_TCP; err = GetAddrInfo(g.host.c_str(), g.port.c_str(), &hints, &g.addresses); if (err) { show_error_message(format_error_message(err).c_str()); return 1; } std::wstring error; g.addresses_iterator = g.addresses; if (!relay_connect_step(error)) { show_error_message(error.c_str()); return 1; } 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; }