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; }