From 19a0a468a92214708f75a6fbd1f0b2f4657a62bf Mon Sep 17 00:00:00 2001
From: Přemysl Eric Janouch 
Date: Mon, 19 Jun 2023 02:04:44 +0200
Subject: Bump liberty, move the UI to liberty-xui.c
Also bump termo.
This deduplicates code between nncmpp and hex,
and adds support for bold + italic attributes under X11.
There is still a lot of space for prettification.
Unfortunately, most of our specialized widgets are quite entangled.
---
 liberty       |    2 +-
 line-editor.c |  327 ----------
 nncmpp.c      | 1883 +++++++++------------------------------------------------
 termo         |    2 +-
 4 files changed, 281 insertions(+), 1933 deletions(-)
 delete mode 100644 line-editor.c
diff --git a/liberty b/liberty
index 0e86ffe..d01a1ff 160000
--- a/liberty
+++ b/liberty
@@ -1 +1 @@
-Subproject commit 0e86ffe7c30a4d52eea35856b792567ca1040f56
+Subproject commit d01a1ff0348174f91bb2d3ba53145cc2c9f50a7f
diff --git a/line-editor.c b/line-editor.c
deleted file mode 100644
index 8e42b5a..0000000
--- a/line-editor.c
+++ /dev/null
@@ -1,327 +0,0 @@
-/*
- * line-editor.c: a line editor component for the TUI part of liberty
- *
- * Copyright (c) 2017 - 2022, Přemysl Eric Janouch 
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
- * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
- * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
- * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- *
- */
-
-// This is here just for IDE code model reasons
-#ifndef HAVE_LIBERTY
-#include "liberty/liberty.c"
-#include "liberty/liberty-tui.c"
-#endif
-
-static void
-row_buffer_append_c (struct row_buffer *self, ucs4_t c, chtype attrs)
-{
-	struct row_char current = { .attrs = attrs, .c = c };
-	struct row_char invalid = { .attrs = attrs, .c = '?', .width = 1 };
-
-	current.width = uc_width (current.c, locale_charset ());
-	if (current.width < 0 || !app_is_character_in_locale (current.c))
-		current = invalid;
-
-	ARRAY_RESERVE (self->chars, 1);
-	self->chars[self->chars_len++] = current;
-	self->total_width += current.width;
-}
-
-// --- Line editor -------------------------------------------------------------
-
-enum line_editor_action
-{
-	LINE_EDITOR_B_CHAR,                 ///< Go back a character
-	LINE_EDITOR_F_CHAR,                 ///< Go forward a character
-	LINE_EDITOR_B_WORD,                 ///< Go back a word
-	LINE_EDITOR_F_WORD,                 ///< Go forward a word
-	LINE_EDITOR_HOME,                   ///< Go to start of line
-	LINE_EDITOR_END,                    ///< Go to end of line
-
-	LINE_EDITOR_UPCASE_WORD,            ///< Convert word to uppercase
-	LINE_EDITOR_DOWNCASE_WORD,          ///< Convert word to lowercase
-	LINE_EDITOR_CAPITALIZE_WORD,        ///< Capitalize word
-
-	LINE_EDITOR_B_DELETE,               ///< Delete last character
-	LINE_EDITOR_F_DELETE,               ///< Delete next character
-	LINE_EDITOR_B_KILL_WORD,            ///< Delete last word
-	LINE_EDITOR_B_KILL_LINE,            ///< Delete everything up to BOL
-	LINE_EDITOR_F_KILL_LINE,            ///< Delete everything up to EOL
-};
-
-struct line_editor
-{
-	int point;                          ///< Caret index into line data
-	ucs4_t *line;                       ///< Line data, 0-terminated
-	int *w;                             ///< Codepoint widths, 0-terminated
-	size_t len;                         ///< Editor length
-	size_t alloc;                       ///< Editor allocated
-	char prompt;                        ///< Prompt character
-
-	void (*on_changed) (void);          ///< Callback on text change
-	void (*on_end) (bool);              ///< Callback on abort
-};
-
-static void
-line_editor_free (struct line_editor *self)
-{
-	free (self->line);
-	free (self->w);
-}
-
-/// Notify whomever invoked the editor that it's been either confirmed or
-/// cancelled and clean up editor state
-static void
-line_editor_abort (struct line_editor *self, bool status)
-{
-	self->on_end (status);
-	self->on_changed = NULL;
-
-	free (self->line);
-	self->line = NULL;
-	free (self->w);
-	self->w = NULL;
-	self->alloc = 0;
-	self->len = 0;
-	self->point = 0;
-	self->prompt = 0;
-}
-
-/// Start the line editor; remember to fill in "change" and "end" callbacks
-static void
-line_editor_start (struct line_editor *self, char prompt)
-{
-	self->alloc = 16;
-	self->line = xcalloc (sizeof *self->line, self->alloc);
-	self->w = xcalloc (sizeof *self->w, self->alloc);
-	self->len = 0;
-	self->point = 0;
-	self->prompt = prompt;
-}
-
-static void
-line_editor_changed (struct line_editor *self)
-{
-	self->line[self->len] = 0;
-	self->w[self->len] = 0;
-
-	if (self->on_changed)
-		self->on_changed ();
-}
-
-static void
-line_editor_move (struct line_editor *self, int to, int from, int len)
-{
-	memmove (self->line + to, self->line + from,
-		sizeof *self->line * len);
-	memmove (self->w + to, self->w + from,
-		sizeof *self->w * len);
-}
-
-static void
-line_editor_insert (struct line_editor *self, ucs4_t codepoint)
-{
-	while (self->alloc - self->len < 2 /* inserted + sentinel */)
-	{
-		self->alloc <<= 1;
-		self->line = xreallocarray
-			(self->line, sizeof *self->line, self->alloc);
-		self->w = xreallocarray
-			(self->w, sizeof *self->w, self->alloc);
-	}
-
-	line_editor_move (self, self->point + 1, self->point,
-		self->len - self->point);
-	self->line[self->point] = codepoint;
-	self->w[self->point] = app_is_character_in_locale (codepoint)
-		? uc_width (codepoint, locale_charset ())
-		: 1 /* the replacement question mark */;
-
-	self->point++;
-	self->len++;
-	line_editor_changed (self);
-}
-
-static bool
-line_editor_action (struct line_editor *self, enum line_editor_action action)
-{
-	switch (action)
-	{
-	default:
-		return soft_assert (!"unknown line editor action");
-
-	case LINE_EDITOR_B_CHAR:
-		if (self->point < 1)
-			return false;
-		do self->point--;
-		while (self->point > 0
-			&& !self->w[self->point]);
-		return true;
-	case LINE_EDITOR_F_CHAR:
-		if (self->point + 1 > (int) self->len)
-			return false;
-		do self->point++;
-		while (self->point < (int) self->len
-			&& !self->w[self->point]);
-		return true;
-	case LINE_EDITOR_B_WORD:
-	{
-		if (self->point < 1)
-			return false;
-		int i = self->point;
-		while (i && self->line[--i] == ' ');
-		while (i-- && self->line[i] != ' ');
-		self->point = ++i;
-		return true;
-	}
-	case LINE_EDITOR_F_WORD:
-	{
-		if (self->point + 1 > (int) self->len)
-			return false;
-		int i = self->point;
-		while (i < (int) self->len && self->line[i] == ' ') i++;
-		while (i < (int) self->len && self->line[i] != ' ') i++;
-		self->point = i;
-		return true;
-	}
-	case LINE_EDITOR_HOME:
-		self->point = 0;
-		return true;
-	case LINE_EDITOR_END:
-		self->point = self->len;
-		return true;
-
-	case LINE_EDITOR_UPCASE_WORD:
-	{
-		int i = self->point;
-		for (; i < (int) self->len && self->line[i] == ' '; i++);
-		for (; i < (int) self->len && self->line[i] != ' '; i++)
-			self->line[i] = uc_toupper (self->line[i]);
-		self->point = i;
-		line_editor_changed (self);
-		return true;
-	}
-	case LINE_EDITOR_DOWNCASE_WORD:
-	{
-		int i = self->point;
-		for (; i < (int) self->len && self->line[i] == ' '; i++);
-		for (; i < (int) self->len && self->line[i] != ' '; i++)
-			self->line[i] = uc_tolower (self->line[i]);
-		self->point = i;
-		line_editor_changed (self);
-		return true;
-	}
-	case LINE_EDITOR_CAPITALIZE_WORD:
-	{
-		int i = self->point;
-		ucs4_t (*converter) (ucs4_t) = uc_totitle;
-		for (; i < (int) self->len && self->line[i] == ' '; i++);
-		for (; i < (int) self->len && self->line[i] != ' '; i++)
-		{
-			self->line[i] = converter (self->line[i]);
-			converter = uc_tolower;
-		}
-		self->point = i;
-		line_editor_changed (self);
-		return true;
-	}
-
-	case LINE_EDITOR_B_DELETE:
-	{
-		if (self->point < 1)
-			return false;
-		int len = 1;
-		while (self->point - len > 0
-			&& !self->w[self->point - len])
-			len++;
-		line_editor_move (self, self->point - len, self->point,
-			self->len - self->point);
-		self->len -= len;
-		self->point -= len;
-		line_editor_changed (self);
-		return true;
-	}
-	case LINE_EDITOR_F_DELETE:
-	{
-		if (self->point + 1 > (int) self->len)
-			return false;
-		int len = 1;
-		while (self->point + len < (int) self->len
-			&& !self->w[self->point + len])
-			len++;
-		self->len -= len;
-		line_editor_move (self, self->point, self->point + len,
-			self->len - self->point);
-		line_editor_changed (self);
-		return true;
-	}
-	case LINE_EDITOR_B_KILL_WORD:
-	{
-		if (self->point < 1)
-			return false;
-
-		int i = self->point;
-		while (i && self->line[--i] == ' ');
-		while (i-- && self->line[i] != ' ');
-		i++;
-
-		line_editor_move (self, i, self->point, (self->len - self->point));
-		self->len -= self->point - i;
-		self->point = i;
-		line_editor_changed (self);
-		return true;
-	}
-	case LINE_EDITOR_B_KILL_LINE:
-		self->len -= self->point;
-		line_editor_move (self, 0, self->point, self->len);
-		self->point = 0;
-		line_editor_changed (self);
-		return true;
-	case LINE_EDITOR_F_KILL_LINE:
-		self->len = self->point;
-		line_editor_changed (self);
-		return true;
-	}
-}
-
-static int
-line_editor_write (const struct line_editor *self, struct row_buffer *row,
-	int width, chtype attrs)
-{
-	if (self->prompt)
-	{
-		hard_assert (self->prompt < 127);
-		row_buffer_append_c (row, self->prompt, attrs);
-		width--;
-	}
-
-	int following = 0;
-	for (size_t i = self->point; i < self->len; i++)
-		following += self->w[i];
-
-	int preceding = 0;
-	size_t start = self->point;
-	while (start && preceding < width / 2)
-		preceding += self->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 && width - preceding - following > 2 /* widest char */)
-		preceding += self->w[--start];
-
-	// XXX: we should also show < > indicators for overflow but it'd probably
-	//   considerably complicate this algorithm
-	for (; start < self->len; start++)
-		row_buffer_append_c (row, self->line[start], attrs);
-	return !!self->prompt + preceding;
-}
diff --git a/nncmpp.c b/nncmpp.c
index 3b45f06..cd8b1a3 100644
--- a/nncmpp.c
+++ b/nncmpp.c
@@ -70,23 +70,15 @@ enum
 #define LIBERTY_WANT_PROTO_HTTP
 #define LIBERTY_WANT_PROTO_MPD
 #include "liberty/liberty.c"
-#include "liberty/liberty-tui.c"
 
-#define HAVE_LIBERTY
-#include "line-editor.c"
+#ifdef WITH_X11
+#define LIBERTY_XUI_WANT_X11
+#endif // WITH_X11
+#include "liberty/liberty-xui.c"
 
 #include 
 #include 
 #include 
