From 0219dbd02615498f4a931e33ef2337e0bbf9b526 Mon Sep 17 00:00:00 2001 From: Přemysl Eric Janouch Date: Sun, 9 Jul 2023 07:06:19 +0200 Subject: Add a Win32 frontend for xC This has been more of an exercise. The performance of Msftedit.dll is rather abysmal, and its interface isn't the most accomodating. That said, the frontend is quite usable, at least on Windows 10+. --- xW/.clang-format | 11 + xW/CMakeLists.txt | 97 +++ xW/config.h.in | 6 + xW/xW-highlighted.svg | 24 + xW/xW-resources.h | 12 + xW/xW.cpp | 1864 +++++++++++++++++++++++++++++++++++++++++++++++++ xW/xW.manifest | 11 + xW/xW.rc | 23 + xW/xW.svg | 24 + 9 files changed, 2072 insertions(+) create mode 100644 xW/.clang-format create mode 100644 xW/CMakeLists.txt create mode 100644 xW/config.h.in create mode 100644 xW/xW-highlighted.svg create mode 100644 xW/xW-resources.h create mode 100644 xW/xW.cpp create mode 100644 xW/xW.manifest create mode 100644 xW/xW.rc create mode 100644 xW/xW.svg (limited to 'xW') diff --git a/xW/.clang-format b/xW/.clang-format new file mode 100644 index 0000000..025ef20 --- /dev/null +++ b/xW/.clang-format @@ -0,0 +1,11 @@ +BasedOnStyle: LLVM +ColumnLimit: 80 +IndentWidth: 4 +TabWidth: 4 +UseTab: ForContinuationAndIndentation +AlwaysBreakAfterReturnType: AllDefinitions +BreakBeforeBraces: Linux +SpaceAfterCStyleCast: true +AlignAfterOpenBracket: DontAlign +AlignOperands: DontAlign +SpacesBeforeTrailingComments: 2 diff --git a/xW/CMakeLists.txt b/xW/CMakeLists.txt new file mode 100644 index 0000000..eb374b9 --- /dev/null +++ b/xW/CMakeLists.txt @@ -0,0 +1,97 @@ +# The last version with Windows XP support is 3.13, we want to keep that +cmake_minimum_required (VERSION 3.10) + +file (READ ../xK-version project_version) +configure_file (../xK-version xK-version.tag COPYONLY) +string (STRIP "${project_version}" project_version) + +# This is an entirely separate CMake project--the main executables only build +# on Windows within Cygwin, and this Windows executable only builds on Linux +# cross-compiled, so you'd want to build them independently anyway. +project (xW VERSION "${project_version}" + DESCRIPTION "Win32 frontend for xC" LANGUAGES CXX) + +set (CMAKE_CXX_STANDARD 17) + +add_definitions (-DUNICODE -D_UNICODE) +add_compile_options ("$<$:/utf-8>") +add_compile_options ("$<$:-Wall;-Wextra>") +add_compile_options ("$<$:-Wall;-Wextra>") +add_link_options ("$<$:-static;-municode>") + +set (project_config ${PROJECT_BINARY_DIR}/config.h) +configure_file (${PROJECT_SOURCE_DIR}/config.h.in ${project_config}) +include_directories (${PROJECT_SOURCE_DIR} ${PROJECT_BINARY_DIR}) + +# Icon generation utilities +# TODO: Shove this into liberty as a CMake module, similar to AddThreads, +# and remove the copies in the parent CMakeLists.txt as well as in tdv. +if (NOT ${CMAKE_VERSION} VERSION_LESS 3.18.0) + set (find_program_REQUIRE REQUIRED) +endif () + +function (icon_to_png name svg size output_dir output) + set (_dimensions ${size}x${size}) + set (_png_path ${output_dir}/hicolor/${_dimensions}/apps) + set (_png ${_png_path}/${name}.png) + set (${output} ${_png} PARENT_SCOPE) + + find_program (rsvg_convert_EXECUTABLE rsvg-convert ${find_program_REQUIRE}) + add_custom_command (OUTPUT ${_png} + COMMAND ${CMAKE_COMMAND} -E make_directory ${_png_path} + COMMAND ${rsvg_convert_EXECUTABLE} --output=${_png} + --width=${size} --height=${size} ${svg} + DEPENDS ${svg} + COMMENT "Generating ${name} ${_dimensions} application icon" VERBATIM) +endfunction () + +function (icon_for_win32 pngs ico) + find_program (icotool_EXECUTABLE icotool ${find_program_REQUIRE}) + add_custom_command (OUTPUT ${ico} + COMMAND ${icotool_EXECUTABLE} -c -o ${ico} ${pngs} + DEPENDS ${pngs} + COMMENT "Generating Windows program icon" VERBATIM) +endfunction () + +# Rasterize SVG icons +set (icon_ico_list) +foreach (icon xW xW-highlighted) + set (icon_png_list) + foreach (icon_size 16 32 48 256) + icon_to_png (${icon} ${PROJECT_SOURCE_DIR}/${icon}.svg + ${icon_size} ${PROJECT_BINARY_DIR}/icons icon_png) + list (APPEND icon_png_list ${icon_png}) + endforeach () + set (icon_ico ${PROJECT_BINARY_DIR}/${icon}.ico) + icon_for_win32 ("${icon_png_list}" ${icon_ico}) + list (APPEND icon_ico_list ${icon_ico}) +endforeach () + +set_property (SOURCE xW.rc APPEND PROPERTY OBJECT_DEPENDS ${icon_ico_list}) + +# Build the main executable and link it +set (root "${PROJECT_SOURCE_DIR}/..") + +find_program (awk_EXECUTABLE awk ${find_program_REQUIRE}) +add_custom_command (OUTPUT xC-proto.cpp + COMMAND ${CMAKE_COMMAND} -E env LC_ALL=C ${awk_EXECUTABLE} + -f ${root}/liberty/tools/lxdrgen.awk + -f ${root}/liberty/tools/lxdrgen-cpp.awk + -v PrefixCamel=Relay + ${root}/xC.lxdr > xC-proto.cpp + DEPENDS + ${root}/liberty/tools/lxdrgen.awk + ${root}/liberty/tools/lxdrgen-cpp.awk + ${root}/xC.lxdr + COMMENT "Generating xC relay protocol code" VERBATIM) +add_custom_target (xC-proto DEPENDS ${PROJECT_BINARY_DIR}/xC-proto.cpp) + +add_executable (xW WIN32 xW.cpp xW.rc xW.manifest ${project_config} + ${root}/liberty/tools/lxdrgen-cpp-win32.cpp) +target_link_libraries (xW comctl32 ws2_32) +add_dependencies (xW xC-proto) + +# At least with MinGW, this is a fully independent portable executable +install (TARGETS xW DESTINATION .) +set (CPACK_GENERATOR ZIP) +include (CPack) diff --git a/xW/config.h.in b/xW/config.h.in new file mode 100644 index 0000000..2bfe8fc --- /dev/null +++ b/xW/config.h.in @@ -0,0 +1,6 @@ +#ifndef CONFIG_H +#define CONFIG_H + +#define PROGRAM_VERSION "${project_version}" + +#endif // ! CONFIG_H diff --git a/xW/xW-highlighted.svg b/xW/xW-highlighted.svg new file mode 100644 index 0000000..cdd8105 --- /dev/null +++ b/xW/xW-highlighted.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/xW/xW-resources.h b/xW/xW-resources.h new file mode 100644 index 0000000..caa37ad --- /dev/null +++ b/xW/xW-resources.h @@ -0,0 +1,12 @@ +#define IDI_ICON 1 +#define IDI_HIGHLIGHTED 2 +#define IDA_ACCELERATORS 10 + +// Named after input_add_functions() in xC. +#define ID_PREVIOUS_BUFFER 11 +#define ID_NEXT_BUFFER 12 +#define ID_SWITCH_BUFFER 13 +#define ID_GOTO_HIGHLIGHT 14 +#define ID_GOTO_ACTIVITY 15 +#define ID_TOGGLE_UNIMPORTANT 16 +#define ID_DISPLAY_FULL_LOG 17 diff --git a/xW/xW.cpp b/xW/xW.cpp new file mode 100644 index 0000000..bbd208a --- /dev/null +++ b/xW/xW.cpp @@ -0,0 +1,1864 @@ +/* + * xW.cpp: Win32 frontend for xC + * + * Copyright (c) 2023, Přemysl Eric Janouch + * + * 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 +#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. + + HICON hicon; ///< Normal program icon + HICON hiconHighlighted; ///< Highlighted program icon + + HFONT hfont; ///< Normal variant of the UI font + HFONT hfontBold; ///< Bold variant of the UI font + + LOGFONT fontlog; ///< UI font characteristics + LONG font_height; ///< UI font height in pixels + + // Networking: + + addrinfoW *addresses; ///< GetAddrInfo() result + addrinfoW *addresses_iterator; ///< Currently processed address + SOCKET socket; ///< Relay socket + WSAEVENT event; ///< Relay socket event + std::vector 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; +} + +// --- Networking -------------------------------------------------------------- + +static bool +relay_try_read(std::wstring &error) +{ + auto &r = g.read_buffer; + char buffer[8192] = {}; + int err = {}; + while (true) { + int n = recv(g.socket, buffer, sizeof buffer, 0); + if (!n) { + error = L"Server closed the connection."; + return false; + } else if (n != SOCKET_ERROR) { + r.insert(r.end(), buffer, buffer + n); + } else if ((err = WSAGetLastError()) != WSAEWOULDBLOCK) { + error = format_error_message(err); + return false; + } else { + break; + } + } + return true; +} + +static bool +relay_try_write(std::wstring &error) +{ + auto &w = g.write_buffer; + int err = {}; + while (!w.empty()) { + int n = send(g.socket, + reinterpret_cast(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()); + + // Call relay_try_write() separately. +} + +static void +relay_send_now(Relay::CommandData *data, Callback callback = {}) +{ + relay_send(data, callback); + + // TODO(p): Either tear down here, or run relay_try_write() from a timer. + std::wstring error; + if (!relay_try_write(error)) + show_error_message(error.c_str()); +} + +// --- Buffers ----------------------------------------------------------------- + +static Buffer * +buffer_by_name(const std::wstring &name) +{ + for (auto &b : g.buffers) + if (b.buffer_name == name) + return &b; + return nullptr; +} + +static void +buffer_activate(const std::wstring &name) +{ + auto activate = new Relay::CommandData_BufferActivate(); + activate->buffer_name = name; + relay_send_now(activate); +} + +static void +buffer_toggle_unimportant(const std::wstring &name) +{ + auto toggle = new Relay::CommandData_BufferToggleUnimportant(); + toggle->buffer_name = name; + relay_send_now(toggle); +} + +// --- Current buffer ---------------------------------------------------------- + +static void +buffer_toggle_log( + const std::wstring &error, const Relay::ResponseData_BufferLog *response) +{ + if (!response) { + show_error_message(error.c_str()); + return; + } + + std::wstring log; + if (!LibertyXDR::utf8_to_wstring( + response->log.data(), response->log.size(), log)) { + show_error_message(L"Invalid encoding."); + return; + } + + std::wstring filtered; + for (auto wch : log) { + if (wch == L'\n') + filtered += L"\r\n"; + else + filtered += wch; + } + + SetWindowText(g.hwndBufferLog, filtered.c_str()); + ShowWindow(g.hwndBuffer, SW_HIDE); + ShowWindow(g.hwndBufferLog, SW_SHOW); +} + +static void +buffer_toggle_log() +{ + if (IsWindowVisible(g.hwndBufferLog)) { + ShowWindow(g.hwndBufferLog, SW_HIDE); + ShowWindow(g.hwndBuffer, SW_SHOW); + SetWindowText(g.hwndBufferLog, L""); + return; + } + + auto log = new Relay::CommandData_BufferLog(); + log->buffer_name = g.buffer_current; + relay_send_now(log, [name = g.buffer_current](auto error, auto response) { + if (g.buffer_current != name) + return; + buffer_toggle_log(error, + dynamic_cast(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; + auto b = buffer_by_name(g.buffer_current); + if (b) { + if (!b->modes.empty()) + status += L"(+" + b->modes + L")"; + if (b->hide_unimportant) + status += L""; + } + + // Buffer scrolling would cause a ton of flickering redraws. + int length = GetWindowTextLength(g.hwndStatus); + std::wstring buffer(length, {}); + GetWindowText(g.hwndStatus, buffer.data(), length + 1); + if (buffer != status) + SetWindowText(g.hwndStatus, status.c_str()); +} + +// --- Rich Edit formatting ---------------------------------------------------- + +static COLORREF +convert_color(int16_t color) +{ + static const uint16_t base16[] = { + 0x000, 0x800, 0x080, 0x880, 0x008, 0x808, 0x088, 0xccc, + 0x888, 0xf00, 0x0f0, 0xff0, 0x00f, 0xf0f, 0x0ff, 0xfff, + }; + if (color < 16) { + uint8_t r = 0xf & (base16[color] >> 8); + uint8_t g = 0xf & (base16[color] >> 4); + uint8_t b = 0xf & (base16[color]); + return RGB(r * 0x11, g * 0x11, b * 0x11); + } + if (color >= 216) { + uint8_t g = 8 + (color - 216) * 10; + return RGB(g, g, g); + } + + uint8_t i = color - 16, r = i / 36 >> 0, g = (i / 6 >> 0) % 6, b = i % 6; + return RGB( + !r ? 0 : 55 + 40 * r, + !g ? 0 : 55 + 40 * g, + !b ? 0 : 55 + 40 * b); +} + +static CHARFORMAT2 +default_charformat() +{ + // Everything we leave out will be kept as it was. + // So, e.g., there is no way to "unset" a monospace font. + CHARFORMAT2 reset = {}; + reset.cbSize = sizeof reset; + reset.dwMask = CFM_BOLD | CFM_ITALIC | CFM_UNDERLINE | CFM_STRIKEOUT | + CFM_COLOR | CFM_BACKCOLOR | CFM_FACE | CFM_LINK; + reset.dwEffects = CFE_AUTOCOLOR | CFE_AUTOBACKCOLOR; + lstrcpyn(reset.szFaceName, g.fontlog.lfFaceName, sizeof reset.szFaceName); + return reset; +} + +static void +convert_item_formatting(Relay::ItemData *item, CHARFORMAT2 &cf, bool &inverse) +{ + if (dynamic_cast(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(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_line(std::vector::const_iterator begin, + std::vector::const_iterator line) +{ + CHARRANGE cr = {}; + cr.cpMin = cr.cpMax = GetWindowTextLength(g.hwndBuffer); + SendMessage(g.hwndBuffer, EM_EXSETSEL, 0, (LPARAM) &cr); + + // The Rich Edit control makes the window cursor transparent + // each time you add an independent newline character. Avoid that. + // (Sadly, this also makes Windows 7 end lines with a bogus space that + // has the CHARFORMAT2 of what we flush that newline together with.) + bool sameline = !cr.cpMin; + + time_t current_unix = line->when / 1000; + time_t last_unix = (line != begin) + ? (line - 1)->when / 1000 + : time(NULL); + + tm current = {}, last = {}; + (void) localtime_s(¤t, ¤t_unix); + (void) localtime_s(&last, &last_unix); + if (last.tm_year != current.tm_year || + last.tm_mon != current.tm_mon || + last.tm_mday != current.tm_mday) { + wchar_t buffer[64] = {}; + wcsftime(buffer, sizeof buffer, &L"\n%x\n"[sameline], ¤t); + sameline = true; + + CHARFORMAT2 cf = default_charformat(); + cf.dwEffects |= CFE_BOLD; + richedit_replacesel(g.hwndBuffer, &cf, buffer); + } + { + wchar_t buffer[64] = {}; + wcsftime(buffer, sizeof buffer, &L"\n%H:%M:%S"[sameline], ¤t); + + CHARFORMAT2 cf = default_charformat(); + cf.dwEffects &= ~(CFE_AUTOCOLOR | CFE_AUTOBACKCOLOR); + cf.crTextColor = RGB(0xbb, 0xbb, 0xbb); + cf.crBackColor = RGB(0xf8, 0xf8, 0xf8); + richedit_replacesel(g.hwndBuffer, &cf, buffer); + cf = default_charformat(); + richedit_replacesel(g.hwndBuffer, &cf, L" "); + } + + // Tabstops won't quite help us here, since we need it centred. + std::wstring prefix; + CHARFORMAT2 pcf = default_charformat(); + lstrcpyn(pcf.szFaceName, L"Courier New", sizeof pcf.szFaceName); + // This looks better, but it may trigger a repaint bug in richedit. +#if 1 + pcf.dwEffects |= CFE_BOLD; +#endif + switch (line->rendition) { + break; case Relay::Rendition::BARE: + break; case Relay::Rendition::INDENT: + prefix = L" "; + break; case Relay::Rendition::STATUS: + prefix = L" - "; + break; case Relay::Rendition::ERROR: + prefix = L"=!= "; + pcf.dwEffects &= ~CFE_AUTOCOLOR; + pcf.crTextColor = RGB(0xff, 0, 0); + break; case Relay::Rendition::JOIN: + prefix = L"——> "; + pcf.dwEffects &= ~CFE_AUTOCOLOR; + pcf.crTextColor = RGB(0, 0x88, 0); + break; case Relay::Rendition::PART: + prefix = L"<—— "; + pcf.dwEffects &= ~CFE_AUTOCOLOR; + pcf.crTextColor = RGB(0x88, 0, 0); + break; case Relay::Rendition::ACTION: + prefix = L" * "; + pcf.dwEffects &= ~CFE_AUTOCOLOR; + pcf.crTextColor = RGB(0x88, 0, 0); + } + + if (line->leaked) { + pcf.dwEffects &= ~CFE_AUTOCOLOR; + pcf.crTextColor = GetSysColor(COLOR_GRAYTEXT); + if (!prefix.empty()) + richedit_replacesel(g.hwndBuffer, &pcf, prefix.c_str()); + + std::wstring text; + for (const auto &it : line->items) + text += it.text; + + CHARFORMAT2 format = default_charformat(); + format.dwEffects &= ~CFE_AUTOCOLOR; + format.crTextColor = GetSysColor(COLOR_GRAYTEXT); + richedit_replacesel(g.hwndBuffer, &format, text.c_str()); + } else { + if (!prefix.empty()) + richedit_replacesel(g.hwndBuffer, &pcf, prefix.c_str()); + for (const auto &it : line->items) + richedit_replacesel(g.hwndBuffer, &it.format, it.text.c_str()); + } +} + +static void +buffer_print_separator() +{ + CHARFORMAT2 format = default_charformat(); + format.dwEffects &= ~CFE_AUTOCOLOR; + format.crTextColor = RGB(0xff, 0x5f, 0x00); + richedit_replacesel(g.hwndBuffer, &format, L"\n---"); +} + +static void +refresh_buffer(const Buffer &b) +{ + HCURSOR oldCursor = SetCursor(LoadCursor(NULL, IDC_WAIT)); + SendMessage(g.hwndBuffer, WM_SETREDRAW, (WPARAM) FALSE, 0); + SetWindowText(g.hwndBuffer, L""); + + // PFM_OFFSET could also be used, but the result isn't very nice. + // + // PFM_BORDER is not implemented, at most we can try to construct + // an OLE object the width of the screen and see how it clips + // (this is a lot of code). + size_t i = 0, mark_before = b.lines.size() - + b.new_messages - b.new_unimportant_messages; + for (auto line = b.lines.begin(); line != b.lines.end(); ++line) { + if (i == mark_before) + buffer_print_separator(); + if (!line->is_unimportant || !b.hide_unimportant) + buffer_print_line(b.lines.begin(), line); + + i++; + } + + buffer_scroll_to_bottom(); + + SendMessage(g.hwndBuffer, WM_SETREDRAW, (WPARAM) TRUE, 0); + InvalidateRect(g.hwndBuffer, NULL, TRUE); + SetCursor(oldCursor); +} + +// --- Event processing -------------------------------------------------------- + +static void +relay_process_buffer_line(Buffer &b, Relay::EventData_BufferLine &m) +{ + // Initial sync: skip all other processing, let highlights be. + auto bc = buffer_by_name(g.buffer_current); + if (!bc) { + b.lines.push_back(convert_buffer_line(m)); + return; + } + + // Retained mode is complicated. + bool display = (!m.is_unimportant || !bc->hide_unimportant) && + (b.buffer_name == g.buffer_current || m.leak_to_active); + bool to_bottom = display && + buffer_at_bottom(); + bool visible = display && + to_bottom && + !IsIconic(g.hwndMain) && + !IsWindowVisible(g.hwndBufferLog); + bool separate = display && + !visible && !bc->new_messages && !bc->new_unimportant_messages; + + auto line = b.lines.insert(b.lines.end(), convert_buffer_line(m)); + if (!(visible || m.leak_to_active) || + b.new_messages || b.new_unimportant_messages) { + if (line->is_unimportant || m.leak_to_active) + b.new_unimportant_messages++; + else + b.new_messages++; + } + + if (m.leak_to_active) { + auto line = bc->lines.insert(bc->lines.end(), convert_buffer_line(m)); + line->leaked = true; + if (!visible || bc->new_messages || bc->new_unimportant_messages) { + if (line->is_unimportant) + bc->new_unimportant_messages++; + else + bc->new_messages++; + } + } + if (separate) + buffer_print_separator(); + if (display) + buffer_print_line(bc->lines.begin(), bc->lines.end() - 1); + if (to_bottom) + buffer_scroll_to_bottom(); + + if (line->is_highlight || (!visible && !line->is_unimportant && + b.kind == Relay::BufferKind::PRIVATE_MESSAGE)) { + // TODO(p): Avoid the PC speaker, which is also unreliable. + Beep(800, 100); + + if (!visible) { + b.highlighted = true; + refresh_icon(); + } + } + + refresh_buffer_list(); +} + +static void +relay_process_callbacks(uint32_t command_seq, + const std::wstring& error, const Relay::ResponseData *response) +{ + auto &callbacks = g.command_callbacks; + auto handler = callbacks.find(command_seq); + if (handler == callbacks.end()) { + // TODO(p): Warn about an unawaited response. + } else { + if (handler->second) + handler->second(error, response); + callbacks.erase(handler); + } + + // We don't particularly care about wraparound issues. + while (!callbacks.empty() && callbacks.begin()->first <= command_seq) { + auto front = callbacks.begin(); + if (front->second) + front->second(L"No response", nullptr); + callbacks.erase(front); + } +} + +static std::wstring input_get_contents(); + +static void +relay_process_message(const Relay::EventMessage &m) +{ + switch (m.data->event) { + case Relay::Event::ERROR: + { + auto data = dynamic_cast(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.buffer_name; + + refresh_buffer_list(); + if (b->buffer_name == g.buffer_current) + refresh_status(); + 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 = input_get_contents(); + SendMessage(g.hwndInput, EM_GETSEL, + (WPARAM) &old->input_start, (LPARAM) &old->input_end); + + // Note that we effectively overwrite the newest line + // with the current textarea contents, and jump there. + old->history_at = old->history.size(); + } + + if (IsWindowVisible(g.hwndBufferLog)) + buffer_toggle_log(); + if (!IsIconic(g.hwndMain)) + b->highlighted = false; + SendMessage(g.hwndBufferList, LB_SETCURSEL, b - g.buffers.data(), 0); + + refresh_topic(b->topic); + refresh_buffer(*b); + refresh_prompt(); + refresh_status(); + + SetWindowText(g.hwndInput, b->input.c_str()); + SendMessage(g.hwndInput, EM_SETSEL, + (WPARAM) b->input_start, (LPARAM) b->input_end); + SetFocus(g.hwndInput); + break; + } + case Relay::Event::BUFFER_INPUT: + { + auto &data = dynamic_cast(*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.event); + g.event = NULL; + + g.read_buffer.clear(); + g.write_buffer.clear(); +} + +static bool +relay_connect_step(std::wstring& error) +{ + addrinfoW *&p = g.addresses_iterator; + while (p) { + g.socket = socket(p->ai_family, p->ai_socktype, p->ai_protocol); + if (g.socket != INVALID_SOCKET) + break; + p = p->ai_next; + } + if (!p) { + error = L"Failed to create a socket."; + return false; + } + + g.event = WSACreateEvent(); + if (WSAEventSelect(g.socket, g.event, + FD_CONNECT | FD_READ | FD_WRITE | FD_CLOSE)) + error = format_error_message(WSAGetLastError()); + else if (!connect(g.socket, p->ai_addr, (int) p->ai_addrlen)) + error = L"Connection succeeded unexpectedly early."; + else if (int err = WSAGetLastError(); err != WSAEWOULDBLOCK) + error = format_error_message(err); + else + return true; + + relay_destroy_socket(); + return false; +} + +static bool +relay_process_connect_event(int err, std::wstring &error) +{ + addrinfoW *&p = g.addresses_iterator; + if (err) { + relay_destroy_socket(); + if (!(p = p->ai_next)) { + error = L"Connection failed."; + return false; + } + return relay_connect_step(error); + } + + g.read_buffer.clear(); + g.write_buffer.clear(); + + auto hello = new Relay::CommandData_Hello(); + hello->version = Relay::VERSION; + relay_send(hello); + // The message will be flushed at the upcoming FD_WRITE notification. + return true; +} + +static bool +relay_process_socket_event(int event, int err, std::wstring &error) +{ + if (err) { + relay_destroy_socket(); + error = format_error_message(err); + return false; + } + + switch (event) { + case FD_READ: + if (!relay_try_read(error) || + !relay_process_buffer(error) || + !relay_try_write(error)) + return false; + break; + case FD_WRITE: + if (!relay_try_write(error)) + return false; + break; + case FD_CLOSE: + // Handling this seems excessive, since we also get EOF while reading. + // But we may not receive an FD_READ notification for it. + error = L"Connection closed."; + return false; + } + return true; +} + +static bool +relay_process_socket_events(std::wstring &error) +{ + WSANETWORKEVENTS wne = {}; + if (WSAEnumNetworkEvents(g.socket, g.event, &wne)) { + error = format_error_message(WSAGetLastError()); + return false; + } + + // TODO(p): Offer reconnecting. + // TODO(p): Consider disabling UI controls while disconnected. + if (wne.lNetworkEvents & FD_CONNECT && + !relay_process_connect_event(wne.iErrorCode[FD_CONNECT_BIT], error)) + return false; + + for (auto bit : {FD_READ_BIT, FD_WRITE_BIT, FD_CLOSE_BIT}) + if ((wne.lNetworkEvents & (1 << bit)) && + !relay_process_socket_event((1 << bit), wne.iErrorCode[bit], error)) + return false; + return true; +} + +// --- Input line -------------------------------------------------------------- + +static std::wstring +input_get_contents() +{ + int length = GetWindowTextLength(g.hwndInput); + std::wstring buffer(length, {}); + GetWindowText(g.hwndInput, buffer.data(), length + 1); + return buffer; +} + +static void +input_set_contents(const std::wstring &input) +{ + SetWindowText(g.hwndInput, input.c_str()); + if (input.size()) + SendMessage(g.hwndInput, EM_SETSEL, input.size(), input.size()); +} + +static bool +input_submit() +{ + auto b = buffer_by_name(g.buffer_current); + if (!b) + return false; + + auto input = new Relay::CommandData_BufferInput(); + input->buffer_name = b->buffer_name; + input->text = input_get_contents(); + + // Buffer::history[Buffer::history.size()] is virtual, + // and is represented either by edit contents when it's currently + // being edited, or by Buffer::input in all other cases. + b->history.push_back(input->text); + b->history_at = b->history.size(); + input_set_contents({}); + + relay_send_now(input); + return true; +} + +struct InputStamp { + DWORD start = {}; + DWORD end = {}; + std::wstring input; +}; + +static InputStamp +input_stamp() +{ + DWORD start = {}, end = {}; + SendMessage(g.hwndInput, EM_GETSEL, (WPARAM) &start, (LPARAM) &end); + return {start, end, input_get_contents()}; +} + +static void +input_complete(const InputStamp &state, const std::wstring &error, + const Relay::ResponseData_BufferComplete *response) +{ + if (!response) { + show_error_message(error.c_str()); + return; + } + + std::string utf8; + if (!LibertyXDR::wstring_to_utf8(state.input.substr(0, state.start), utf8)) + return; + std::wstring preceding; + if (!LibertyXDR::utf8_to_wstring( + reinterpret_cast(utf8.c_str()), response->start, + preceding)) + return; + + if (response->completions.size() > 0) { + auto insert = response->completions.at(0); + if (response->completions.size() == 1) + insert += L" "; + + SendMessage(g.hwndInput, EM_SETSEL, preceding.length(), state.end); + SendMessage(g.hwndInput, EM_REPLACESEL, TRUE, (LPARAM) insert.c_str()); + } + + // TODO(p): Avoid the PC speaker, which is also unreliable. + if (response->completions.size() != 1) + Beep(800, 100); + + // TODO(p): Show all completion options. +} + +static bool +input_complete() +{ + // TODO(p): Also add an increasing counter to the stamp. + auto state = input_stamp(); + if (state.start != state.end) + return false; + + std::string utf8; + if (!LibertyXDR::wstring_to_utf8(state.input.substr(0, state.start), utf8)) + return false; + + auto complete = new Relay::CommandData_BufferComplete(); + complete->buffer_name = g.buffer_current; + complete->text = state.input; + complete->position = utf8.length(); + relay_send_now(complete, [state](auto error, auto response) { + auto stamp = input_stamp(); + if (std::make_tuple(stamp.start, stamp.end, stamp.input) != + std::make_tuple(state.start, state.end, state.input)) + return; + input_complete(stamp, error, + dynamic_cast(response)); + }); + return true; +} + +static boolean +input_wants(const MSG *message) +{ + switch (message->message) { + case WM_KEYDOWN: + // Shift-Tab can go to the dialog manager. + return message->wParam == VK_RETURN || + (message->wParam == VK_TAB && !(GetKeyState(VK_SHIFT) & 0x8000)); + case WM_SYSCHAR: + switch (message->wParam) { + case 'p': return true; + case 'n': return true; + } + } + return false; +} + +static LRESULT CALLBACK +input_proc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, + UINT_PTR uIdSubclass, [[maybe_unused]] DWORD_PTR dwRefData) +{ + switch (uMsg) { + case WM_GETDLGCODE: + { + // https://devblogs.microsoft.com/oldnewthing/20070627-00/?p=26243 + LRESULT lResult = DefSubclassProc(hWnd, uMsg, wParam, lParam); + if (lParam && input_wants((MSG *) lParam)) + lResult |= DLGC_WANTMESSAGE; + return lResult; + } + case WM_SYSCHAR: + { + auto b = buffer_by_name(g.buffer_current); + if (!b) + break; + + // TODO(p): Emacs-style cursor movement shortcuts. + switch (wParam) { + case 'p': + { + if (b->history_at < 1) + break; + if (b->history_at == b->history.size()) + b->input = input_get_contents(); + input_set_contents(b->history.at(--b->history_at)); + return 0; + } + case 'n': + { + if (b->history_at >= b->history.size()) + break; + input_set_contents(++b->history_at == b->history.size() + ? b->input + : b->history.at(b->history_at)); + return 0; + } + } + break; + } + case WM_KEYDOWN: + { + HWND scrollable = IsWindowVisible(g.hwndBufferLog) + ? g.hwndBufferLog + : g.hwndBuffer; + + switch (wParam) { + case VK_PRIOR: + SendMessage(scrollable, EM_SCROLL, SB_PAGEUP, 0); + return 0; + case VK_NEXT: + SendMessage(scrollable, EM_SCROLL, SB_PAGEDOWN, 0); + return 0; + } + break; + } + case WM_CHAR: + { + // This could be implemented more precisely, but it will do. + relay_send_now(new Relay::CommandData_Active()); + + switch (wParam) { + case VK_RETURN: + if (!input_submit()) + break; + return 0; + case VK_TAB: + if (!input_complete()) + break; + return 0; + } + break; + } + case WM_NCDESTROY: + // https://devblogs.microsoft.com/oldnewthing/20031111-00/?p=41883 + RemoveWindowSubclass(hWnd, input_proc, uIdSubclass); + break; + } + return DefSubclassProc(hWnd, uMsg, wParam, lParam); +} + +// --- General UI -------------------------------------------------------------- + +static LRESULT CALLBACK +bufferlist_proc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, + UINT_PTR uIdSubclass, [[maybe_unused]] DWORD_PTR dwRefData) +{ + switch (uMsg) { + case WM_MBUTTONUP: + { + POINT p = {LOWORD(lParam), HIWORD(lParam)}; + ClientToScreen(hWnd, &p); + int index = LBItemFromPt(hWnd, p, FALSE); + if (wParam || index < 0 || (size_t) index > g.buffers.size()) + break; + + auto input = new Relay::CommandData_BufferInput(); + input->buffer_name = g.buffer_current; + input->text = L"/buffer close " + g.buffers.at(index).buffer_name; + relay_send_now(input); + return 0; + } + case WM_NCDESTROY: + // https://devblogs.microsoft.com/oldnewthing/20031111-00/?p=41883 + RemoveWindowSubclass(hWnd, bufferlist_proc, uIdSubclass); + break; + } + return DefSubclassProc(hWnd, uMsg, wParam, lParam); +} + +static LRESULT CALLBACK +richedit_proc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, + UINT_PTR uIdSubclass, [[maybe_unused]] DWORD_PTR dwRefData) +{ + switch (uMsg) { + case WM_GETDLGCODE: + { + // https://devblogs.microsoft.com/oldnewthing/20070627-00/?p=26243 + LRESULT lResult = DefSubclassProc(hWnd, uMsg, wParam, lParam); + lResult &= ~DLGC_WANTTAB; + if (lParam && + ((MSG *) lParam)->message == WM_KEYDOWN && + ((MSG *) lParam)->wParam == VK_TAB) + lResult &= ~DLGC_WANTMESSAGE; + return lResult; + } + case WM_VSCROLL: + { + // Dragging the scrollbar doesn't result in EN_VSCROLL. + LRESULT lResult = DefSubclassProc(hWnd, uMsg, wParam, lParam); + refresh_status(); + return lResult; + } + case WM_NCDESTROY: + // https://devblogs.microsoft.com/oldnewthing/20031111-00/?p=41883 + RemoveWindowSubclass(hWnd, richedit_proc, uIdSubclass); + break; + } + return DefSubclassProc(hWnd, uMsg, wParam, lParam); +} + +static LRESULT CALLBACK +log_proc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, + UINT_PTR uIdSubclass, [[maybe_unused]] DWORD_PTR dwRefData) +{ + switch (uMsg) { + case WM_GETDLGCODE: + { + // https://devblogs.microsoft.com/oldnewthing/20070627-00/?p=26243 + // https://devblogs.microsoft.com/oldnewthing/20031114-00/?p=41823 + LRESULT lResult = DefSubclassProc(hWnd, uMsg, wParam, lParam); + lResult &= ~(DLGC_WANTTAB | DLGC_HASSETSEL); + if (lParam && + ((MSG *) lParam)->message == WM_KEYDOWN && + ((MSG *) lParam)->wParam == VK_TAB) + lResult &= ~DLGC_WANTMESSAGE; + return lResult; + } + case WM_NCDESTROY: + // https://devblogs.microsoft.com/oldnewthing/20031111-00/?p=41883 + RemoveWindowSubclass(hWnd, log_proc, uIdSubclass); + break; + } + return DefSubclassProc(hWnd, uMsg, wParam, lParam); +} + +static void +process_resize(UINT w, UINT h) +{ + // Font height, control height (accounts for padding and borders) + int fh = g.font_height, ch = fh + 10; + + int top = 0; + MoveWindow(g.hwndTopic, 5, 5, w - 10, fh, FALSE); + top += ch; + + int bottom = 0; + MoveWindow(g.hwndInput, 3, h - ch - 3, w - 6, ch, FALSE); + bottom += ch + 3; + MoveWindow(g.hwndPrompt, 5, h - bottom - fh - 5, w / 2 - 5, fh, FALSE); + MoveWindow(g.hwndStatus, w / 2, h - bottom - fh - 5, w / 2 - 5, fh, FALSE); + bottom += ch; + + bool to_bottom = buffer_at_bottom(); + MoveWindow(g.hwndBufferList, 3, top, 150, h - top - bottom, FALSE); + MoveWindow(g.hwndBuffer, 156, top, w - 159, h - top - bottom, FALSE); + MoveWindow(g.hwndBufferLog, 156, top, w - 159, h - top - bottom, FALSE); + if (to_bottom) + buffer_scroll_to_bottom(); + + InvalidateRect(g.hwndMain, NULL, TRUE); +} + +static void +process_bufferlist_drawitem(PDRAWITEMSTRUCT dis) +{ + // Just always redraw it entirely, disregarding dis->itemAction. + COLORREF foreground = GetSysColor(dis->itemState & ODS_SELECTED + ? COLOR_HIGHLIGHTTEXT : COLOR_WINDOWTEXT); + COLORREF background = GetSysColor(dis->itemState & ODS_SELECTED + ? COLOR_HIGHLIGHT : COLOR_WINDOW); + if (dis->itemState & ODS_DISABLED) + foreground = GetSysColor(COLOR_GRAYTEXT); + + HFONT oldFont = NULL; + std::wstring text; + if (dis->itemID != (UINT) -1 && dis->itemID < g.buffers.size()) { + const Buffer& b = g.buffers.at(dis->itemID); + text = b.buffer_name; + if (b.buffer_name != g.buffer_current && b.new_messages) { + text += L" (" + std::to_wstring(b.new_messages) + L")"; + oldFont = (HFONT) SelectObject(dis->hDC, g.hfontBold); + } + if (b.highlighted) + foreground = RGB(0xff, 0x5f, 0x00); + } + + COLORREF oldForeground = SetTextColor(dis->hDC, foreground); + COLORREF oldBackground = SetBkColor(dis->hDC, background); + + // Old Windows hardcoded two pixels, and so will we. + ExtTextOut(dis->hDC, dis->rcItem.left + 2, dis->rcItem.top, + ETO_CLIPPED | ETO_OPAQUE, &dis->rcItem, + text.c_str(), text.length(), NULL); + if (oldFont) + SelectObject(dis->hDC, oldFont); + + SetTextColor(dis->hDC, oldForeground); + SetBkColor(dis->hDC, oldBackground); + + if (dis->itemState & ODS_FOCUS) + DrawFocusRect(dis->hDC, &dis->rcItem); +} + +static void +process_bufferlist_notification(WORD code) +{ + if (code == LBN_SELCHANGE) { + auto i = (size_t) SendMessage(g.hwndBufferList, LB_GETCURSEL, 0, 0); + if (i < g.buffers.size()) + buffer_activate(g.buffers.at(i).buffer_name); + } +} + +static void +process_accelerator(WORD id) +{ + // Buffer indexes rotated to start after the current buffer. + std::vector rotated(g.buffers.size()); + size_t start = 0; + for (auto it = g.buffers.begin(); it != g.buffers.end(); ++it) + if (it->buffer_name == g.buffer_current) { + start = it - g.buffers.begin(); + break; + } + for (auto &index : rotated) + index = ++start % g.buffers.size(); + + auto b = buffer_by_name(g.buffer_current); + switch (id) { + case ID_PREVIOUS_BUFFER: + if (rotated.size() > 0) { + size_t i = (rotated.back() ? rotated.back() : g.buffers.size()) - 1; + buffer_activate(g.buffers[i].buffer_name); + } + return; + case ID_NEXT_BUFFER: + if (rotated.size() > 0) + buffer_activate(g.buffers[rotated.front()].buffer_name); + return; + case ID_SWITCH_BUFFER: + if (!g.buffer_last.empty()) + buffer_activate(g.buffer_last); + return; + case ID_GOTO_HIGHLIGHT: + for (auto i : rotated) + if (g.buffers[i].highlighted) { + buffer_activate(g.buffers[i].buffer_name); + break; + } + return; + case ID_GOTO_ACTIVITY: + for (auto i : rotated) + if (g.buffers[i].new_messages) { + buffer_activate(g.buffers[i].buffer_name); + break; + } + return; + case ID_TOGGLE_UNIMPORTANT: + if (b) + buffer_toggle_unimportant(b->buffer_name); + return; + case ID_DISPLAY_FULL_LOG: + if (b) + buffer_toggle_log(); + return; + } +} + +static LRESULT CALLBACK +window_proc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) +{ + switch (uMsg) { + case WM_DESTROY: + PostQuitMessage(0); + return 0; + case WM_SIZE: + process_resize(LOWORD(lParam), HIWORD(lParam)); + return 0; + case WM_ACTIVATE: + if (LOWORD(wParam) == WA_INACTIVE) + g.hwndLastFocused = GetFocus(); + else if (g.hwndLastFocused) + SetFocus(g.hwndLastFocused); + return 0; + case WM_MEASUREITEM: + { + auto mis = (PMEASUREITEMSTRUCT) lParam; + mis->itemHeight = g.font_height; + return TRUE; + } + case WM_DRAWITEM: + { + auto dis = (PDRAWITEMSTRUCT) lParam; + if (dis->hwndItem == g.hwndBufferList) + process_bufferlist_drawitem(dis); + return TRUE; + } + case WM_SYSCOMMAND: + { + auto b = buffer_by_name(g.buffer_current); + if (b && wParam == SC_RESTORE) { + b->highlighted = false; + refresh_icon(); + } + // Here we absolutely must pass to DefWindowProc(). + break; + } + case WM_COMMAND: + if (!lParam) + process_accelerator(LOWORD(wParam)); + else if (lParam == (LPARAM) g.hwndBufferList) + process_bufferlist_notification(HIWORD(wParam)); + else if (lParam == (LPARAM) g.hwndBuffer && + HIWORD(wParam) == EN_VSCROLL) + refresh_status(); + return 0; + case WM_NOTIFY: + switch (((LPNMHDR) lParam)->code) { + case EN_LINK: + { + auto link = (ENLINK *) lParam; + if (link->msg == WM_LBUTTONUP) { + TEXTRANGE tr = {}; + tr.chrg = link->chrg; + tr.lpstrText = new wchar_t[tr.chrg.cpMax - tr.chrg.cpMin + 1](); + SendMessage( + link->nmhdr.hwndFrom, EM_GETTEXTRANGE, 0, (LPARAM) &tr); + ShellExecute( + NULL, L"open", tr.lpstrText, NULL, NULL, SW_SHOWNORMAL); + delete[] tr.lpstrText; + } + break; + } + } + } + return DefWindowProc(hWnd, uMsg, wParam, lParam); +} + +static void +get_font() +{ + // To enable the "Make text bigger" scaling functionality. + NONCLIENTMETRICS ncm = {}; + ncm.cbSize = sizeof ncm; + if (SystemParametersInfo(SPI_GETNONCLIENTMETRICS, sizeof ncm, &ncm, 0)) { + g.hfont = CreateFontIndirect(&ncm.lfMessageFont); + + LOGFONT bold = g.fontlog = ncm.lfMessageFont; + bold.lfWeight = FW_BOLD; + g.hfontBold = CreateFontIndirect(&bold); + } + + if (!g.hfont) + g.hfont = g.hfontBold = (HFONT) GetStockObject(DEFAULT_GUI_FONT); + + // There doesn't seem to be a better way than through a drawing context. + HDC hdc = GetDC(NULL); + HFONT oldFont = (HFONT) SelectObject(hdc, g.hfont); + TEXTMETRIC tm = {}; + GetTextMetrics(hdc, &tm); + SelectObject(hdc, oldFont); + ReleaseDC(NULL, hdc); + + g.font_height = tm.tmHeight; +} + +static BOOL CALLBACK +set_font(HWND child, LPARAM font) +{ + SendMessage(child, WM_SETFONT, font, (LPARAM) TRUE); + return TRUE; +} + +static bool +process_messages(HACCEL accelerators) +{ + MSG message = {}; + while (PeekMessage(&message, NULL, 0, 0, TRUE)) { + if (message.message == WM_QUIT) + return false; + if (TranslateAccelerator(g.hwndMain, accelerators, &message)) + continue; + + // https://devblogs.microsoft.com/oldnewthing/20031021-00/?p=42083 + if (IsDialogMessage(g.hwndMain, &message)) + continue; + + TranslateMessage(&message); + DispatchMessage(&message); + } + return true; +} + +int WINAPI +wWinMain(HINSTANCE hInstance, [[maybe_unused]] HINSTANCE hPrevInstance, + PWSTR pCmdLine, int nCmdShow) +{ + setlocale(LC_ALL, ""); + + WSADATA wd = {}; + int err = {}; + if ((err = WSAStartup(MAKEWORD(2, 2), &wd))) { + show_error_message(format_error_message(err).c_str()); + return 1; + } + + INITCOMMONCONTROLSEX icc = {}; + icc.dwICC = 0; + icc.dwSize = sizeof icc; + (void) InitCommonControlsEx(&icc); + + // TODO(p): The control doesn't seem to support visual styles at all, + // try to figure out how to immitate it with WM_NCPAINT, + // GetThemeBackgroundContentRect(), DrawThemeBackground(), etc., + // and remember to handle the case of visual styles being disabled, + // perhaps by using DrawEdge() → InflateRect(-CXBORDER, -CYBORDER). + // This is by no means simple. + // + // Example implementation: https://web.archive.org/web/20210707175627 + // /http://www.codeguru.com/cpp/w-d/dislog/miscellaneous/article.php/c8729 + // /XP-Theme-Support-for-Rich-Edit-and-Custom-Controls.htm + if (!LoadLibrary(L"Msftedit.dll")) { + show_error_message(format_error_message(GetLastError()).c_str()); + return 1; + } + + // WINE calls WM_MEASUREITEM as soon as the listbox is created. + // TODO(p): Watch for WM_SETTINGCHANGE/SPI_SETNONCLIENTMETRICS, + // reset all fonts in all widgets, and the topic background colour. + get_font(); + + g.hicon = + LoadIcon(hInstance, MAKEINTRESOURCE(IDI_ICON)); + g.hiconHighlighted = + LoadIcon(hInstance, MAKEINTRESOURCE(IDI_HIGHLIGHTED)); + + WNDCLASSEX wc = {}; + wc.cbSize = sizeof wc; + wc.lpfnWndProc = window_proc; + wc.hInstance = hInstance; + wc.hIcon = g.hicon; + wc.hCursor = LoadCursor(NULL, IDC_ARROW); + wc.hbrBackground = GetSysColorBrush(COLOR_3DFACE); + wc.lpszClassName = L"xW"; + if (!RegisterClassEx(&wc)) + return 1; + + g.hwndMain = CreateWindowEx(WS_EX_CONTROLPARENT, L"xW", L"xW", + WS_OVERLAPPEDWINDOW, + CW_USEDEFAULT, CW_USEDEFAULT, 600, 400, NULL, NULL, hInstance, NULL); + + // We're lucky to not need much user user interface, + // because Win32 is in many aspects quite difficult to work with. + HMENU id = 0; + g.hwndTopic = CreateWindowEx(0, MSFTEDIT_CLASS, L"", + WS_VISIBLE | WS_CHILD | WS_TABSTOP | + ES_AUTOHSCROLL | ES_READONLY, + 0, 0, 100, 20, g.hwndMain, ++id, hInstance, NULL); + g.hwndBufferList = CreateWindowEx(WS_EX_CLIENTEDGE, WC_LISTBOX, L"", + WS_VISIBLE | WS_CHILD | WS_TABSTOP | WS_VSCROLL | + LBS_NOINTEGRALHEIGHT | LBS_NOTIFY | LBS_NODATA | LBS_OWNERDRAWFIXED, + 0, 20, 50, 40, g.hwndMain, ++id, hInstance, NULL); + g.hwndBuffer = CreateWindowEx(WS_EX_CLIENTEDGE, MSFTEDIT_CLASS, L"", + WS_VISIBLE | WS_CHILD | WS_TABSTOP | WS_VSCROLL | + ES_MULTILINE | ES_READONLY | ES_SAVESEL, + 50, 20, 50, 40, g.hwndMain, ++id, hInstance, NULL); + g.hwndBufferLog = CreateWindowEx(WS_EX_CLIENTEDGE, WC_EDIT, L"", + WS_CHILD | WS_TABSTOP | WS_VSCROLL | + ES_MULTILINE | ES_READONLY, + 50, 20, 50, 40, g.hwndMain, ++id, hInstance, NULL); + g.hwndPrompt = CreateWindowEx(0, WC_STATIC, L"Connecting...", + WS_VISIBLE | WS_CHILD, + 0, 60, 50, 20, g.hwndMain, ++id, hInstance, NULL); + g.hwndStatus = CreateWindowEx(0, WC_STATIC, L"", + WS_VISIBLE | WS_CHILD | ES_RIGHT, + 50, 60, 50, 20, g.hwndMain, ++id, hInstance, NULL); + g.hwndInput = CreateWindowEx(WS_EX_CLIENTEDGE, WC_EDIT, L"", + WS_VISIBLE | WS_CHILD | WS_TABSTOP | + ES_AUTOHSCROLL, + 0, 80, 100, 20, g.hwndMain, ++id, hInstance, NULL); + + SendMessage(g.hwndTopic, EM_SETBKGNDCOLOR, 0, GetSysColor(COLOR_3DFACE)); + // The 1 probably means AURL_ENABLEEA only in later versions. + SendMessage(g.hwndTopic, EM_AUTOURLDETECT, 1, 0); + SendMessage(g.hwndTopic, EM_SETEVENTMASK, 0, ENM_LINK); + SendMessage(g.hwndTopic, EM_SETUNDOLIMIT, 0, 0); + SetWindowSubclass(g.hwndTopic, richedit_proc, 0, 0); + SendMessage(g.hwndBuffer, EM_AUTOURLDETECT, 1, 0); + SendMessage(g.hwndBuffer, EM_SETEVENTMASK, 0, ENM_LINK | ENM_SCROLL); + SendMessage(g.hwndBuffer, EM_SETUNDOLIMIT, 0, 0); + SetWindowSubclass(g.hwndBufferList, bufferlist_proc, 0, 0); + SetWindowSubclass(g.hwndBuffer, richedit_proc, 0, 0); + SetWindowSubclass(g.hwndBufferLog, log_proc, 0, 0); + + RECT client_rect = {}; + if (GetClientRect(g.hwndMain, &client_rect)) { + process_resize(client_rect.right - client_rect.left, + client_rect.bottom - client_rect.top); + } + + EnumChildWindows(g.hwndMain, (WNDENUMPROC) set_font, (LPARAM) g.hfont); + SendMessage(g.hwndPrompt, WM_SETFONT, (WPARAM) g.hfontBold, (LPARAM) TRUE); + SetFocus(g.hwndInput); + SetWindowSubclass(g.hwndInput, input_proc, 0, 0); + ShowWindow(g.hwndMain, nCmdShow); + + HACCEL accelerators = + LoadAccelerators(hInstance, MAKEINTRESOURCE(IDA_ACCELERATORS)); + if (!accelerators) { + show_error_message(format_error_message(GetLastError()).c_str()); + return 1; + } + + int argc = 0; + LPWSTR *argv = CommandLineToArgvW(pCmdLine, &argc); + if (argc < 2) { + show_error_message( + L"You must pass the relay address and port on the command line."); + return 1; + } + + // We have a few suboptimal asynchronous options: + // a) WSAAsyncGetHostByName() requires us to distinguish hostnames + // from IP literals manually, + // b) GetAddrInfoEx() only supports asynchronous operation since Windows 8, + // c) run this from a thread. + addrinfoW hints = {}; + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + hints.ai_protocol = IPPROTO_TCP; + err = GetAddrInfo(argv[0], argv[1], &hints, &g.addresses); + LocalFree(argv); + if (err) { + show_error_message(format_error_message(err).c_str()); + return 1; + } + + std::wstring error; + g.addresses_iterator = g.addresses; + if (!relay_connect_step(error)) { + show_error_message(error.c_str()); + return 1; + } + + while (process_messages(accelerators)) { + DWORD result = MsgWaitForMultipleObjects( + g.event != NULL, &g.event, FALSE, INFINITE, QS_ALLINPUT); + if (result == WAIT_FAILED) { + show_error_message(format_error_message(GetLastError()).c_str()); + return 1; + } + if (g.event != NULL && result == WAIT_OBJECT_0 && + !relay_process_socket_events(error)) { + show_error_message(error.c_str()); + return 1; + } + } + FreeAddrInfo(g.addresses); + WSACleanup(); + return 0; +} diff --git a/xW/xW.manifest b/xW/xW.manifest new file mode 100644 index 0000000..68bb0f4 --- /dev/null +++ b/xW/xW.manifest @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/xW/xW.rc b/xW/xW.rc new file mode 100644 index 0000000..858b104 --- /dev/null +++ b/xW/xW.rc @@ -0,0 +1,23 @@ +#include +#include "xW-resources.h" + +// Beware of this madness https://gitlab.kitware.com/cmake/cmake/-/issues/23066 +CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "xW.manifest" + +IDI_ICON ICON "xW.ico" +IDI_HIGHLIGHTED ICON "xW-highlighted.ico" + +IDA_ACCELERATORS ACCELERATORS +BEGIN + "^p", ID_PREVIOUS_BUFFER + "^n", ID_NEXT_BUFFER + VK_F5, ID_PREVIOUS_BUFFER, VIRTKEY + VK_F6, ID_NEXT_BUFFER, VIRTKEY + VK_PRIOR, ID_PREVIOUS_BUFFER, CONTROL, VIRTKEY + VK_NEXT, ID_NEXT_BUFFER, CONTROL, VIRTKEY + VK_TAB, ID_SWITCH_BUFFER, CONTROL, VIRTKEY + "!", ID_GOTO_HIGHLIGHT, ALT + "a", ID_GOTO_ACTIVITY, ALT + "H", ID_TOGGLE_UNIMPORTANT, ALT + "h", ID_DISPLAY_FULL_LOG, ALT +END diff --git a/xW/xW.svg b/xW/xW.svg new file mode 100644 index 0000000..d68c63e --- /dev/null +++ b/xW/xW.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + -- cgit v1.2.3-70-g09d2