From 70fa5597528a8a175e461037184a215796341653 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=99emysl=20Eric=20Janouch?= Date: Wed, 14 Jun 2023 09:26:23 +0200 Subject: [PATCH] xF: finish the frontend using liberty-xui This has long been the missing piece in our frontend collection. After including the termo library in a recent rewrite, we've kind of received an option to use liberty-xui, the terminal/X11/AppKit library spawned off from nncmpp. The frontend for the most part resembles xP, with some missing xC features on top. We do not care about text layouting or X11 text selection, seeing as that requires rather extensive changes, and I'm not planning to use this frontend other than for testing. General formatting support will arrive in a following commit. It should build for macOS as well with minor changes, but we already have xM in our collection, so this is not finished. Bump liberty. --- CMakeLists.txt | 51 +- NEWS | 2 + README.adoc | 12 +- liberty | 2 +- xC.adoc | 2 +- xF.adoc | 39 + xF.c | 3046 ++++++++++++++++++++++++++++++++++++++++++++++-- 7 files changed, 3061 insertions(+), 93 deletions(-) create mode 100644 xF.adoc diff --git a/CMakeLists.txt b/CMakeLists.txt index 8b32b59..7409118 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -9,7 +9,7 @@ project (xK VERSION "${project_version}" DESCRIPTION "IRC daemon, bot, TUI client and its web frontend" LANGUAGES C) # Options -option (WANT_XF "Build xF" OFF) +option (WANT_XF "Build the xF frontend" OFF) # Moar warnings set (CMAKE_C_STANDARD 99) @@ -171,6 +171,7 @@ add_custom_command (OUTPUT xC-proto.c add_custom_target (xC-proto DEPENDS ${PROJECT_BINARY_DIR}/xC-proto.c) # Build +set (project_binaries xB xC xD) foreach (name xB xC xD) add_executable (${name} ${name}.c ${project_config}) target_link_libraries (${name} PRIVATE ${project_libraries}) @@ -183,13 +184,29 @@ target_link_libraries (xC PRIVATE ${xC_libraries}) target_include_directories (xC PRIVATE ${Termo_INCLUDE_DIRS}) if (WANT_XF) - pkg_check_modules (x11 REQUIRED x11 xrender xft fontconfig) - include_directories (${x11_INCLUDE_DIRS}) - link_directories (${x11_LIBRARY_DIRS}) + list (APPEND project_binaries xF) + + find_package (Unistring REQUIRED) + + pkg_check_modules (x11 x11 xrender xft fontconfig libpng) + option (WITH_X11 "Build xF with X11 support" "${x11_FOUND}") + if (WITH_X11 AND NOT x11_FOUND) + message (FATAL_ERROR "Some X11 libraries were not found") + endif () add_executable (xF xF.c ${project_config}) add_dependencies (xF xC-proto) - target_link_libraries (xF PRIVATE ${x11_LIBRARIES} ${project_libraries}) + target_include_directories (xF PRIVATE + ${Unistring_INCLUDE_DIRS} ${Termo_INCLUDE_DIRS}) + target_link_libraries (xF PRIVATE + ${Unistring_LIBRARIES} ${Termo_LIBRARIES} + ${ncursesw_LIBRARIES} ${project_libraries}) + if (WITH_X11) + target_compile_definitions (xF PRIVATE WITH_X11) + target_include_directories (xF PRIVATE ${x11_INCLUDE_DIRS}) + target_link_directories (xF PRIVATE ${x11_LIBRARY_DIRS}) + target_link_libraries (xF PRIVATE ${x11_LIBRARIES}) + endif () add_threads (xF) endif () @@ -230,13 +247,33 @@ add_custom_target (clang-tidy WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}) # Installation -install (TARGETS xB xC xD DESTINATION ${CMAKE_INSTALL_BINDIR}) +install (TARGETS ${project_binaries} DESTINATION ${CMAKE_INSTALL_BINDIR}) install (FILES LICENSE DESTINATION ${CMAKE_INSTALL_DOCDIR}) install (DIRECTORY plugins/xB/ DESTINATION ${CMAKE_INSTALL_DATADIR}/xB/plugins USE_SOURCE_PERMISSIONS) install (DIRECTORY plugins/xC/ DESTINATION ${CMAKE_INSTALL_DATADIR}/xC/plugins) +# Rasterize and install SVG icons +if (WANT_XF AND WITH_X11) + include (IconUtils) + + set (icon_base ${PROJECT_BINARY_DIR}/icons) + set (icon_png_list) + foreach (icon_size 16 32 48) + icon_to_png (xF ${PROJECT_SOURCE_DIR}/xF.svg + ${icon_size} ${icon_base} icon_png) + list (APPEND icon_png_list ${icon_png}) + endforeach () + + add_custom_target (icons ALL DEPENDS ${icon_png_list}) + + install (FILES xF.svg + DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/scalable/apps) + install (DIRECTORY ${icon_base} + DESTINATION ${CMAKE_INSTALL_DATADIR}) +endif () + # Generate documentation from text markup find_program (ASCIIDOCTOR_EXECUTABLE asciidoctor) find_program (A2X_EXECUTABLE a2x) @@ -245,7 +282,7 @@ if (NOT ASCIIDOCTOR_EXECUTABLE AND NOT A2X_EXECUTABLE) "falling back to a substandard manual page generator") endif () -foreach (page xB xC xD) +foreach (page ${project_binaries}) set (page_output "${PROJECT_BINARY_DIR}/${page}.1") list (APPEND project_MAN_PAGES "${page_output}") if (ASCIIDOCTOR_EXECUTABLE) diff --git a/NEWS b/NEWS index 4ff4a9b..d4cd1ab 100644 --- a/NEWS +++ b/NEWS @@ -15,6 +15,8 @@ Unreleased * prime.lua: prevented a DoS opportunity on excessively large numbers + * Added a terminal/X11 frontend called xF + * Bumped relay protocol version diff --git a/README.adoc b/README.adoc index 6a25838..415ab79 100644 --- a/README.adoc +++ b/README.adoc @@ -34,9 +34,9 @@ including link:xC.adoc#_key_bindings[keyboard shortcuts]. image::xP.webp[align="center"] -xA, xT, xW, xM --------------- -Fyne, Qt Widgets, Win32, Cocoa frontends for 'xC'. +xA, xF, xT, xW, xM +------------------ +Fyne, terminal/X11, Qt Widgets, Win32, Cocoa frontends for 'xC'. Using them is not recommended. xD @@ -87,15 +87,17 @@ or as a https://git.janouch.name/p/nixexprs[Nix derivation]. Building -------- Build-only dependencies: CMake, pkg-config, awk, liberty (included), - asciidoctor or asciidoc (recommended but optional) + + asciidoctor or asciidoc (recommended but optional), rsvg-convert (for 'xF') + Common runtime dependencies: openssl + Additionally for 'xC': curses, lua >= 5.3 (optional), termo (included) + +Additionally for 'xF': ncursesw, libunistring, termo (included), + x11 + xft + libpng (X11) $ git clone --recursive https://git.janouch.name/p/xK.git $ mkdir xK/build $ cd xK/build $ cmake .. -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=RelWithDebInfo \ - -DWANT_READLINE=ON -DWANT_LIBEDIT=OFF -DWITH_LUA=ON + -DWITH_LUA=ON -DWITH_XF=OFF $ make To install the application, you can do either the usual: diff --git a/liberty b/liberty index fbb577f..e90abbd 160000 --- a/liberty +++ b/liberty @@ -1 +1 @@ -Subproject commit fbb577f3f819c12907d0c05a5cf49ed9bb00b8be +Subproject commit e90abbd03c6c26c4f17ad7ea63472e2c54d22e68 diff --git a/xC.adoc b/xC.adoc index 894f953..23ff37e 100644 --- a/xC.adoc +++ b/xC.adoc @@ -326,4 +326,4 @@ or submit pull requests. See also -------- -*less*(1) +*less*(1), *xF*(1) diff --git a/xF.adoc b/xF.adoc new file mode 100644 index 0000000..fe53cf5 --- /dev/null +++ b/xF.adoc @@ -0,0 +1,39 @@ +xF(1) +===== +:doctype: manpage +:manmanual: xK Manual +:mansource: xK {release-version} + +Name +---- +xF - Terminal/X11 frontend for xC + +Synopsis +-------- +*xF* [_OPTION_]... RELAY-ADDRESS + +Description +----------- +*xF* is an example frontend for the relay interface of the *xC* IRC client. +It normally runs in the terminal, and can also use a graphical interface +when built with support for one. + +Options +------- +*-g*, *--gui*:: + Use the graphical interface even when running from a terminal. + +*-h*, *--help*:: + Display a help message and exit. + +*-V*, *--version*:: + Output version information and exit. + +Reporting bugs +-------------- +Use https://git.janouch.name/p/xK to report bugs, request features, +or submit pull requests. + +See also +-------- +*xC*(1) diff --git a/xF.c b/xF.c index 054871d..a0397c5 100644 --- a/xF.c +++ b/xF.c @@ -1,7 +1,7 @@ /* * xF.c: a toothless IRC client frontend * - * Copyright (c) 2022, Přemysl Eric Janouch + * Copyright (c) 2022 - 2026, Přemysl Eric Janouch * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted. @@ -22,124 +22,2979 @@ #include "common.c" #include "xC-proto.c" -#include -#include -#include -#include -#include +#include +#include + +#ifdef WITH_X11 +#define LIBERTY_XUI_WANT_X11 +#endif // WITH_X11 +#include "liberty/liberty-xui.c" + +// --- Relay model ------------------------------------------------------------- + +// ~~~ Buffer lines ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +// See: struct relay_event_data_buffer_line +struct buffer_line +{ + LIST_HEADER (struct buffer_line) + + /// Leaked from another buffer, but temporarily staying in another one. + bool leaked; + + bool is_unimportant; + bool is_highlight; + enum relay_rendition rendition; + uint64_t when; ///< Unix timestamp in milliseconds + char text[]; +}; + +static struct buffer_line * +buffer_line_new_text (const char *text, enum relay_rendition rendition, + uint64_t when) +{ + size_t len = strlen (text); + struct buffer_line *self = xcalloc (1, sizeof *self + len + 1); + memcpy (self->text, text, len + 1); + self->rendition = rendition; + self->when = when; + return self; +} + +static struct buffer_line * +buffer_line_clone (const struct buffer_line *line) +{ + struct buffer_line *self = + buffer_line_new_text (line->text, line->rendition, line->when); + self->leaked = line->leaked; + self->is_unimportant = line->is_unimportant; + self->is_highlight = line->is_highlight; + return self; +} + +// ~~~ Buffers ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +// See: struct relay_event_data_buffer_update +struct buffer +{ + LIST_HEADER (struct buffer) + + char *buffer_name; + enum relay_buffer_kind kind; + char *server_name; + char *topic; + char *modes; + bool hide_unimportant; + + // Transcript: + + struct buffer_line *lines; + struct buffer_line *lines_tail; + int scroll_offset; ///< XUI pixels above the bottom + + // Input: + + char *input; ///< Saved input draft + int input_point; ///< Saved caret index + struct strv history; + size_t history_at; ///< `history.len` selects the draft + + // Activity: + + uint32_t new_messages; + uint32_t new_unimportant_messages; + bool highlighted; +}; + +static bool +buffer_has_unread_marker (const struct buffer *self) +{ + return self->lines + && (self->new_messages || self->new_unimportant_messages); +} + +static bool +buffer_line_is_visible (const struct buffer *b, const struct buffer_line *line) +{ + return !b->hide_unimportant || !line->is_unimportant; +} + +static struct buffer * +buffer_new (const char *name) +{ + struct buffer *self = xcalloc (1, sizeof *self); + self->buffer_name = xstrdup (name); + self->history = strv_make (); + return self; +} + +static void +buffer_clear (struct buffer *self) +{ + LIST_FOR_EACH (struct buffer_line, iter, self->lines) + free (iter); + self->lines = self->lines_tail = NULL; +} + +static void +buffer_pop_excess_lines (struct buffer *self) +{ + size_t count = 0; + LIST_FOR_EACH (struct buffer_line, line, self->lines) + count++; + + uint64_t unread = (uint64_t) self->new_messages + + self->new_unimportant_messages; + size_t old = count - MIN ((uint64_t) count, unread); + size_t to_delete = old > 1000 ? old - 1000 : 0; + while (to_delete--) + { + struct buffer_line *line = self->lines; + LIST_UNLINK_WITH_TAIL (self->lines, self->lines_tail, line); + free (line); + } +} + +static void +buffer_destroy (struct buffer *self) +{ + free (self->buffer_name); + free (self->server_name); + free (self->topic); + free (self->modes); + free (self->input); + strv_free (&self->history); + buffer_clear (self); + free (self); +} + +// ~~~ Servers ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +// See: struct relay_event_data_server_update +struct server +{ + enum relay_server_state state; + char *user; + char *user_modes; +}; + +static struct server * +server_new (void) +{ + struct server *self = xcalloc (1, sizeof *self); + return self; +} + +static void +server_destroy (struct server *self) +{ + free (self->user); + free (self->user_modes); + free (self); +} + +// --- Input actions ----------------------------------------------------------- + +enum action +{ + ACTION_NONE, + ACTION_QUIT, + ACTION_REDRAW, + ACTION_RECONNECT, + ACTION_BUFFER_PREVIOUS, + ACTION_BUFFER_NEXT, + ACTION_BUFFER_LAST, + ACTION_BUFFER_HIGHLIGHT, + ACTION_BUFFER_ACTIVITY, + ACTION_BUFFER_1, + ACTION_BUFFER_2, + ACTION_BUFFER_3, + ACTION_BUFFER_4, + ACTION_BUFFER_5, + ACTION_BUFFER_6, + ACTION_BUFFER_7, + ACTION_BUFFER_8, + ACTION_BUFFER_9, + ACTION_BUFFER_10, + ACTION_TOGGLE_UNIMPORTANT, + ACTION_LOG, + ACTION_FORMAT, + ACTION_FORMAT_BOLD, + ACTION_FORMAT_ITALIC, + ACTION_FORMAT_UNDERLINE, + ACTION_SCROLL_UP, + ACTION_SCROLL_DOWN, + ACTION_SUBMIT, + ACTION_COMPLETE, + ACTION_HISTORY_PREVIOUS, + ACTION_HISTORY_NEXT, + ACTION_HISTORY_FIRST, + ACTION_HISTORY_LAST, + // Line-editor forwards: + ACTION_EDITOR_B_CHAR, + ACTION_EDITOR_F_CHAR, + ACTION_EDITOR_B_WORD, + ACTION_EDITOR_F_WORD, + ACTION_EDITOR_HOME, + ACTION_EDITOR_END, + ACTION_EDITOR_UPCASE_WORD, + ACTION_EDITOR_DOWNCASE_WORD, + ACTION_EDITOR_CAPITALIZE_WORD, + ACTION_EDITOR_B_DELETE, + ACTION_EDITOR_F_DELETE, + ACTION_EDITOR_B_KILL_WORD, + ACTION_EDITOR_B_KILL_LINE, + ACTION_EDITOR_F_KILL_LINE, +}; + +struct binding +{ + termo_key_t decoded; ///< Decoded key definition + enum action action; ///< Action to take +}; + +// --- Display model ----------------------------------------------------------- + +#define ATTRIBUTE_TABLE(XX) \ + XX (NORMAL, -1, -1, 0) \ + XX (CHROME, -1, 255, A_UNDERLINE) \ + XX (TIME, 250, 255, A_DIM) \ + XX (CURRENT, 0, 255, 0) \ + XX (ACTIVITY, -1, -1, A_BOLD) \ + XX (HIGHLIGHT, 202, -1, A_BOLD) \ + XX (UNREAD, 202, -1, 0) \ + XX (JOIN, 108, -1, 0) \ + XX (PART, 138, -1, 0) \ + XX (ERROR, 1, -1, A_BOLD) \ + XX (ACTION, 1, -1, 0) \ + XX (LEAKED, 248, -1, 0) \ + XX (PROMPT, -1, 255, A_BOLD) \ + XX (SCROLLBAR, 250, -1, 0) + +enum app_attribute +{ +#define XX(name, fg, bg, attrs) ATTRIBUTE_ ## name, + ATTRIBUTE_TABLE (XX) +#undef XX + ATTRIBUTE_COUNT, +}; + +static struct attrs g_attrs[ATTRIBUTE_COUNT] = +{ +#define XX(name, fg, bg, attrs_) \ + [ATTRIBUTE_ ## name] = { fg, bg, attrs_ }, + ATTRIBUTE_TABLE (XX) +#undef XX +}; + +#undef ATTRIBUTE_TABLE // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +enum { UNREAD_MARKER_HEIGHT = 1 }; + +enum app_widget_id +{ + WIDGET_NONE, + WIDGET_BUTTON, + WIDGET_BUFFER, + WIDGET_TRANSCRIPT, + WIDGET_SCROLLBAR, +}; + +struct app_backend +{ + struct widget *(*make_editor) (chtype attrs); +}; + +// --- Request state ----------------------------------------------------------- + +struct completion_request +{ + bool pending; ///< Request in flight + uint32_t request_seq; ///< Relay request number + char *buffer_name; ///< Stamp: buffer name + char *text; ///< Stamp: input line contents + uint32_t position; ///< Stamp: UTF-8 byte offset in `text` +}; + +struct log_view +{ + bool pending; ///< Request in flight + uint32_t request_seq; ///< Relay request number + char *buffer_name; ///< Stamp: the buffer we want logs for + + bool visible; ///< We see the log rather than backlog + struct strv lines; ///< Log split into lines + int offset; ///< XUI pixels above the bottom +}; + +// --- Relay transport --------------------------------------------------------- + +enum relay_connection_state +{ + RELAY_DISCONNECTED, + RELAY_CONNECTING, + RELAY_SYNCHRONIZING, + RELAY_CONNECTED, +}; + +enum { RELAY_RECONNECT_DELAY = 5 * 1000 }; + +struct relay_client +{ + struct connector connector; ///< Valid while `connecting` + bool connecting; ///< `connector` requires disposal + enum relay_connection_state state; ///< Transport/synchronization phase + struct poller_timer reconnect_event; + char *host; ///< Reconnect target + char *port; ///< Reconnect service + char *last_error; ///< Shown while disconnected + + int socket_fd; ///< Connected socket, or -1 + struct poller_fd socket_event; ///< Read/write readiness for the socket + struct str read_buffer; ///< Unprocessed framed input + struct str write_buffer; ///< Serialized frames awaiting write + uint32_t command_seq; ///< Last assigned outgoing command + size_t pending_commands; ///< Contiguous pending sequence count + uint32_t first_pending_command; ///< Range head; valid when pending + + uint8_t *session_id; ///< Owned resumption token + uint32_t session_id_len; ///< Byte length of `session_id` + uint32_t last_event_seq; ///< Last accepted event, for resumption +}; + +// --- Application state ------------------------------------------------------- + static struct { - bool polling; - struct connector connector; - int socket; + struct poller poller; ///< Poller + bool polling; ///< The event loop is running + struct poller_fd signal_event; ///< There are signals to process + + struct relay_client relay; ///< Relay connection + + // Relay state: + + struct buffer *buffers; ///< Ordered list of all buffers + struct buffer *buffers_tail; ///< The tail of all buffers + struct buffer *buffer_current; ///< The current buffer + struct buffer *buffer_last; ///< Last used buffer + + struct str_map servers; ///< All servers + + struct buffer_line *local_lines; ///< Local messages awaiting sync + struct buffer_line *local_lines_tail; + struct completion_request completion; + struct log_view log; + + // User interface: + + struct attrs attrs[ATTRIBUTE_COUNT]; + struct line_editor editor; ///< Message editor + int64_t last_active; ///< Last ACTIVE command timestamp + bool dragging_scrollbar; ///< Mouse drag updates the viewport + + struct binding *keys; ///< Parsed xC-style key bindings + size_t keys_len; + + bool inserting_attribute; ///< M-m awaits an attribute selector + struct app_backend backend; } g; -static void -on_connector_connecting (void *user_data, const char *address) -{ - (void) user_data; - print_status ("connecting to %s...", address); -} +static int g_signal_pipe[2] = { -1, -1 }; +static volatile sig_atomic_t g_termination_requested; +static volatile sig_atomic_t g_winch_received; static void -on_connector_error (void *user_data, const char *error) +app_quit (void) { - (void) user_data; - print_status ("connection failed: %s", error); -} - -static void -on_connector_failure (void *user_data) -{ - (void) user_data; - exit_fatal ("giving up"); -} - -static void -on_connector_connected (void *user_data, int socket, const char *hostname) -{ - (void) user_data; - (void) hostname; + // So far there's nothing for us to wait on, so let's just stop looping. g.polling = false; - g.socket = socket; +} + +// --- Buffer line logic ------------------------------------------------------- + +static void +app_recheck_current_highlight (void) +{ + if (g.buffer_current && g.buffer_current->highlighted + && !g.log.visible && !g.buffer_current->scroll_offset) + g.buffer_current->highlighted = false; +} + +static bool +app_push_buffer_line (struct buffer *b, + struct buffer_line *line, bool leak_to_active) +{ + struct buffer *current = g.buffer_current; + hard_assert (current); + + bool displayed = buffer_line_is_visible (current, line) + && (b == current || leak_to_active); + bool visible = !g.log.visible && !current->scroll_offset + && (b == current || leak_to_active); + bool current_had_unread = buffer_has_unread_marker (current); + // TODO: Also discount an unmapped/minimized window once XUI exposes that. + + LIST_APPEND_WITH_TAIL (b->lines, b->lines_tail, line); + if (!(visible || leak_to_active) || buffer_has_unread_marker (b)) + { + if (line->is_unimportant || leak_to_active) + b->new_unimportant_messages++; + else + b->new_messages++; + } + if (leak_to_active) + { + struct buffer_line *leaked = buffer_line_clone (line); + leaked->leaked = true; + LIST_APPEND_WITH_TAIL (current->lines, current->lines_tail, leaked); + if (!visible || buffer_has_unread_marker (current)) + { + if (leaked->is_unimportant) + current->new_unimportant_messages++; + else + current->new_messages++; + } + } + + if (current->scroll_offset) + { + if (displayed) + current->scroll_offset += g_xui.vunit; + if (!current_had_unread && buffer_has_unread_marker (current)) + current->scroll_offset += UNREAD_MARKER_HEIGHT; + } + + buffer_pop_excess_lines (b); + if (leak_to_active) + buffer_pop_excess_lines (current); + return visible; +} + +// --- Log redirection --------------------------------------------------------- + +// TODO(p): Think this over, this design is icky. + +static struct buffer * +buffer_global (void) +{ + LIST_FOR_EACH (struct buffer, buffer, g.buffers) + if (buffer->kind == RELAY_BUFFER_KIND_GLOBAL) + return buffer; + return NULL; } static void -protocol_test (const char *host, const char *port) +app_flush_local_lines (void) { - struct poller poller = {}; - poller_init (&poller); + struct buffer *global = buffer_global (); + if (!global || !g.buffer_current) + return; - connector_init (&g.connector, &poller); - g.connector.on_connecting = on_connector_connecting; - g.connector.on_error = on_connector_error; - g.connector.on_connected = on_connector_connected; - g.connector.on_failure = on_connector_failure; + LIST_FOR_EACH (struct buffer_line, line, g.local_lines) + { + LIST_UNLINK_WITH_TAIL (g.local_lines, g.local_lines_tail, line); + app_push_buffer_line (global, line, g.buffer_current != global); + } + xui_invalidate (); +} - connector_add_target (&g.connector, host, port); +static void +app_queue_local_line (enum relay_rendition rendition, const char *text) +{ + struct timespec tp = {}; + hard_assert (clock_gettime (CLOCK_REALTIME, &tp) != -1); + uint64_t when = (uint64_t) tp.tv_sec * 1000 + tp.tv_nsec / 1000000; + struct buffer_line *line = buffer_line_new_text (text, rendition, when); + LIST_APPEND_WITH_TAIL (g.local_lines, g.local_lines_tail, line); + app_flush_local_lines (); +} - g.polling = true; - while (g.polling) - poller_run (&poller); +static void +app_log_handler (void *user_data, const char *quote, const char *fmt, + va_list ap) +{ + (void) user_data; + static bool in_processing; + if (in_processing) + return; + in_processing = true; - connector_free (&g.connector); + struct str message = str_make (); + str_append (&message, quote); + str_append_vprintf (&message, fmt, ap); - struct str s = str_make (); - str_pack_u32 (&s, 0); - struct relay_command_message m = {}; - m.data.hello.command = RELAY_COMMAND_HELLO; - m.data.hello.version = RELAY_VERSION; - if (!relay_command_message_serialize (&m, &s)) + enum relay_rendition rendition = RELAY_RENDITION_BARE; + if (!strcmp (quote, "-- ")) + rendition = RELAY_RENDITION_STATUS; + else if (!strcmp (quote, "warning: ") + || !strcmp (quote, "error: ") + || !strcmp (quote, "fatal: ")) + rendition = RELAY_RENDITION_ERROR; + app_queue_local_line (rendition, message.str); + str_free (&message); + + in_processing = false; +} + +// --- Editor ------------------------------------------------------------------ + +static void +app_editor_changed (void) +{ + xui_invalidate (); +} + +static void +app_editor_init (void) +{ + line_editor_start (&g.editor, 0); + g.editor.on_changed = app_editor_changed; +} + +static void +app_editor_free (void) +{ + line_editor_free (&g.editor); +} + +static void +app_editor_insert (ucs4_t codepoint) +{ + line_editor_insert (&g.editor, codepoint); + if (iscntrl_ascii (codepoint)) + g.editor.w[g.editor.point - 1] = 2; +} + +static size_t +app_editor_position (void) +{ + size_t position = 0; + for (int i = 0; i < g.editor.point; i++) + { + uint8_t encoded[6]; + int len = u8_uctomb (encoded, g.editor.line[i], sizeof encoded); + if (len > 0) + position += len; + } + return position; +} + +static char * +app_editor_text (void) +{ + size_t len = 0; + char *result = + (char *) u32_to_u8 (g.editor.line, g.editor.len + 1, NULL, &len); + return result ? result : xstrdup (""); +} + +static void +app_editor_set (const char *text) +{ + line_editor_free (&g.editor); + memset (&g.editor, 0, sizeof g.editor); + line_editor_start (&g.editor, 0); + + const char *input = text ? text : ""; + struct utf8_iter iter = { .s = input, .len = strlen (input) }; + int32_t codepoint = 0; + size_t codepoint_len = 0; + while ((codepoint = utf8_iter_next (&iter, &codepoint_len)) >= 0) + app_editor_insert (codepoint); + g.editor.on_changed = app_editor_changed; + xui_invalidate (); +} + +// --- Relay ------------------------------------------------------------------- + +// ~~~ Outgoing commands ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +static void +relay_update_poller (const struct pollfd *pfd) +{ + int new_events = POLLIN; + if (g.relay.write_buffer.len) + new_events |= POLLOUT; + + hard_assert (new_events != 0); + if (!pfd || pfd->events != new_events) + poller_fd_set (&g.relay.socket_event, new_events); +} + +static bool +relay_send (struct relay_command_message *m, uint32_t *command_seq) +{ + if (g.relay.socket_fd == -1) + { + relay_command_message_free (m); + return false; + } + + struct str frame = str_make (); + str_pack_u32 (&frame, 0); + m->command_seq = ++g.relay.command_seq; + if (!relay_command_message_serialize (m, &frame)) exit_fatal ("serialization failed"); - uint32_t len = htonl (s.len - sizeof len); - memcpy (s.str, &len, sizeof len); - if (errno = 0, write (g.socket, s.str, s.len) != (ssize_t) s.len) - exit_fatal ("short send or error: %s", strerror (errno)); + uint32_t len = htonl (frame.len - sizeof len); + memcpy (frame.str, &len, sizeof len); + str_append_str (&g.relay.write_buffer, &frame); + str_free (&frame); - char buf[1 << 20] = ""; - while (errno = 0, read (g.socket, &len, sizeof len) == sizeof len) + if (!g.relay.pending_commands) + g.relay.first_pending_command = m->command_seq; + g.relay.pending_commands++; + if (command_seq) + *command_seq = m->command_seq; + relay_command_message_free (m); + + relay_update_poller (NULL); + xui_invalidate (); + return true; +} + +// ~~~ Completion request ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +static void +completion_request_start (struct completion_request *self, uint32_t request_seq, + char *text_owned, const char *buffer_name, uint32_t position) +{ + hard_assert (!self->pending); + self->pending = true; + self->request_seq = request_seq; + self->text = text_owned; + self->buffer_name = xstrdup (buffer_name); + self->position = position; +} + +static void +completion_request_reset (struct completion_request *self) +{ + self->pending = false; + self->request_seq = 0; + self->position = 0; + cstr_set (&self->text, NULL); + cstr_set (&self->buffer_name, NULL); +} + +static void +app_process_completion_response + (const struct relay_response_data_buffer_complete *response) +{ + char *current = app_editor_text (); + bool current_request = g.buffer_current + && g.completion.buffer_name + && !strcmp (g.buffer_current->buffer_name, g.completion.buffer_name) + && !strcmp (current, g.completion.text) + && app_editor_position () == g.completion.position; + + if (current_request + && response->start <= g.completion.position + && g.completion.position <= strlen (current)) { - len = ntohl (len); - if (errno = 0, read (g.socket, buf, MIN (len, sizeof buf)) != len) - exit_fatal ("short read or error: %s", strerror (errno)); - - struct msg_unpacker r = msg_unpacker_make (buf, len); - struct relay_event_message m = {}; - if (!relay_event_message_deserialize (&m, &r)) - exit_fatal ("deserialization failed"); - if (msg_unpacker_get_available (&r)) - exit_fatal ("trailing data"); - - printf ("event: %d\n", m.data.event); - relay_event_message_free (&m); + if (response->completions_len) + { + struct str completed = str_make (); + str_append_data (&completed, current, response->start); + str_append_str (&completed, &response->completions[0]); + if (response->completions_len == 1) + str_append_c (&completed, ' '); + str_append (&completed, current + g.completion.position); + app_editor_set (completed.str); + str_free (&completed); + } + if (response->completions_len != 1) + g_xui.ui->beep (); } - exit_fatal ("short read or error: %s", strerror (errno)); + else if (current_request) + g_xui.ui->beep (); + + free (current); + completion_request_reset (&g.completion); +} + +// ~~~ Log request ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +static void +log_view_request_start (struct log_view *self, uint32_t request_seq, + const char *buffer_name) +{ + hard_assert (!self->pending); + self->pending = true; + self->request_seq = request_seq; + self->buffer_name = xstrdup (buffer_name); +} + +static void +log_view_request_reset (struct log_view *self) +{ + self->pending = false; + self->request_seq = 0; + cstr_set (&self->buffer_name, NULL); +} + +static void +log_view_hide (struct log_view *self) +{ + self->visible = false; + strv_free (&self->lines); + self->lines = (struct strv) {}; + self->offset = 0; +} + +static void +log_view_free (struct log_view *self) +{ + log_view_request_reset (self); + log_view_hide (self); +} + +static void +log_view_show (struct log_view *self, char *text_owned) +{ + strv_reset (&self->lines); + cstr_split (text_owned, "\r\n", true, &self->lines); + free (text_owned); + self->visible = true; +} + +static void +app_process_log_response (const struct relay_response_data_buffer_log *response) +{ + if (g.buffer_current && g.log.buffer_name + && !strcmp (g.buffer_current->buffer_name, g.log.buffer_name)) + { + log_view_show (&g.log, + utf8_validate ((const char *) response->log, response->log_len) + ? xstrndup ((const char *) response->log, response->log_len) + : xstrdup ("Log is not valid UTF-8.")); + } + log_view_request_reset (&g.log); + g.log.offset = 0; + xui_invalidate (); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -int -main (int argc, char *argv[]) +static void +relay_acknowledge_command (uint32_t command_seq) { + uint32_t acknowledged = + command_seq - g.relay.first_pending_command + 1; + if (!acknowledged || acknowledged > g.relay.pending_commands) + return; + + if (g.completion.pending + && (uint32_t) (g.completion.request_seq - g.relay.first_pending_command) + < acknowledged) + completion_request_reset (&g.completion); + if (g.log.pending + && (uint32_t) (g.log.request_seq - g.relay.first_pending_command) + < acknowledged) + log_view_request_reset (&g.log); + + g.relay.pending_commands -= acknowledged; + g.relay.first_pending_command = command_seq + 1; + xui_invalidate (); +} + +static void +relay_process_error (const struct relay_event_data_error *e) +{ + if (g.completion.pending + && e->command_seq == g.completion.request_seq) + { + g_xui.ui->beep (); + completion_request_reset (&g.completion); + } + if (g.log.pending && e->command_seq == g.log.request_seq) + log_view_request_reset (&g.log); + print_error ("%s", e->error.str); +} + +static void +app_reset_relay_state (void) +{ + g.buffer_current = NULL; + g.buffer_last = NULL; + + LIST_FOR_EACH (struct buffer, iter, g.buffers) + buffer_destroy (iter); + g.buffers = g.buffers_tail = NULL; + + str_map_free (&g.servers); + g.servers = str_map_make ((str_map_free_fn) server_destroy); + + g.relay.pending_commands = 0; + g.relay.first_pending_command = 0; + completion_request_reset (&g.completion); + log_view_request_reset (&g.log); + + log_view_hide (&g.log); + app_editor_set (""); +} + +static void +relay_process_hello_response (const struct relay_response_data_hello *response) +{ + bool resumed = g.relay.session_id_len == response->session_id_len + && g.relay.session_id_len + && !memcmp (g.relay.session_id, response->session_id, + response->session_id_len); + if (!resumed) + app_reset_relay_state (); + + free (g.relay.session_id); + g.relay.session_id = NULL; + if (response->session_id_len) + { + g.relay.session_id = xmalloc (response->session_id_len); + memcpy (g.relay.session_id, response->session_id, + response->session_id_len); + } + g.relay.session_id_len = response->session_id_len; + g.relay.state = resumed ? RELAY_CONNECTED : RELAY_SYNCHRONIZING; + cstr_set (&g.relay.last_error, NULL); + xui_invalidate (); +} + +static void +relay_process_response (const struct relay_event_data_response *e) +{ + if (e->data.command == RELAY_COMMAND_HELLO) + relay_process_hello_response (&e->data.hello); + else if (g.completion.pending + && e->command_seq == g.completion.request_seq + && e->data.command == RELAY_COMMAND_BUFFER_COMPLETE) + app_process_completion_response (&e->data.buffer_complete); + else if (g.log.pending + && e->command_seq == g.log.request_seq + && e->data.command == RELAY_COMMAND_BUFFER_LOG) + app_process_log_response (&e->data.buffer_log); +} + +// ~~~ State synchronization ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +static struct buffer * +buffer_find (const char *name) +{ + LIST_FOR_EACH (struct buffer, buffer, g.buffers) + if (!strcmp (buffer->buffer_name, name)) + return buffer; + return NULL; +} + +static char * +relay_items_text (const union relay_item_data *items, uint32_t items_len) +{ + struct str s = str_make (); + for (uint32_t i = 0; i < items_len; i++) + if (items[i].kind == RELAY_ITEM_TEXT) + str_append_str (&s, &items[i].text.text); + return str_steal (&s); +} + +static struct buffer_line * +buffer_line_new (const struct relay_event_data_buffer_line *m) +{ + char *text = relay_items_text (m->items, m->items_len); + struct buffer_line *self = + buffer_line_new_text (text, m->rendition, m->when); + free (text); + + self->is_unimportant = m->is_unimportant; + self->is_highlight = m->is_highlight; + return self; +} + +static void +relay_process_buffer_line (struct buffer *b, + const struct relay_event_data_buffer_line *m) +{ + // Initial sync: skip all other processing, let highlights be. + if (!g.buffer_current) + { + struct buffer_line *line = buffer_line_new (m); + LIST_APPEND_WITH_TAIL (b->lines, b->lines_tail, line); + return; + } + + struct buffer_line *line = buffer_line_new (m); + bool visible = app_push_buffer_line (b, line, m->leak_to_active); + if (m->is_highlight || (!visible && !m->is_unimportant + && b->kind == RELAY_BUFFER_KIND_PRIVATE_MESSAGE)) + { + g_xui.ui->beep (); + if (!visible) + b->highlighted = true; + } +} + +static const char * +relay_message_buffer_name (const struct relay_event_message *m) +{ + switch (m->data.event) + { + case RELAY_EVENT_BUFFER_LINE: + return m->data.buffer_line.buffer_name.str; + case RELAY_EVENT_BUFFER_UPDATE: + return m->data.buffer_update.buffer_name.str; + case RELAY_EVENT_BUFFER_STATS: + return m->data.buffer_stats.buffer_name.str; + case RELAY_EVENT_BUFFER_RENAME: + return m->data.buffer_rename.buffer_name.str; + case RELAY_EVENT_BUFFER_REMOVE: + return m->data.buffer_remove.buffer_name.str; + case RELAY_EVENT_BUFFER_ACTIVATE: + return m->data.buffer_activate.buffer_name.str; + case RELAY_EVENT_BUFFER_INPUT: + return m->data.buffer_input.buffer_name.str; + case RELAY_EVENT_BUFFER_CLEAR: + return m->data.buffer_clear.buffer_name.str; + default: + return NULL; + } +} + +static const char * +relay_message_server_name (const struct relay_event_message *m) +{ + switch (m->data.event) + { + case RELAY_EVENT_SERVER_UPDATE: + return m->data.server_update.server_name.str; + case RELAY_EVENT_SERVER_RENAME: + return m->data.server_rename.server_name.str; + case RELAY_EVENT_SERVER_REMOVE: + return m->data.server_remove.server_name.str; + default: + return NULL; + } +} + +static const char * +relay_buffer_update_server_name (const struct relay_event_data_buffer_update *e) +{ + switch (e->context.kind) + { + case RELAY_BUFFER_KIND_SERVER: + return e->context.server.server_name.str; + case RELAY_BUFFER_KIND_CHANNEL: + return e->context.channel.server_name.str; + case RELAY_BUFFER_KIND_PRIVATE_MESSAGE: + return e->context.private_message.server_name.str; + default: + return NULL; + } +} + +static void +relay_process_buffer_update (struct buffer *b, + const struct relay_event_data_buffer_update *e) +{ + b->kind = e->context.kind; + b->hide_unimportant = e->hide_unimportant; + + const char *server_name = relay_buffer_update_server_name (e); + cstr_set (&b->server_name, NULL); + if (server_name) + b->server_name = xstrdup (server_name); + + cstr_set (&b->topic, NULL); + cstr_set (&b->modes, NULL); + if (b->kind == RELAY_BUFFER_KIND_CHANNEL) + { + const struct relay_buffer_context_channel *channel = + &e->context.channel; + b->topic = relay_items_text (channel->topic, channel->topic_len); + b->modes = xstrdup (channel->modes.str); + } +} + +static void +relay_process_buffer_activate (struct buffer *b) +{ + if (g.buffer_current) + { + g.buffer_current->new_messages = 0; + g.buffer_current->new_unimportant_messages = 0; + g.buffer_current->highlighted = false; + buffer_pop_excess_lines (g.buffer_current); + cstr_set (&g.buffer_current->input, app_editor_text ()); + g.buffer_current->input_point = g.editor.point; + g.buffer_current->history_at = g.buffer_current->history.len; + } + else + { + LIST_FOR_EACH (struct buffer, iter, g.buffers) + buffer_pop_excess_lines (iter); + } + + g.buffer_last = g.buffer_current; + g.buffer_current = b; + log_view_hide (&g.log); + if (b) + { + b->scroll_offset = 0; + app_editor_set (b->input); + g.editor.point = MIN (b->input_point, (int) g.editor.len); + app_recheck_current_highlight (); + } + app_flush_local_lines (); +} + +static bool +relay_process_buffer_event (struct relay_event_message *m) +{ + const char *buffer_name = relay_message_buffer_name (m); + hard_assert (buffer_name); + + struct buffer *b = buffer_find (buffer_name); + if (!b) + { + if (m->data.event != RELAY_EVENT_BUFFER_UPDATE) + { + print_warning ("unknown buffer: %s", buffer_name); + return false; + } + + b = buffer_new (buffer_name); + LIST_APPEND_WITH_TAIL (g.buffers, g.buffers_tail, b); + } + + switch (m->data.event) + { + case RELAY_EVENT_BUFFER_LINE: + relay_process_buffer_line (b, &m->data.buffer_line); + break; + case RELAY_EVENT_BUFFER_UPDATE: + relay_process_buffer_update (b, &m->data.buffer_update); + break; + case RELAY_EVENT_BUFFER_STATS: + b->new_messages = m->data.buffer_stats.new_messages; + b->new_unimportant_messages + = m->data.buffer_stats.new_unimportant_messages; + b->highlighted = m->data.buffer_stats.highlighted; + break; + case RELAY_EVENT_BUFFER_RENAME: + free (b->buffer_name); + b->buffer_name = xstrdup (m->data.buffer_rename.new.str); + break; + case RELAY_EVENT_BUFFER_REMOVE: + LIST_UNLINK_WITH_TAIL (g.buffers, g.buffers_tail, b); + if (g.buffer_current == b) + g.buffer_current = NULL; + if (g.buffer_last == b) + g.buffer_last = NULL; + buffer_destroy (b); + break; + case RELAY_EVENT_BUFFER_ACTIVATE: + relay_process_buffer_activate (b); + break; + case RELAY_EVENT_BUFFER_INPUT: + if (b->history_at == b->history.len) + b->history_at++; + strv_append (&b->history, m->data.buffer_input.text.str); + break; + case RELAY_EVENT_BUFFER_CLEAR: + buffer_clear (b); + break; + default: + hard_assert (!"not a buffer event"); + } + return true; +} + +static void +relay_process_server_update (struct server *server, + const struct relay_event_data_server_update *e) +{ + server->state = e->data.state; + + cstr_set (&server->user, NULL); + cstr_set (&server->user_modes, NULL); + if (server->state == RELAY_SERVER_STATE_REGISTERED) + { + server->user = xstrdup (e->data.registered.user.str); + server->user_modes = xstrdup (e->data.registered.user_modes.str); + } +} + +static void +relay_process_server_rename (const char *server_name, const char *new_name) +{ + str_map_set (&g.servers, new_name, + str_map_steal (&g.servers, server_name)); + LIST_FOR_EACH (struct buffer, iter, g.buffers) + if (iter->server_name && !strcmp (iter->server_name, server_name)) + cstr_set (&iter->server_name, xstrdup (new_name)); +} + +static bool +relay_process_server_event (struct relay_event_message *m) +{ + const char *server_name = relay_message_server_name (m); + hard_assert (server_name); + + struct server *server = str_map_find (&g.servers, server_name); + if (!server) + { + if (m->data.event != RELAY_EVENT_SERVER_UPDATE) + { + print_warning ("unknown server: %s", server_name); + return false; + } + + server = server_new (); + str_map_set (&g.servers, server_name, server); + } + + switch (m->data.event) + { + case RELAY_EVENT_SERVER_UPDATE: + relay_process_server_update (server, &m->data.server_update); + break; + case RELAY_EVENT_SERVER_RENAME: + relay_process_server_rename (server_name, + m->data.server_rename.new.str); + break; + case RELAY_EVENT_SERVER_REMOVE: + str_map_set (&g.servers, server_name, NULL); + break; + default: + hard_assert (!"not a server event"); + } + return true; +} + +static bool +relay_process_message (struct msg_unpacker *r, struct relay_event_message *m) +{ + if (!relay_event_message_deserialize (m, r) + || msg_unpacker_get_available (r)) + { + print_error ("deserialization failed"); + return false; + } + + g.relay.last_event_seq = m->event_seq; + + bool changed = false; + switch (m->data.event) + { + case RELAY_EVENT_ERROR: + relay_process_error (&m->data.error); + relay_acknowledge_command (m->data.error.command_seq); + return true; + case RELAY_EVENT_RESPONSE: + relay_process_response (&m->data.response); + relay_acknowledge_command (m->data.response.command_seq); + return true; + case RELAY_EVENT_PING: + { + struct relay_command_message response = {}; + response.data.ping_response.command = RELAY_COMMAND_PING_RESPONSE; + response.data.ping_response.event_seq = m->event_seq; + relay_send (&response, NULL); + return true; + } + + case RELAY_EVENT_BUFFER_LINE: + case RELAY_EVENT_BUFFER_UPDATE: + case RELAY_EVENT_BUFFER_STATS: + case RELAY_EVENT_BUFFER_RENAME: + case RELAY_EVENT_BUFFER_REMOVE: + case RELAY_EVENT_BUFFER_ACTIVATE: + case RELAY_EVENT_BUFFER_INPUT: + case RELAY_EVENT_BUFFER_CLEAR: + changed = relay_process_buffer_event (m); + if (m->data.event == RELAY_EVENT_BUFFER_ACTIVATE + && g.relay.state == RELAY_SYNCHRONIZING) + g.relay.state = RELAY_CONNECTED; + break; + + case RELAY_EVENT_SERVER_UPDATE: + case RELAY_EVENT_SERVER_RENAME: + case RELAY_EVENT_SERVER_REMOVE: + changed = relay_process_server_event (m); + break; + default: + break; + } + if (changed) + xui_invalidate (); + return true; +} + +// ~~~ Framing and I/O ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +static bool +relay_process_buffer (void) +{ + struct str *buf = &g.relay.read_buffer; + size_t offset = 0; + while (true) + { + uint32_t frame_len = 0; + struct msg_unpacker r = + msg_unpacker_make (buf->str + offset, buf->len - offset); + if (!msg_unpacker_u32 (&r, &frame_len)) + break; + + r.len = MIN (r.len, sizeof frame_len + frame_len); + if (msg_unpacker_get_available (&r) < frame_len) + break; + + struct relay_event_message m = {}; + bool ok = relay_process_message (&r, &m); + relay_event_message_free (&m); + if (!ok) + return false; + + offset += r.offset; + } + + str_remove_slice (buf, 0, offset); + return true; +} + +static bool +relay_try_read (void) +{ + struct str *buf = &g.relay.read_buffer; + ssize_t n_read; + + while ((n_read = read (g.relay.socket_fd, buf->str + buf->len, + buf->alloc - buf->len - 1 /* null byte */)) > 0) + { + buf->len += n_read; + if (!relay_process_buffer ()) + break; + str_reserve (buf, 512); + } + + if (n_read < 0) + { + if (errno == EAGAIN || errno == EINTR) + return true; + + print_debug ("%s: %s: %s", __func__, "read", strerror (errno)); + } + return false; +} + +static bool +relay_try_write (void) +{ + struct str *buf = &g.relay.write_buffer; + ssize_t n_written; + + while (buf->len) + { + n_written = write (g.relay.socket_fd, buf->str, buf->len); + if (n_written >= 0) + { + str_remove_slice (buf, 0, n_written); + continue; + } + if (errno == EAGAIN || errno == EINTR) + return true; + + print_debug ("%s: %s: %s", __func__, "write", strerror (errno)); + return false; + } + return true; +} + +static void +relay_disconnect (void) +{ + if (g.relay.connecting) + { + connector_free (&g.relay.connector); + g.relay.connecting = false; + } + if (g.relay.socket_fd != -1) + { + poller_fd_reset (&g.relay.socket_event); + xclose (g.relay.socket_fd); + g.relay.socket_fd = -1; + } + str_reset (&g.relay.read_buffer); + str_reset (&g.relay.write_buffer); + g.relay.state = RELAY_DISCONNECTED; +} + +static void +relay_schedule_reconnect (int delay_ms) +{ + poller_timer_set (&g.relay.reconnect_event, delay_ms); + g.relay.state = RELAY_DISCONNECTED; + xui_invalidate (); +} + +static void +relay_on_ready (const struct pollfd *pfd, void *user_data) +{ + (void) user_data; + if (relay_try_read () && relay_try_write ()) + relay_update_poller (pfd); + else + { + print_error ("disconnected"); + cstr_set (&g.relay.last_error, xstrdup ("Connection lost")); + relay_disconnect (); + relay_schedule_reconnect (RELAY_RECONNECT_DELAY); + } +} + +// ~~~ Connection establishment ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +static void +relay_on_connector_connecting (void *user_data, const char *address) +{ + (void) user_data; + print_status ("connecting to %s...", address); + g.relay.state = RELAY_CONNECTING; + xui_invalidate (); +} + +static void +relay_on_connector_error (void *user_data, const char *error) +{ + (void) user_data; + print_status ("connection failed: %s", error); + cstr_set (&g.relay.last_error, xstrdup (error)); + xui_invalidate (); +} + +static void +relay_on_connector_failure (void *user_data) +{ + (void) user_data; + print_error ("connection failed"); + relay_disconnect (); + relay_schedule_reconnect (RELAY_RECONNECT_DELAY); +} + +static void +relay_on_connector_connected (void *user_data, int socket, + const char *hostname) +{ + (void) user_data; + (void) hostname; + + connector_free (&g.relay.connector); + g.relay.connecting = false; + g.relay.state = RELAY_SYNCHRONIZING; + cstr_set (&g.relay.last_error, NULL); + + set_blocking (socket, false); + set_cloexec (socket); + + // We already buffer our output, so reduce latencies. + int yes = 1; + soft_assert (setsockopt (socket, IPPROTO_TCP, TCP_NODELAY, + &yes, sizeof yes) != -1); + + g.relay.socket_fd = socket; + g.relay.socket_event = poller_fd_make (&g.poller, g.relay.socket_fd); + g.relay.socket_event.dispatcher = (poller_fd_fn) relay_on_ready; + + str_reset (&g.relay.read_buffer); + str_reset (&g.relay.write_buffer); + + struct relay_command_message m = {}; + m.data.hello.command = RELAY_COMMAND_HELLO; + m.data.hello.version = RELAY_VERSION; + m.data.hello.session_id_len = g.relay.session_id_len; + if (g.relay.session_id_len) + { + m.data.hello.session_id = xmalloc (g.relay.session_id_len); + memcpy (m.data.hello.session_id, g.relay.session_id, + g.relay.session_id_len); + m.data.hello.event_seq = g.relay.last_event_seq + 1; + } + relay_send (&m, NULL); + xui_invalidate (); +} + +static void +relay_connect (void) +{ + hard_assert (!g.relay.connecting); + hard_assert (g.relay.socket_fd == -1); + + connector_init (&g.relay.connector, &g.poller); + g.relay.connecting = true; + g.relay.state = RELAY_CONNECTING; + g.relay.connector.on_connecting = relay_on_connector_connecting; + g.relay.connector.on_error = relay_on_connector_error; + g.relay.connector.on_connected = relay_on_connector_connected; + g.relay.connector.on_failure = relay_on_connector_failure; + + connector_add_target (&g.relay.connector, g.relay.host, g.relay.port); + xui_invalidate (); +} + +static void +relay_on_reconnect_timer (void *user_data) +{ + (void) user_data; + relay_connect (); +} + +static void +relay_reconnect_now (void) +{ + relay_disconnect (); + relay_schedule_reconnect (0); +} + +// --- Viewport policy --------------------------------------------------------- + +static size_t +app_visible_line_count (const struct buffer *b) +{ + size_t count = 0; + LIST_FOR_EACH (struct buffer_line, line, b->lines) + if (buffer_line_is_visible (b, line)) + count++; + return count; +} + +static const struct buffer_line * +app_unread_marker_line (const struct buffer *b) +{ + if (!buffer_has_unread_marker (b)) + return NULL; + + uint64_t unread = (uint64_t) b->new_messages + b->new_unimportant_messages; + const struct buffer_line *marker = b->lines_tail; + while (unread > 1 && marker->prev) + { + unread--; + marker = marker->prev; + } + return marker; +} + +static int +app_viewport_height (void) +{ + return MAX (0, g_xui.height - 3 * g_xui.vunit); +} + +static int +app_buffer_content_height (const struct buffer *b) +{ + uint64_t height = app_visible_line_count (b) * (uint64_t) g_xui.vunit; + if (buffer_has_unread_marker (b)) + height += UNREAD_MARKER_HEIGHT; + return MIN ((uint64_t) INT_MAX, height); +} + +static int +app_content_height (void) +{ + if (g.log.visible) + { + return MIN ((uint64_t) INT_MAX, + g.log.lines.len * (uint64_t) g_xui.vunit); + } + return g.buffer_current ? app_buffer_content_height (g.buffer_current) : 0; +} + +static int * +app_content_scroll_offset (void) +{ + if (g.log.visible) + return &g.log.offset; + return g.buffer_current ? &g.buffer_current->scroll_offset : NULL; +} + +static int +app_content_top (void) +{ + int total = app_content_height (); + int *offset = app_content_scroll_offset (); + return MAX (0, total - app_viewport_height () - (offset ? *offset : 0)); +} + +static bool +app_scroll (int amount) +{ + int *offset = app_content_scroll_offset (); + if (!offset) + return false; + + int maximum = MAX (0, app_content_height () - app_viewport_height ()); + int64_t requested = (int64_t) *offset + amount; + int updated = MAX (0, MIN ((int64_t) maximum, requested)); + if (updated == *offset) + return false; + + *offset = updated; + app_recheck_current_highlight (); + xui_invalidate (); + return true; +} + +// --- Terminal widgets -------------------------------------------------------- + +static void +tui_render_editor (struct widget *self) +{ + struct row_buffer buf = row_buffer_make (); + const struct line_editor *e = &g.editor; + + int following = 0; + for (size_t i = e->point; i < e->len; i++) + following += e->w[i]; + + int preceding = 0; + size_t start = e->point; + while (start && preceding < self->width / 2) + preceding += e->w[--start]; + + // There can be one extra space at the end of the line but this way we + // don't need to care about non-spacing marks following full-width chars + while (start && self->width - preceding - following > 2 /* widest char */) + preceding += e->w[--start]; + + for (; start < e->len; start++) + { + ucs4_t codepoint = e->line[start]; + if (!iscntrl_ascii (codepoint)) + { + row_buffer_append_c (&buf, codepoint, self->attrs); + continue; + } + row_buffer_append_c (&buf, '^', self->attrs | A_REVERSE); + row_buffer_append_c (&buf, codepoint == 127 ? '?' : codepoint + 64, + self->attrs | A_REVERSE); + } + tui_flush_buffer (self, &buf); + + // FIXME: This should be at the end of of tui_render(). + move (self->y, self->x + preceding); + curs_set (1); +} + +static struct widget * +tui_make_editor (chtype attrs) +{ + struct widget *widget = xcalloc (1, sizeof *widget + 1); + widget->on_render = tui_render_editor; + widget->attrs = attrs; + widget->width = -1; + widget->height = 1; + return widget; +} + +// --- X11 widgets ------------------------------------------------------------- + +#ifdef WITH_X11 + +static void +x11_render_editor (struct widget *self) +{ + x11_render_padding (self); + + struct x11_font *font = x11_widget_font (self); + XftColor color = { .color = *x11_fg (self) }; + + // A simplistic adaptation of tui_render_editor() follows. + // TODO: Make this scroll around the caret, and fade like labels. + int x = self->x; + int caret = x; + for (size_t i = 0; i < g.editor.len; i++) + { + if ((int) i == g.editor.point) + caret = x; + + ucs4_t codepoint = g.editor.line[i]; + if (!iscntrl_ascii (codepoint)) + { + uint8_t encoded[7] = {}; + if (u8_uctomb (encoded, codepoint, sizeof encoded - 1) > 0) + x += x11_font_draw (font, &color, x, self->y, (char *) encoded); + continue; + } + + char escaped[] = { '^', codepoint == 127 ? '?' : codepoint + 64, 0 }; + int width = x11_font_hadvance (font, escaped); + XRenderFillRectangle (g_xui.dpy, PictOpSrc, g_xui.x11_pixmap_picture, + x11_fg (self), x, self->y, width, self->height); + XftColor reverse = { .color = *x11_bg (self) }; + x += x11_font_draw (font, &reverse, x, self->y, escaped); + } + if (g.editor.point == (int) g.editor.len) + caret = x; + + XRenderFillRectangle (g_xui.dpy, PictOpSrc, g_xui.x11_pixmap_picture, + &color.color, caret, self->y, 2, self->height); +} + +static struct widget * +x11_make_editor (chtype attrs) +{ + struct widget *widget = xcalloc (1, sizeof *widget + 1); + widget->on_render = x11_render_editor; + widget->attrs = attrs; + widget->width = -1; + widget->height = g_xui.vunit; + return widget; +} + +static struct xui_scrollbar_part +app_x11_compute_scrollbar (const struct xui_scrollbar *self, long visible) +{ + long minimum = MIN (visible, g_xui.vunit); + if (self->total < visible) + return (struct xui_scrollbar_part) { 0, 0 }; + if (visible <= minimum) + return (struct xui_scrollbar_part) { visible, 0 }; + + // Pre-remove the minimum slider and the endpoint before distributing + // the rest of the available height proportionally. + double available = visible - minimum - 1; + + double lenf = minimum + available * visible / self->total, length = 0; + long offset = 1 + available * self->top / self->total + + modf (lenf, &length); + + if (self->top == 0) + return (struct xui_scrollbar_part) { length, 0 }; + if (self->top + visible >= self->total) + return (struct xui_scrollbar_part) { length, visible - length }; + + return (struct xui_scrollbar_part) { length, offset }; +} + +// XXX: I've screwed up and liberty doesn't allow for pixel-perfect scrollbars. +static void +app_x11_render_scrollbar (struct widget *widget) +{ + struct xui_scrollbar *self = (struct xui_scrollbar *) widget; + x11_render_padding (widget); + + struct xui_scrollbar_part bar = + app_x11_compute_scrollbar (self, widget->height); + XRenderFillRectangle (g_xui.dpy, PictOpSrc, g_xui.x11_pixmap_picture, + x11_fg_attrs (widget->attrs), + widget->x, + widget->y + bar.start, + widget->width, + bar.length); +} + +#endif // WITH_X11 + +// --- XUI --------------------------------------------------------------------- + +// Use {INFINITY, INFINITY} once for stopping a subpath, twice to end icon data. +#define STOP {INFINITY, INFINITY} +static const struct xui_icon_point app_icon_reconnect[] = +{ + {0, 12}, {0, 6}, {3, 3}, {13, 3}, {13, 0}, {20, 4.5}, + {13, 9}, {13, 6}, {3, 6}, {3, 10}, STOP, + {0, 15.5}, {7, 11}, {7, 14}, {17, 14}, {17, 10}, {20, 8}, + {20, 14}, {17, 17}, {7, 17}, {7, 20}, STOP, STOP, +}; +#undef STOP + +static bool +app_on_insufficient_color (void) +{ + // Not worth caring about in this mostly demonstrational application. + return true; +} + +static void +app_on_clipboard_copy (const char *text) +{ + (void) text; + // TODO(p): We should show a message. +} + +static void +app_select_backend (void) +{ + g.backend.make_editor = tui_make_editor; +#ifdef WITH_X11 + if (g_xui.ui == &x11_ui) + g.backend.make_editor = x11_make_editor; +#endif // WITH_X11 +} + +// --- Layout and rendering ---------------------------------------------------- + +struct layout +{ + struct widget *head; + struct widget *tail; +}; + +static struct widget * +app_push (struct layout *l, struct widget *w) +{ + LIST_APPEND_WITH_TAIL (l->head, l->tail, w); + return w; +} + +static struct widget * +app_label (enum app_attribute attribute, const char *text, bool fill) +{ + struct widget *widget = + g_xui.ui->label (g_attrs[attribute].attrs, 0, text ? text : ""); + if (fill) + widget->width = -1; + return widget; +} + +static struct widget * +app_button (enum app_attribute attribute, const char *text, enum action action) +{ + const struct xui_icon_point *icon = + action == ACTION_RECONNECT ? app_icon_reconnect : NULL; + struct widget *widget = + g_xui.ui->symbol (g_attrs[attribute].attrs, text, icon); + widget->widget_id = WIDGET_BUTTON; + widget->userdata = action; + return widget; +} + +static struct widget * +app_padding (enum app_attribute attribute, float width) +{ + return g_xui.ui->padding (g_attrs[attribute].attrs, width, 1); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static struct widget * +app_make_activity_indicator (void) +{ + struct widget *indicator = app_label (ATTRIBUTE_CHROME, "⋯", false); + if (g.relay.pending_commands) + return indicator; + + struct widget *spacer = app_padding (ATTRIBUTE_CHROME, 0); + spacer->width = indicator->width; + widget_destroy (indicator); + return spacer; +} + +static struct widget * +app_make_title (void) +{ + struct layout row = {}; + app_push (&row, app_padding (ATTRIBUTE_CHROME, 0.25)); + app_push (&row, app_label (ATTRIBUTE_CHROME, + g.buffer_current ? g.buffer_current->topic : NULL, true)); + app_push (&row, app_padding (ATTRIBUTE_CHROME, 0.5)); + app_push (&row, app_make_activity_indicator ()); + app_push (&row, app_padding (ATTRIBUTE_CHROME, 0.5)); + app_push (&row, app_button (ATTRIBUTE_CHROME, "@", ACTION_RECONNECT)) + ->attrs |= A_BOLD; + app_push (&row, app_padding (ATTRIBUTE_CHROME, 0.25)); + return xui_hbox (row.head); +} + +static struct widget * +app_make_buffer_row (struct buffer *b, int index) +{ + enum app_attribute attribute = ATTRIBUTE_NORMAL; + if (b == g.buffer_current) + attribute = ATTRIBUTE_CURRENT; + else if (b->highlighted) + attribute = ATTRIBUTE_HIGHLIGHT; + else if (b->new_messages) + attribute = ATTRIBUTE_ACTIVITY; + + char *label = b != g.buffer_current && b->new_messages + ? xstrdup_printf ("%s (%u)", b->buffer_name, b->new_messages) + : xstrdup (b->buffer_name); + + struct layout row = {}; + app_push (&row, app_padding (attribute, 0.25)); + app_push (&row, app_label (attribute, label, true)); + app_push (&row, app_padding (attribute, 0.25)); + free (label); + + struct widget *widget = xui_hbox (row.head); + widget->widget_id = WIDGET_BUFFER; + widget->userdata = index; + return widget; +} + +static struct widget * +app_make_buffer_list (void) +{ + struct layout rows = {}; + int index = 0; + LIST_FOR_EACH (struct buffer, buffer, g.buffers) + app_push (&rows, app_make_buffer_row (buffer, index++)); + + struct widget *list = xui_vbox (rows.head); + list->width = MIN (20 * g_xui.hunit, g_xui.width / 3); + list->height = -1; + return list; +} + +static const char * +app_line_mark (enum relay_rendition rendition) +{ + switch (rendition) + { + case RELAY_RENDITION_INDENT: return " "; + case RELAY_RENDITION_STATUS: return "– "; + case RELAY_RENDITION_ERROR: return "⚠ "; + case RELAY_RENDITION_JOIN: return "→ "; + case RELAY_RENDITION_PART: return "← "; + case RELAY_RENDITION_ACTION: return "✶ "; + default: return ""; + } +} + +static enum app_attribute +app_line_attribute (const struct buffer_line *line) +{ + if (line->leaked) + return ATTRIBUTE_LEAKED; + + switch (line->rendition) + { + case RELAY_RENDITION_ERROR: return ATTRIBUTE_ERROR; + case RELAY_RENDITION_JOIN: return ATTRIBUTE_JOIN; + case RELAY_RENDITION_PART: return ATTRIBUTE_PART; + case RELAY_RENDITION_ACTION: return ATTRIBUTE_ACTION; + default: return ATTRIBUTE_NORMAL; + } +} + +static struct widget * +app_make_line (const struct buffer_line *line) +{ + time_t when = line->when / 1000; + struct tm tm = {}; + char timestamp[16] = ""; + if (localtime_r (&when, &tm)) + strftime (timestamp, sizeof timestamp, "%H:%M:%S", &tm); + + enum app_attribute attribute = app_line_attribute (line); + struct layout row = {}; + app_push (&row, app_padding (ATTRIBUTE_TIME, 0.25)); + app_push (&row, app_label (ATTRIBUTE_TIME, timestamp, false)); + app_push (&row, app_padding (ATTRIBUTE_TIME, 0.25)); + app_push (&row, app_padding (ATTRIBUTE_NORMAL, 0.5)); + app_push (&row, app_label (attribute, + app_line_mark (line->rendition), false)); + app_push (&row, app_label (attribute, line->text, true)); + app_push (&row, app_padding (ATTRIBUTE_NORMAL, 0.25)); + return xui_hbox (row.head); +} + +static void +tui_render_unread_marker (struct widget *self) +{ + struct row_buffer buffer = row_buffer_make (); + for (int i = 0; i < self->width; i++) + row_buffer_append_c (&buffer, '-', + i < 8 ? g_attrs[ATTRIBUTE_TIME].attrs : self->attrs); + tui_flush_buffer (self, &buffer); +} + +static struct widget * +app_make_unread_marker (void) +{ + if (g_xui.ui == &tui_ui) + { + struct widget *marker = xcalloc (1, sizeof *marker + 1); + marker->on_render = tui_render_unread_marker; + marker->attrs = g_attrs[ATTRIBUTE_UNREAD].attrs; + marker->width = -1; + marker->height = UNREAD_MARKER_HEIGHT; + return marker; + } + + // Reversing swaps the orange foreground into the padding background. + struct widget *marker = g_xui.ui->padding + (g_attrs[ATTRIBUTE_UNREAD].attrs | A_REVERSE, 0, 0); + marker->width = -1; + marker->height = UNREAD_MARKER_HEIGHT; + return marker; +} + +static struct widget * +app_make_log_line (const char *text) +{ + struct layout line = {}; + app_push (&line, app_padding (ATTRIBUTE_NORMAL, 0.25)); + app_push (&line, app_label (ATTRIBUTE_NORMAL, text, true)); + app_push (&line, app_padding (ATTRIBUTE_NORMAL, 0.25)); + return xui_hbox (line.head); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +struct transcript_slice +{ + int view_top; ///< Top of the viewport in content + int clip_top; ///< First visible content pixel + int clip_bottom; ///< One past the last visible pixel + int y; ///< Current content position + int first_y; ///< First appended row, or -1 +}; + +static struct transcript_slice +app_make_transcript_slice (void) +{ + int *scroll_offset = app_content_scroll_offset (); + hard_assert (scroll_offset); + + int total = app_content_height (); + int viewport = app_viewport_height (); + int maximum = MAX (0, total - viewport); + *scroll_offset = MAX (0, MIN (*scroll_offset, maximum)); + + int view_top = total - viewport - *scroll_offset; + return (struct transcript_slice) + { + .view_top = view_top, + .clip_top = MAX (0, view_top), + .clip_bottom = MIN (total, view_top + viewport), + .first_y = -1, + }; +} + +static bool +app_transcript_row_is_visible (struct transcript_slice *slice, int height) +{ + int top = slice->y; + slice->y += height; + if (slice->y <= slice->clip_top || top >= slice->clip_bottom) + return false; + if (slice->first_y < 0) + slice->first_y = top; + return true; +} + +static int +app_transcript_slice_offset (const struct transcript_slice *slice) +{ + return slice->first_y < 0 ? 0 : slice->first_y - slice->view_top; +} + +static int +app_append_log_rows (struct layout *rows) +{ + struct transcript_slice slice = app_make_transcript_slice (); + size_t first = slice.clip_top / g_xui.vunit; + size_t end = slice.clip_bottom / g_xui.vunit + + !!(slice.clip_bottom % g_xui.vunit); + end = MIN (end, g.log.lines.len); + slice.y = first * g_xui.vunit; + for (size_t i = first; i < end; i++) + if (app_transcript_row_is_visible (&slice, g_xui.vunit)) + app_push (rows, app_make_log_line (g.log.lines.vector[i])); + return app_transcript_slice_offset (&slice); +} + +static int +app_append_buffer_rows (struct layout *rows) +{ + struct buffer *b = g.buffer_current; + if (!b) + return 0; + + const struct buffer_line *marker = app_unread_marker_line (b); + struct transcript_slice slice = app_make_transcript_slice (); + LIST_FOR_EACH (struct buffer_line, line, b->lines) + { + if (line == marker + && app_transcript_row_is_visible (&slice, UNREAD_MARKER_HEIGHT)) + app_push (rows, app_make_unread_marker ()); + if (buffer_line_is_visible (b, line) + && app_transcript_row_is_visible (&slice, g_xui.vunit)) + app_push (rows, app_make_line (line)); + } + return app_transcript_slice_offset (&slice); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static void +app_on_transcript_allocated (struct widget *self) +{ + xui_on_vbox_allocated (self); + + struct widget *spacer = self->children; + hard_assert (spacer); + widget_move (spacer, self->x - spacer->x, self->y - spacer->y); + spacer->width = self->width; + spacer->height = self->height; + + int y = self->y + self->userdata; + LIST_FOR_EACH (struct widget, child, spacer->next) + { + widget_move (child, self->x - child->x, y - child->y); + y += child->height; + } +} + +static struct widget * +app_make_transcript (void) +{ + struct layout rows = {}; + struct widget *spacer = + g_xui.ui->padding (g_attrs[ATTRIBUTE_NORMAL].attrs, 0, 0); + spacer->width = spacer->height = -1; + app_push (&rows, spacer); + + int offset = g.log.visible + ? app_append_log_rows (&rows) + : app_append_buffer_rows (&rows); + + struct widget *transcript = xui_vbox (rows.head); + transcript->on_allocated = app_on_transcript_allocated; + transcript->widget_id = WIDGET_TRANSCRIPT; + transcript->userdata = offset; + transcript->width = transcript->height = -1; + return transcript; +} + +static struct widget * +app_make_middle (void) +{ + struct layout columns = {}; + app_push (&columns, app_make_buffer_list ()); + + struct layout viewport = {}; + app_push (&viewport, app_make_transcript ()); + int viewport_height = app_viewport_height (); + if (viewport_height > 0 && app_content_height () > viewport_height) + { + struct widget *scrollbar = g_xui.ui->scrollbar + (g_attrs[ATTRIBUTE_SCROLLBAR].attrs, + app_content_top (), app_content_height ()); +#ifdef WITH_X11 + if (g_xui.ui == &x11_ui) + scrollbar->on_render = app_x11_render_scrollbar; +#endif // WITH_X11 + scrollbar->widget_id = WIDGET_SCROLLBAR; + app_push (&viewport, scrollbar); + } + + struct widget *viewport_box = xui_hbox (viewport.head); + viewport_box->width = viewport_box->height = -1; + app_push (&columns, viewport_box); + + struct widget *middle = xui_hbox (columns.head); + middle->height = -1; + return middle; +} + +static struct widget * +app_make_status (void) +{ + struct str status = str_make (); + switch (g.relay.state) + { + case RELAY_DISCONNECTED: + str_append (&status, "Relay: disconnected"); + if (g.relay.last_error) + str_append_printf (&status, ": %s", g.relay.last_error); + break; + case RELAY_CONNECTING: + str_append (&status, "Relay: connecting..."); + break; + case RELAY_SYNCHRONIZING: + str_append (&status, "Synchronizing..."); + break; + case RELAY_CONNECTED: + if (!g.buffer_current) + { + str_append (&status, "Synchronizing..."); + break; + } + str_append (&status, g.buffer_current->buffer_name); + if (g.buffer_current->modes && *g.buffer_current->modes) + str_append_printf (&status, "(+%s)", g.buffer_current->modes); + if (g.buffer_current->hide_unimportant) + str_append (&status, ""); + break; + default: + hard_assert (!"invalid relay state"); + } + + struct layout row = {}; + app_push (&row, app_padding (ATTRIBUTE_CHROME, 0.25)); + app_push (&row, app_label (ATTRIBUTE_CHROME, status.str, true)); + int *scroll_offset = app_content_scroll_offset (); + if (g.relay.state == RELAY_CONNECTED + && scroll_offset && *scroll_offset) + { + app_push (&row, app_label (ATTRIBUTE_CHROME, "↓", false)); + } + app_push (&row, app_padding (ATTRIBUTE_CHROME, 0.5)); + app_push (&row, app_button (ATTRIBUTE_CHROME, "B", ACTION_FORMAT_BOLD)) + ->attrs |= A_BOLD; + app_push (&row, app_padding (ATTRIBUTE_CHROME, 0.5)); +#ifdef A_ITALIC + app_push (&row, app_button (ATTRIBUTE_CHROME, "I", ACTION_FORMAT_ITALIC)) + ->attrs |= A_ITALIC; +#else + app_push (&row, app_button (ATTRIBUTE_CHROME, "I", ACTION_FORMAT_ITALIC)); +#endif + app_push (&row, app_padding (ATTRIBUTE_CHROME, 0.5)); + app_push (&row, app_button (ATTRIBUTE_CHROME, "U", ACTION_FORMAT_UNDERLINE)) + ->attrs |= A_UNDERLINE; + app_push (&row, app_padding (ATTRIBUTE_CHROME, 0.5)); + app_push (&row, app_button (ATTRIBUTE_CHROME, + g.log.visible ? "Hide log" : "Log", ACTION_LOG)); + app_push (&row, app_padding (ATTRIBUTE_CHROME, 0.25)); + str_free (&status); + return xui_hbox (row.head); +} + +static char * +app_prompt (void) +{ + if (!g.buffer_current || !g.buffer_current->server_name) + return xstrdup (""); + + struct server *server = + str_map_find (&g.servers, g.buffer_current->server_name); + if (!server) + return xstrdup (""); + if (server->state == RELAY_SERVER_STATE_REGISTERED) + return server->user_modes && *server->user_modes + ? xstrdup_printf ("%s(%s)", server->user, server->user_modes) + : xstrdup (server->user); + + switch (server->state) + { + case RELAY_SERVER_STATE_CONNECTING: return xstrdup ("(Connecting)"); + case RELAY_SERVER_STATE_CONNECTED: return xstrdup ("(Connected)"); + case RELAY_SERVER_STATE_DISCONNECTING: return xstrdup ("(Disconnecting)"); + default: return xstrdup ("(Disconnected)"); + } +} + +static struct widget * +app_make_input (void) +{ + struct layout row = {}; + char *prompt = app_prompt (); + if (*prompt) + { + app_push (&row, app_padding (ATTRIBUTE_PROMPT, 0.25)); + app_push (&row, app_label (ATTRIBUTE_PROMPT, prompt, false)); + app_push (&row, app_padding (ATTRIBUTE_PROMPT, 0.25)); + app_push (&row, app_padding (ATTRIBUTE_NORMAL, 0.5)); + } + else + app_push (&row, app_padding (ATTRIBUTE_NORMAL, 0.25)); + free (prompt); + app_push (&row, g.backend.make_editor (g_attrs[ATTRIBUTE_NORMAL].attrs)); + return xui_hbox (row.head); +} + +static void +app_layout (void) +{ + struct layout rows = {}; + app_push (&rows, app_make_title ()); + app_push (&rows, app_make_middle ()); + app_push (&rows, app_make_status ()); + app_push (&rows, app_make_input ()); + + struct widget *root = xui_vbox (rows.head); + root->width = g_xui.width; + root->height = g_xui.height; + g_xui.widgets = root; +} + +// --- Commands and interaction ------------------------------------------------ + +// ~~~ Buffer input and history ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +static void +app_send_buffer_activate (struct buffer *b) +{ + if (!b) + return; + + struct relay_command_message m = {}; + m.data.buffer_activate.command = RELAY_COMMAND_BUFFER_ACTIVATE; + m.data.buffer_activate.buffer_name = str_make (); + str_append (&m.data.buffer_activate.buffer_name, b->buffer_name); + relay_send (&m, NULL); +} + +static void +app_submit_input (void) +{ + if (!g.buffer_current) + return; + + char *text = app_editor_text (); + + struct relay_command_message m = {}; + m.data.buffer_input.command = RELAY_COMMAND_BUFFER_INPUT; + m.data.buffer_input.buffer_name = str_make (); + m.data.buffer_input.text = str_make (); + str_append (&m.data.buffer_input.buffer_name, + g.buffer_current->buffer_name); + str_append (&m.data.buffer_input.text, text); + + if (!relay_send (&m, NULL)) + { + free (text); + return; + } + + strv_append_owned (&g.buffer_current->history, text); + g.buffer_current->history_at = g.buffer_current->history.len; + cstr_set (&g.buffer_current->input, xstrdup ("")); + app_editor_set (""); +} + +static bool +app_history (int diff) +{ + struct buffer *b = g.buffer_current; + if (!b) + return false; + if (diff < 0 && !b->history_at) + return false; + if (diff > 0 && b->history_at >= b->history.len) + return false; + + if (b->history_at == b->history.len) + cstr_set (&b->input, app_editor_text ()); + b->history_at += diff; + app_editor_set (b->history_at == b->history.len + ? b->input + : b->history.vector[b->history_at]); + return true; +} + +static bool +app_history_at (size_t position) +{ + struct buffer *b = g.buffer_current; + if (!b || position > b->history.len || position == b->history_at) + return false; + + if (b->history_at == b->history.len) + cstr_set (&b->input, app_editor_text ()); + b->history_at = position; + app_editor_set (position == b->history.len + ? b->input + : b->history.vector[position]); + return true; +} + +// ~~~ Viewport and navigation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +static struct buffer * +app_buffer_by_index (int index) +{ + LIST_FOR_EACH (struct buffer, buffer, g.buffers) + if (!index--) + return buffer; + return NULL; +} + +static bool +app_widget_contains (const struct widget *widget, int x, int y) +{ + return x >= widget->x && x < widget->x + widget->width + && y >= widget->y && y < widget->y + widget->height; +} + +static struct widget * +app_interactive_widget_at (struct widget *list, int x, int y) +{ + LIST_FOR_EACH (struct widget, widget, list) + { + if (!app_widget_contains (widget, x, y)) + continue; + + struct widget *child = + app_interactive_widget_at (widget->children, x, y); + if (child) + return child; + if (widget->widget_id != WIDGET_NONE) + return widget; + break; + } + return NULL; +} + +static struct widget * +app_widget_by_id (struct widget *list, int widget_id) +{ + LIST_FOR_EACH (struct widget, widget, list) + { + if (widget->widget_id == widget_id) + return widget; + + struct widget *child = app_widget_by_id (widget->children, widget_id); + if (child) + return child; + } + return NULL; +} + +static bool +app_scrollbar_seek (struct widget *scrollbar, int y) +{ + if (!scrollbar || scrollbar->height <= 0) + return false; + + int total = app_content_height (); + int visible = app_viewport_height (); + int maximum = MAX (0, total - visible); + int top = (double) MAX (0, MIN (scrollbar->height, y)) + / scrollbar->height * total - visible / 2; + top = MAX (0, MIN (maximum, top)); + + int *offset = app_content_scroll_offset (); + if (!offset) + return false; + + *offset = maximum - top; + app_recheck_current_highlight (); + xui_invalidate (); + return true; +} + +static void +app_notify_active (void) +{ + int64_t now = clock_msec (CLOCK_BEST); + if (g.last_active && now - g.last_active < 5000) + return; + + struct relay_command_message m = {}; + m.data.command = RELAY_COMMAND_ACTIVE; + if (relay_send (&m, NULL)) + g.last_active = now; +} + +static void +app_toggle_unimportant (void) +{ + if (!g.buffer_current) + return; + + struct relay_command_message m = {}; + m.data.buffer_toggle_unimportant.command = + RELAY_COMMAND_BUFFER_TOGGLE_UNIMPORTANT; + m.data.buffer_toggle_unimportant.buffer_name = str_make (); + str_append (&m.data.buffer_toggle_unimportant.buffer_name, + g.buffer_current->buffer_name); + relay_send (&m, NULL); +} + +static void +app_switch_buffer (int direction) +{ + if (!g.buffers) + return; + + struct buffer *next = NULL; + if (!g.buffer_current) + next = g.buffers; + else if (direction > 0) + next = g.buffer_current->next ? g.buffer_current->next : g.buffers; + else + next = g.buffer_current->prev ? g.buffer_current->prev : g.buffers_tail; + app_send_buffer_activate (next); +} + +static bool +app_switch_buffer_matching (bool highlighted) +{ + if (!g.buffers || !g.buffer_current) + return false; + + struct buffer *b = g.buffer_current, *stop = b; + while ((b = b->next ? b->next : g.buffers) != stop) + if ((highlighted && b->highlighted) + || (!highlighted && b->new_messages)) + { + app_send_buffer_activate (b); + return true; + } + return false; +} + +static void +app_switch_buffer_number (int number) +{ + struct buffer *b = app_buffer_by_index (number - 1); + if (b == g.buffer_current && g.buffer_last) + b = g.buffer_last; + app_send_buffer_activate (b); +} + +static bool +app_complete (void) +{ + if (!g.buffer_current || g.completion.pending) + return false; + + char *text = app_editor_text (); + size_t position = app_editor_position (); + if (position > UINT32_MAX) + { + free (text); + return false; + } + + struct relay_command_message m = {}; + m.data.buffer_complete.command = RELAY_COMMAND_BUFFER_COMPLETE; + m.data.buffer_complete.buffer_name = + str_from_cstr (g.buffer_current->buffer_name); + m.data.buffer_complete.text = str_from_cstr (text); + m.data.buffer_complete.position = position; + + uint32_t request_seq = 0; + if (!relay_send (&m, &request_seq)) + { + free (text); + return false; + } + + completion_request_start (&g.completion, request_seq, text, + g.buffer_current->buffer_name, position); + return true; +} + +static bool +app_toggle_log (void) +{ + if (g.log.visible) + { + log_view_hide (&g.log); + app_recheck_current_highlight (); + xui_invalidate (); + return true; + } + if (!g.buffer_current || g.log.pending) + return false; + + struct relay_command_message m = {}; + m.data.buffer_log.command = RELAY_COMMAND_BUFFER_LOG; + m.data.buffer_log.buffer_name = + str_from_cstr (g.buffer_current->buffer_name); + + uint32_t request_seq = 0; + if (!relay_send (&m, &request_seq)) + return false; + + log_view_request_start (&g.log, request_seq, g.buffer_current->buffer_name); + return true; +} + +// ~~~ Action and mouse dispatch ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +static const enum line_editor_action g_editor_actions[] = +{ + LINE_EDITOR_B_CHAR, + LINE_EDITOR_F_CHAR, + LINE_EDITOR_B_WORD, + LINE_EDITOR_F_WORD, + LINE_EDITOR_HOME, + LINE_EDITOR_END, + LINE_EDITOR_UPCASE_WORD, + LINE_EDITOR_DOWNCASE_WORD, + LINE_EDITOR_CAPITALIZE_WORD, + LINE_EDITOR_B_DELETE, + LINE_EDITOR_F_DELETE, + LINE_EDITOR_B_KILL_WORD, + LINE_EDITOR_B_KILL_LINE, + LINE_EDITOR_F_KILL_LINE, +}; + +static bool +app_process_editor_action (enum action action) +{ + hard_assert (action >= ACTION_EDITOR_B_CHAR + && action <= ACTION_EDITOR_F_KILL_LINE); + + size_t index = action - ACTION_EDITOR_B_CHAR; + hard_assert (index < N_ELEMENTS (g_editor_actions)); + + enum line_editor_action editor_action = g_editor_actions[index]; + bool handled = line_editor_action (&g.editor, editor_action); + if (handled) + xui_invalidate (); + return handled; +} + +static bool +app_process_action (enum action action) +{ + if (action >= ACTION_EDITOR_B_CHAR + && action <= ACTION_EDITOR_F_KILL_LINE) + return app_process_editor_action (action); + + int page = MAX (1, app_viewport_height ()); + + switch (action) + { + case ACTION_QUIT: + app_quit (); + return true; + case ACTION_REDRAW: + if (g_xui.ui == &tui_ui) + clear (); + xui_invalidate (); + return true; + case ACTION_RECONNECT: + relay_reconnect_now (); + return true; + + case ACTION_BUFFER_PREVIOUS: + app_switch_buffer (-1); + return true; + case ACTION_BUFFER_NEXT: + app_switch_buffer (+1); + return true; + case ACTION_BUFFER_LAST: + if (!g.buffer_last) + return false; + app_send_buffer_activate (g.buffer_last); + return true; + case ACTION_BUFFER_HIGHLIGHT: + return app_switch_buffer_matching (true); + case ACTION_BUFFER_ACTIVITY: + return app_switch_buffer_matching (false); + case ACTION_BUFFER_1: + case ACTION_BUFFER_2: + case ACTION_BUFFER_3: + case ACTION_BUFFER_4: + case ACTION_BUFFER_5: + case ACTION_BUFFER_6: + case ACTION_BUFFER_7: + case ACTION_BUFFER_8: + case ACTION_BUFFER_9: + case ACTION_BUFFER_10: + app_switch_buffer_number (action - ACTION_BUFFER_1 + 1); + return true; + case ACTION_TOGGLE_UNIMPORTANT: + app_toggle_unimportant (); + return true; + case ACTION_LOG: + return app_toggle_log (); + case ACTION_FORMAT: + g.inserting_attribute = true; + return true; + case ACTION_FORMAT_BOLD: + app_editor_insert (0x02); + return true; + case ACTION_FORMAT_ITALIC: + app_editor_insert (0x1d); + return true; + case ACTION_FORMAT_UNDERLINE: + app_editor_insert (0x1f); + return true; + case ACTION_SCROLL_UP: + return app_scroll (+page); + case ACTION_SCROLL_DOWN: + return app_scroll (-page); + case ACTION_SUBMIT: + app_submit_input (); + return true; + case ACTION_COMPLETE: + return app_complete (); + case ACTION_HISTORY_PREVIOUS: + return app_history (-1); + case ACTION_HISTORY_NEXT: + return app_history (+1); + case ACTION_HISTORY_FIRST: + return g.buffer_current && g.buffer_current->history.len + && app_history_at (0); + case ACTION_HISTORY_LAST: + return g.buffer_current + && app_history_at (g.buffer_current->history.len); + default: + return false; + } +} + +static bool +app_process_mouse (termo_mouse_event_t type, int x, int y, int button, + int modifiers) +{ + (void) modifiers; + app_notify_active (); + if (type == TERMO_MOUSE_RELEASE) + { + g.dragging_scrollbar = false; + return true; + } + if (type == TERMO_MOUSE_DRAG) + { + if (!g.dragging_scrollbar) + return true; + struct widget *scrollbar = + app_widget_by_id (g_xui.widgets, WIDGET_SCROLLBAR); + return app_scrollbar_seek (scrollbar, + scrollbar ? y - scrollbar->y : 0); + } + if (type != TERMO_MOUSE_PRESS) + return true; + + if (button == 4 || button == 5) + { + struct widget *transcript = + app_widget_by_id (g_xui.widgets, WIDGET_TRANSCRIPT); + return transcript && app_widget_contains (transcript, x, y) + && app_scroll ((button == 4 ? 1 : -1) * 3 * g_xui.vunit); + } + if (button != 1) + return false; + + struct widget *target = app_interactive_widget_at (g_xui.widgets, x, y); + if (target && target->widget_id == WIDGET_SCROLLBAR) + { + g.dragging_scrollbar = true; + return app_scrollbar_seek (target, y - target->y); + } + if (target && target->widget_id == WIDGET_BUTTON) + return app_process_action (target->userdata); + if (target && target->widget_id == WIDGET_BUFFER) + { + app_send_buffer_activate (app_buffer_by_index (target->userdata)); + return true; + } + return false; +} + +// ~~~ Key bindings and terminal input ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +static int +app_binding_cmp (const void *a, const void *b) +{ + const struct binding *aa = a, *bb = b; + return termo_keycmp (g_xui.tk, &aa->decoded, &bb->decoded); +} + +static const struct +{ + const char *key; ///< Key definition + enum action action; ///< Action to take +} +g_default_bindings[] = +{ + { "C-q", ACTION_QUIT }, + { "C-l", ACTION_REDRAW }, + { "C-F5", ACTION_RECONNECT }, + { "M-PageUp", ACTION_BUFFER_PREVIOUS }, + { "C-PageUp", ACTION_BUFFER_PREVIOUS }, + { "C-p", ACTION_BUFFER_PREVIOUS }, + { "F5", ACTION_BUFFER_PREVIOUS }, + { "M-PageDown", ACTION_BUFFER_NEXT }, + { "C-PageDown", ACTION_BUFFER_NEXT }, + { "C-n", ACTION_BUFFER_NEXT }, + { "F6", ACTION_BUFFER_NEXT }, + { "M-Tab", ACTION_BUFFER_LAST }, + { "M-!", ACTION_BUFFER_HIGHLIGHT }, + { "M-a", ACTION_BUFFER_ACTIVITY }, + { "M-H", ACTION_TOGGLE_UNIMPORTANT }, + { "M-h", ACTION_LOG }, + { "M-m", ACTION_FORMAT }, + { "PageUp", ACTION_SCROLL_UP }, + { "PageDown", ACTION_SCROLL_DOWN }, + { "Enter", ACTION_SUBMIT }, + { "C-j", ACTION_SUBMIT }, + { "Tab", ACTION_COMPLETE }, + { "M-p", ACTION_HISTORY_PREVIOUS }, + { "Up", ACTION_HISTORY_PREVIOUS }, + { "M-n", ACTION_HISTORY_NEXT }, + { "Down", ACTION_HISTORY_NEXT }, + { "M-<", ACTION_HISTORY_FIRST }, + { "M->", ACTION_HISTORY_LAST }, + { "C-a", ACTION_EDITOR_HOME }, + { "Home", ACTION_EDITOR_HOME }, + { "C-e", ACTION_EDITOR_END }, + { "End", ACTION_EDITOR_END }, + { "C-b", ACTION_EDITOR_B_CHAR }, + { "Left", ACTION_EDITOR_B_CHAR }, + { "C-f", ACTION_EDITOR_F_CHAR }, + { "Right", ACTION_EDITOR_F_CHAR }, + { "M-b", ACTION_EDITOR_B_WORD }, + { "M-f", ACTION_EDITOR_F_WORD }, + { "M-u", ACTION_EDITOR_UPCASE_WORD }, + { "M-l", ACTION_EDITOR_DOWNCASE_WORD }, + { "M-c", ACTION_EDITOR_CAPITALIZE_WORD }, + { "C-h", ACTION_EDITOR_B_DELETE }, + { "DEL", ACTION_EDITOR_B_DELETE }, + { "Backspace", ACTION_EDITOR_B_DELETE }, + { "C-d", ACTION_EDITOR_F_DELETE }, + { "Delete", ACTION_EDITOR_F_DELETE }, + { "C-w", ACTION_EDITOR_B_KILL_WORD }, + { "C-u", ACTION_EDITOR_B_KILL_LINE }, + { "C-k", ACTION_EDITOR_F_KILL_LINE }, + { "M-1", ACTION_BUFFER_1 }, + { "M-2", ACTION_BUFFER_2 }, + { "M-3", ACTION_BUFFER_3 }, + { "M-4", ACTION_BUFFER_4 }, + { "M-5", ACTION_BUFFER_5 }, + { "M-6", ACTION_BUFFER_6 }, + { "M-7", ACTION_BUFFER_7 }, + { "M-8", ACTION_BUFFER_8 }, + { "M-9", ACTION_BUFFER_9 }, + { "M-0", ACTION_BUFFER_10 }, +}; + +static void +app_init_bindings (void) +{ + g.keys_len = N_ELEMENTS (g_default_bindings); + g.keys = xcalloc (g.keys_len, sizeof *g.keys); + for (size_t i = 0; i < g.keys_len; i++) + { + const char *rest = termo_strpkey_utf8 (g_xui.tk, + g_default_bindings[i].key, &g.keys[i].decoded, + TERMO_FORMAT_ALTISMETA); + hard_assert (rest && !*rest); + g.keys[i].action = g_default_bindings[i].action; + } + qsort (g.keys, g.keys_len, sizeof *g.keys, app_binding_cmp); +} + +static ucs4_t +app_format_attribute (ucs4_t selector) +{ + switch (selector) + { + case 'b': case 'b' ^ 96: return 0x02; + case 'c': return 0x03; + case 'q': case 'm': return 0x11; + case 'v': return 0x16; + case 'i': case 'i' ^ 96: case ']': return 0x1d; + case 's': case 's' ^ 96: + case 'x': case 'x' ^ 96: case '^': return 0x1e; + case 'u': case 'u' ^ 96: case '_': return 0x1f; + case 'r': case 'o': return 0x0f; + default: return 0; + } +} + +static bool +app_process_termo_event (termo_key_t *event) +{ + app_notify_active (); + if (event->type == TERMO_TYPE_FOCUS) + { + xui_invalidate (); + return true; + } + + if (g.inserting_attribute) + { + g.inserting_attribute = false; + if (event->type != TERMO_TYPE_KEY) + return false; + + ucs4_t attribute = app_format_attribute (event->code.codepoint); + if (!attribute) + return false; + app_editor_insert (attribute); + xui_invalidate (); + return true; + } + + struct binding dummy = { .decoded = *event }, *binding = + bsearch (&dummy, g.keys, g.keys_len, sizeof *g.keys, app_binding_cmp); + if (binding) + return app_process_action (binding->action); + if (event->type == TERMO_TYPE_KEY && !event->modifiers) + { + app_editor_insert (event->code.codepoint); + xui_invalidate (); + return true; + } + return false; +} + +// --- Signals and application lifecycle --------------------------------------- + +static void +signals_postpone_handling (char signal_id) +{ + int original_errno = errno; + if (write (g_signal_pipe[1], &signal_id, 1) == -1) + soft_assert (errno == EAGAIN); + errno = original_errno; +} + +static void +signals_superhandler (int signum) +{ + switch (signum) + { + case SIGWINCH: + g_winch_received = true; + signals_postpone_handling ('w'); + break; + case SIGINT: + case SIGTERM: + g_termination_requested = true; + signals_postpone_handling ('t'); + break; + default: + hard_assert (!"unhandled signal"); + } +} + +static void +signals_init (void) +{ + if (pipe (g_signal_pipe) == -1) + exit_fatal ("%s: %s", "pipe", strerror (errno)); + + set_cloexec (g_signal_pipe[0]); + set_cloexec (g_signal_pipe[1]); + set_blocking (g_signal_pipe[0], false); + set_blocking (g_signal_pipe[1], false); + signal (SIGPIPE, SIG_IGN); + + struct sigaction sa; + sa.sa_flags = SA_RESTART; + sa.sa_handler = signals_superhandler; + sigemptyset (&sa.sa_mask); + if (sigaction (SIGWINCH, &sa, NULL) == -1 + || sigaction (SIGINT, &sa, NULL) == -1 + || sigaction (SIGTERM, &sa, NULL) == -1) + exit_fatal ("sigaction: %s", strerror (errno)); +} + +static void +signals_free (void) +{ + soft_assert (signal (SIGWINCH, SIG_DFL) != SIG_ERR); + soft_assert (signal (SIGINT, SIG_DFL) != SIG_ERR); + soft_assert (signal (SIGTERM, SIG_DFL) != SIG_ERR); + soft_assert (signal (SIGPIPE, SIG_DFL) != SIG_ERR); + + poller_fd_reset (&g.signal_event); + if (g_signal_pipe[0] != -1) + { + xclose (g_signal_pipe[0]); + g_signal_pipe[0] = -1; + } + if (g_signal_pipe[1] != -1) + { + xclose (g_signal_pipe[1]); + g_signal_pipe[1] = -1; + } +} + +static void +signals_on_readable (const struct pollfd *fd, void *user_data) +{ + (void) user_data; + char signals[32]; + while (read (fd->fd, signals, sizeof signals) > 0) + ; + + if (g_termination_requested) + app_quit (); + if (g_winch_received) + { + g_winch_received = false; + if (g_xui.ui->winch) + g_xui.ui->winch (); + } +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static void +app_init_context (const char *host, const char *port) +{ + poller_init (&g.poller); + app_editor_init (); + + g.relay.host = xstrdup (host); + g.relay.port = xstrdup (port); + g.relay.reconnect_event = poller_timer_make (&g.poller); + g.relay.reconnect_event.dispatcher = relay_on_reconnect_timer; + + g.relay.socket_fd = -1; + g.relay.read_buffer = str_make (); + g.relay.write_buffer = str_make (); + + g.servers = str_map_make ((str_map_free_fn) server_destroy); +} + +static void +app_free_context (void) +{ + app_reset_relay_state (); + LIST_FOR_EACH (struct buffer_line, iter, g.local_lines) + free (iter); + g.local_lines = g.local_lines_tail = NULL; + + log_view_free (&g.log); + str_map_free (&g.servers); + + relay_disconnect (); + free (g.relay.host); + free (g.relay.port); + free (g.relay.last_error); + str_free (&g.relay.read_buffer); + str_free (&g.relay.write_buffer); + free (g.relay.session_id); + + free (g.keys); + app_editor_free (); + poller_free (&g.poller); +} + +// --- Main -------------------------------------------------------------------- + +static const char * +app_parse_options (int argc, char *argv[], bool *requested_gui) +{ +#ifndef WITH_X11 + (void) requested_gui; +#endif // ! WITH_X11 + static const struct opt opts[] = { +#ifdef WITH_X11 + { 'g', "gui", NULL, 0, "use graphical interface even from a terminal" }, +#endif // WITH_X11 { 'h', "help", NULL, 0, "display this help and exit" }, { 'V', "version", NULL, 0, "output version information and exit" }, { 0, NULL, NULL, 0, NULL } }; struct opt_handler oh = opt_handler_make (argc, argv, opts, - "HOST:PORT", "X11 frontend for xC."); - + "HOST:PORT", "Terminal/X11 frontend for xC."); int c; while ((c = opt_handler_get (&oh)) != -1) switch (c) { +#ifdef WITH_X11 + case 'g': + *requested_gui = true; + break; +#endif // WITH_X11 case 'h': opt_handler_usage (&oh, stdout); exit (EXIT_SUCCESS); @@ -160,13 +3015,46 @@ main (int argc, char *argv[]) exit (EXIT_FAILURE); } opt_handler_free (&oh); + return argv[0]; +} - char *address = xstrdup (argv[0]); +int +main (int argc, char *argv[]) +{ + bool requested_gui = false; + char *address = xstrdup (app_parse_options (argc, argv, &requested_gui)); const char *port = NULL, *host = tokenize_host_port (address, &port); if (!port) exit_fatal ("missing port number/service name"); - // TODO: Actually implement an X11-based user interface. - protocol_test (host, port); + if (!setlocale (LC_CTYPE, "")) + print_warning ("failed to set the locale"); + + app_init_context (host, port); + free (address); + + signals_init (); + g.signal_event = poller_fd_make (&g.poller, g_signal_pipe[0]); + g.signal_event.dispatcher = signals_on_readable; + poller_fd_set (&g.signal_event, POLLIN); + + xui_preinit (); + app_init_bindings (); + g_log_message_real = app_log_handler; + + xui_start (&g.poller, requested_gui, g_attrs, ATTRIBUTE_COUNT); + app_select_backend (); + xui_invalidate (); + + poller_timer_set (&g.relay.reconnect_event, 0); + + g.polling = true; + while (g.polling) + poller_run (&g.poller); + + xui_stop (); + g_log_message_real = log_message_stdio; + signals_free (); + app_free_context (); return 0; }