-#include 
-#include 
-
-// ncurses is notoriously retarded for input handling, we need something
-// different if only to receive mouse events reliably.
-//
-// 2021 update: ncurses is mostly reliable now, though rxvt-unicode only
-// supports the 1006 mode that ncurses also supports mode starting with 9.25.
-#include "termo.h"
 
 // We need cURL to extract links from Internet stream playlists.  It'd be way
 // too much code to do this all by ourselves, and there's nothing better around.
@@ -108,29 +100,12 @@ enum
 #include 
 #endif  // WITH_PULSE
 
-// Elementary port of the TUI to X11.
-#ifdef WITH_X11
-#include 
-#include 
-#include 
-#include 
-#include 
-#endif  // WITH_X11
-
 #define APP_TITLE  PROGRAM_NAME         ///< Left top corner
 
 #include "nncmpp-actions.h"
 
 // --- Utilities ---------------------------------------------------------------
 
-static int64_t
-clock_msec (clockid_t clock)
-{
-	struct timespec tp;
-	hard_assert (clock_gettime (clock, &tp) != -1);
-	return (int64_t) tp.tv_sec * 1000 + (int64_t) tp.tv_nsec / 1000000;
-}
-
 static void
 shell_quote (const char *str, struct str *output)
 {
@@ -1177,39 +1152,13 @@ enum
 	WIDGET_LIST, WIDGET_SCROLLBAR, WIDGET_MESSAGE,
 };
 
-struct widget;
-
-/// Draw a widget on the window
-typedef void (*widget_render_fn) (struct widget *self);
-
-/// A minimal abstraction appropriate for both TUI and GUI widgets.
-/// Units for the widget's region are frontend-specific.
-/// Having this as a linked list simplifies layouting and memory management.
-struct widget
-{
-	LIST_HEADER (struct widget)
-
-	int x;                              ///< X coordinate
-	int y;                              ///< Y coordinate
-	int width;                          ///< Width, initialized by UI methods
-	int height;                         ///< Height, initialized by UI methods
-
-	widget_render_fn on_render;         ///< Render callback
-	struct widget *children;            ///< Child widgets of containers
-	chtype attrs;                       ///< Rendition, in Curses terms
-
-	short id;                           ///< Post-layouting identification
-	short subid;                        ///< Action ID/Tab index/...
-	char text[];                        ///< Any text label
-};
-
 struct layout
 {
 	struct widget *head;
 	struct widget *tail;
 };
 
-struct ui
+struct app_ui
 {
 	struct widget *(*padding) (chtype attrs, float width, float height);
 	struct widget *(*label) (chtype attrs, const char *label);
@@ -1220,31 +1169,9 @@ struct ui
 	struct widget *(*list) (void);
 	struct widget *(*editor) (chtype attrs);
 
-	void (*render) (void);
-	void (*flip) (void);
-	void (*winch) (void);
-	void (*destroy) (void);
-
 	bool have_icons;
 };
 
-static void
-widget_destroy (struct widget *self)
-{
-	LIST_FOR_EACH (struct widget, w, self->children)
-		widget_destroy (w);
-	free (self);
-}
-
-static void
-widget_move (struct widget *w, int dx, int dy)
-{
-	w->x += dx;
-	w->y += dy;
-	LIST_FOR_EACH (struct widget, child, w->children)
-		widget_move (child, dx, dy);
-}
-
 /// Replaces negative widths amongst widgets in the sublist by redistributing
 /// any width remaining after all positive claims are satisfied from "width".
 /// Also unifies heights to the maximum value of the run, and returns it.
@@ -1327,24 +1254,6 @@ enum player_state { PLAYER_STOPPED, PLAYER_PLAYING, PLAYER_PAUSED };
 // around a pointer to this, hence it is a simple global variable as well.
 // There is enough global state as it is.
 
-#ifdef WITH_X11
-
-/// Wraps Xft fonts into a linked list with fallbacks.
-struct x11_font_link
-{
-	struct x11_font_link *next;
-	XftFont *font;
-};
-
-struct x11_font
-{
-	struct x11_font_link *list;         ///< Fonts of varying Unicode coverage
-	FcPattern *pattern;                 ///< Original unsubstituted pattern
-	FcCharSet *unavailable;             ///< Couldn't find a font for these
-};
-
-#endif  // WITH_X11
-
 static struct app_context
 {
 	// Event loop:
@@ -1395,14 +1304,8 @@ static struct app_context
 
 	// User interface:
 
-	struct ui *ui;                      ///< User interface interface
-	struct widget *widgets;             ///< Layouted widgets
-	int ui_width;                       ///< Window width
-	int ui_height;                      ///< Window height
-	int ui_hunit;                       ///< Horizontal unit
-	int ui_vunit;                       ///< Vertical unit
-	bool ui_focused;                    ///< Whether the window has focus
-	short ui_dragging;                  ///< ID of any dragged widget
+	struct app_ui *ui;                  ///< User interface interface
+	int ui_dragging;                    ///< ID of any dragged widget
 
 #ifdef WITH_FFTW
 	struct spectrum spectrum;           ///< Spectrum analyser
@@ -1415,36 +1318,10 @@ static struct app_context
 #endif  // WITH_PULSE
 	bool pulse_control_requested;       ///< PulseAudio control desired by user
 
-#ifdef WITH_X11
-	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_regular;        ///< Regular font
-	struct x11_font xft_bold;           ///< Bold font
-	struct x11_font xft_italic;         ///< Italic font
-	char *x11_selection;                ///< CLIPBOARD selection
-
-	XRenderColor x_fg[ATTRIBUTE_COUNT]; ///< Foreground per attribute
-	XRenderColor x_bg[ATTRIBUTE_COUNT]; ///< Background per attribute
-#endif  // WITH_X11
-
 	struct line_editor editor;          ///< Line editor
-	struct poller_idle refresh_event;   ///< Refresh the window's contents
-	struct poller_idle flip_event;      ///< Draw rendered widgets on screen
 
 	// Terminal:
 
-	termo_t *tk;                        ///< termo handle (TUI/X11)
-	struct poller_timer tk_timer;       ///< termo timeout timer
-	bool locale_is_utf8;                ///< The locale is Unicode
 	bool use_partial_boxes;             ///< Use Unicode box drawing chars
 
 	struct attrs attrs[ATTRIBUTE_COUNT];
@@ -1693,6 +1570,13 @@ app_init_attributes (void)
 #undef XX
 }
 
+static bool
+app_on_insufficient_color (void)
+{
+	app_init_attributes ();
+	return true;
+}
+
 static void
 app_init_context (void)
 {
@@ -1716,22 +1600,6 @@ app_init_context (void)
 	pulse_init (&g.pulse, NULL);
 #endif  // WITH_PULSE
 
-	TERMO_CHECK_VERSION;
-	if (!(g.tk = termo_new (STDIN_FILENO, NULL, TERMO_FLAG_NOSTART)))
-		exit_fatal ("failed to initialize termo");
-
-	// This is also approximately what libunistring does internally,
-	// since the locale name is canonicalized by locale_charset().
-	// Note that non-Unicode locales are handled pretty inefficiently.
-	g.locale_is_utf8 = !strcasecmp_ascii (locale_charset (), "UTF-8");
-
-	// It doesn't work 100% (e.g. incompatible with undelining in urxvt)
-	// TODO: make this configurable
-	g.use_partial_boxes = g.locale_is_utf8;
-
-	// Presumably, although not necessarily; unsure if queryable at all
-	g.ui_focused = true;
-
 	app_init_attributes ();
 }
 
@@ -1764,9 +1632,6 @@ app_free_context (void)
 	poller_free (&g.poller);
 	free (g.message);
 	free (g.message_detail);
-
-	if (g.tk)
-		termo_destroy (g.tk);
 }
 
 static void
@@ -1779,32 +1644,8 @@ app_quit (void)
 	g.polling = false;
 }
 
-static bool
-app_is_character_in_locale (ucs4_t ch)
-{
-	// Avoid the overhead joined with calling iconv() for all characters.
-	if (g.locale_is_utf8)
-		return true;
-
-	// The library really creates a new conversion object every single time
-	// and doesn't provide any smarter APIs.  Luckily, most users use UTF-8.
-	size_t len;
-	char *tmp = u32_conv_to_encoding (locale_charset (), iconveh_error,
-		&ch, 1, NULL, NULL, &len);
-	if (!tmp)
-		return false;
-	free (tmp);
-	return true;
-}
-
 // --- Layouting ---------------------------------------------------------------
 
-static void
-app_invalidate (void)
-{
-	poller_idle_set (&g.refresh_event);
-}
-
 static void
 app_append_layout (struct layout *l, struct layout *dest)
 {
@@ -1836,7 +1677,7 @@ app_flush_layout_full (struct layout *l, int width, struct layout *dest)
 static void
 app_flush_layout (struct layout *l, struct layout *out)
 {
-	app_flush_layout_full (l, g.ui_width, out);
+	app_flush_layout_full (l, g_xui.width, out);
 }
 
 static struct widget *
@@ -2060,7 +1901,7 @@ app_layout_tabs (struct layout *out)
 		struct widget *w = app_push (&l,
 			g.ui->label (attrs[iter == g.active_tab], iter->name));
 		w->id = WIDGET_TAB;
-		w->subid = ++i;
+		w->userdata = ++i;
 	}
 
 	app_push_fill (&l, g.ui->padding (attrs[0], 1, 1));
@@ -2135,12 +1976,12 @@ app_layout_row (struct tab *tab, int item_index)
 
 	bool override_colors = true;
 	if (item_index == tab->item_selected)
-		row_attrs = g.ui_focused
+		row_attrs = g_xui.focused
 			? APP_ATTR (SELECTION) : APP_ATTR (DEFOCUSED);
 	else if (tab->item_mark > -1 &&
 	   ((item_index >= tab->item_mark && item_index <= tab->item_selected)
 	 || (item_index >= tab->item_selected && item_index <= tab->item_mark)))
-		row_attrs = g.ui_focused
+		row_attrs = g_xui.focused
 			? APP_ATTR (MULTISELECT) : APP_ATTR (DEFOCUSED);
 	else
 		override_colors = false;
@@ -2172,10 +2013,10 @@ app_layout_view (struct layout *out, int height)
 	struct widget *list = app_push_fill (&l, g.ui->list ());
 	list->id = WIDGET_LIST;
 	list->height = height;
-	list->width = g.ui_width;
+	list->width = g_xui.width;
 
 	struct tab *tab = g.active_tab;
-	if ((int) tab->item_count * g.ui_vunit > list->height)
+	if ((int) tab->item_count * g_xui.vunit > list->height)
 	{
 		struct widget *scrollbar = g.ui->scrollbar (APP_ATTR (SCROLLBAR));
 		list->width -= scrollbar->width;
@@ -2183,7 +2024,7 @@ app_layout_view (struct layout *out, int height)
 	}
 
 	int to_show = MIN ((int) tab->item_count - tab->item_top,
-		ceil ((double) list->height / g.ui_vunit));
+		ceil ((double) list->height / g_xui.vunit));
 
 	struct layout children = {};
 	for (int row = 0; row < to_show; row++)
@@ -2338,7 +2179,7 @@ app_layout_statusbar (struct layout *out)
 static struct widget *
 app_widget_by_id (int id)
 {
-	LIST_FOR_EACH (struct widget, w, g.widgets)
+	LIST_FOR_EACH (struct widget, w, g_xui.widgets)
 		if (w->id == id)
 			return w;
 	return NULL;
@@ -2357,7 +2198,7 @@ app_visible_items_height (void)
 static int
 app_visible_items (void)
 {
-	return app_visible_items_height () / g.ui_vunit;
+	return app_visible_items_height () / g_xui.vunit;
 }
 
 /// Checks what items are visible and returns if the range was alright
@@ -2385,45 +2226,27 @@ app_fix_view_range (void)
 }
 
 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)
