From fd247d070ac4a5fe5d5e08a5529e907cf15dcdc4 Mon Sep 17 00:00:00 2001
From: Přemysl Eric Janouch <p@janouch.name>
Date: Mon, 12 Jun 2023 21:26:25 +0200
Subject: WIP: xF: show something through actually using X11

WIP:
 - Convert formatter items to an inner representation
 - Basic text layout with basic splitting
 - Conclude the commit as purely read-only but otherwise pretty
---
 xF.c | 1088 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
 1 file changed, 1046 insertions(+), 42 deletions(-)

diff --git a/xF.c b/xF.c
index 054871d..98dfbf8 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 <p@janouch.name>
+ * Copyright (c) 2022 - 2024, Přemysl Eric Janouch <p@janouch.name>
  *
  * Permission to use, copy, modify, and/or distribute this software for any
  * purpose with or without fee is hereby granted.
@@ -22,21 +22,556 @@
 #include "common.c"
 #include "xC-proto.c"
 
+#define LIBERTY_XDG_WANT_X11
+#define LIBERTY_XDG_WANT_ICONS
+#include "liberty/liberty-xdg.c"
+
 #include <X11/Xatom.h>
 #include <X11/Xlib.h>
 #include <X11/keysym.h>
 #include <X11/XKBlib.h>
 #include <X11/Xft/Xft.h>
 
+// --- Frontend ----------------------------------------------------------------
+
+// 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;
+	char text[];
+};
+
+// See: struct relay_event_data_buffer_update
+struct buffer
+{
+	LIST_HEADER (struct buffer)
+
+	char *buffer_name;
+	enum relay_buffer_kind kind;
+	char *server_name;
+	struct buffer_line *lines;
+	struct buffer_line *lines_tail;
+
+	// Stats:
+
+	uint32_t new_messages;
+	uint32_t new_unimportant_messages;
+	bool highlighted;
+};
+
+static void
+buffer_destroy (struct buffer *self)
+{
+	free (self->buffer_name);
+	free (self->server_name);
+	LIST_FOR_EACH (struct buffer_line, iter, self->lines)
+		free (iter);
+	free (self);
+}
+
+// See: struct relay_event_data_server_update
+struct server
+{
+	enum relay_server_state state;
+	char *user;
+	char *user_modes;
+};
+
+static void
+server_destroy (struct server *self)
+{
+	free (self->user);
+	free (self->user_modes);
+	free (self);
+}
+
 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 
