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