-		g.ui->flip ();
-}
-
-static void
-app_on_refresh (void *user_data)
+app_layout (void)
 {
-	(void) user_data;
-	poller_idle_reset (&g.refresh_event);
-
 	struct layout top = {}, bottom = {};
 	app_layout_header (&top);
 	app_layout_statusbar (&bottom);
 
-	int available_height = g.ui_height;
+	int available_height = g_xui.height;
 	if (top.tail)
 		available_height -= top.tail->y + top.tail->height;
 	if (bottom.tail)
 		available_height -= bottom.tail->y + bottom.tail->height;
 
-	LIST_FOR_EACH (struct widget, w, g.widgets)
-		widget_destroy (w);
-
 	struct layout widgets = {};
 	app_append_layout (&top, &widgets);
 	app_layout_view (&widgets, available_height);
 	app_append_layout (&bottom, &widgets);
-	g.widgets = widgets.head;
+	g_xui.widgets = widgets.head;
 
 	app_fix_view_range();
 
-	g.ui->render ();
-	poller_idle_set (&g.flip_event);
+	curs_set (0);
 }
 
 // --- Actions -----------------------------------------------------------------
@@ -2433,7 +2256,7 @@ static bool
 app_scroll (int n)
 {
 	g.active_tab->item_top += n;
-	app_invalidate ();
+	xui_invalidate ();
 	return app_fix_view_range ();
 }
 
@@ -2477,7 +2300,7 @@ app_move_selection (int diff)
 
 	bool result = !diff || tab->item_selected != fixed;
 	tab->item_selected = fixed;
-	app_invalidate ();
+	xui_invalidate ();
 
 	app_ensure_selection_visible ();
 	return result;
@@ -2489,7 +2312,7 @@ app_show_message (char *message, char *detail)
 	cstr_set (&g.message, message);
 	cstr_set (&g.message_detail, detail);
 	poller_timer_set (&g.message_timer, 5000);
-	app_invalidate ();
+	xui_invalidate ();
 }
 
 static void
@@ -2501,7 +2324,19 @@ app_hide_message (void)
 	cstr_set (&g.message, NULL);
 	cstr_set (&g.message_detail, NULL);
 	poller_timer_reset (&g.message_timer);
-	app_invalidate ();
+	xui_invalidate ();
+}
+
+static void
+app_on_clipboard_copy (const char *text)
+{
+	app_show_message (xstrdup ("Text copied to clipboard: "), xstrdup (text));
+}
+
+static struct widget *
+app_make_label (chtype attrs, const char *label)
+{
+	return g_xui.ui->label (attrs, 0, label);
 }
 
 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@@ -2510,7 +2345,7 @@ static void
 app_prepend_tab (struct tab *tab)
 {
 	LIST_PREPEND (g.tabs, tab);
-	app_invalidate ();
+	xui_invalidate ();
 }
 
 static void
@@ -2521,7 +2356,7 @@ app_switch_tab (struct tab *tab)
 
 	g.last_tab = g.active_tab;
 	g.active_tab = tab;
-	app_invalidate ();
+	xui_invalidate ();
 }
 
 static bool
@@ -2694,7 +2529,7 @@ app_process_action (enum action action)
 	struct tab *tab = g.active_tab;
 	if (tab->on_action && tab->on_action (action))
 	{
-		app_invalidate ();
+		xui_invalidate ();
 		return true;
 	}
 
@@ -2707,7 +2542,7 @@ app_process_action (enum action action)
 		return true;
 	case ACTION_REDRAW:
 		clear ();
-		app_invalidate ();
+		xui_invalidate ();
 		return true;
 
 	case ACTION_ABORT:
@@ -2715,14 +2550,14 @@ app_process_action (enum action action)
 		if (tab->item_mark > -1)
 		{
 			tab->item_mark = -1;
-			app_invalidate ();
+			xui_invalidate ();
 			return true;
 		}
 		return false;
 	case ACTION_MPD_COMMAND:
 		line_editor_start (&g.editor, ':');
 		g.editor.on_end = app_on_mpd_command_editor_end;
-		app_invalidate ();
+		xui_invalidate ();
 		app_hide_message ();
 		return true;
 	default:
@@ -2735,7 +2570,7 @@ app_process_action (enum action action)
 		 || !tab->item_count || tab->item_selected < 0)
 			return false;
 
-		app_invalidate ();
+		xui_invalidate ();
 		if (tab->item_mark > -1)
 			tab->item_mark = -1;
 		else
@@ -2745,7 +2580,7 @@ app_process_action (enum action action)
 		line_editor_start (&g.editor, '/');
 		g.editor.on_changed = incremental_search_on_changed;
 		g.editor.on_end = incremental_search_on_end;
-		app_invalidate ();
+		xui_invalidate ();
 		app_hide_message ();
 		return true;
 
@@ -2808,7 +2643,7 @@ app_process_action (enum action action)
 		{
 			g.active_tab->item_selected = 0;
 			app_ensure_selection_visible ();
-			app_invalidate ();
+			xui_invalidate ();
 		}
 		return true;
 	case ACTION_GOTO_BOTTOM:
@@ -2817,7 +2652,7 @@ app_process_action (enum action action)
 			g.active_tab->item_selected =
 				MAX (0, (int) g.active_tab->item_count - 1);
 			app_ensure_selection_visible ();
-			app_invalidate ();
+			xui_invalidate ();
 		}
 		return true;
 
@@ -2847,7 +2682,7 @@ app_process_action (enum action action)
 static bool
 app_editor_process_action (enum action action)
 {
-	app_invalidate ();
+	xui_invalidate ();
 	switch (action)
 	{
 	case ACTION_ABORT:
@@ -2907,7 +2742,7 @@ app_process_left_mouse_click (struct widget *w, int x, int y, int modifiers)
 	switch (w->id)
 	{
 	case WIDGET_BUTTON:
-		app_process_action (w->subid);
+		app_process_action (w->userdata);
 		break;
 	case WIDGET_GAUGE:
 	{
@@ -2926,7 +2761,7 @@ app_process_left_mouse_click (struct widget *w, int x, int y, int modifiers)
 		struct tab *tab = g.help_tab;
 		int i = 0;
 		LIST_FOR_EACH (struct tab, iter, g.tabs)
-			if (++i == w->subid)
+			if (++i == w->userdata)
 				tab = iter;
 
 		app_switch_tab (tab);
@@ -2935,7 +2770,7 @@ app_process_left_mouse_click (struct widget *w, int x, int y, int modifiers)
 	case WIDGET_LIST:
 	{
 		struct tab *tab = g.active_tab;
-		int row_index = y / g.ui_vunit;
+		int row_index = y / g_xui.vunit;
 		if (row_index < 0
 		 || row_index >= (int) tab->item_count - tab->item_top)
 			return false;
@@ -2949,7 +2784,7 @@ app_process_left_mouse_click (struct widget *w, int x, int y, int modifiers)
 
 		tab->item_selected = row_index + tab->item_top;
 		app_ensure_selection_visible ();
-		app_invalidate ();
+		xui_invalidate ();
 
 		if (modifiers & APP_KEYMOD_DOUBLE_CLICK)
 			app_process_action (ACTION_CHOOSE);
@@ -2961,7 +2796,7 @@ app_process_left_mouse_click (struct widget *w, int x, int y, int modifiers)
 		int visible_items = app_visible_items ();
 		tab->item_top = (double) y / w->height
 			* (int) tab->item_count - visible_items / 2;
-		app_invalidate ();
+		xui_invalidate ();
 		app_fix_view_range ();
 		break;
 	}
@@ -2999,11 +2834,11 @@ app_process_mouse (termo_mouse_event_t type, int x, int y, int button,
 	if (g.editor.line)
 	{
 		line_editor_abort (&g.editor, false);
-		app_invalidate ();
+		xui_invalidate ();
 	}
 
 	struct widget *target = NULL;
-	LIST_FOR_EACH (struct widget, w, g.widgets)
+	LIST_FOR_EACH (struct widget, w, g_xui.widgets)
 		if (x >= w->x && x < w->x + w->width
 		 && y >= w->y && y < w->y + w->height)
 			target = w;
@@ -3149,7 +2984,7 @@ static int
 app_binding_cmp (const void *a, const void *b)
 {
 	const struct binding *aa = a, *bb = b;
-	int cmp = termo_keycmp (g.tk, &aa->decoded, &bb->decoded);
+	int cmp = termo_keycmp (g_xui.tk, &aa->decoded, &bb->decoded);
 	return cmp ? cmp : bb->order - aa->order;
 }
 
@@ -3160,7 +2995,7 @@ app_next_binding (struct str_map_iter *iter, termo_key_t *key, int *action)
 	while ((v = str_map_iter_next (iter)))
 	{
 		*action = ACTION_NONE;
-		if (*termo_strpkey_utf8 (g.tk,
+		if (*termo_strpkey_utf8 (g_xui.tk,
 			iter->link->key, key, TERMO_FORMAT_ALTISMETA))
 			print_error ("%s: invalid binding", iter->link->key);
 		else if (v->type == CONFIG_ITEM_NULL)
@@ -3189,7 +3024,7 @@ app_init_bindings (const char *keymap,
 	termo_key_t decoded;
 	for (size_t i = 0; i < defaults_len; i++)
 	{
-		hard_assert (!*termo_strpkey_utf8 (g.tk,
+		hard_assert (!*termo_strpkey_utf8 (g_xui.tk,
 			defaults[i].key, &decoded, TERMO_FORMAT_ALTISMETA));
 		a[a_len++] = (struct binding) { decoded, defaults[i].action, order++ };
 	}
@@ -3211,7 +3046,8 @@ app_init_bindings (const char *keymap,
 	for (size_t in = 0; in < a_len; in++)
 	{
 		a[in].order = 0;
-		if (!out || termo_keycmp (g.tk, &a[in].decoded, &a[out - 1].decoded))
+		if (!out
+		 || termo_keycmp (g_xui.tk, &a[in].decoded, &a[out - 1].decoded))
 			a[out++] = a[in];
 	}
 
@@ -3223,14 +3059,15 @@ static char *
 app_strfkey (const termo_key_t *key)
 {
 	// For display purposes, this is highly desirable
-	int flags = termo_get_flags (g.tk);
-	termo_set_flags (g.tk, flags | TERMO_FLAG_SPACESYMBOL);
+	int flags = termo_get_flags (g_xui.tk);
+	termo_set_flags (g_xui.tk, flags | TERMO_FLAG_SPACESYMBOL);
 	termo_key_t fixed = *key;
-	termo_canonicalise (g.tk, &fixed);
-	termo_set_flags (g.tk, flags);
+	termo_canonicalise (g_xui.tk, &fixed);
+	termo_set_flags (g_xui.tk, flags);
 
 	char buf[16] = "";
-	termo_strfkey_utf8 (g.tk, buf, sizeof buf, &fixed, TERMO_FORMAT_ALTISMETA);
+	termo_strfkey_utf8 (g_xui.tk,
+		buf, sizeof buf, &fixed, TERMO_FORMAT_ALTISMETA);
 	return xstrdup (buf);
 }
 
@@ -3244,8 +3081,7 @@ app_process_termo_event (termo_key_t *event)
 	bool handled = false;
 	if ((handled = event->type == TERMO_TYPE_FOCUS))
 	{
-		g.ui_focused = !!event->code.focused;
-		app_invalidate ();
+		xui_invalidate ();
 		// Senseless fall-through
 	}
 
@@ -3264,7 +3100,7 @@ app_process_termo_event (termo_key_t *event)
 			return handled;
 
 		line_editor_insert (&g.editor, event->code.codepoint);
-		app_invalidate ();
+		xui_invalidate ();
 		return true;
 	}
 	if ((binding = bsearch (&dummy, g_normal_keys, g_normal_keys_len,
@@ -3433,7 +3269,7 @@ current_tab_update (void)
 	g_current_tab.item_count = g.playlist.len;
 	g_current_tab.item_mark =
 		MIN ((int) g.playlist.len - 1, g_current_tab.item_mark);
-	app_invalidate ();
+	xui_invalidate ();
 }
 
 static struct tab *
@@ -3695,7 +3531,7 @@ library_tab_load_data (const struct strv *data)
 	if (g_library_tab.super.item_selected >= (int) len)
 		app_move_selection (0);
 
-	app_invalidate ();
+	xui_invalidate ();
 }
 
 static void
@@ -3858,7 +3694,7 @@ library_tab_on_action (enum action action)
 		library_tab_load_data (&empty);
 		strv_free (&empty);
 
-		app_invalidate ();
+		xui_invalidate ();
 		return true;
 	}
 	case ACTION_MPD_ADD:
@@ -4402,7 +4238,7 @@ info_tab_on_item_layout (size_t item_index)
 	{
 		char *prefix = xstrdup_printf ("%s:", item->prefix);
 		app_push (&l, g.ui->label (A_BOLD, prefix))
-			->width = 8 * g.ui_hunit;
+			->width = 8 * g_xui.hunit;
 		app_push (&l, g.ui->padding (0, 0.5, 1));
 	}
 
@@ -4533,7 +4369,7 @@ info_tab_on_plugin_stdout (const struct pollfd *fd, void *user_data)
 	case SOCKET_IO_EOF:
 		info_tab_plugin_abort ();
 		info_tab_update ();
-		app_invalidate ();
+		xui_invalidate ();
 	}
 }
 
@@ -4624,7 +4460,7 @@ info_tab_on_action (enum action action)
 	case ACTION_CHOOSE:
 		info_tab_plugin_run (item->plugin, item_list_get (&g.playlist, g.song));
 		info_tab_update ();
-		app_invalidate ();
+		xui_invalidate ();
 		return true;
 	default:
 		return false;
@@ -4838,7 +4674,7 @@ debug_tab_push (char *message, chtype attrs)
 	item->attrs = attrs;
 	item->timestamp = clock_msec (CLOCK_REALTIME);
 
-	app_invalidate ();
+	xui_invalidate ();
 }
 
 static struct tab *
@@ -4866,7 +4702,7 @@ spectrum_redraw (void)
 	if (spectrum)
 		spectrum->on_render (spectrum);
 
-	poller_idle_set (&g.flip_event);
+	poller_idle_set (&g_xui.flip_event);
 }
 
 // When any problem occurs with the FIFO, we'll just give up on it completely
@@ -4880,7 +4716,7 @@ spectrum_discard_fifo (void)
 		g.spectrum_fd = -1;
 
 		spectrum_free (&g.spectrum);
-		app_invalidate ();
+		xui_invalidate ();
 	}
 }
 
@@ -4952,7 +4788,7 @@ spectrum_setup_fifo (void)
 
 	if (!path)
 		print_error ("spectrum: %s", "FIFO path could not be resolved");
-	else if (!g.locale_is_utf8)
+	else if (!g_xui.locale_is_utf8)
 		print_error ("spectrum: %s", "UTF-8 locale required");
 	else if (!spectrum_init (&g.spectrum, (char *) spectrum_format,
 		spectrum_bars->value.integer, spectrum_fps->value.integer, &e))
@@ -5025,10 +4861,10 @@ mpd_on_outputs_response (const struct mpd_response *response,
 	else
 	{
 		pulse_init (&g.pulse, &g.poller);
-		g.pulse.on_update = app_invalidate;
+		g.pulse.on_update = xui_invalidate;
 	}
 
-	app_invalidate ();
+	xui_invalidate ();
 }
 
 static void
@@ -5048,7 +4884,7 @@ static void
 pulse_disable (void)
 {
 	pulse_free (&g.pulse);
-	app_invalidate ();
+	xui_invalidate ();
 }
 
 #else  // ! WITH_PULSE
@@ -5157,7 +4993,7 @@ mpd_update_playback_state (void)
 	if (g.playlist_version != last_playlist_version)
 		mpd_update_playlist_time ();
 
-	app_invalidate ();
+	xui_invalidate ();
 }
 
 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@@ -5295,7 +5131,7 @@ mpd_on_elapsed_time_tick (void *user_data)
 	// Try to get called on the next round second of playback
 	poller_timer_set (&g.elapsed_event, 1000 - elapsed_msec);
 
-	app_invalidate ();
+	xui_invalidate ();
 }
 
 static void
@@ -5505,72 +5341,17 @@ app_on_reconnect (void *user_data)
 		mpd_queue_reconnect ();
 	}
 	free (address);
-	app_invalidate ();
+	xui_invalidate ();
 }
 
 // --- TUI ---------------------------------------------------------------------
 
-static void
-tui_flush_buffer (struct widget *self, struct row_buffer *buf)
-{
-	move (self->y, self->x);
-
-	int space = MIN (self->width, g.ui_width - self->x);
-	row_buffer_align (buf, space, self->attrs);
-	row_buffer_flush (buf);
-	row_buffer_free (buf);
-}
-
-static void
-tui_render_padding (struct widget *self)
-{
-	struct row_buffer buf = row_buffer_make ();
-	tui_flush_buffer (self, &buf);
-}
-
-static struct widget *
-tui_make_padding (chtype attrs, float width, float height)
-{
-	struct widget *w = xcalloc (1, sizeof *w + 2);
-	w->text[0] = ' ';
-	w->on_render = tui_render_padding;
-	w->attrs = attrs;
-	w->width = width * 2;
-	w->height = height;
-	return w;
-}
-
-static void
-tui_render_label (struct widget *self)
-{
-	struct row_buffer buf = row_buffer_make ();
-	row_buffer_append (&buf, self->text, self->attrs);
-	tui_flush_buffer (self, &buf);
-}
-
-static struct widget *
-tui_make_label (chtype attrs, const char *label)
-{
-	size_t len = strlen (label);
-	struct widget *w = xcalloc (1, sizeof *w + len + 1);
-	w->on_render = tui_render_label;
-	w->attrs = attrs;
-	memcpy (w->text, label, len);
-
-	struct row_buffer buf = row_buffer_make ();
-	row_buffer_append (&buf, w->text, w->attrs);
-	w->width = buf.total_width;
-	w->height = 1;
-	row_buffer_free (&buf);
-	return w;
-}
-
 static struct widget *
 tui_make_button (chtype attrs, const char *label, enum action a)
 {
-	struct widget *w = tui_make_label (attrs, label);
+	struct widget *w = tui_make_label (attrs, 0, label);
 	w->id = WIDGET_BUTTON;
-	w->subid = a;
+	w->userdata = a;
 	return w;
 }
 
@@ -5710,20 +5491,12 @@ tui_make_scrollbar (chtype attrs)
 	return w;
 }
 