+/// Wraps Xft fonts into a linked list with fallbacks.
+struct x11_font_link
+{
+	struct x11_font_link *next;
+	XftFont *font;
+};
+
+enum
+{
+	X11_FONT_BOLD      = 1 << 0,
+	X11_FONT_ITALIC    = 1 << 1,
+	X11_FONT_MONOSPACE = 1 << 2,
+};
+
+struct x11_font
+{
+	struct x11_font *next;              ///< Next in a linked list
+
+	struct x11_font_link *list;         ///< Fonts of varying Unicode coverage
+	unsigned style;                     ///< X11_FONT_* flags
+	FcPattern *pattern;                 ///< Original unsubstituted pattern
+	FcCharSet *unavailable;             ///< Couldn't find a font for these
+};
+
 static struct
 {
-	bool polling;
+	struct poller poller;               ///< Poller
+	bool polling;                       ///< The event loop is running
+
+	// Relay plumbing:
+
 	struct connector connector;
-	int socket;
+	int socket_fd;                      ///< Backend TCP socket
+	struct poller_fd socket_event;      ///< The socket can be read/written to
+	struct str read_buffer;             ///< Unprocessed input
+	struct str write_buffer;            ///< Output yet to be sent out
+
+	uint32_t command_seq;               ///< Outgoing message counter
+
+	// 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
+
+	// User interface:
+
+	int ui_width;                       ///< Window width
+	int ui_height;                      ///< Window height
+	bool ui_focused;                    ///< Whether the window has focus
+
+	XIM x11_im;                         ///< Input method
+	XIC x11_ic;                         ///< Input method context
+	Display *dpy;                       ///< X display handle
+	struct poller_fd x11_event;         ///< X11 events on wire
+	struct poller_idle xpending_event;  ///< X11 events possibly in I/O queues
+	int xkb_base_event_code;            ///< Xkb base event code
+	Window x11_window;                  ///< Application window
+	Pixmap x11_pixmap;                  ///< Off-screen bitmap
+	Region x11_clip;                    ///< Invalidated region
+	Picture x11_pixmap_picture;         ///< XRender wrap for x11_pixmap
+	XftDraw *xft_draw;                  ///< Xft rendering context
+	struct x11_font *xft_fonts;         ///< Font collection
+	char *x11_selection;                ///< CLIPBOARD selection
+
+	const char *x11_fontname;           ///< Fontconfig font name
+	const char *x11_fontname_monospace; ///< Fontconfig monospace font name
+
+	struct poller_idle refresh_event;   ///< Refresh the window's contents
+	struct poller_idle flip_event;      ///< Draw rendered widgets on screen
+}
+g =
+{
+	.x11_fontname = "sans\\-serif-11",
+	.x11_fontname_monospace = "monospace-11",
+};
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static void
+app_init_context (void)
+{
+	poller_init (&g.poller);
+
+	g.socket_fd = -1;
+	g.read_buffer = str_make ();
+	g.write_buffer = str_make ();
+
+	g.servers = str_map_make ((str_map_free_fn) server_destroy);
+
+	// Presumably, although not necessarily; unsure if queryable at all
+	g.ui_focused = true;
+}
+
+static void
+app_quit (void)
+{
+	// So far there's nothing for us to wait on, so let's just stop looping.
+	g.polling = false;
+}
+
+static void
+app_invalidate (void)
+{
+	poller_idle_set (&g.refresh_event);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+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 struct buffer_line *
+buffer_line_new (struct relay_event_data_buffer_line *m)
+{
+	struct str s = str_make ();
+	for (uint32_t i = 0; i < m->items_len; i++)
+		if (m->items[i].kind == RELAY_ITEM_TEXT)
+			str_append_str (&s, &m->items[i].text.text);
+
+	struct buffer_line *self = xcalloc (1, sizeof *self + s.len + 1);
+	memcpy (self->text, s.str, s.len + 1);
+	str_free (&s);
+
+	self->is_unimportant = m->is_unimportant;
+	self->is_highlight   = m->is_highlight;
+	self->rendition      = m->rendition;
+	self->when           = m->when;
+	return self;
+}
+
+static void
+relay_process_buffer_line (struct buffer *buffer,
+	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 (buffer->lines, buffer->lines_tail, line);
+		return;
+	}
+
+	// TODO: Track window on-screen visibility.
+	bool visible = buffer == g.buffer_current || m->leak_to_active;
+
+	// TODO: Port the rest from xP.js.
+	struct buffer_line *line = buffer_line_new (m);
+	LIST_APPEND_WITH_TAIL (buffer->lines, buffer->lines_tail, line);
+	if (!(visible || m->leak_to_active)
+	 || buffer->new_messages || buffer->new_unimportant_messages)
+	{
+		if (line->is_unimportant || m->leak_to_active)
+			buffer->new_unimportant_messages++;
+		else
+			buffer->new_messages++;
+	}
+
+	if (m->leak_to_active)
+	{
+		struct buffer *bc = g.buffer_current;
+		struct buffer_line *line = buffer_line_new (m);
+		line->leaked = true;
+		LIST_APPEND_WITH_TAIL (bc->lines, bc->lines_tail, line);
+		if (!visible || bc->new_messages || bc->new_unimportant_messages)
+		{
+			if (line->is_unimportant)
+				bc->new_unimportant_messages++;
+			else
+				bc->new_messages++;
+		}
+	}
+
+	if (line->is_highlight || (!visible && !line->is_unimportant
+		&& buffer->kind == RELAY_BUFFER_KIND_PRIVATE_MESSAGE))
+	{
+		// TODO: Play a beep sample.
+		if (!visible)
+			buffer->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 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;
+	}
+
+	const char *buffer_name = relay_message_buffer_name (m);
+	struct buffer *buffer = NULL;
+	if (buffer_name && !(buffer = buffer_find (buffer_name)))
+	{
+		// TODO: Maybe handle BUFFER_ACTIVATE the way xP does.
+		if (m->data.event != RELAY_EVENT_BUFFER_UPDATE)
+		{
+			print_warning ("unknown buffer: %s", buffer_name);
+			return true;
+		}
+
+		buffer = xcalloc (1, sizeof *buffer);
+		buffer->buffer_name = xstrdup (buffer_name);
+		LIST_APPEND_WITH_TAIL (g.buffers, g.buffers_tail, buffer);
+	}
+
+	const char *server_name = relay_message_server_name (m);
+	struct server *server = NULL;
+	if (server_name && !(server = str_map_find (&g.servers, server_name)))
+	{
+		if (m->data.event != RELAY_EVENT_SERVER_UPDATE)
+		{
+			print_warning ("unknown server: %s", server_name);
+			return true;
+		}
+
+		server = xcalloc (1, sizeof *server);
+		str_map_set (&g.servers, server_name, server);
+	}
+
+	switch (m->data.event)
+	{
+	case RELAY_EVENT_PING:
+		// TODO: While not important, we should implement it.
+		return true;
+
+	case RELAY_EVENT_BUFFER_LINE:
+		relay_process_buffer_line (buffer, &m->data.buffer_line);
+		break;
+	case RELAY_EVENT_BUFFER_UPDATE:
+		buffer->kind = m->data.buffer_update.context.kind;
+
+		server_name = relay_buffer_update_server_name (&m->data.buffer_update);
+		cstr_set (&buffer->server_name, NULL);
+		if (server_name)
+			buffer->server_name = xstrdup (server_name);
+
+		// TODO: More kind-dependent state.
+		break;
+	case RELAY_EVENT_BUFFER_STATS:
+		buffer->new_messages
+			= m->data.buffer_stats.new_messages;
+		buffer->new_unimportant_messages
+			= m->data.buffer_stats.new_unimportant_messages;
+		buffer->highlighted
+			= m->data.buffer_stats.highlighted;
+		break;
+	case RELAY_EVENT_BUFFER_RENAME:
+		free (buffer->buffer_name);
+		buffer->buffer_name = xstrdup (m->data.buffer_rename.new.str);
+		break;
+	case RELAY_EVENT_BUFFER_REMOVE:
+		LIST_UNLINK_WITH_TAIL (g.buffers, g.buffers_tail, buffer);
+		if (g.buffer_current == buffer)
+			g.buffer_current = NULL;
+		if (g.buffer_last == buffer)
+			g.buffer_last = NULL;
+		buffer_destroy (buffer);
+		break;
+	case RELAY_EVENT_BUFFER_ACTIVATE:
+		if (g.buffer_current)
+		{
+			g.buffer_current->new_messages = 0;
+			g.buffer_current->new_unimportant_messages = 0;
+			g.buffer_current->highlighted = false;
+		}
+
+		g.buffer_last = g.buffer_current;
+		g.buffer_current = buffer;
+		// TODO: Switch the input line.
+		break;
+	case RELAY_EVENT_BUFFER_CLEAR:
+		LIST_FOR_EACH (struct buffer_line, iter, buffer->lines)
+			free (iter);
+		buffer->lines = NULL;
+		break;
+
+	case RELAY_EVENT_SERVER_UPDATE:
+		server->state = m->data.server_update.data.state;
+
+		cstr_set (&server->user, NULL);
+		cstr_set (&server->user_modes, NULL);
+		if (server->state == RELAY_SERVER_STATE_REGISTERED)
+		{
+			server->user =
+				xstrdup (m->data.server_update.data.registered.user.str);
+			server->user_modes =
+				xstrdup (m->data.server_update.data.registered.user_modes.str);
+		}
+		break;
+	case RELAY_EVENT_SERVER_RENAME:
+		str_map_set (&g.servers, m->data.server_rename.new.str,
+			str_map_steal (&g.servers, server_name));
+		break;
+	case RELAY_EVENT_SERVER_REMOVE:
+		str_map_set (&g.servers, server_name, NULL);
+		break;
+
+	default:
+		return true;
+	}
+	app_invalidate ();
+	return true;
+}
+
+static bool
+relay_process_buffer (void)
+{
+	struct str *buf = &g.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.read_buffer;
+	ssize_t n_read;
+
+	while ((n_read = read (g.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;
 }
-g;
+
+static bool
+relay_try_write (void)
+{
+	struct str *buf = &g.write_buffer;
+	ssize_t n_written;
+
+	while (buf->len)
+	{
+		n_written = write (g.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_update_poller (const struct pollfd *pfd)
+{
+	int new_events = POLLIN;
+	if (g.write_buffer.len)
+		new_events |= POLLOUT;
+
+	hard_assert (new_events != 0);
+	if (!pfd || pfd->events != new_events)
+		poller_fd_set (&g.socket_event, new_events);
+}
+
+static void
+on_relay_ready (const struct pollfd *pfd, void *user_data)
+{
+	if (relay_try_read () && relay_try_write ())
+		relay_update_poller (pfd);
+	else
+	{
+		// TODO: Probably autoreconnect.
+		exit_fatal ("disconnected");
+	}
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 
 static void
 on_connector_connecting (void *user_data, const char *address)
@@ -64,65 +599,526 @@ on_connector_connected (void *user_data, int socket, const char *hostname)
 {
 	(void) user_data;
 	(void) hostname;
-	g.polling = false;
-	g.socket = socket;
+	connector_free (&g.connector);
+
+	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.socket_fd = socket;
+	g.socket_event = poller_fd_make (&g.poller, g.socket_fd);
+	g.socket_event.dispatcher = (poller_fd_fn) on_relay_ready;
+
+	str_reset (&g.read_buffer);
+	str_reset (&g.write_buffer);
+
+	struct str *s = &g.write_buffer;
+	str_pack_u32 (s, 0);
+	struct relay_command_message m = {};
+	m.command_seq = ++g.command_seq;
+	m.data.hello.command = RELAY_COMMAND_HELLO;
+	m.data.hello.version = RELAY_VERSION;
+	if (!relay_command_message_serialize (&m, s))
+		exit_fatal ("serialization failed");
+
+	uint32_t len = htonl (s->len - sizeof len);
+	memcpy (s->str, &len, sizeof len);
+
+	relay_update_poller (NULL);
 }
 
 static void
-protocol_test (const char *host, const char *port)
+relay_connect (const char *host, const char *port)
 {
-	struct poller poller = {};
-	poller_init (&poller);
-
-	connector_init (&g.connector, &poller);
+	connector_init (&g.connector, &g.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;
 
 	connector_add_target (&g.connector, host, port);
+}
 
-	g.polling = true;
-	while (g.polling)
-		poller_run (&poller);
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 
-	connector_free (&g.connector);
+static XRenderColor x11_default_fg = { .alpha = 0xffff };
+static XRenderColor x11_default_bg = { 0xffff, 0xffff, 0xffff, 0xffff };
+static XErrorHandler x11_default_error_handler;
 
-	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))
-		exit_fatal ("serialization failed");
+static struct x11_font_link *
+x11_font_link_new (XftFont *font)
+{
+	struct x11_font_link *self = xcalloc (1, sizeof *self);
+	self->font = font;
+	return self;
+}
 
-	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));
+static void
+x11_font_link_destroy (struct x11_font_link *self)
+{
+	XftFontClose (g.dpy, self->font);
+	free (self);
+}
 
-	char buf[1 << 20] = "";
-	while (errno = 0, read (g.socket, &len, sizeof len) == sizeof len)
+static struct x11_font_link *
+x11_font_link_open (FcPattern *pattern)
+{
+	XftFont *font = XftFontOpenPattern (g.dpy, pattern);
+	if (!font)
 	{
-		len = ntohl (len);
-		if (errno = 0, read (g.socket, buf, MIN (len, sizeof buf)) != len)
-			exit_fatal ("short read or error: %s", strerror (errno));
+		FcPatternDestroy (pattern);
+		return NULL;
+	}
+	return x11_font_link_new (font);
+}
 
-		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");
+static struct x11_font *
+x11_font_open (unsigned style)
+{
+	FcPattern *pattern = (style & X11_FONT_MONOSPACE)
+		? FcNameParse ((const FcChar8 *) g.x11_fontname_monospace)
+		: FcNameParse ((const FcChar8 *) g.x11_fontname);
+	if (style & X11_FONT_BOLD)
+		FcPatternAdd (pattern, FC_STYLE, (FcValue) {
+			.type = FcTypeString, .u.s = (FcChar8 *) "Bold" }, FcFalse);
+	if (style & X11_FONT_ITALIC)
+		FcPatternAdd (pattern, FC_STYLE, (FcValue) {
+			.type = FcTypeString, .u.s = (FcChar8 *) "Italic" }, FcFalse);
 
-		printf ("event: %d\n", m.data.event);
-		relay_event_message_free (&m);
+	FcPattern *substituted = FcPatternDuplicate (pattern);
+	FcConfigSubstitute (NULL, substituted, FcMatchPattern);
+
+	FcResult result = 0;
+	FcPattern *match = XftFontMatch (g.dpy,
+		DefaultScreen (g.dpy), substituted, &result);
+	FcPatternDestroy (substituted);
+	struct x11_font_link *link = NULL;
+	if (!match || !(link = x11_font_link_open (match)))
+	{
+		FcPatternDestroy (pattern);
+		return NULL;
 	}
-	exit_fatal ("short read or error: %s", strerror (errno));
+
+	struct x11_font *self = xcalloc (1, sizeof *self);
+	self->list = link;
+	self->style = style;
+	self->pattern = pattern;
+	self->unavailable = FcCharSetCreate ();
+	return self;
+}
+
+static void
+x11_font_destroy (struct x11_font *self)
+{
+	FcPatternDestroy (self->pattern);
+	FcCharSetDestroy (self->unavailable);
+	LIST_FOR_EACH (struct x11_font_link, iter, self->list)
+		x11_font_link_destroy (iter);
+	free (self);
 }
 
 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 
+static void
+x11_init_pixmap (void)
+{
+	int screen = DefaultScreen (g.dpy);
+	g.x11_pixmap = XCreatePixmap (g.dpy, g.x11_window,
+		MAX (1, g.ui_width), MAX (1, g.ui_height),
+		DefaultDepth (g.dpy, screen));
+
+	Visual *visual = DefaultVisual (g.dpy, screen);
+	XRenderPictFormat *format = XRenderFindVisualFormat (g.dpy, visual);
+	g.x11_pixmap_picture
+		= XRenderCreatePicture (g.dpy, g.x11_pixmap, format, 0, NULL);
+}
+
+static void
+on_x11_selection_request (XSelectionRequestEvent *ev)
+{
+	Atom xa_targets = XInternAtom (g.dpy, "TARGETS", False);
+	Atom xa_compound_text = XInternAtom (g.dpy, "COMPOUND_TEXT", False);
+	Atom xa_utf8 = XInternAtom (g.dpy, "UTF8_STRING", False);
+	Atom targets[] = { xa_targets, XA_STRING, xa_compound_text, xa_utf8 };
+
+	XEvent response = {};
+	bool ok = false;
+	Atom property = ev->property ? ev->property : ev->target;
+	if (!g.x11_selection)
+		goto out;
+
+	XICCEncodingStyle style = 0;
+	if ((ok = ev->target == xa_targets))
+	{
+		XChangeProperty (g.dpy, ev->requestor, property,
+			XA_ATOM, 32, PropModeReplace,
+			(const unsigned char *) targets, N_ELEMENTS (targets));
+		goto out;
+	}
+	else if (ev->target == XA_STRING)
+		style = XStringStyle;
+	else if (ev->target == xa_compound_text)
+		style = XCompoundTextStyle;
+	else if (ev->target == xa_utf8)
+		style = XUTF8StringStyle;
+	else
+		goto out;
+
+	// XXX: We let it crash us with BadLength, but we may, e.g., use INCR.
+	XTextProperty text = {};
+	if ((ok = !Xutf8TextListToTextProperty
+		 (g.dpy, &g.x11_selection, 1, style, &text)))
+	{
+		XChangeProperty (g.dpy, ev->requestor, property,
+			text.encoding, text.format, PropModeReplace,
+			text.value, text.nitems);
+	}
+	XFree (text.value);
+
+out:
+	response.xselection.type = SelectionNotify;
+	// XXX: We should check it against the event causing XSetSelectionOwner().
+	response.xselection.time = ev->time;
+	response.xselection.requestor = ev->requestor;
+	response.xselection.selection = ev->selection;
+	response.xselection.target = ev->target;
+	response.xselection.property = ok ? property : None;
+	XSendEvent (g.dpy, ev->requestor, False, 0, &response);
+}
+
+static bool
+on_x11_input_event (XEvent *e)
+{
+	if (e->type != KeyPress)
+		return false;
+
+	// A kibibyte long buffer will have to suffice for anyone.
+	XKeyEvent *ev = &e->xkey;
+	char buf[1 << 10] = {}, *p = buf;
+	KeySym keysym = None;
+	Status status = 0;
+	int len = Xutf8LookupString
+		(g.x11_ic, ev, buf, sizeof buf, &keysym, &status);
+	if (status == XBufferOverflow)
+		print_warning ("input method overflow");
+
+	// TODO: Implement an input line.
+	switch (keysym)
+	{
+	case XK_q:
+		app_quit ();
+		return true;
+	}
+	return false;
+}
+
+static void
+on_x11_event (XEvent *ev)
+{
+	switch (ev->type)
+	{
+	case Expose:
+	{
+		XRectangle r = { ev->xexpose.x, ev->xexpose.y,
+			ev->xexpose.width, ev->xexpose.height };
+		XUnionRectWithRegion (&r, g.x11_clip, g.x11_clip);
+		poller_idle_set (&g.flip_event);
+		break;
+	}
+	case ConfigureNotify:
+		if (g.ui_width == ev->xconfigure.width
+		 && g.ui_height == ev->xconfigure.height)
+			break;
+
+		g.ui_width = ev->xconfigure.width;
+		g.ui_height = ev->xconfigure.height;
+
+		XRenderFreePicture (g.dpy, g.x11_pixmap_picture);
+		XFreePixmap (g.dpy, g.x11_pixmap);
+		x11_init_pixmap ();
+		XftDrawChange (g.xft_draw, g.x11_pixmap);
+		app_invalidate ();
+		break;
+	case SelectionRequest:
+		on_x11_selection_request (&ev->xselectionrequest);
+		break;
+	case SelectionClear:
+		cstr_set (&g.x11_selection, NULL);
+		break;
+	// UnmapNotify can be received when restarting the window manager.
+	// Should this turn out to be unreliable (window not destroyed by WM
+	// upon closing), opt for the WM_DELETE_WINDOW protocol as well.
+	case DestroyNotify:
+		app_quit ();
+		break;
+	case FocusIn:
+		g.ui_focused = true;
+		app_invalidate ();
+		break;
+	case FocusOut:
+		g.ui_focused = false;
+		app_invalidate ();
+		break;
+	case KeyPress:
+	case ButtonPress:
+	case ButtonRelease:
+	case MotionNotify:
+		if (!on_x11_input_event (ev))
+			XkbBell (g.dpy, ev->xany.window, 0, None);
+	}
+}
+
+static void
+on_x11_pending (void *user_data)
+{
+	(void) user_data;
+
+	XkbEvent ev;
+	while (XPending (g.dpy))
+	{
+		if (XNextEvent (g.dpy, &ev.core))
+			exit_fatal ("XNextEvent returned non-zero");
+		if (XFilterEvent (&ev.core, None))
+			continue;
+
+		on_x11_event (&ev.core);
+	}
+
+	poller_idle_reset (&g.xpending_event);
+}
+
+static void
+on_x11_ready (const struct pollfd *pfd, void *user_data)
+{
+	(void) pfd;
+	on_x11_pending (user_data);
+}
+
+static int
+on_x11_error (Display *dpy, XErrorEvent *event)
+{
+	// Without opting for WM_DELETE_WINDOW, this window can become destroyed
+	// and hence invalid at any time.  We don't use the Window much,
+	// so we should be fine ignoring these errors.
+	if ((event->error_code == BadWindow && event->resourceid == g.x11_window)
+	 || (event->error_code == BadDrawable && event->resourceid == g.x11_window))
+		return app_quit (), 0;
+
+	// XXX: The simplest possible way of discarding selection management errors.
+	//   XCB would be a small win here, but it is a curse at the same time.
+	if (event->error_code == BadWindow && event->resourceid != g.x11_window)
+		return 0;
+
+	return x11_default_error_handler (dpy, event);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static void
+x11_init (void)
+{
+	// https://tedyin.com/posts/a-brief-intro-to-linux-input-method-framework/
+	if (!XSupportsLocale ())
+		print_warning ("locale not supported by Xlib");
+	XSetLocaleModifiers ("");
+
+	if (!(g.dpy = XkbOpenDisplay
+		(NULL, &g.xkb_base_event_code, NULL, NULL, NULL, NULL)))
+		exit_fatal ("cannot open display");
+	if (!XftDefaultHasRender (g.dpy))
+		exit_fatal ("XRender is not supported");
+	if (!(g.x11_im = XOpenIM (g.dpy, NULL, NULL, NULL)))
+		exit_fatal ("failed to open an input method");
+
+	x11_default_error_handler = XSetErrorHandler (on_x11_error);
+
+	set_cloexec (ConnectionNumber (g.dpy));
+	g.x11_event = poller_fd_make (&g.poller, ConnectionNumber (g.dpy));
+	g.x11_event.dispatcher = on_x11_ready;
+	poller_fd_set (&g.x11_event, POLLIN);
+
+	// Whenever something causes Xlib to read its socket, it can make
+	// the I/O event above fail to trigger for whatever might have ended up
+	// in its queue.  So always use this instead of XSync:
+	g.xpending_event = poller_idle_make (&g.poller);
+	g.xpending_event.dispatcher = on_x11_pending;
+	poller_idle_set (&g.xpending_event);
+
+	struct xdg_xsettings settings = xdg_xsettings_make ();
+	xdg_xsettings_update (&settings, g.dpy);
+
+	if (!FcInit ())
+		print_warning ("Fontconfig initialization failed");
+	if (!(g.xft_fonts = x11_font_open (0)))
+		exit_fatal ("cannot open a font");
+
+	int screen = DefaultScreen (g.dpy);
+	Colormap cmap = DefaultColormap (g.dpy, screen);
+	XColor default_bg =
+	{
+		.red    = x11_default_bg.red,
+		.green  = x11_default_bg.green,
+		.blue   = x11_default_bg.blue,
+	};
+	if (!XAllocColor (g.dpy, cmap, &default_bg))
+		exit_fatal ("X11 setup failed");
+
+	XSetWindowAttributes attrs =
+	{
+		.event_mask = StructureNotifyMask | ExposureMask | FocusChangeMask
+			| KeyPressMask | ButtonPressMask | ButtonReleaseMask
+			| Button1MotionMask,
+		.bit_gravity = NorthWestGravity,
+		.background_pixel = default_bg.pixel,
+	};
+
+	// Base the window's size on the regular font size.
+	// Roughly trying to match the 80x24 default dimensions of terminals.
+	g.ui_height = 24 * g.xft_fonts->list->font->height;
+	g.ui_width = g.ui_height * 4 / 3;
+
+	long im_event_mask = 0;
+	if (!XGetIMValues (g.x11_im, XNFilterEvents, &im_event_mask, NULL))
+		attrs.event_mask |= im_event_mask;
+
+	Visual *visual = DefaultVisual (g.dpy, screen);
+	g.x11_window = XCreateWindow (g.dpy, RootWindow (g.dpy, screen), 100, 100,
+		g.ui_width, g.ui_height, 0, CopyFromParent, InputOutput, visual,
+		CWEventMask | CWBackPixel | CWBitGravity, &attrs);
+	g.x11_clip = XCreateRegion ();
+
+	XTextProperty prop = {};
+	char *name = PROGRAM_NAME;
+	if (!Xutf8TextListToTextProperty (g.dpy, &name, 1, XUTF8StringStyle, &prop))
+		XSetWMName (g.dpy, g.x11_window, &prop);
+	XFree (prop.value);
+
+	// This is a rather GNOME-centric mechanism, but it's better than nothing.
+	const char *icon_theme_name = NULL;
+	const struct xdg_xsettings_setting *setting =
+		str_map_find (&settings.settings, "Net/IconThemeName");
+	if (setting != NULL && setting->type == XDG_XSETTINGS_STRING)
+		icon_theme_name = setting->string.str;
+	icon_theme_set_window_icon (g.dpy, g.x11_window, icon_theme_name, name);
+	xdg_xsettings_free (&settings);
+
+	// TODO: It is possible to do, e.g., on-the-spot.
+	XIMStyle im_style = XIMPreeditNothing | XIMStatusNothing;
+	XIMStyles *im_styles = NULL;
+	bool im_style_found = false;
+	if (!XGetIMValues (g.x11_im, XNQueryInputStyle, &im_styles, NULL)
+	 && im_styles)
+	{
+		for (unsigned i = 0; i < im_styles->count_styles; i++)
+			im_style_found |= im_styles->supported_styles[i] == im_style;
+		XFree (im_styles);
+	}
+	if (!im_style_found)
+		print_warning ("failed to find the desired input method style");
+	if (!(g.x11_ic = XCreateIC (g.x11_im,
+			XNInputStyle, im_style,
+			XNClientWindow, g.x11_window,
+			NULL)))
+		exit_fatal ("failed to open an input context");
+
+	XSetICFocus (g.x11_ic);
+
+	x11_init_pixmap ();
+	g.xft_draw = XftDrawCreate (g.dpy, g.x11_pixmap, visual, cmap);
+
+	XMapWindow (g.dpy, g.x11_window);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static bool
+render_line (int *bottom, const char *text)
+{
+	XftFont *font = g.xft_fonts->list->font;
+	XftColor color = { .color = x11_default_fg };
+	XftDrawStringUtf8 (g.xft_draw, &color, font, 0, *bottom - font->descent,
+		(const FcChar8 *) text, strlen (text));
+	return (*bottom -= font->height) > 0;
+}
+
+static void
+x11_render (void)
+{
+	XRenderFillRectangle (g.dpy, PictOpSrc, g.x11_pixmap_picture,
+		&x11_default_bg, 0, 0, g.ui_width, g.ui_height);
+
+	int bottom = g.ui_height;
+	render_line (&bottom, "xF");
+	if (!g.buffer_current)
+		render_line (&bottom, "-");
+	else
+	{
+		struct buffer *buffer = g.buffer_current;
+		render_line (&bottom, buffer->buffer_name);
+		struct buffer_line *line = buffer->lines_tail;
+		for (; line; line = line->prev)
+			if (!render_line (&bottom, line->text))
+				break;
+	}
+
+	XRectangle r = { 0, 0, g.ui_width, g.ui_height };
+	XUnionRectWithRegion (&r, g.x11_clip, g.x11_clip);
+	poller_idle_set (&g.xpending_event);
+}
+
+static void
+x11_flip (void)
+{
+	// This exercise in futility doesn't seem to affect CPU usage much.
+	XRectangle r = {};
+	XClipBox (g.x11_clip, &r);
+	XCopyArea (g.dpy, g.x11_pixmap, g.x11_window,
+		DefaultGC (g.dpy, DefaultScreen (g.dpy)),
+		r.x, r.y, r.width, r.height, r.x, r.y);
+
+	XSubtractRegion (g.x11_clip, g.x11_clip, g.x11_clip);
+	poller_idle_set (&g.xpending_event);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static void
+app_on_flip (void *user_data)
+{
+	(void) user_data;
+	poller_idle_reset (&g.flip_event);
+
+	// Waste of time, and may cause X11 to render uninitialised pixmaps.
+	if (g.polling && !g.refresh_event.active)
+		x11_flip ();
+}
+
+static void
+app_on_refresh (void *user_data)
+{
+	(void) user_data;
+	poller_idle_reset (&g.refresh_event);
+
+	x11_render ();
+	poller_idle_set (&g.flip_event);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static void
+app_init_poller_events (void)
+{
+	g.refresh_event = poller_idle_make (&g.poller);
+	g.refresh_event.dispatcher = app_on_refresh;
+
+	g.flip_event = poller_idle_make (&g.poller);
+	g.flip_event.dispatcher = app_on_flip;
+}
+
 int
 main (int argc, char *argv[])
 {
@@ -166,7 +1162,15 @@ main (int argc, char *argv[])
 	if (!port)
 		exit_fatal ("missing port number/service name");
 
-	// TODO: Actually implement an X11-based user interface.
-	protocol_test (host, port);
+	app_init_context ();
+	app_init_poller_events ();
+	x11_init ();
+
+	relay_connect (host, port);
+	free (address);
+
+	g.polling = true;
+	while (g.polling)
+		poller_run (&g.poller);
 	return 0;
 }
-- 
cgit v1.2.3-70-g09d2