-static void
-tui_render_list (struct widget *self)
-{
-	LIST_FOR_EACH (struct widget, w, self->children)
-		w->on_render (w);
-}
-
 static struct widget *
 tui_make_list (void)
 {
 	struct widget *w = xcalloc (1, sizeof *w + 1);
 	w->width = -1;
 	w->height = g.active_tab->item_count;
-	w->on_render = tui_render_list;
 	return w;
 }
 
@@ -5731,10 +5504,37 @@ static void
 tui_render_editor (struct widget *self)
 {
 	struct row_buffer buf = row_buffer_make ();
-	int caret = line_editor_write (&g.editor, &buf, self->width, self->attrs);
+	const struct line_editor *e = &g.editor;
+	int width = self->width;
+	if (e->prompt)
+	{
+		hard_assert (e->prompt < 127);
+		row_buffer_append_c (&buf, e->prompt, self->attrs);
+		width--;
+	}
+
+	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 < 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 && width - preceding - following > 2 /* widest char */)
+		preceding += e->w[--start];
+
+	// XXX: we should also show < > indicators for overflow but it'd probably
+	//   considerably complicate this algorithm
+	for (; start < e->len; start++)
+		row_buffer_append_c (&buf, e->line[start], self->attrs);
 	tui_flush_buffer (self, &buf);
 
 	// FIXME: This should be at the end of of tui_render().
+	int caret = !!e->prompt + preceding;
 	move (self->y, self->x + caret);
 	curs_set (1);
 }
@@ -5751,600 +5551,130 @@ tui_make_editor (chtype attrs)
 	return w;
 }
 
-static void
-tui_render (void)
-{
-	erase ();
-	curs_set (0);
-
-	LIST_FOR_EACH (struct widget, w, g.widgets)
-		if (w->width >= 0 && w->height >= 0)
-			w->on_render (w);
-}
-
-static void
-tui_flip (void)
-{
-	// Curses handles double-buffering for us automatically.
-	refresh ();
-}
-
-static void
-tui_winch (void)
-{
-	// The standard endwin/refresh sequence makes the terminal flicker
-#if defined HAVE_RESIZETERM && defined TIOCGWINSZ
-	struct winsize size;
-	if (!ioctl (STDOUT_FILENO, TIOCGWINSZ, (char *) &size))
-	{
-		char *row = getenv ("LINES");
-		char *col = getenv ("COLUMNS");
-		unsigned long tmp;
-		resizeterm (
-			(row && xstrtoul (&tmp, row, 10)) ? tmp : size.ws_row,
-			(col && xstrtoul (&tmp, col, 10)) ? tmp : size.ws_col);
-	}
-#else  // HAVE_RESIZETERM && TIOCGWINSZ
-	endwin ();
-	refresh ();
-#endif  // HAVE_RESIZETERM && TIOCGWINSZ
-
-	g.ui_width = COLS;
-	g.ui_height = LINES;
-	app_invalidate ();
-}
-
-static void
-tui_destroy (void)
-{
-	endwin ();
-}
-
-static struct ui tui_ui =
+static struct app_ui app_tui_ui =
 {
 	.padding     = tui_make_padding,
-	.label       = tui_make_label,
+	.label       = app_make_label,
 	.button      = tui_make_button,
 	.gauge       = tui_make_gauge,
 	.spectrum    = tui_make_spectrum,
 	.scrollbar   = tui_make_scrollbar,
 	.list        = tui_make_list,
 	.editor      = tui_make_editor,
-
-	.render      = tui_render,
-	.flip        = tui_flip,
-	.winch       = tui_winch,
-	.destroy     = tui_destroy,
 };
 
-// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+// --- X11 ---------------------------------------------------------------------
 
-static void
-tui_on_tty_event (termo_key_t *event, int64_t event_ts)
-{
-	// Simple double click detection via release--press delay, only a bit
-	// complicated by the fact that we don't know what's being released
-	static termo_key_t last_event;
-	static int64_t last_event_ts;
-	static int last_button;
+#ifdef WITH_X11
 
-	int y, x, button, y_last, x_last, modifiers = 0;
-	termo_mouse_event_t type, type_last;
-	if (termo_interpret_mouse (g.tk, event, &type, &button, &y, &x))
+// On a 20x20 raster to make it feasible to design on paper.
+#define X11_STOP {INFINITY, INFINITY}
+static const XPointDouble
+	x11_icon_previous[] =
 	{
-		if (termo_interpret_mouse
-			(g.tk, &last_event, &type_last, NULL, &y_last, &x_last)
-		 && event_ts - last_event_ts < 500
-		 && type_last == TERMO_MOUSE_RELEASE && type == TERMO_MOUSE_PRESS
-		 && y_last == y && x_last == x && last_button == button)
-		{
-			modifiers |= APP_KEYMOD_DOUBLE_CLICK;
-			// Prevent interpreting triple clicks as two double clicks.
-			last_button = 0;
-		}
-		else if (type == TERMO_MOUSE_PRESS)
-			last_button = button;
+		{10, 0}, {0, 10}, {10, 20}, X11_STOP,
+		{20, 0}, {10, 10}, {20, 20}, X11_STOP, X11_STOP,
+	},
+	x11_icon_pause[] =
+	{
+		{1, 0}, {7, 0}, {7, 20}, {1, 20}, X11_STOP,
+		{13, 0}, {19, 0}, {19, 20}, {13, 20}, X11_STOP, X11_STOP,
+	},
+	x11_icon_play[] =
+	{
+		{0, 0}, {20, 10}, {0, 20}, X11_STOP, X11_STOP,
+	},
+	x11_icon_stop[] =
+	{
+		{0, 0}, {20, 0}, {20, 20}, {0, 20}, X11_STOP, X11_STOP,
+	},
+	x11_icon_next[] =
+	{
+		{0, 0}, {10, 10}, {0, 20}, X11_STOP,
+		{10, 0}, {20, 10}, {10, 20}, X11_STOP, X11_STOP,
+	},
+	x11_icon_repeat[] =
+	{
+		{0, 12}, {0, 6}, {3, 3}, {13, 3}, {13, 0}, {20, 4.5},
+		{13, 9}, {13, 6}, {3, 6}, {3, 10}, X11_STOP,
+		{0, 15.5}, {7, 11}, {7, 14}, {17, 14}, {17, 10}, {20, 8},
+		{20, 14}, {17, 17}, {7, 17}, {7, 20}, X11_STOP, X11_STOP,
+	},
+	x11_icon_random[] =
+	{
+		{0, 6}, {0, 3}, {5, 3}, {6, 4.5}, {4, 7.5}, {3, 6}, X11_STOP,
+		{9, 15.5}, {11, 12.5}, {12, 14}, {13, 14}, {13, 11}, {20, 15.5},
+		{13, 20}, {13, 17}, {10, 17}, X11_STOP,
+		{0, 17}, {0, 14}, {3, 14}, {10, 3}, {13, 3}, {13, 0}, {20, 4.5},
+		{13, 9}, {13, 6}, {12, 6}, {5, 17}, X11_STOP, X11_STOP,
+	},
+	x11_icon_single[] =
+	{
+		{7, 6}, {7, 4}, {9, 2}, {12, 2}, {12, 15}, {14, 15}, {14, 18},
+		{7, 18}, {7, 15}, {9, 15}, {9, 6}, X11_STOP, X11_STOP,
+	},
+	x11_icon_consume[] =
+	{
+		{0, 13}, {0, 7}, {4, 3}, {10, 3}, {14, 7}, {5, 10}, {14, 13},
+		{10, 17}, {4, 17}, X11_STOP,
+		{16, 12}, {16, 8}, {20, 8}, {20, 12}, X11_STOP, X11_STOP,
+	};
 
-		if (!app_process_mouse (type, x, y, button, modifiers))
-			beep ();
+static const XPointDouble *
+x11_icon_for_action (enum action action)
+{
+	switch (action)
+	{
+	case ACTION_MPD_PREVIOUS:
+		return x11_icon_previous;
+	case ACTION_MPD_TOGGLE:
+		return g.state == PLAYER_PLAYING ? x11_icon_pause : x11_icon_play;
+	case ACTION_MPD_STOP:
+		return x11_icon_stop;
+	case ACTION_MPD_NEXT:
+		return x11_icon_next;
+	case ACTION_MPD_REPEAT:
+		return x11_icon_repeat;
+	case ACTION_MPD_RANDOM:
+		return x11_icon_random;
+	case ACTION_MPD_SINGLE:
+		return x11_icon_single;
+	case ACTION_MPD_CONSUME:
+		return x11_icon_consume;
+	default:
+		return NULL;
 	}
-	else if (!app_process_termo_event (event))
-		beep ();
-
-	last_event = *event;
-	last_event_ts = event_ts;
 }
 
 static void
-tui_on_tty_readable (const struct pollfd *fd, void *user_data)
+x11_render_button (struct widget *self)
 {
-	(void) user_data;
-	if (fd->revents & ~(POLLIN | POLLHUP | POLLERR))
-		print_debug ("fd %d: unexpected revents: %d", fd->fd, fd->revents);
+	x11_render_padding (self);
 
-	poller_timer_reset (&g.tk_timer);
-	termo_advisereadable (g.tk);
+	const XPointDouble *icon = x11_icon_for_action (self->userdata);
+	if (!icon)
+	{
+		x11_render_label (self);
+		return;
+	}
 
-	termo_key_t event = {};
-	int64_t event_ts = clock_msec (CLOCK_BEST);
-	termo_result_t res;
-	while ((res = termo_getkey (g.tk, &event)) == TERMO_RES_KEY)
-		tui_on_tty_event (&event, event_ts);
+	size_t total = 0;
+	for (size_t i = 0; icon[i].x != INFINITY || icon[i - 1].x != INFINITY; i++)
+		total++;
 
-	if (res == TERMO_RES_AGAIN)
-		poller_timer_set (&g.tk_timer, termo_get_waittime (g.tk));
-	else if (res == TERMO_RES_ERROR || res == TERMO_RES_EOF)
-		app_quit ();
-}
+	// TODO: There should be an attribute for buttons, to handle this better.
+	XRenderColor color = *x11_fg (self);
+	if (!(self->attrs & A_BOLD))
+	{
+		color.alpha /= 2;
+		color.red /= 2;
+		color.green /= 2;
+		color.blue /= 2;
+	}
 
-static void
-tui_on_key_timer (void *user_data)
-{
-	(void) user_data;
-
-	termo_key_t event;
-	if (termo_getkey_force (g.tk, &event) == TERMO_RES_KEY)
-		if (!app_process_termo_event (&event))
-			beep ();
-}
-
-static void
-tui_init (void)
-{
-	poller_fd_set (&g.tty_event, POLLIN);
-	if (!termo_start (g.tk) || !initscr () || nonl () == ERR)
-		exit_fatal ("failed to set up the terminal");
-
-	termo_set_mouse_tracking_mode (g.tk, TERMO_MOUSE_TRACKING_DRAG);
-
-	g.ui = &tui_ui;
-	g.ui_width = COLS;
-	g.ui_height = LINES;
-	g.ui_vunit = 1;
-	g.ui_hunit = 1;
-
-	// By default we don't use any colors so they're not required...
-	if (start_color () == ERR
-	 || use_default_colors () == ERR
-	 || COLOR_PAIRS <= ATTRIBUTE_COUNT)
-		return;
-
-	for (int a = 0; a < ATTRIBUTE_COUNT; a++)
-	{
-		// ...thus we can reset back to defaults even after initializing some
-		// FIXME: that's a lie now, MULTISELECT requires a colour
-		if (g.attrs[a].fg >= COLORS || g.attrs[a].fg < -1
-		 || g.attrs[a].bg >= COLORS || g.attrs[a].bg < -1)
-		{
-			app_init_attributes ();
-			return;
-		}
-
-		init_pair (a + 1, g.attrs[a].fg, g.attrs[a].bg);
-		g.attrs[a].attrs |= COLOR_PAIR (a + 1);
-	}
-}
-
-// --- X11 ---------------------------------------------------------------------
-
-#ifdef WITH_X11
-
-static XRenderColor x11_default_fg = { .alpha = 0xffff };
-static XRenderColor x11_default_bg = { 0xffff, 0xffff, 0xffff, 0xffff };
-static XErrorHandler x11_default_error_handler;
-
-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;
-}
-
-static void
-x11_font_link_destroy (struct x11_font_link *self)
-{
-	XftFontClose (g.dpy, self->font);
-	free (self);
-}
-
-static struct x11_font_link *
-x11_font_link_open (FcPattern *pattern)
-{
-	XftFont *font = XftFontOpenPattern (g.dpy, pattern);
-	if (!font)
-	{
-		FcPatternDestroy (pattern);
-		return NULL;
-	}
-	return x11_font_link_new (font);
-}
-
-static bool
-x11_font_open (struct x11_font *self, FcPattern *pattern)
-{
-	FcPattern *substituted = FcPatternDuplicate (pattern);
-	FcConfigSubstitute (NULL, substituted, FcMatchPattern);
-
-	FcResult result = 0;
-	FcPattern *match
-		= XftFontMatch (g.dpy, DefaultScreen (g.dpy), substituted, &result);
-	FcPatternDestroy (substituted);
-	if (!match || !(self->list = x11_font_link_open (match)))
-	{
-		FcPatternDestroy (pattern);
-		return false;
-	}
-
-	self->pattern = pattern;
-	self->unavailable = FcCharSetCreate ();
-	return true;
-}
-
-static void
-x11_font_free (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);
-}
-
-/// Find or instantiate a font that can render the character given by cp.
-static XftFont *
-x11_font_cover_codepoint (struct x11_font *self, ucs4_t cp)
-{
-	if (FcCharSetHasChar (self->unavailable, cp))
-		return self->list->font;
-
-	struct x11_font_link **used = &self->list;
-	for (; *used; used = &(*used)->next)
-		if (XftCharExists (g.dpy, (*used)->font, cp))
-			return (*used)->font;
-
-	FcCharSet *set = FcCharSetCreate ();
-	FcCharSetAddChar (set, cp);
-	FcPattern *needle = FcPatternDuplicate (self->pattern);
-	FcPatternAddCharSet (needle, FC_CHARSET, set);
-	FcConfigSubstitute (NULL, needle, FcMatchPattern);
-
-	FcResult result = 0;
-	FcPattern *match
-		= XftFontMatch (g.dpy, DefaultScreen (g.dpy), needle, &result);
-	FcCharSetDestroy (set);
-	FcPatternDestroy (needle);
-	if (!match)
-		goto fail;
-
-	struct x11_font_link *new = x11_font_link_open (match);
-	if (!new)
-		goto fail;
-
-	// The reverse may happen simply due to race conditions.
-	if (XftCharExists (g.dpy, new->font, cp))
-		return (*used = new)->font;
-
-	x11_font_link_destroy (new);
-fail:
-	FcCharSetAddChar (self->unavailable, cp);
-	return self->list->font;
-}
-
-// TODO: Perhaps produce an array of FT_UInt glyph indexes, mainly so that
-//   x11_font_{hadvance,draw,render}() can use the same data, through the use
-//   of a new function that collects the spans in a data structure.
-static size_t
-x11_font_span (struct x11_font *self, const uint8_t *text, XftFont **font)
-{
-	hard_assert (self->list != NULL);
-
-	// Xft similarly just stops on invalid UTF-8.
-	ucs4_t cp = 0;
-	const uint8_t *p = text;
-	if (!(p = u8_next (&cp, p)))
-		return 0;
-
-	*font = x11_font_cover_codepoint (self, cp);
-	for (const uint8_t *end = NULL; (end = u8_next (&cp, p)); p = end)
-	{
-		if (x11_font_cover_codepoint (self, cp) != *font)
-			break;
-	}
-	return p - text;
-}
-
-static int
-x11_font_draw (struct x11_font *self, XftColor *color, int x, int y,
-	const char *text)
-{
-	int advance = 0;
-	size_t len = 0;
-	XftFont *font = NULL;
-	while ((len = x11_font_span (self, (const uint8_t *) text, &font)))
-	{
-		if (color)
-		{
-			XftDrawStringUtf8 (g.xft_draw, color, font,
-				x + advance, y + self->list->font->ascent,
-				(const FcChar8 *) text, len);
-		}
-
-		XGlyphInfo extents = {};
-		XftTextExtentsUtf8 (g.dpy, font, (const FcChar8 *) text, len, &extents);
-		text += len;
-		advance += extents.xOff;
-	}
-	return advance;
-}
-
-static int
-x11_font_hadvance (struct x11_font *self, const char *text)
-{
-	return x11_font_draw (self, NULL, 0, 0, text);
-}
-
-static int
-x11_font_render (struct x11_font *self, int op, Picture src, int srcx, int srcy,
-	int x, int y, const char *text)
-{
-	int advance = 0;
-	size_t len = 0;
-	XftFont *font = NULL;
-	while ((len = x11_font_span (self, (const uint8_t *) text, &font)))
-	{
-		if (src)
-		{
-			XftTextRenderUtf8 (g.dpy, op, src, font, g.x11_pixmap_picture,
-				srcx, srcy, x + advance, y + self->list->font->ascent,
-				(const FcChar8 *) text, len);
-		}
-
-		XGlyphInfo extents = {};
-		XftTextExtentsUtf8 (g.dpy, font, (const FcChar8 *) text, len, &extents);
-		text += len;
-		advance += extents.xOff;
-	}
-	return advance;
-}
-
-// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-static struct x11_font *
-x11_widget_font (struct widget *self)
-{
-	if (self->attrs & A_BOLD)
-		return &g.xft_bold;
-	if (self->attrs & A_ITALIC)
-		return &g.xft_italic;
-	return &g.xft_regular;
-}
-
-static XRenderColor *
-x11_fg_attrs (chtype attrs)
-{
-	int pair = PAIR_NUMBER (attrs);
-	if (!pair--)
-		return &x11_default_fg;
-	return (attrs & A_REVERSE) ? &g.x_bg[pair] : &g.x_fg[pair];
-}
-
-static XRenderColor *
-x11_fg (struct widget *self)
-{
-	return x11_fg_attrs (self->attrs);
-}
-
-static XRenderColor *
-x11_bg_attrs (chtype attrs)
-{
-	int pair = PAIR_NUMBER (attrs);
-	if (!pair--)
-		return &x11_default_bg;
-	return (attrs & A_REVERSE) ? &g.x_fg[pair] : &g.x_bg[pair];
-}
-
-static XRenderColor *
-x11_bg (struct widget *self)
-{
-	return x11_bg_attrs (self->attrs);
-}
-
-static void
-x11_render_padding (struct widget *self)
-{
-	if (PAIR_NUMBER (self->attrs))
-	{
-		XRenderFillRectangle (g.dpy, PictOpSrc, g.x11_pixmap_picture,
-			x11_bg (self), self->x, self->y, self->width, self->height);
-	}
-	if (self->attrs & A_UNDERLINE)
-	{
-		XRenderFillRectangle (g.dpy, PictOpSrc, g.x11_pixmap_picture,
-			x11_fg (self), self->x, self->y + self->height - 1, self->width, 1);
-	}
-}
-
-static struct widget *
-x11_make_padding (chtype attrs, float width, float height)
-{
-	struct widget *w = xcalloc (1, sizeof *w + 2);
-	w->text[0] = ' ';
-	w->on_render = x11_render_padding;
-	w->attrs = attrs;
-	w->width = g.ui_vunit * width;
-	w->height = g.ui_vunit * height;
-	return w;
-}
-
-static void
-x11_render_label (struct widget *self)
-{
-	x11_render_padding (self);
-
-	int space = MIN (self->width, g.ui_width - self->x);
-	if (space <= 0)
-		return;
-
-	// TODO: Try to avoid re-measuring on each render.
-	struct x11_font *font = x11_widget_font (self);
-	int advance = x11_font_hadvance (font, self->text);
-	if (advance <= space)
-	{
-		XftColor color = { .color = *x11_fg (self) };
-		x11_font_draw (font, &color, self->x, self->y, self->text);
-		return;
-	}
-
-	// XRender doesn't extend gradients beyond their end stops.
-	XRenderColor solid = *x11_fg (self), colors[3] = { solid, solid, solid };
-	colors[2].alpha = 0;
-
-	double portion = MIN (1, 2.0 * font->list->font->height / space);
-	XFixed stops[3] = { 0, XDoubleToFixed (1 - portion), XDoubleToFixed (1) };
-	XLinearGradient gradient = { {}, { XDoubleToFixed (space), 0 } };
-
-	// Note that this masking is a very expensive operation.
-	Picture source =
-		XRenderCreateLinearGradient (g.dpy, &gradient, stops, colors, 3);
-	x11_font_render (font, PictOpOver, source, -self->x, 0, self->x, self->y,
-		self->text);
-	XRenderFreePicture (g.dpy, source);
-}
-
-static struct widget *
-x11_make_label (chtype attrs, const char *label)
-{
-	// Xft renders combining marks by themselves, NFC improves it a bit.
-	// We'd have to use HarfBuzz to do this correctly.
-	size_t label_len = strlen (label) + 1, normalized_len = 0;
-	uint8_t *normalized = u8_normalize (UNINORM_NFC,
-		(const uint8_t *) label, label_len, NULL, &normalized_len);
-	if (!normalized)
-	{
-		normalized = memcpy (xmalloc (label_len), label, label_len);
-		normalized_len = label_len;
-	}
-
-	struct widget *w = xcalloc (1, sizeof *w + normalized_len);
-	w->on_render = x11_render_label;
-	w->attrs = attrs;
-	memcpy (w->text, normalized, normalized_len);
-	free (normalized);
-
-	struct x11_font *font = x11_widget_font (w);
-	w->width = x11_font_hadvance (font, w->text);
-	w->height = font->list->font->height;
-	return w;
-}
-
-// On a 20x20 raster to make it feasible to design on paper.
-#define X11_STOP {INFINITY, INFINITY}
-static const XPointDouble
-	x11_icon_previous[] =
-	{
-		{10, 0}, {0, 10}, {10, 20}, X11_STOP,
-		{20, 0}, {10, 10}, {20, 20}, X11_STOP, X11_STOP,
-	},
-	x11_icon_pause[] =
-	{
-		{1, 0}, {7, 0}, {7, 20}, {1, 20}, X11_STOP,
-		{13, 0}, {19, 0}, {19, 20}, {13, 20}, X11_STOP, X11_STOP,
-	},
-	x11_icon_play[] =
-	{
-		{0, 0}, {20, 10}, {0, 20}, X11_STOP, X11_STOP,
-	},
-	x11_icon_stop[] =
-	{
-		{0, 0}, {20, 0}, {20, 20}, {0, 20}, X11_STOP, X11_STOP,
-	},
-	x11_icon_next[] =
-	{
-		{0, 0}, {10, 10}, {0, 20}, X11_STOP,
-		{10, 0}, {20, 10}, {10, 20}, X11_STOP, X11_STOP,
-	},
-	x11_icon_repeat[] =
-	{
-		{0, 12}, {0, 6}, {3, 3}, {13, 3}, {13, 0}, {20, 4.5},
-		{13, 9}, {13, 6}, {3, 6}, {3, 10}, X11_STOP,
-		{0, 15.5}, {7, 11}, {7, 14}, {17, 14}, {17, 10}, {20, 8},
-		{20, 14}, {17, 17}, {7, 17}, {7, 20}, X11_STOP, X11_STOP,
-	},
-	x11_icon_random[] =
-	{
-		{0, 6}, {0, 3}, {5, 3}, {6, 4.5}, {4, 7.5}, {3, 6}, X11_STOP,
-		{9, 15.5}, {11, 12.5}, {12, 14}, {13, 14}, {13, 11}, {20, 15.5},
-		{13, 20}, {13, 17}, {10, 17}, X11_STOP,
-		{0, 17}, {0, 14}, {3, 14}, {10, 3}, {13, 3}, {13, 0}, {20, 4.5},
-		{13, 9}, {13, 6}, {12, 6}, {5, 17}, X11_STOP, X11_STOP,
-	},
-	x11_icon_single[] =
-	{
-		{7, 6}, {7, 4}, {9, 2}, {12, 2}, {12, 15}, {14, 15}, {14, 18},
-		{7, 18}, {7, 15}, {9, 15}, {9, 6}, X11_STOP, X11_STOP,
-	},
-	x11_icon_consume[] =
-	{
-		{0, 13}, {0, 7}, {4, 3}, {10, 3}, {14, 7}, {5, 10}, {14, 13},
-		{10, 17}, {4, 17}, X11_STOP,
-		{16, 12}, {16, 8}, {20, 8}, {20, 12}, X11_STOP, X11_STOP,
-	};
-
-static const XPointDouble *
-x11_icon_for_action (enum action action)
-{
-	switch (action)
-	{
-	case ACTION_MPD_PREVIOUS:
-		return x11_icon_previous;
-	case ACTION_MPD_TOGGLE:
-		return g.state == PLAYER_PLAYING ? x11_icon_pause : x11_icon_play;
-	case ACTION_MPD_STOP:
-		return x11_icon_stop;
-	case ACTION_MPD_NEXT:
-		return x11_icon_next;
-	case ACTION_MPD_REPEAT:
-		return x11_icon_repeat;
-	case ACTION_MPD_RANDOM:
-		return x11_icon_random;
-	case ACTION_MPD_SINGLE:
-		return x11_icon_single;
-	case ACTION_MPD_CONSUME:
-		return x11_icon_consume;
-	default:
-		return NULL;
-	}
-}
-
-static void
-x11_render_button (struct widget *self)
-{
-	x11_render_padding (self);
-
-	const XPointDouble *icon = x11_icon_for_action (self->subid);
-	if (!icon)
-	{
-		x11_render_label (self);
-		return;
-	}
-
-	size_t total = 0;
-	for (size_t i = 0; icon[i].x != INFINITY || icon[i - 1].x != INFINITY; i++)
-		total++;
-
-	// TODO: There should be an attribute for buttons, to handle this better.
-	XRenderColor color = *x11_fg (self);
-	if (!(self->attrs & A_BOLD))
-	{
-		color.alpha /= 2;
-		color.red /= 2;
-		color.green /= 2;
-		color.blue /= 2;
-	}
-
-	Picture source = XRenderCreateSolidFill (g.dpy, &color);
-	const XRenderPictFormat *format
-		= XRenderFindStandardFormat (g.dpy, PictStandardA8);
+	Picture source = XRenderCreateSolidFill (g_xui.dpy, &color);
+	const XRenderPictFormat *format
+		= XRenderFindStandardFormat (g_xui.dpy, PictStandardA8);
 
 	int x = self->x, y = self->y + (self->height - self->width) / 2;
 	XPointDouble buffer[total], *p = buffer;
@@ -6357,27 +5687,27 @@ x11_render_button (struct widget *self)
 		}
 		else if (p != buffer)
 		{
-			XRenderCompositeDoublePoly (g.dpy, PictOpOver,
-				source, g.x11_pixmap_picture, format,
+			XRenderCompositeDoublePoly (g_xui.dpy, PictOpOver,
+				source, g_xui.x11_pixmap_picture, format,
 				0, 0, 0, 0, buffer, p - buffer, EvenOddRule);
 			p = buffer;
 		}
-	XRenderFreePicture (g.dpy, source);
+	XRenderFreePicture (g_xui.dpy, source);
 }
 
 static struct widget *
 x11_make_button (chtype attrs, const char *label, enum action a)
 {
-	struct widget *w = x11_make_label (attrs, label);
+	struct widget *w = x11_make_label (attrs, 0, label);
 	w->id = WIDGET_BUTTON;
-	w->subid = a;
+	w->userdata = a;
 
 	if (x11_icon_for_action (a))
 	{
 		w->on_render = x11_render_button;
 
 		// It should be padded by the caller horizontally.
-		w->height = g.ui_vunit;
+		w->height = g_xui.vunit;
 		w->width = w->height * 3 / 4;
 	}
 	return w;
@@ -6391,13 +5721,13 @@ x11_render_gauge (struct widget *self)
 		return;
 
 	int part = (float) g.song_elapsed / g.song_duration * self->width;
-	XRenderFillRectangle (g.dpy, PictOpSrc, g.x11_pixmap_picture,
+	XRenderFillRectangle (g_xui.dpy, PictOpSrc, g_xui.x11_pixmap_picture,
 		x11_bg_attrs (APP_ATTR (ELAPSED)),
 		self->x,
 		self->y + self->height / 8,
 		part,
 		self->height * 3 / 4);
-	XRenderFillRectangle (g.dpy, PictOpSrc, g.x11_pixmap_picture,
+	XRenderFillRectangle (g_xui.dpy, PictOpSrc, g_xui.x11_pixmap_picture,
 		x11_bg_attrs (APP_ATTR (REMAINS)),
 		self->x + part,
 		self->y + self->height / 8,
@@ -6413,7 +5743,7 @@ x11_make_gauge (chtype attrs)
 	w->on_render = x11_render_gauge;
 	w->attrs = attrs;
 	w->width = -1;
-	w->height = g.ui_vunit;
+	w->height = g_xui.vunit;
 	return w;
 }
 
@@ -6437,13 +5767,13 @@ x11_render_spectrum (struct widget *self)
 		};
 	}
 
-	XRenderFillRectangles (g.dpy, PictOpSrc, g.x11_pixmap_picture,
+	XRenderFillRectangles (g_xui.dpy, PictOpSrc, g_xui.x11_pixmap_picture,
 		x11_fg (self), rectangles, N_ELEMENTS (rectangles));
 #endif  // WITH_FFTW
 
 	// Enable the spectrum_redraw() hack.
 	XRectangle r = { self->x, self->y, self->width, self->height };
-	XUnionRectWithRegion (&r, g.x11_clip, g.x11_clip);
+	XUnionRectWithRegion (&r, g_xui.x11_clip, g_xui.x11_clip);
 }
 
 static struct widget *
@@ -6452,8 +5782,8 @@ x11_make_spectrum (chtype attrs, int width)
 	struct widget *w = xcalloc (1, sizeof *w + 1);
 	w->on_render = x11_render_spectrum;
 	w->attrs = attrs;
-	w->width = width * g.ui_vunit / 2;
-	w->height = g.ui_vunit;
+	w->width = width * g_xui.vunit / 2;
+	w->height = g_xui.vunit;
 	return w;
 }
 
@@ -6464,9 +5794,9 @@ x11_render_scrollbar (struct widget *self)
 
 	struct tab *tab = g.active_tab;
 	struct scrollbar bar =
-		app_compute_scrollbar (tab, app_visible_items_height (), g.ui_vunit);
+		app_compute_scrollbar (tab, app_visible_items_height (), g_xui.vunit);
 
-	XRenderFillRectangle (g.dpy, PictOpSrc, g.x11_pixmap_picture,
+	XRenderFillRectangle (g_xui.dpy, PictOpSrc, g_xui.x11_pixmap_picture,
 		x11_fg_attrs (self->attrs),
 		self->x,
 		self->y + bar.start,
@@ -6480,30 +5810,15 @@ x11_make_scrollbar (chtype attrs)
 	struct widget *w = xcalloc (1, sizeof *w + 1);
 	w->on_render = x11_render_scrollbar;
 	w->attrs = attrs;
-	w->width = g.ui_vunit / 2;
+	w->width = g_xui.vunit / 2;
 	return w;
 }
 
-static void
-x11_render_list (struct widget *self)
-{
-	// We could do that for all widgets, but it would be kind-of pointless.
-	// We need to go through Xft, or XftTextRenderUtf8() might skip glyphs.
-	XftDrawSetClipRectangles (g.xft_draw, 0, 0,
-		&(XRectangle) { self->x, self->y, self->width, self->height }, 1);
-
-	x11_render_padding (self);
-	LIST_FOR_EACH (struct widget, w, self->children)
-		w->on_render (w);
-
-	XftDrawSetClip (g.xft_draw, None);
-}
-
 static struct widget *
 x11_make_list (void)
 {
 	struct widget *w = xcalloc (1, sizeof *w + 1);
-	w->on_render = x11_render_list;
+	w->on_render = x11_render_padding;
 	return w;
 }
 
@@ -6520,19 +5835,19 @@ x11_render_editor (struct widget *self)
 	XGlyphInfo extents = {};
 	if (g.editor.prompt)
 	{
-		FT_UInt i = XftCharIndex (g.dpy, font, g.editor.prompt);
-		XftDrawGlyphs (g.xft_draw, &color, font, x, y, &i, 1);
-		XftGlyphExtents (g.dpy, font, &i, 1, &extents);
-		x += extents.xOff + g.ui_vunit / 4;
+		FT_UInt i = XftCharIndex (g_xui.dpy, font, g.editor.prompt);
+		XftDrawGlyphs (g_xui.xft_draw, &color, font, x, y, &i, 1);
+		XftGlyphExtents (g_xui.dpy, font, &i, 1, &extents);
+		x += extents.xOff + g_xui.vunit / 4;
 	}
 
 	// TODO: Adapt x11_font_{hadvance,draw}().
 	// TODO: Make this scroll around the caret, and fade like labels.
-	XftDrawString32 (g.xft_draw, &color, font, x, y,
+	XftDrawString32 (g_xui.xft_draw, &color, font, x, y,
 		g.editor.line, g.editor.len);
 
-	XftTextExtents32 (g.dpy, font, g.editor.line, g.editor.point, &extents);
-	XRenderFillRectangle (g.dpy, PictOpSrc, g.x11_pixmap_picture,
+	XftTextExtents32 (g_xui.dpy, font, g.editor.line, g.editor.point, &extents);
+	XRenderFillRectangle (g_xui.dpy, PictOpSrc, g_xui.x11_pixmap_picture,
 		&color.color, x + extents.xOff, self->y, 2, self->height);
 }
 
@@ -6544,64 +5859,14 @@ x11_make_editor (chtype attrs)
 	w->on_render = x11_render_editor;
 	w->attrs = attrs;
 	w->width = -1;
-	w->height = g.ui_vunit;
+	w->height = g_xui.vunit;
 	return w;
 }
 
-static void
-x11_render (void)
-{
-	XRenderFillRectangle (g.dpy, PictOpSrc, g.x11_pixmap_picture,
-		&x11_default_bg, 0, 0, g.ui_width, g.ui_height);
-	LIST_FOR_EACH (struct widget, w, g.widgets)
-		if (w->width && w->height)
-			w->on_render (w);
-
-	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
-x11_destroy (void)
-{
-	XDestroyIC (g.x11_ic);
-	XCloseIM (g.x11_im);
-	XDestroyRegion (g.x11_clip);
-	XDestroyWindow (g.dpy, g.x11_window);
-	XRenderFreePicture (g.dpy, g.x11_pixmap_picture);
-	XFreePixmap (g.dpy, g.x11_pixmap);
-	XftDrawDestroy (g.xft_draw);
-	x11_font_free (&g.xft_regular);
-	x11_font_free (&g.xft_bold);
-	x11_font_free (&g.xft_italic);
-	cstr_set (&g.x11_selection, NULL);
-
-	poller_fd_reset (&g.x11_event);
-	XCloseDisplay (g.dpy);
-
-	// Xft hooks called in XCloseDisplay() don't clean up everything.
-	FcFini ();
-}
-
-static struct ui x11_ui =
+static struct app_ui app_x11_ui =
 {
 	.padding     = x11_make_padding,
-	.label       = x11_make_label,
+	.label       = app_make_label,
 	.button      = x11_make_button,
 	.gauge       = x11_make_gauge,
 	.spectrum    = x11_make_spectrum,
@@ -6609,604 +5874,9 @@ static struct ui x11_ui =
 	.list        = x11_make_list,
 	.editor      = x11_make_editor,
 
-	.render      = x11_render,
-	.flip        = x11_flip,
-	.destroy     = x11_destroy,
 	.have_icons  = true,
 };
 
-// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-static termo_sym_t
-x11_convert_keysym (KeySym keysym)
-{
-	// Leaving out TERMO_TYPE_FUNCTION, TERMO_SYM_DEL (N/A),
-	// and TERMO_SYM_SPACE (governed by TERMO_FLAG_SPACESYMBOL, not in use).
-	switch (keysym)
-	{
-	case XK_BackSpace:     return TERMO_SYM_BACKSPACE;
-	case XK_Tab:           return TERMO_SYM_TAB;
-	case XK_ISO_Left_Tab:  return TERMO_SYM_TAB;
-	case XK_Return:        return TERMO_SYM_ENTER;
-	case XK_Escape:        return TERMO_SYM_ESCAPE;
-
-	case XK_Up:            return TERMO_SYM_UP;
-	case XK_Down:          return TERMO_SYM_DOWN;
-	case XK_Left:          return TERMO_SYM_LEFT;
-	case XK_Right:         return TERMO_SYM_RIGHT;
-	case XK_Begin:         return TERMO_SYM_BEGIN;
-	case XK_Find:          return TERMO_SYM_FIND;
-	case XK_Insert:        return TERMO_SYM_INSERT;
-	case XK_Delete:        return TERMO_SYM_DELETE;
-	case XK_Select:        return TERMO_SYM_SELECT;
-	case XK_Page_Up:       return TERMO_SYM_PAGEUP;
-	case XK_Page_Down:     return TERMO_SYM_PAGEDOWN;
-	case XK_Home:          return TERMO_SYM_HOME;
-	case XK_End:           return TERMO_SYM_END;
-
-	case XK_Cancel:        return TERMO_SYM_CANCEL;
-	case XK_Clear:         return TERMO_SYM_CLEAR;
-	// TERMO_SYM_CLOSE
-	// TERMO_SYM_COMMAND
-	// TERMO_SYM_COPY
-	// TERMO_SYM_EXIT
-	case XK_Help:          return TERMO_SYM_HELP;
-	// TERMO_SYM_MARK
-	// TERMO_SYM_MESSAGE
-	// TERMO_SYM_MOVE
-	// TERMO_SYM_OPEN
-	// TERMO_SYM_OPTIONS
-	case XK_Print:         return TERMO_SYM_PRINT;
-	case XK_Redo:          return TERMO_SYM_REDO;
-	// TERMO_SYM_REFERENCE
-	// TERMO_SYM_REFRESH
-	// TERMO_SYM_REPLACE
-	// TERMO_SYM_RESTART
-	// TERMO_SYM_RESUME
-	// TERMO_SYM_SAVE
-	// TERMO_SYM_SUSPEND
-	case XK_Undo:          return TERMO_SYM_UNDO;
-
-	case XK_KP_0:          return TERMO_SYM_KP0;
-	case XK_KP_1:          return TERMO_SYM_KP1;
-	case XK_KP_2:          return TERMO_SYM_KP2;
-	case XK_KP_3:          return TERMO_SYM_KP3;
-	case XK_KP_4:          return TERMO_SYM_KP4;
-	case XK_KP_5:          return TERMO_SYM_KP5;
-	case XK_KP_6:          return TERMO_SYM_KP6;
-	case XK_KP_7:          return TERMO_SYM_KP7;
-	case XK_KP_8:          return TERMO_SYM_KP8;
-	case XK_KP_9:          return TERMO_SYM_KP9;
-	case XK_KP_Enter:      return TERMO_SYM_KPENTER;
-	case XK_KP_Add:        return TERMO_SYM_KPPLUS;
-	case XK_KP_Subtract:   return TERMO_SYM_KPMINUS;
-	case XK_KP_Multiply:   return TERMO_SYM_KPMULT;
-	case XK_KP_Divide:     return TERMO_SYM_KPDIV;
-	case XK_KP_Separator:  return TERMO_SYM_KPCOMMA;
-	case XK_KP_Decimal:    return TERMO_SYM_KPPERIOD;
-	case XK_KP_Equal:      return TERMO_SYM_KPEQUALS;
-	}
-	return TERMO_SYM_UNKNOWN;
-}
-
-static bool
-on_x11_keypress (XEvent *e)
-{
-	// 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");
-
-	termo_key_t key = {};
-	if (ev->state & ShiftMask)
-		key.modifiers |= TERMO_KEYMOD_SHIFT;
-	if (ev->state & ControlMask)
-		key.modifiers |= TERMO_KEYMOD_CTRL;
-	if (ev->state & Mod1Mask)
-		key.modifiers |= TERMO_KEYMOD_ALT;
-
-	if (keysym >= XK_F1 && keysym <= XK_F35)
-	{
-		key.type = TERMO_TYPE_FUNCTION;
-		key.code.number = 1 + keysym - XK_F1;
-		return app_process_termo_event (&key);
-	}
-	if ((key.code.sym = x11_convert_keysym (keysym)) != TERMO_SYM_UNKNOWN)
-	{
-		key.type = TERMO_TYPE_KEYSYM;
-		return app_process_termo_event (&key);
-	}
-
-	bool result = true;
-	if (len)
-	{
-		key.type = TERMO_TYPE_KEY;
-		key.modifiers &= ~TERMO_KEYMOD_SHIFT;
-
-		int32_t cp = 0;
-		struct utf8_iter iter = { .s = buf, .len = len };
-		size_t cp_len = 0;
-		while ((cp = utf8_iter_next (&iter, &cp_len)) >= 0)
-		{
-			termo_key_t k = key;
-			memcpy (k.multibyte, p, MIN (cp_len, sizeof k.multibyte - 1));
-			p += cp_len;
-
-			// This is all unfortunate, but probably in the right place.
-			if (!cp)
-			{
-				k.code.codepoint = ' ';
-				if (ev->state & ShiftMask)
-					k.modifiers |= TERMO_KEYMOD_SHIFT;
-			}
-			else if (cp >= 32)
-				k.code.codepoint = cp;
-			else if (ev->state & ShiftMask)
-				k.code.codepoint = cp + 64;
-			else
-				k.code.codepoint = cp + 96;
-			if (!app_process_termo_event (&k))
-				result = false;
-		}
-	}
-	return result;
-}
-
-static void
-x11_init_pixmap (void)
-{
-	int screen = DefaultScreen (g.dpy);
-	g.x11_pixmap = XCreatePixmap (g.dpy, g.x11_window,
-		g.ui_width, 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 char *
-x11_find_text (struct widget *list, int x, int y)
-{
-	struct widget *target = NULL;
-	LIST_FOR_EACH (struct widget, w, list)
-		if (x >= w->x && x < w->x + w->width
-		 && y >= w->y && y < w->y + w->height)
-			target = w;
-	if (!target)
-		return NULL;
-
-	char *result = x11_find_text (target->children, x, y);
-	if (result)
-		return result;
-	return xstrdup (target->text);
-}
-
-// TODO: OSC 52 exists for terminals, so make it possible to enable that there.
-static bool
-x11_process_press (int x, int y, int button, int modifiers)
-{
-	if (button != Button3)
-		goto out;
-
-	char *text = x11_find_text (g.widgets, x, y);
-	if (!text || !*(cstr_strip_in_place (text, " \t")))
-	{
-		free (text);
-		goto out;
-	}
-
-	cstr_set (&g.x11_selection, text);
-	XSetSelectionOwner (g.dpy, XInternAtom (g.dpy, "CLIPBOARD", False),
-		g.x11_window, CurrentTime);
-	app_show_message (xstrdup ("Text copied to clipboard: "),
-		xstrdup (g.x11_selection));
-	return true;
-
-out:
-	return app_process_mouse (TERMO_MOUSE_PRESS, x, y, button, modifiers);
-}
-
-static int
-x11_state_to_modifiers (unsigned int state)
-{
-	int modifiers = 0;
-	if (state & ShiftMask)    modifiers |= TERMO_KEYMOD_SHIFT;
-	if (state & ControlMask)  modifiers |= TERMO_KEYMOD_CTRL;
-	if (state & Mod1Mask)     modifiers |= TERMO_KEYMOD_ALT;
-	return modifiers;
-}
-
-static bool
-on_x11_input_event (XEvent *ev)
-{
-	static XEvent last_press_event;
-	if (ev->type == KeyPress)
-	{
-		last_press_event = (XEvent) {};
-		return on_x11_keypress (ev);
-	}
-	if (ev->type == MotionNotify)
-	{
-		return app_process_mouse (TERMO_MOUSE_DRAG,
-			ev->xmotion.x, ev->xmotion.y, 1 /* Button1MotionMask */,
-			x11_state_to_modifiers (ev->xmotion.state));
-	}
-
-	// This is nearly the same as tui_on_tty_event().
-	int x = ev->xbutton.x, y = ev->xbutton.y;
-	unsigned int button = ev->xbutton.button;
-	int modifiers = x11_state_to_modifiers (ev->xbutton.state);
-	if (ev->type == ButtonPress
-	 && ev->xbutton.time - last_press_event.xbutton.time < 500
-	 && abs (last_press_event.xbutton.x - x) < 5
-	 && abs (last_press_event.xbutton.y - y) < 5
-	 && last_press_event.xbutton.button == button)
-	{
-		modifiers |= APP_KEYMOD_DOUBLE_CLICK;
-		// Prevent interpreting triple clicks as two double clicks.
-		last_press_event = (XEvent) {};
-	}
-	else if (ev->type == ButtonPress)
-		last_press_event = *ev;
-
-	if (ev->type == ButtonPress)
-		return x11_process_press (x, y, button, modifiers);
-	if (ev->type == ButtonRelease)
-		return app_process_mouse
-			(TERMO_MOUSE_RELEASE, x, y, button, modifiers);
-	return false;
-}
-
-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 void
-on_x11_event (XEvent *ev)
-{
-	termo_key_t key = {};
-	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:
-		key.type = TERMO_TYPE_FOCUS;
-		key.code.focused = true;
-		app_process_termo_event (&key);
-		break;
-	case FocusOut:
-		key.type = TERMO_TYPE_FOCUS;
-		key.code.focused = false;
-		app_process_termo_event (&key);
-		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 XRenderColor
-x11_convert_color (int color)
-{
-	hard_assert (color >= 0 && color <= 255);
-
-	static const uint16_t base[16] =
-	{
-		0x000, 0x800, 0x080, 0x880, 0x008, 0x808, 0x088, 0xccc,
-		0x888, 0xf00, 0x0f0, 0xff0, 0x00f, 0xf0f, 0x0ff, 0xfff,
-	};
-
-	XRenderColor c = { .alpha = 0xffff };
-	if (color < 16)
-	{
-		c.red   = 0x1111 *        (base[color] >> 8);
-		c.green = 0x1111 * (0xf & (base[color] >> 4));
-		c.blue  = 0x1111 * (0xf & (base[color]));
-	}
-	else if (color >= 232)
-		c.red = c.green = c.blue = 0x0101 * (8 + (color - 232) * 10);
-	else
-	{
-		color -= 16;
-
-		int r =  color / 36;
-		int g = (color / 6) % 6;
-		int b = (color % 6);
-		c.red   = 0x0101 * !!r * (55 + 40 * r);
-		c.green = 0x0101 * !!g * (55 + 40 * g);
-		c.blue  = 0x0101 * !!b * (55 + 40 * b);
-	}
-	return c;
-}
-
-static void
-x11_init_attributes (void)
-{
-	for (int a = 0; a < ATTRIBUTE_COUNT; a++)
-	{
-		g.x_fg[a] = x11_default_fg;
-		g.x_bg[a] = x11_default_bg;
-		if (g.attrs[a].fg >= 256 || g.attrs[a].fg < -1
-		 || g.attrs[a].bg >= 256 || g.attrs[a].bg < -1)
-			continue;
-
-		if (g.attrs[a].fg != -1)
-			g.x_fg[a] = x11_convert_color (g.attrs[a].fg);
-		if (g.attrs[a].bg != -1)
-			g.x_bg[a] = x11_convert_color (g.attrs[a].bg);
-
-		g.attrs[a].attrs |= COLOR_PAIR (a + 1);
-	}
-}
-
-static void
-x11_init_fonts (void)
-{
-	// TODO: Try to use Gtk/FontName from the _XSETTINGS_S%d selection,
-	//   as well as Net/DoubleClick*.  See the XSETTINGS proposal for details.
-	//   https://www.freedesktop.org/wiki/Specifications/XSettingsRegistry/
-	const char *name = get_config_string (g.config.root, "settings.x11_font");
-
-	if (!FcInit ())
-		print_warning ("FontConfig initialization failed");
-
-	FcPattern *query_regular = FcNameParse ((const FcChar8 *) name);
-	FcPattern *query_bold = FcPatternDuplicate (query_regular);
-	FcPatternAdd (query_bold, FC_STYLE, (FcValue) {
-		.type = FcTypeString, .u.s = (FcChar8 *) "Bold" }, FcFalse);
-	FcPattern *query_italic = FcPatternDuplicate (query_regular);
-	FcPatternAdd (query_italic, FC_STYLE, (FcValue) {
-		.type = FcTypeString, .u.s = (FcChar8 *) "Italic" }, FcFalse);
-
-	if (!x11_font_open (&g.xft_regular, query_regular))
-		exit_fatal ("cannot open font: %s", name);
-	if (!x11_font_open (&g.xft_bold, query_bold))
-		exit_fatal ("cannot open bold font: %s", name);
-	if (!x11_font_open (&g.xft_italic, query_italic))
-		exit_fatal ("cannot open italic font: %s", name);
-}
-
-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);
-
-	x11_init_attributes ();
-	x11_init_fonts ();
-
-	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,
-	};
-
-	// Approximate the average width of a character to half of the em unit.
-	g.ui_vunit = g.xft_regular.list->font->height;
-	g.ui_hunit = g.ui_vunit / 2;
-	// 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.ui_vunit;
-	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);
-
-	// 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);
-	g.ui = &x11_ui;
-
-	XMapWindow (g.dpy, g.x11_window);
-}
-
 #endif  // WITH_X11
 
 // --- Signals -----------------------------------------------------------------
@@ -7294,8 +5964,8 @@ app_on_signal_pipe_readable (const struct pollfd *fd, void *user_data)
 	if (g_winch_received)
 	{
 		g_winch_received = false;
-		if (g.ui->winch)
-			g.ui->winch ();
+		if (g_xui.ui->winch)
+			g_xui.ui->winch ();
 	}
 }
 
@@ -7329,7 +5999,7 @@ app_log_handler (void *user_data, const char *quote, const char *fmt,
 	app_show_message (xstrndup (message.str, quote_len),
 		xstrdup (message.str + quote_len));
 
-	if (g_verbose_mode && (g.ui != &tui_ui || !isatty (STDERR_FILENO)))
+	if (g_verbose_mode && (g_xui.ui != &tui_ui || !isatty (STDERR_FILENO)))
 		fprintf (stderr, "%s\n", message.str);
 	if (g_debug_tab.active)
 		debug_tab_push (str_steal (&message),
@@ -7349,12 +6019,6 @@ app_init_poller_events (void)
 	g.message_timer = poller_timer_make (&g.poller);
 	g.message_timer.dispatcher = app_on_message_timer;
 
-	// Always initialized, but only activated with the TUI.
-	g.tty_event = poller_fd_make (&g.poller, STDIN_FILENO);
-	g.tty_event.dispatcher = tui_on_tty_readable;
-	g.tk_timer = poller_timer_make (&g.poller);
-	g.tk_timer.dispatcher = tui_on_key_timer;
-
 	g.connect_event = poller_timer_make (&g.poller);
 	g.connect_event.dispatcher = app_on_reconnect;
 	poller_timer_set (&g.connect_event, 0);
@@ -7363,12 +6027,34 @@ app_init_poller_events (void)
 	g.elapsed_event.dispatcher = g.elapsed_poll
 		? mpd_on_elapsed_time_tick_poll
 		: mpd_on_elapsed_time_tick;
+}
 
-	g.refresh_event = poller_idle_make (&g.poller);
-	g.refresh_event.dispatcher = app_on_refresh;
+static void
+app_init_ui (bool requested_x11)
+{
+	xui_preinit ();
 
-	g.flip_event = poller_idle_make (&g.poller);
-	g.flip_event.dispatcher = app_on_flip;
+	g_normal_keys = app_init_bindings ("normal",
+		g_normal_defaults, N_ELEMENTS (g_normal_defaults), &g_normal_keys_len);
+	g_editor_keys = app_init_bindings ("editor",
+		g_editor_defaults, N_ELEMENTS (g_editor_defaults), &g_editor_keys_len);
+
+	// It doesn't work 100% (e.g. incompatible with undelining in urxvt)
+	// TODO: make this configurable
+	g.use_partial_boxes = g_xui.locale_is_utf8;
+
+#ifdef WITH_X11
+	g_xui.x11_fontname = get_config_string (g.config.root, "settings.x11_font");
+#endif  // WITH_X11
+
+	xui_start (&g.poller, requested_x11, g.attrs, N_ELEMENTS (g.attrs));
+
+#ifdef WITH_X11
+	if (g_xui.ui == &x11_ui)
+		g.ui = &app_x11_ui;
+	else
+#endif  // WITH_X11
+		g.ui = &app_tui_ui;
 }
 
 static void
@@ -7450,18 +6136,7 @@ main (int argc, char *argv[])
 	app_load_configuration ();
 	signals_setup_handlers ();
 	app_init_poller_events ();
-
-#ifdef WITH_X11
-	if (requested_x11 || (!isatty (STDIN_FILENO) && getenv ("DISPLAY")))
-		x11_init ();
-	else
-#endif  // WITH_X11
-		tui_init ();
-
-	g_normal_keys = app_init_bindings ("normal",
-		g_normal_defaults, N_ELEMENTS (g_normal_defaults), &g_normal_keys_len);
-	g_editor_keys = app_init_bindings ("editor",
-		g_editor_defaults, N_ELEMENTS (g_editor_defaults), &g_editor_keys_len);
+	app_init_ui (requested_x11);
 
 	if (g_debug_mode)
 		app_prepend_tab (debug_tab_init ());
@@ -7485,7 +6160,7 @@ main (int argc, char *argv[])
 	while (g.polling)
 		poller_run (&g.poller);
 
-	g.ui->destroy ();
+	xui_stop ();
 	g_log_message_real = log_message_stdio;
 	app_free_context ();
 	return 0;
diff --git a/termo b/termo
index 8265f07..2518b53 160000
--- a/termo
+++ b/termo
@@ -1 +1 @@
-Subproject commit 8265f075b176b33680012094aa1ced5721e55ac9
+Subproject commit 2518b53e5ae4579bf84ed58fa7a62806f64e861c
-- 
cgit v1.2.3-70-g09d2