/*
 * nncmpp -- the MPD client you never knew you needed
 *
 * Copyright (c) 2016 - 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.
 *
 * 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.
 *
 */

#include "config.h"

// We "need" to have an enum for attributes before including liberty.
// Avoiding colours in the defaults here in order to support dumb terminals.
#define ATTRIBUTE_TABLE(XX)                              \
	XX( NORMAL,      normal,       -1, -1, 0           ) \
	XX( HIGHLIGHT,   highlight,    -1, -1, A_BOLD      ) \
	/* Gauge                                          */ \
	XX( ELAPSED,     elapsed,      -1, -1, A_REVERSE   ) \
	XX( REMAINS,     remains,      -1, -1, A_UNDERLINE ) \
	/* Tab bar                                        */ \
	XX( TAB_BAR,     tab_bar,      -1, -1, A_REVERSE   ) \
	XX( TAB_ACTIVE,  tab_active,   -1, -1, A_BOLD      ) \
	/* Listview                                       */ \
	XX( HEADER,      header,       -1, -1, A_UNDERLINE ) \
	XX( EVEN,        even,         -1, -1, 0           ) \
	XX( ODD,         odd,          -1, -1, 0           ) \
	XX( DIRECTORY,   directory,    -1, -1, 0           ) \
	XX( SELECTION,   selection,    -1, -1, A_REVERSE   ) \
	/* Cyan is good with both black and white.
	 * Can't use A_REVERSE because bold'd be bright.
	 * Unfortunately ran out of B&W attributes.       */ \
	XX( MULTISELECT, multiselect,  -1,  6, 0           ) \
	/* This ought to be indicative enough.            */ \
	XX( DEFOCUSED,   defocused,    -1, -1, A_UNDERLINE ) \
	XX( SCROLLBAR,   scrollbar,    -1, -1, 0           ) \
	/* These are for debugging only                   */ \
	XX( WARNING,     warning,       3, -1, 0           ) \
	XX( ERROR,       error,         1, -1, 0           ) \
	XX( INCOMING,    incoming,      2, -1, 0           ) \
	XX( OUTGOING,    outgoing,      4, -1, 0           )

enum
{
#define XX(name, config, fg_, bg_, attrs_) ATTRIBUTE_ ## name,
	ATTRIBUTE_TABLE (XX)
#undef XX
	ATTRIBUTE_COUNT
};

// My battle-tested C framework acting as a GLib replacement.  Its one big
// disadvantage is missing support for i18n but that can eventually be added
// as an optional feature.  Localised applications look super awkward, though.

// User data for logger functions to enable formatted logging
#define print_fatal_data    ((void *) ATTRIBUTE_ERROR)
#define print_error_data    ((void *) ATTRIBUTE_ERROR)
#define print_warning_data  ((void *) ATTRIBUTE_WARNING)

#define LIBERTY_WANT_POLLER
#define LIBERTY_WANT_ASYNC
#define LIBERTY_WANT_PROTO_HTTP
#define LIBERTY_WANT_PROTO_MPD
#include "liberty/liberty.c"

#ifdef WITH_X11
#define LIBERTY_XUI_WANT_X11
#endif // WITH_X11
#include "liberty/liberty-xui.c"

#include <dirent.h>
#include <locale.h>
#include <math.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.
#include <curl/curl.h>

// The spectrum analyser requires a DFT transform.  The FFTW library is fairly
// efficient, and doesn't have a requirement on the number of bins.
#ifdef WITH_FFTW
#include <fftw3.h>
#endif  // WITH_FFTW

// Remote MPD control needs appropriate volume controls.
#ifdef WITH_PULSE
#include "liberty/liberty-pulse.c"
#include <pulse/context.h>
#include <pulse/error.h>
#include <pulse/introspect.h>
#include <pulse/subscribe.h>
#include <pulse/sample.h>
#endif  // WITH_PULSE

#define APP_TITLE  PROGRAM_NAME         ///< Left top corner

#include "nncmpp-actions.h"

// --- Utilities ---------------------------------------------------------------

static void
shell_quote (const char *str, struct str *output)
{
	// See SUSv3 Shell and Utilities, 2.2.3 Double-Quotes
	str_append_c (output, '"');
	for (const char *p = str; *p; p++)
	{
		if (strchr ("`$\"\\", *p))
			str_append_c (output, '\\');
		str_append_c (output, *p);
	}
	str_append_c (output, '"');
}

static bool
xstrtoul_map (const struct str_map *map, const char *key, unsigned long *out)
{
	const char *field = str_map_find (map, key);
	return field && xstrtoul (out, field, 10);
}

static const char *
xbasename (const char *path)
{
	const char *last_slash = strrchr (path, '/');
	return last_slash ? last_slash + 1 : path;
}

static char *xstrdup0 (const char *s) { return s ? xstrdup (s) : NULL; }

static char *
latin1_to_utf8 (const char *latin1)
{
	struct str converted = str_make ();
	while (*latin1)
	{
		uint8_t c = *latin1++;
		if (c < 0x80)
			str_append_c (&converted, c);
		else
		{
			str_append_c (&converted, 0xC0 | (c >> 6));
			str_append_c (&converted, 0x80 | (c & 0x3F));
		}
	}
	return str_steal (&converted);
}

static void
str_enforce_utf8 (struct str *self)
{
	if (!utf8_validate (self->str, self->len))
	{
		char *sanitized = latin1_to_utf8 (self->str);
		str_reset (self);
		str_append (self, sanitized);
		free (sanitized);
	}
}

static void
cstr_uncapitalize (char *s)
{
	if (isupper (s[0]) && islower (s[1]))
		s[0] = tolower_ascii (s[0]);
}

static int
print_curl_debug (CURL *easy, curl_infotype type, char *data, size_t len,
	void *ud)
{
	(void) easy;
	(void) ud;
	(void) type;

	char copy[len + 1];
	for (size_t i = 0; i < len; i++)
	{
		uint8_t c = data[i];
		copy[i] = !iscntrl_ascii (c) || c == '\n' ? c : '.';
	}
	copy[len] = '\0';

	char *next;
	for (char *p = copy; p; p = next)
	{
		if ((next = strchr (p, '\n')))
			*next++ = '\0';
		if (!*p)
			continue;

		if (!utf8_validate (p, strlen (p)))
		{
			char *fixed = latin1_to_utf8 (p);
			print_debug ("cURL: %s", fixed);
			free (fixed);
		}
		else
			print_debug ("cURL: %s", p);
	}
	return 0;
}

static char *
mpd_parse_kv (char *line, char **value)
{
	char *key = mpd_client_parse_kv (line, value);
	if (!key)  print_debug ("%s: %s", "erroneous MPD output", line);
	return key;
}

static void
mpd_read_time (const char *value, int *sec, int *optional_msec)
{
	if (!value)
		return;

	char *end = NULL;
	long n = strtol (value, &end, 10);
	if (n < 0 || (*end && *end != '.'))
		return;

	int msec = 0;
	if (*end == '.')
	{
		// In practice, MPD always uses three decimal digits
		size_t digits = strspn (++end, "0123456789");
		if (end[digits])
			return;

		if (digits--) msec += (*end++ - '0') * 100;
		if (digits--) msec += (*end++ - '0') * 10;
		if (digits--) msec +=  *end++ - '0';
	}

	*sec = MIN (INT_MAX, n);
	if (optional_msec)
		*optional_msec = msec;
}

// --- cURL async wrapper ------------------------------------------------------

// You are meant to subclass this structure, no user_data pointers needed
struct poller_curl_task;

/// Receives notification for finished transfers
typedef void (*poller_curl_done_fn)
	(CURLMsg *msg, struct poller_curl_task *task);

struct poller_curl_task
{
	CURL *easy;                         ///< cURL easy interface handle
	char curl_error[CURL_ERROR_SIZE];   ///< cURL error info buffer
	poller_curl_done_fn on_done;        ///< Done callback
};

struct poller_curl_fd
{
	LIST_HEADER (struct poller_curl_fd)
	struct poller_fd fd;                ///< Poller FD
};

struct poller_curl
{
	struct poller *poller;              ///< Parent poller
	struct poller_timer timer;          ///< cURL timer
	CURLM *multi;                       ///< cURL multi interface handle
	struct poller_curl_fd *fds;         ///< List of all FDs

	// TODO: also make sure to dispose of them at the end of the program

	int registered;                     ///< Number of attached easy handles
};

static void
poller_curl_collect (struct poller_curl *self, curl_socket_t s, int ev_bitmask)
{
	int running = 0;
	CURLMcode res;
	// XXX: ignoring errors, in particular CURLM_CALL_MULTI_PERFORM
	if ((res = curl_multi_socket_action (self->multi, s, ev_bitmask, &running)))
		print_debug ("cURL: %s", curl_multi_strerror (res));

	CURLMsg *msg;
	while ((msg = curl_multi_info_read (self->multi, &running)))
		if (msg->msg == CURLMSG_DONE)
		{
			struct poller_curl_task *task = NULL;
			hard_assert (!curl_easy_getinfo
				(msg->easy_handle, CURLINFO_PRIVATE, &task));
			task->on_done (msg, task);
		}
}

static void
poller_curl_on_socket (const struct pollfd *pfd, void *user_data)
{
	int mask = 0;
	if (pfd->revents & POLLIN)  mask |= CURL_CSELECT_IN;
	if (pfd->revents & POLLOUT) mask |= CURL_CSELECT_OUT;
	if (pfd->revents & POLLERR) mask |= CURL_CSELECT_ERR;
	poller_curl_collect (user_data, pfd->fd, mask);
}

static int
poller_curl_on_socket_action (CURL *easy, curl_socket_t s, int what,
	void *user_data, void *socket_data)
{
	(void) easy;
	struct poller_curl *self = user_data;

	struct poller_curl_fd *fd;
	if (!(fd = socket_data))
	{
		set_cloexec (s);

		fd = xmalloc (sizeof *fd);
		LIST_PREPEND (self->fds, fd);

		fd->fd = poller_fd_make (self->poller, s);
		fd->fd.dispatcher = poller_curl_on_socket;
		fd->fd.user_data = self;
		curl_multi_assign (self->multi, s, fd);
	}
	if (what == CURL_POLL_REMOVE)
	{
		// Some annoying cURL bug.  Never trust libraries.
		fd->fd.closed = fcntl(fd->fd.fd, F_GETFL) < 0 && errno == EBADF;

		poller_fd_reset (&fd->fd);
		LIST_UNLINK (self->fds, fd);
		free (fd);
	}
	else
	{
		short events = 0;
		if (what == CURL_POLL_IN)    events = POLLIN;
		if (what == CURL_POLL_OUT)   events =          POLLOUT;
		if (what == CURL_POLL_INOUT) events = POLLIN | POLLOUT;
		poller_fd_set (&fd->fd, events);
	}
	return 0;
}

static void
poller_curl_on_timer (void *user_data)
{
	poller_curl_collect (user_data, CURL_SOCKET_TIMEOUT, 0);
}

static int
poller_curl_on_timer_change (CURLM *multi, long timeout_ms, void *user_data)
{
	(void) multi;
	struct poller_curl *self = user_data;

	if (timeout_ms < 0)
		poller_timer_reset (&self->timer);
	else
		poller_timer_set (&self->timer, timeout_ms);
	return 0;
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

static bool
poller_curl_init (struct poller_curl *self, struct poller *poller,
	struct error **e)
{
	memset (self, 0, sizeof *self);
	if (!(self->multi = curl_multi_init ()))
		return error_set (e, "cURL setup failed");

	CURLMcode mres;
	if ((mres = curl_multi_setopt (self->multi,
			CURLMOPT_SOCKETFUNCTION, poller_curl_on_socket_action))
	 || (mres = curl_multi_setopt (self->multi,
			CURLMOPT_TIMERFUNCTION, poller_curl_on_timer_change))
	 || (mres = curl_multi_setopt (self->multi, CURLMOPT_SOCKETDATA, self))
	 || (mres = curl_multi_setopt (self->multi, CURLMOPT_TIMERDATA, self)))
	{
		curl_multi_cleanup (self->multi);
		self->multi = NULL;
		return error_set (e, "%s: %s",
			"cURL setup failed", curl_multi_strerror (mres));
	}

	self->timer = poller_timer_make ((self->poller = poller));
	self->timer.dispatcher = poller_curl_on_timer;
	self->timer.user_data = self;
	return true;
}

static void
poller_curl_free (struct poller_curl *self)
{
	curl_multi_cleanup (self->multi);
	poller_timer_reset (&self->timer);

	LIST_FOR_EACH (struct poller_curl_fd, iter, self->fds)
	{
		poller_fd_reset (&iter->fd);
		free (iter);
	}
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

/// Initialize a task with a new easy instance that can be used with the poller
static bool
poller_curl_spawn (struct poller_curl_task *task, struct error **e)
{
	CURL *easy;
	if (!(easy = curl_easy_init ()))
		return error_set (e, "cURL setup failed");

	// We already take care of SIGPIPE, and native DNS timeouts are only
	// a problem for people without the AsynchDNS feature.
	//
	// Unfortunately, cURL doesn't allow custom callbacks for DNS.
	// The most we could try is parse out the hostname and provide an address
	// override for it using CURLOPT_RESOLVE.  Or be our own SOCKS4A/5 proxy.

	CURLcode res;
	if ((res = curl_easy_setopt (easy, CURLOPT_NOSIGNAL,    1L))
	 || (res = curl_easy_setopt (easy, CURLOPT_ERRORBUFFER, task->curl_error))
	 || (res = curl_easy_setopt (easy, CURLOPT_PRIVATE,     task)))
	{
		curl_easy_cleanup (easy);
		return error_set (e, "%s", curl_easy_strerror (res));
	}

	task->easy = easy;
	return true;
}

static bool
poller_curl_add (struct poller_curl *self, CURL *easy, struct error **e)
{
	CURLMcode mres;
	// "CURLMOPT_TIMERFUNCTION [...] will be called from within this function"
	if ((mres = curl_multi_add_handle (self->multi, easy)))
		return error_set (e, "%s", curl_multi_strerror (mres));
	self->registered++;
	return true;
}

static bool
poller_curl_remove (struct poller_curl *self, CURL *easy, struct error **e)
{
	CURLMcode mres;
	if ((mres = curl_multi_remove_handle (self->multi, easy)))
		return error_set (e, "%s", curl_multi_strerror (mres));
	self->registered--;
	return true;
}

// --- Compact map -------------------------------------------------------------

// MPD provides us with a hefty amount of little key-value maps.  The overhead
// of str_map for such constant (string -> string) maps is too high and it's
// much better to serialize them (mainly cache locality and memory efficiency).
//
// This isn't intended to be reusable and has case insensitivity built-in.

typedef uint8_t *compact_map_t;         ///< Compacted (string -> string) map

static compact_map_t
compact_map (struct str_map *map)
{
	struct str s = str_make ();
	struct str_map_iter iter = str_map_iter_make (map);

	char *value;
	static const size_t zero = 0, alignment = sizeof zero;
	while ((value = str_map_iter_next (&iter)))
	{
		size_t entry_len = iter.link->key_length + 1 + strlen (value) + 1;
		size_t padding_len = (alignment - entry_len % alignment) % alignment;
		entry_len += padding_len;

		str_append_data (&s, &entry_len, sizeof entry_len);
		str_append_printf (&s, "%s%c%s%c", iter.link->key, 0, value, 0);
		str_append_data (&s, &zero, padding_len);
	}
	str_append_data (&s, &zero, sizeof zero);
	return (compact_map_t) str_steal (&s);
}

static char *
compact_map_find (compact_map_t data, const char *needle)
{
	size_t entry_len;
	while ((entry_len = *(size_t *) data))
	{
		data += sizeof entry_len;
		if (!strcasecmp_ascii (needle, (const char *) data))
			return (char *) data + strlen (needle) + 1;
		data += entry_len;
	}
	return NULL;
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

struct item_list
{
	compact_map_t *items;               ///< Compacted (string -> string) maps
	size_t len;                         ///< Length
	size_t alloc;                       ///< Allocated items
};

static struct item_list
item_list_make (void)
{
	struct item_list self = {};
	self.items = xcalloc ((self.alloc = 16), sizeof *self.items);
	return self;
}

static void
item_list_free (struct item_list *self)
{
	for (size_t i = 0; i < self->len; i++)
		free (self->items[i]);
	free (self->items);
}

static bool
item_list_set (struct item_list *self, int i, struct str_map *item)
{
	if (i < 0 || (size_t) i >= self->len)
		return false;

	free (self->items[i]);
	self->items[i] = compact_map (item);
	return true;
}

static compact_map_t
item_list_get (struct item_list *self, int i)
{
	if (i < 0 || (size_t) i >= self->len || !self->items[i])
		return NULL;
	return self->items[i];
}

static void
item_list_resize (struct item_list *self, size_t len)
{
	// Make the allocated array big enough but not too large
	size_t new_alloc = self->alloc;
	while (new_alloc < len)
		new_alloc <<= 1;
	while ((new_alloc >> 1) >= len
		&& (new_alloc - len) >= 1024)
		new_alloc >>= 1;

	for (size_t i = len; i < self->len; i++)
		free (self->items[i]);
	if (new_alloc != self->alloc)
		self->items = xreallocarray (self->items,
			sizeof *self->items, (self->alloc = new_alloc));
	for (size_t i = self->len; i < len; i++)
		self->items[i] = NULL;

	self->len = len;
}

// --- Spectrum analyzer -------------------------------------------------------

// See http://www.zytrax.com/tech/audio/equalization.html
// for a good write-up about this problem domain

#ifdef WITH_FFTW

struct spectrum
{
	int sampling_rate;                  ///< Number of samples per seconds
	int channels;                       ///< Number of sampled channels
	int bits;                           ///< Number of bits per sample
	int bars;                           ///< Number of output vertical bars

	int bins;                           ///< Number of DFT bins
	int useful_bins;                    ///< Bins up to the Nyquist frequency
	int samples;                        ///< Number of windows to average
	float accumulator_scale;            ///< Scaling factor for accum. values
	int *top_bins;                      ///< Top DFT bin index for each bar
	char *rendered;                     ///< String buffer for the "render"
	float *spectrum;                    ///< The "render" as normalized floats

	void *buffer;                       ///< Input buffer
	size_t buffer_len;                  ///< Input buffer fill level
	size_t buffer_size;                 ///< Input buffer size

	/// Decode the respective part of the buffer into the second half of data
	void (*decode) (struct spectrum *, int sample);

	float *data;                        ///< Normalized audio data
	float *window;                      ///< Sampled window function
	float *windowed;                    ///< data * window
	fftwf_complex *out;                 ///< DFT output
	fftwf_plan p;                       ///< DFT plan/FFTW configuration
	float *accumulator;                 ///< Accumulated powers of samples
};

// - - Windows - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

// Out: float[n] of 0..1
static void
window_hann (float *coefficients, size_t n)
{
	for (size_t i = 0; i < n; i++)
	{
		float sine = sin (M_PI * i / n);
		coefficients[i] = sine * sine;
	}
}

// In: float[n] of -1..1, float[n] of 0..1; out: float[n] of -1..1
static void
window_apply (const float *in, const float *coefficients, float *out, size_t n)
{
	for (size_t i = 0; i < n; i++)
		out[i] = in[i] * coefficients[i];
}

// In: float[n] of 0..1; out: float 0..n, describing the coherent gain
static float
window_coherent_gain (const float *in, size_t n)
{
	float sum = 0;
	for (size_t i = 0; i < n; i++)
		sum += in[i];
	return sum;
}

// - - Decoding  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

static void
spectrum_decode_8 (struct spectrum *s, int sample)
{
	size_t n = s->useful_bins;
	float *data = s->data + n;
	for (int8_t *p = (int8_t *) s->buffer + sample * n * s->channels;
		n--; p += s->channels)
	{
		int32_t acc = 0;
		for (int ch = 0; ch < s->channels; ch++)
			acc += p[ch];
		*data++ = (float) acc / s->channels / -INT8_MIN;
	}
}

static void
spectrum_decode_16 (struct spectrum *s, int sample)
{
	size_t n = s->useful_bins;
	float *data = s->data + n;
	for (int16_t *p = (int16_t *) s->buffer + sample * n * s->channels;
		n--; p += s->channels)
	{
		int32_t acc = 0;
		for (int ch = 0; ch < s->channels; ch++)
			acc += p[ch];
		*data++ = (float) acc / s->channels / -INT16_MIN;
	}
}

static void
spectrum_decode_16_2 (struct spectrum *s, int sample)
{
	size_t n = s->useful_bins;
	float *data = s->data + n;
	for (int16_t *p = (int16_t *) s->buffer + sample * n * 2; n--; p += 2)
		*data++ = ((int32_t) p[0] + p[1]) / 2. / -INT16_MIN;
}

// - - Spectrum analysis - - - - - - - - - - - - - - - - - - - - - - - - - - - -

static const char *spectrum_bars[] =
	{ " ", "▁", "▂", "▃", "▄", "▅", "▆", "▇", "█" };

/// Assuming the input buffer is full, updates the rendered spectrum
static void
spectrum_sample (struct spectrum *s)
{
	memset (s->accumulator, 0, sizeof *s->accumulator * s->useful_bins);

	// Credit for the algorithm goes to Audacity's /src/SpectrumAnalyst.cpp,
	// apparently Welch's method
	for (int sample = 0; sample < s->samples; sample++)
	{
		// We use 50% overlap and start with data from the last run (if any)
		memmove (s->data, s->data + s->useful_bins,
			sizeof *s->data * s->useful_bins);
		s->decode (s, sample);

		window_apply (s->data, s->window, s->windowed, s->bins);
		fftwf_execute (s->p);

		for (int bin = 0; bin < s->useful_bins; bin++)
		{
			// out[0][0] is the DC component, not useful to us
			float re = s->out[bin + 1][0];
			float im = s->out[bin + 1][1];
			s->accumulator[bin] += re * re + im * im;
		}
	}

	int last_bin = 0;
	char *p = s->rendered;
	for (int bar = 0; bar < s->bars; bar++)
	{
		int top_bin = s->top_bins[bar];

		// Think of this as accumulating energies within bands,
		// so that it matches our non-linear hearing--there's no averaging.
		// For more precision, we could employ an "equal loudness contour".
		float acc = 0;
		for (int bin = last_bin; bin < top_bin; bin++)
			acc += s->accumulator[bin];

		last_bin = top_bin;
		float db = 10 * log10f (acc * s->accumulator_scale);
		if (db > 0)
			db = 0;

		// Assuming decibels are always negative (i.e., properly normalized).
		// The division defines the cutoff: 8 * 7 = 56 dB of range.
		int height = N_ELEMENTS (spectrum_bars) - 1 + (int) (db / 7);
		p += strlen (strcpy (p, spectrum_bars[MAX (height, 0)]));

		// Even with slightly the higher display resolutions provided by X11,
		// 60 dB roughly covers the useful range.
		s->spectrum[bar] = MAX (0, 1 + db / 60);
	}
}

static bool
spectrum_init (struct spectrum *s, char *format, int bars, int fps,
	struct error **e)
{
	errno = 0;

	long sampling_rate, bits, channels;
	if (!format
	 || (sampling_rate = strtol (format, &format, 10), *format++ != ':')
	 || (bits          = strtol (format, &format, 10), *format++ != ':')
	 || (channels      = strtol (format, &format, 10), *format)
	 || errno != 0)
		return error_set (e, "invalid format, expected RATE:BITS:CHANNELS");

	if (sampling_rate < 20000 || sampling_rate > INT_MAX)
		return error_set (e, "unsupported sampling rate (%ld)", sampling_rate);
	if (bits != 8 && bits != 16)
		return error_set (e, "unsupported bit count (%ld)", bits);
	if (channels < 1 || channels > INT_MAX)
		return error_set (e, "no channels to sample (%ld)", channels);
	if (bars < 1 || bars > 12)
		return error_set (e, "requested too few or too many bars (%d)", bars);

	// All that can fail henceforth is memory allocation
	*s = (struct spectrum)
	{
		.sampling_rate = sampling_rate,
		.bits          = bits,
		.channels      = channels,
		.bars          = bars,
	};

	// The number of bars is always smaller than that of the samples (bins).
	// Let's start with the equation of the top FFT bin to use for a given bar:
	//   top_bin = (num_bins + 1) ^ (bar / num_bars) - 1
	// N.b. if we didn't subtract, the power function would make this ≥ 1.
	// N.b. we then also need to extend the range by the same amount.
	//
	// We need the amount of bins for the first bar to be at least one:
	//         1 ≤ (num_bins + 1) ^   (1 / num_bars) - 1
	//
	// Solving with Wolfram Alpha gives us:
	//   num_bins ≥ (2 ^ num_bars) - 1  [for y > 0]
	//
	// And we need to remember that half of the FFT bins are useless/missing--
	// FFTW skips useless points past the Nyquist frequency.
	int necessary_bins = 2 << s->bars;

	// Discard frequencies above 20 kHz, which take up a constant ratio
	// of all bins, given by the sampling rate.  A more practical/efficient
	// solution would be to just handle 96/192/... kHz rates as bitshifts.
	//
	// Filtering out sub-20 Hz frequencies would be even more wasteful than
	// this wild DFT size, so we don't even try.  While we may just shift
	// the lowest used bin easily within the extra range provided by this
	// extension (the Nyquist is usually above 22 kHz, and it hardly matters
	// if we go a bit beyond 20 kHz in the last bin), for a small number of bars
	// the first bin already includes audible frequencies, and even for larger
	// numbers it wouldn't be too accurate.  An exact solution would require
	// having the amount of bins be strictly a factor of Nyquist / 20 (stemming
	// from the equation 20 = Nyquist / bins).  Since log2(44100 / 2 / 20) > 10,
	// it would be fairly expensive, and somewhat slowly updating.  Always.
	// (Note that you can increase window overlap to get smoother framerates,
	// but it would remain laggy.)
	double audible_ratio = s->sampling_rate / 2. / 20000;
	s->bins = ceil (necessary_bins * MAX (audible_ratio, 1));
	s->useful_bins = s->bins / 2;

	int used_bins = necessary_bins / 2;
	s->rendered = xcalloc (s->bars * 3 + 1, sizeof *s->rendered);
	s->spectrum = xcalloc (s->bars, sizeof *s->spectrum);
	s->top_bins = xcalloc (s->bars, sizeof *s->top_bins);
	for (int bar = 0; bar < s->bars; bar++)
	{
		int top_bin = floor (pow (used_bins + 1, (bar + 1.) / s->bars)) - 1;
		s->top_bins[bar] = MIN (top_bin, used_bins);
	}

	s->samples = s->sampling_rate / s->bins * 2 / MAX (fps, 1);
	if (s->samples < 1)
		s->samples = 1;

	// XXX: we average the channels but might want to average the DFT results
	if (s->bits == 8)   s->decode = spectrum_decode_8;
	if (s->bits == 16)  s->decode = spectrum_decode_16;

	// Micro-optimize to achieve some piece of mind; it's weak but measurable
	if (s->bits == 16 && s->channels == 2)
		s->decode = spectrum_decode_16_2;

	s->buffer_size = s->samples * s->useful_bins * s->bits / 8 * s->channels;
	s->buffer = xcalloc (1, s->buffer_size);

	// Prepare the window
	s->window = xcalloc (s->bins, sizeof *s->window);
	window_hann (s->window, s->bins);

	// Multiply by 2 for only using half of the DFT's result, then adjust to
	// the total energy of the window.  Both squared, because the accumulator
	// contains squared values.  Compute the average, and convert to decibels.
	// See also the mildly confusing https://dsp.stackexchange.com/a/14945.
	float coherent_gain = window_coherent_gain (s->window, s->bins);
	s->accumulator_scale = 2 * 2 / coherent_gain / coherent_gain / s->samples;

	s->data = xcalloc (s->bins, sizeof *s->data);
	s->windowed = fftw_malloc (s->bins * sizeof *s->windowed);
	s->out = fftw_malloc ((s->useful_bins + 1) * sizeof *s->out);
	s->p = fftwf_plan_dft_r2c_1d (s->bins, s->windowed, s->out, FFTW_MEASURE);
	s->accumulator = xcalloc (s->useful_bins, sizeof *s->accumulator);
	return true;
}

static void
spectrum_free (struct spectrum *s)
{
	free (s->accumulator);
	fftwf_destroy_plan (s->p);
	fftw_free (s->out);
	fftw_free (s->windowed);
	free (s->data);
	free (s->window);
#if 0
	// We don't particularly want to discard wisdom.
	fftwf_cleanup ();
#endif

	free (s->rendered);
	free (s->spectrum);
	free (s->top_bins);
	free (s->buffer);

	memset (s, 0, sizeof *s);
}

#endif  // WITH_FFTW

// --- PulseAudio --------------------------------------------------------------

#ifdef WITH_PULSE

struct pulse
{
	struct poller_timer make_context;   ///< Event to establish connection
	pa_mainloop_api *api;               ///< PulseAudio event loop proxy
	pa_context *context;                ///< PulseAudio connection context
	uint32_t sink_candidate;            ///< Used while searching for MPD
	uint32_t sink;                      ///< The relevant sink or -1
	pa_cvolume sink_volume;             ///< Current volume
	bool sink_muted;                    ///< Currently muted?

	void (*on_update) (void);           ///< Update callback
};

static void
pulse_on_sink_info (pa_context *context, const pa_sink_info *info, int eol,
	void *userdata)
{
	(void) context;
	(void) eol;

	struct pulse *self = userdata;
	if (info)
	{
		self->sink_volume = info->volume;
		self->sink_muted = !!info->mute;
		self->on_update ();
	}
}

static void
pulse_update_from_sink (struct pulse *self)
{
	if (self->sink == PA_INVALID_INDEX)
		return;

	pa_operation_unref (pa_context_get_sink_info_by_index
		(self->context, self->sink, pulse_on_sink_info, self));
}

static void
pulse_on_sink_input_info (pa_context *context,
	const struct pa_sink_input_info *info, int eol, void *userdata)
{
	(void) context;
	(void) eol;

	struct pulse *self = userdata;
	if (!info)
	{
		if ((self->sink = self->sink_candidate) != PA_INVALID_INDEX)
			pulse_update_from_sink (self);
		else
			self->on_update ();
		return;
	}

	// TODO: also save info->mute as a different mute level,
	//   and perhaps info->index (they can appear and disappear)
	const char *name =
		pa_proplist_gets (info->proplist, PA_PROP_APPLICATION_NAME);
	if (name && !strcmp (name, "Music Player Daemon"))
		self->sink_candidate = info->sink;
}

static void
pulse_read_sink_inputs (struct pulse *self)
{
	self->sink_candidate = PA_INVALID_INDEX;
	pa_operation_unref (pa_context_get_sink_input_info_list
		(self->context, pulse_on_sink_input_info, self));
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

static void
pulse_on_event (pa_context *context, pa_subscription_event_type_t event,
	uint32_t index, void *userdata)
{
	(void) context;

	struct pulse *self = userdata;
	switch (event & PA_SUBSCRIPTION_EVENT_FACILITY_MASK)
	{
	case PA_SUBSCRIPTION_EVENT_SINK_INPUT:
		pulse_read_sink_inputs (self);
		break;
	case PA_SUBSCRIPTION_EVENT_SINK:
		if (index == self->sink)
			pulse_update_from_sink (self);
	}
}

static void
pulse_on_subscribe_finish (pa_context *context, int success, void *userdata)
{
	(void) context;

	struct pulse *self = userdata;
	if (success)
		pulse_read_sink_inputs (self);
	else
	{
		print_debug ("PulseAudio failed to subscribe for events");
		self->on_update ();
		pa_context_disconnect (context);
	}
}

static void
pulse_on_context_state_change (pa_context *context, void *userdata)
{
	struct pulse *self = userdata;
	switch (pa_context_get_state (context))
	{
	case PA_CONTEXT_FAILED:
	case PA_CONTEXT_TERMINATED:
		print_debug ("PulseAudio context failed or has been terminated");

		pa_context_unref (context);
		self->context = NULL;
		self->sink = PA_INVALID_INDEX;
		self->on_update ();

		// Retry after an arbitrary delay of 5 seconds
		poller_timer_set (&self->make_context, 5000);
		break;
	case PA_CONTEXT_READY:
		pa_context_set_subscribe_callback (context, pulse_on_event, userdata);
		pa_operation_unref (pa_context_subscribe (context,
			PA_SUBSCRIPTION_MASK_SINK | PA_SUBSCRIPTION_MASK_SINK_INPUT,
			pulse_on_subscribe_finish, userdata));
	default:
		break;
	}
}

static void
pulse_make_context (void *user_data)
{
	struct pulse *self = user_data;
	self->context = pa_context_new (self->api, PROGRAM_NAME);
	pa_context_set_state_callback (self->context,
		pulse_on_context_state_change, self);
	pa_context_connect (self->context, NULL, PA_CONTEXT_NOAUTOSPAWN, NULL);
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

static void
pulse_on_finish (pa_context *context, int success, void *userdata)
{
	(void) context;
	(void) success;
	(void) userdata;

	// Just like... whatever, man
}

static bool
pulse_volume_mute (struct pulse *self)
{
	if (!self->context || self->sink == PA_INVALID_INDEX)
		return false;

	pa_operation_unref (pa_context_set_sink_mute_by_index (self->context,
		self->sink, !self->sink_muted, pulse_on_finish, self));
	return true;
}

static bool
pulse_volume_set (struct pulse *self, int arg)
{
	if (!self->context || self->sink == PA_INVALID_INDEX)
		return false;

	pa_cvolume volume = self->sink_volume;
	if (arg > 0)
		pa_cvolume_inc (&volume, (pa_volume_t)  arg * PA_VOLUME_NORM / 100);
	else
		pa_cvolume_dec (&volume, (pa_volume_t) -arg * PA_VOLUME_NORM / 100);
	pa_operation_unref (pa_context_set_sink_volume_by_index (self->context,
		self->sink, &volume, pulse_on_finish, self));
	return true;
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

static void
pulse_init (struct pulse *self, struct poller *poller)
{
	memset (self, 0, sizeof *self);
	self->sink = PA_INVALID_INDEX;
	if (!poller)
		return;

	self->api = poller_pa_new (poller);

	self->make_context = poller_timer_make (poller);
	self->make_context.dispatcher = pulse_make_context;
	self->make_context.user_data = self;
	poller_timer_set (&self->make_context, 0);
}

static void
pulse_free (struct pulse *self)
{
	if (self->context)
		pa_context_unref (self->context);
	if (self->api)
	{
		poller_pa_destroy (self->api);
		poller_timer_reset (&self->make_context);
	}

	pulse_init (self, NULL);
}

#define VOLUME_PERCENT(x) (((x) * 100 + PA_VOLUME_NORM / 2) / PA_VOLUME_NORM)

static bool
pulse_volume_status (struct pulse *self, struct str *s)
{
	if (!self->context || self->sink == PA_INVALID_INDEX
	 || !self->sink_volume.channels)
		return false;

	if (self->sink_muted)
	{
		str_append (s, "Muted");
		return true;
	}

	str_append_printf (s,
		"%u%%", VOLUME_PERCENT (self->sink_volume.values[0]));
	if (!pa_cvolume_channels_equal_to (&self->sink_volume,
			self->sink_volume.values[0]))
	{
		for (size_t i = 1; i < self->sink_volume.channels; i++)
			str_append_printf (s, " / %u%%",
				VOLUME_PERCENT (self->sink_volume.values[i]));
	}
	return true;
}

#endif  // WITH_PULSE

// --- Application -------------------------------------------------------------

// Function names are prefixed mostly because of curses, which clutters the
// global namespace and makes it harder to distinguish what functions relate to.

// The user interface is focused on conceptual simplicity.  That is important
// since we use a custom toolkit, so code would get bloated rather fast--
// especially given our TUI/GUI duality.
//
// There is an independent top pane displaying general status information,
// followed by a tab bar and a listview served by a per-tab event handler.
//
// For simplicity, the listview can only work with items that are one row high.

// Widget identification, mostly for mouse events.
enum
{
	WIDGET_NONE = 0, WIDGET_BUTTON, WIDGET_GAUGE, WIDGET_VOLUME,
	WIDGET_TAB, WIDGET_SPECTRUM, WIDGET_LIST, WIDGET_SCROLLBAR, WIDGET_MESSAGE,
};

struct layout
{
	struct widget *head;
	struct widget *tail;
};

struct app_ui
{
	struct widget *(*padding) (chtype attrs, float width, float height);
	struct widget *(*label) (chtype attrs, const char *label);
	struct widget *(*button) (chtype attrs, const char *label, enum action a);
	struct widget *(*gauge) (chtype attrs);
	struct widget *(*spectrum) (chtype attrs, int width);
	struct widget *(*scrollbar) (chtype attrs);
	struct widget *(*list) (void);
	struct widget *(*editor) (chtype attrs);

	bool have_icons;
};

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

struct tab;

/// Try to handle an action in the tab
typedef bool (*tab_action_fn) (enum action action);

/// Return a line of widgets for the row
typedef struct layout (*tab_item_layout_fn) (size_t item_index);

struct tab
{
	LIST_HEADER (struct tab)

	char *name;                         ///< Visible identifier
	char *header;                       ///< The header, should there be any

	// Implementation:

	tab_action_fn on_action;            ///< User action handler callback
	tab_item_layout_fn on_item_layout;  ///< Item layout callback

	// Provided by tab owner:

	bool can_multiselect;               ///< Multiple items can be selected
	size_t item_count;                  ///< Total item count

	// Managed by the common handler:

	int item_top;                       ///< Index of the topmost item
	int item_selected;                  ///< Index of the selected item
	int item_mark;                      ///< Multiselect second point index
};

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

enum player_state { PLAYER_STOPPED, PLAYER_PLAYING, PLAYER_PAUSED };

// Basically a container for most of the globals; no big sense in handing
// around a pointer to this, hence it is a simple global variable as well.
// There is enough global state as it is.

static struct app_context
{
	// Event loop:

	struct poller poller;               ///< Poller
	struct poller_curl poller_curl;     ///< cURL abstractor
	bool quitting;                      ///< Quit signal for the event loop
	bool polling;                       ///< The event loop is running

	struct poller_fd tty_event;         ///< Terminal input event
	struct poller_fd signal_event;      ///< Signal FD event

	struct poller_timer message_timer;  ///< Message timeout
	char *message;                      ///< Message to show in the statusbar
	char *message_detail;               ///< Non-emphasized part

	// Connection:

	struct mpd_client client;           ///< MPD client interface
	struct poller_timer connect_event;  ///< MPD reconnect timer

	enum player_state state;            ///< Player state
	struct str_map playback_info;       ///< Current song info

	struct poller_timer elapsed_event;  ///< Seconds elapsed event
	int64_t elapsed_since;              ///< Last tick ts or last elapsed time
	bool elapsed_poll;                  ///< Poll MPD for the elapsed time?

	int song;                           ///< Current song index
	int song_elapsed;                   ///< Song elapsed in seconds
	int song_duration;                  ///< Song duration in seconds
	int volume;                         ///< Current volume

	struct item_list playlist;          ///< Current playlist
	uint32_t playlist_version;          ///< Playlist version
	int playlist_time;                  ///< Play time in seconds

	// Data:

	struct config config;               ///< Program configuration
	struct strv streams;                ///< List of "name NUL URI NUL"
	struct strv enqueue;                ///< Items to enqueue once connected
	struct strv action_names;           ///< User-defined action names
	struct strv action_descriptions;    ///< User-defined action descriptions
	struct strv action_commands;        ///< User-defined action commands

	struct tab *help_tab;               ///< Special help tab
	struct tab *tabs;                   ///< All other tabs
	struct tab *active_tab;             ///< Active tab
	struct tab *last_tab;               ///< Previous tab

	// User interface:

	struct app_ui *ui;                  ///< User interface interface
	int ui_dragging;                    ///< ID of any dragged widget

#ifdef WITH_FFTW
	struct spectrum spectrum;           ///< Spectrum analyser
	int spectrum_fd;                    ///< FIFO file descriptor (non-blocking)
	struct poller_fd spectrum_event;    ///< FIFO watcher
#endif  // WITH_FFTW

#ifdef WITH_PULSE
	struct pulse pulse;                 ///< PulseAudio control
#endif  // WITH_PULSE
	bool pulse_control_requested;       ///< PulseAudio control desired by user

	struct line_editor editor;          ///< Line editor

	// Terminal:

	bool use_partial_boxes;             ///< Use Unicode box drawing chars

	struct attrs attrs[ATTRIBUTE_COUNT];
}
g;

/// Shortcut to retrieve named terminal attributes
#define APP_ATTR(name) g.attrs[ATTRIBUTE_ ## name].attrs

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

static void
tab_init (struct tab *self, const char *name)
{
	memset (self, 0, sizeof *self);

	// Add some padding for decorative purposes
	self->name = xstrdup_printf (" %s ", name);
	self->item_selected = 0;
	self->item_mark = -1;
}

static void
tab_free (struct tab *self)
{
	free (self->name);
}

static struct tab_range { int from, upto; }
tab_selection_range (struct tab *self)
{
	if (self->item_selected < 0 || !self->item_count)
		return (struct tab_range) { -1, -1 };
	if (self->item_mark < 0)
		return (struct tab_range) { self->item_selected, self->item_selected };
	return (struct tab_range) { MIN (self->item_selected, self->item_mark),
		MAX (self->item_selected, self->item_mark) };
}

// --- Configuration -----------------------------------------------------------

static void
on_poll_elapsed_time_changed (struct config_item *item)
{
	// This is only set once, on application startup
	g.elapsed_poll = item->value.boolean;
}

static void
on_pulseaudio_changed (struct config_item *item)
{
	// This is only set once, on application startup
	g.pulse_control_requested = item->value.boolean;
}

static const struct config_schema g_config_settings[] =
{
	{ .name      = "address",
	  .comment   = "Address to connect to the MPD server",
	  .type      = CONFIG_ITEM_STRING,
	  .default_  = "\"localhost\"" },
	{ .name      = "password",
	  .comment   = "Password to use for MPD authentication",
	  .type      = CONFIG_ITEM_STRING },

	// NOTE: this is unused--in theory we could allow manual metadata adjustment
	// NOTE: the "config" command may return "music_directory" for local clients
	{ .name      = "root",
	  .comment   = "Where all the files MPD is playing are located",
	  .type      = CONFIG_ITEM_STRING },

#ifdef WITH_FFTW
	{ .name      = "spectrum_path",
	  .comment   = "Visualizer feed path to a FIFO audio output",
	  .type      = CONFIG_ITEM_STRING },
	// MPD's "outputs" command doesn't include this information
	{ .name      = "spectrum_format",
	  .comment   = "Visualizer feed data format",
	  .type      = CONFIG_ITEM_STRING,
	  .default_  = "\"44100:16:2\"" },
	// 10 is about the useful limit, then it gets too computationally expensive
	{ .name      = "spectrum_bars",
	  .comment   = "Number of computed audio spectrum bars",
	  .type      = CONFIG_ITEM_INTEGER,
	  .default_  = "8" },
	{ .name      = "spectrum_fps",
	  .comment   = "Maximum frames per second, affects CPU usage",
	  .type      = CONFIG_ITEM_INTEGER,
	  .default_  = "30" },
#endif  // WITH_FFTW

#ifdef WITH_PULSE
	{ .name      = "pulseaudio",
	  .comment   = "Look up MPD in PulseAudio for improved volume controls",
	  .type      = CONFIG_ITEM_BOOLEAN,
	  .on_change = on_pulseaudio_changed,
	  .default_  = "off" },
#endif  // WITH_PULSE

#ifdef WITH_X11
	{ .name      = "x11_font",
	  .comment   = "Fontconfig name/pattern for the X11 font to use",
	  .type      = CONFIG_ITEM_STRING,
	  .default_  = "`sans\\-serif-11`" },
#endif  // WITH_X11

	// Disabling this minimises MPD traffic and has the following caveats:
	//  - when MPD stalls on retrieving audio data, we keep ticking
	//  - when the "play" succeeds in ACTION_MPD_REPLACE for the same item as
	//    is currently playing, we do not reset g.song_elapsed (we could ask
	//    for a response which feels racy, or rethink the mechanism there)
	{ .name      = "poll_elapsed_time",
	  .comment   = "Whether to actively poll MPD for the elapsed time",
	  .type      = CONFIG_ITEM_BOOLEAN,
	  .on_change = on_poll_elapsed_time_changed,
	  .default_  = "on" },
	{}
};

static const struct config_schema g_config_colors[] =
{
#define XX(name_, config, fg_, bg_, attrs_) \
	{ .name = #config, .type = CONFIG_ITEM_STRING },
	ATTRIBUTE_TABLE (XX)
#undef XX
	{}
};

static const struct config_schema g_config_actions[] =
{
	{ .name      = "description",
	  .comment   = "Human-readable description of the action",
	  .type      = CONFIG_ITEM_STRING },
	{ .name      = "command",
	  .comment   = "Shell command to run",
	  .type      = CONFIG_ITEM_STRING },
	{}
};

static const char *
get_config_string (struct config_item *root, const char *key)
{
	struct config_item *item = config_item_get (root, key, NULL);
	hard_assert (item);
	if (item->type == CONFIG_ITEM_NULL)
		return NULL;
	hard_assert (config_item_type_is_string (item->type));
	return item->value.string.str;
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

static void
load_config_settings (struct config_item *subtree, void *user_data)
{
	config_schema_apply_to_object (g_config_settings, subtree, user_data);
}

static void
load_config_colors (struct config_item *subtree, void *user_data)
{
	config_schema_apply_to_object (g_config_colors,   subtree, user_data);

	// The attributes cannot be changed dynamically right now, so it doesn't
	// make much sense to make use of "on_change" callbacks either.
	// For simplicity, we should reload the entire table on each change anyway.
	const char *value;
#define XX(name, config, fg_, bg_, attrs_) \
	if ((value = get_config_string (subtree, #config))) \
		g.attrs[ATTRIBUTE_ ## name] = attrs_decode (value);
	ATTRIBUTE_TABLE (XX)
#undef XX
}

static int
app_casecmp (const uint8_t *a, const uint8_t *b)
{
	int res;
	// XXX: this seems to produce some strange results
	if (u8_casecmp (a, strlen ((const char *) a), b, strlen ((const char *) b),
		NULL, NULL, &res))
		res = u8_strcmp (a, b);
	return res;
}

static int
strv_sort_utf8_cb (const void *a, const void *b)
{
	return app_casecmp (*(const uint8_t **) a, *(const uint8_t **) b);
}

static void
load_config_streams (struct config_item *subtree, void *user_data)
{
	(void) user_data;

	// XXX: we can't use the tab in load_config_streams() because it hasn't
	//   been initialized yet, and we cannot initialize it before the
	//   configuration has been loaded.  Thus we load it into the app_context.
	struct str_map_iter iter = str_map_iter_make (&subtree->value.object);
	struct config_item *item;
	while ((item = str_map_iter_next (&iter)))
		if (!config_item_type_is_string (item->type))
			print_warning ("`%s': stream URIs must be strings", iter.link->key);
		else
		{
			strv_append_owned (&g.streams, xstrdup_printf ("%s%c%s",
				iter.link->key, 0, item->value.string.str));
		}
	qsort (g.streams.vector, g.streams.len,
		sizeof *g.streams.vector, strv_sort_utf8_cb);
}

static void
load_config_actions (struct config_item *subtree, void *user_data)
{
	(void) user_data;

	struct str_map_iter iter = str_map_iter_make (&subtree->value.object);
	while (str_map_iter_next (&iter))
		strv_append (&g.action_names, iter.link->key);
	qsort (g.action_names.vector, g.action_names.len,
		sizeof *g.action_names.vector, strv_sort_utf8_cb);

	for (size_t i = 0; i < g.action_names.len; i++)
	{
		const char *name = g.action_names.vector[i];
		struct config_item *item = config_item_get (subtree, name, NULL);
		hard_assert (item != NULL);
		if (item->type != CONFIG_ITEM_OBJECT)
			exit_fatal ("`%s': invalid user action, expected an object", name);

		config_schema_apply_to_object (g_config_actions, item, NULL);
		config_schema_call_changed (item);

		const char *description = get_config_string (item, "description");
		const char *command =  get_config_string (item, "command");
		strv_append (&g.action_descriptions, description ? description : name);
		strv_append (&g.action_commands, command ? command : "");
	}
}

static void
app_load_configuration (void)
{
	struct config *config = &g.config;
	config_register_module (config, "settings", load_config_settings, NULL);
	config_register_module (config, "colors",   load_config_colors,   NULL);
	config_register_module (config, "streams",  load_config_streams,  NULL);
	// This must run before bindings are parsed in app_init_ui().
	config_register_module (config, "actions",  load_config_actions,  NULL);

	// Bootstrap configuration, so that we can access schema items at all
	config_load (config, config_item_object ());

	char *filename = resolve_filename
		(PROGRAM_NAME ".conf", resolve_relative_config_filename);
	if (!filename)
		return;

	struct error *e = NULL;
	struct config_item *root = config_read_from_file (filename, &e);
	free (filename);

	if (e)
	{
		print_error ("error loading configuration: %s", e->message);
		error_free (e);
		exit (EXIT_FAILURE);
	}
	if (root)
	{
		config_load (&g.config, root);
		config_schema_call_changed (g.config.root);
	}
}

// --- Application -------------------------------------------------------------

static void
app_init_attributes (void)
{
#define XX(name, config, fg_, bg_, attrs_)      \
	g.attrs[ATTRIBUTE_ ## name].fg    = fg_;    \
	g.attrs[ATTRIBUTE_ ## name].bg    = bg_;    \
	g.attrs[ATTRIBUTE_ ## name].attrs = attrs_;
	ATTRIBUTE_TABLE (XX)
#undef XX
}

static bool
app_on_insufficient_color (void)
{
	app_init_attributes ();
	return true;
}

static void
app_init_context (void)
{
	poller_init (&g.poller);
	hard_assert (poller_curl_init (&g.poller_curl, &g.poller, NULL));
	g.client = mpd_client_make (&g.poller);
	g.song_elapsed = g.song_duration = g.volume = g.song = -1;
	g.playlist = item_list_make ();
	g.config = config_make ();
	g.streams = strv_make ();
	g.enqueue = strv_make ();
	g.action_names = strv_make ();
	g.action_descriptions = strv_make ();
	g.action_commands = strv_make ();

	g.playback_info = str_map_make (free);
	g.playback_info.key_xfrm = tolower_ascii_strxfrm;

#ifdef WITH_FFTW
	g.spectrum_fd = -1;
#endif  // WITH_FFTW

#ifdef WITH_PULSE
	pulse_init (&g.pulse, NULL);
#endif  // WITH_PULSE

	app_init_attributes ();
}

static void
app_free_context (void)
{
	mpd_client_free (&g.client);
	str_map_free (&g.playback_info);
	strv_free (&g.streams);
	strv_free (&g.enqueue);
	strv_free (&g.action_names);
	strv_free (&g.action_descriptions);
	strv_free (&g.action_commands);
	item_list_free (&g.playlist);

#ifdef WITH_FFTW
	spectrum_free (&g.spectrum);
	if (g.spectrum_fd != -1)
	{
		poller_fd_reset (&g.spectrum_event);
		xclose (g.spectrum_fd);
	}
#endif  // WITH_FFTW

#ifdef WITH_PULSE
	pulse_free (&g.pulse);
#endif  // WITH_PULSE

	line_editor_free (&g.editor);

	config_free (&g.config);
	poller_curl_free (&g.poller_curl);
	poller_free (&g.poller);
	free (g.message);
	free (g.message_detail);
}

static void
app_quit (void)
{
	g.quitting = true;

	// So far there's nothing for us to wait on, so let's just stop looping;
	// otherwise we might want to e.g. cleanly bring down the MPD interface
	g.polling = false;
}

// --- Layouting ---------------------------------------------------------------

static void
app_append_layout (struct layout *l, struct layout *dest)
{
	struct widget *last = dest->tail;
	if (!last)
		*dest = *l;
	else if (l->head)
	{
		// Assuming there is no unclaimed vertical space.
		LIST_FOR_EACH (struct widget, w, l->head)
			widget_move (w, 0, last->y + last->height);

		last->next = l->head;
		l->head->prev = last;
		dest->tail = l->tail;
	}

	*l = (struct layout) {};
}

/// Replaces negative widths amongst widgets in the layout by redistributing
/// any width remaining after all positive claims are satisfied from "width".
/// Also unifies heights to the maximum value of the run.
/// Then the widths are taken as final, and used to initialize X coordinates.
static void
app_flush_layout_full (struct layout *l, int width, struct layout *dest)
{
	hard_assert (l != NULL && l->head != NULL);

	int parts = 0, max_height = 0;
	LIST_FOR_EACH (struct widget, w, l->head)
	{
		max_height = MAX (max_height, w->height);
		if (w->width < 0)
			parts -= w->width;
		else
			width -= w->width;
	}

	int remaining = MAX (width, 0), part_width = parts ? remaining / parts : 0;
	struct widget *last = NULL;
	LIST_FOR_EACH (struct widget, w, l->head)
	{
		w->height = max_height;
		if (w->width < 0)
		{
			remaining -= (w->width *= -part_width);
			last = w;
		}
	}
	if (last)
		last->width += remaining;

	int x = 0;
	LIST_FOR_EACH (struct widget, w, l->head)
	{
		widget_move (w, x - w->x, 0);
		x += w->width;
	}

	app_append_layout (l, dest);
}

static void
app_flush_layout (struct layout *l, struct layout *out)
{
	app_flush_layout_full (l, g_xui.width, out);
}

static struct widget *
app_push (struct layout *l, struct widget *w)
{
	LIST_APPEND_WITH_TAIL (l->head, l->tail, w);
	return w;
}

static struct widget *
app_push_fill (struct layout *l, struct widget *w)
{
	w->width = -1;
	LIST_APPEND_WITH_TAIL (l->head, l->tail, w);
	return w;
}

/// Write the given UTF-8 string padded with spaces.
/// @param[in] attrs  Text attributes for the text, including padding.
static void
app_layout_text (const char *str, chtype attrs, struct layout *out)
{
	struct layout l = {};
	app_push (&l, g.ui->padding (attrs, 0.25, 1));
	app_push_fill (&l, g.ui->label (attrs, str));
	app_push (&l, g.ui->padding (attrs, 0.25, 1));
	app_flush_layout (&l, out);
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

static void
app_layout_song_info (struct layout *out)
{
	compact_map_t map;
	if (!(map = item_list_get (&g.playlist, g.song)))
		return;

	chtype attrs[2] = { APP_ATTR (NORMAL), APP_ATTR (HIGHLIGHT) };

	// Split the path for files lying within MPD's "music_directory".
	const char *file = compact_map_find (map, "file");
	const char *subroot_basename = NULL;
	if (file && *file != '/' && !strstr (file, "://"))
	{
		const char *last_slash = strrchr (file, '/');
		if (last_slash)
			subroot_basename = last_slash + 1;
		else
			subroot_basename = file;
	}

	const char *title = NULL;
	const char *name = compact_map_find (map, "name");
	if ((title = compact_map_find (map, "title"))
	 || (title = name)
	 || (title = subroot_basename)
	 || (title = file))
	{
		struct layout l = {};
		app_push (&l, g.ui->padding (attrs[0], 0.25, 1));
		app_push (&l, g.ui->label (attrs[1], title));
		app_push_fill (&l, g.ui->padding (attrs[0], 0, 1));
		app_push (&l, g.ui->padding (attrs[0], 0.25, 1));
		app_flush_layout (&l, out);
	}

	// Showing a blank line is better than having the controls jump around
	// while switching between files that we do and don't have enough data for.
	struct layout l = {};
	app_push (&l, g.ui->padding (attrs[0], 0.25, 1));

	char *artist = compact_map_find (map, "artist");
	char *album  = compact_map_find (map, "album");
	if (artist || album)
	{
		if (artist)
		{
			app_push (&l, g.ui->label (attrs[0], "by "));
			app_push (&l, g.ui->label (attrs[1], artist));
		}
		if (album)
		{
			app_push (&l, g.ui->label (attrs[0], &" from "[!artist]));
			app_push (&l, g.ui->label (attrs[1], album));
		}
	}
	else if (subroot_basename && subroot_basename != file)
	{
		char *parent = xstrndup (file, subroot_basename - file - 1);
		app_push (&l, g.ui->label (attrs[0], "in "));
		app_push (&l, g.ui->label (attrs[1], parent));
		free (parent);
	}
	else if (file && *file != '/' && strstr (file, "://")
		&& name && name != title)
	{
		// This is likely to contain the name of an Internet radio.
		app_push (&l, g.ui->label (attrs[1], name));
	}

	app_push_fill (&l, g.ui->padding (attrs[0], 0, 1));
	app_push (&l, g.ui->padding (attrs[0], 0.25, 1));
	app_flush_layout (&l, out);
}

static char *
app_time_string (int seconds)
{
	int minutes = seconds / 60; seconds %= 60;
	int hours   = minutes / 60; minutes %= 60;

	struct str s = str_make ();
	if (hours)
		str_append_printf (&s, "%d:%02d:", hours, minutes);
	else
		str_append_printf (&s, "%d:", minutes);

	str_append_printf (&s, "%02d", seconds);
	return str_steal (&s);
}

static void
app_layout_status (struct layout *out)
{
	bool stopped = g.state == PLAYER_STOPPED;
	if (!stopped)
		app_layout_song_info (out);

	chtype attrs[2] = { APP_ATTR (NORMAL), APP_ATTR (HIGHLIGHT) };
	struct layout l = {};

	app_push (&l, g.ui->padding (attrs[0], 0.25, 1));
	app_push (&l, g.ui->button (attrs[!stopped], "<<", ACTION_MPD_PREVIOUS));
	app_push (&l, g.ui->padding (attrs[0], 0.5, 1));
	const char *toggle = g.state == PLAYER_PLAYING ? "||" : "|>";
	app_push (&l, g.ui->button (attrs[1], toggle, ACTION_MPD_TOGGLE));
	app_push (&l, g.ui->padding (attrs[0], 0.5, 1));
	app_push (&l, g.ui->button (attrs[!stopped], "[]", ACTION_MPD_STOP));
	app_push (&l, g.ui->padding (attrs[0], 0.5, 1));
	app_push (&l, g.ui->button (attrs[!stopped], ">>", ACTION_MPD_NEXT));
	app_push (&l, g.ui->padding (attrs[0], 1, 1));

	if (stopped)
		app_push_fill (&l, g.ui->label (attrs[0], "Stopped"));
	else
	{
		if (g.song_elapsed >= 0)
		{
			char *s = app_time_string (g.song_elapsed);
			app_push (&l, g.ui->label (attrs[0], s));
			free (s);
		}
		if (g.song_duration >= 1)
		{
			char *s = app_time_string (g.song_duration);
			app_push (&l, g.ui->label (attrs[0], " / "));
			app_push (&l, g.ui->label (attrs[0], s));
			free (s);
		}

		app_push (&l, g.ui->padding (attrs[0], 1, 1));
	}

	struct str volume = str_make ();
#ifdef WITH_PULSE
	if (g.pulse_control_requested)
	{
		if (pulse_volume_status (&g.pulse, &volume))
		{
			if (g.volume >= 0 && g.volume != 100)
				str_append_printf (&volume, " (%d%%)", g.volume);
		}
		else
		{
			if (g.volume >= 0)
				str_append_printf (&volume, "(%d%%)", g.volume);
		}
	}
	else
#endif  // WITH_PULSE
	if (g.volume >= 0)
		str_append_printf (&volume, "%3d%%", g.volume);

	if (!stopped && g.song_elapsed >= 0 && g.song_duration >= 1)
		app_push (&l, g.ui->gauge (attrs[0]))
			->id = WIDGET_GAUGE;
	else
		app_push_fill (&l, g.ui->padding (attrs[0], 0, 1));

	if (volume.len)
	{
		app_push (&l, g.ui->padding (attrs[0], 1, 1));
		app_push (&l, g.ui->label (attrs[0], volume.str))
			->id = WIDGET_VOLUME;
	}
	str_free (&volume);

	app_push (&l, g.ui->padding (attrs[0], 0.25, 1));
	app_flush_layout (&l, out);
}

static void
app_layout_tabs (struct layout *out)
{
	chtype attrs[2] = { APP_ATTR (TAB_BAR), APP_ATTR (TAB_ACTIVE) };
	struct layout l = {};

	// The help tab is disguised so that it's not too intruding
	app_push (&l, g.ui->padding (attrs[g.active_tab == g.help_tab], 0.25, 1))
		->id = WIDGET_TAB;
	app_push (&l, g.ui->label (attrs[g.active_tab == g.help_tab], APP_TITLE))
		->id = WIDGET_TAB;

	// XXX: attrs[0]?
	app_push (&l, g.ui->padding (attrs[g.active_tab == g.help_tab], 0.5, 1))
		->id = WIDGET_TAB;

	int i = 0;
	LIST_FOR_EACH (struct tab, iter, g.tabs)
	{
		struct widget *w = app_push (&l,
			g.ui->label (attrs[iter == g.active_tab], iter->name));
		w->id = WIDGET_TAB;
		w->userdata = ++i;
	}

	app_push_fill (&l, g.ui->padding (attrs[0], 1, 1));

#ifdef WITH_FFTW
	// This seems like the most reasonable, otherwise unoccupied space
	if (g.spectrum_fd != -1)
	{
		app_push (&l, g.ui->spectrum (attrs[0], g.spectrum.bars))
			->id = WIDGET_SPECTRUM;
	}
#endif  // WITH_FFTW

	app_flush_layout (&l, out);
}

static void
app_layout_padding (chtype attrs, struct layout *out)
{
	struct layout l = {};
	app_push_fill (&l, g.ui->padding (attrs, 0, 0.125));
	app_flush_layout (&l, out);
}

static void
app_layout_header (struct layout *out)
{
	if (g.client.state == MPD_CONNECTED)
	{
		app_layout_padding (APP_ATTR (NORMAL), out);
		app_layout_status (out);
		app_layout_padding (APP_ATTR (NORMAL), out);
	}

	app_layout_tabs (out);

	const char *header = g.active_tab->header;
	if (header)
		app_layout_text (header, APP_ATTR (HEADER), out);
}

/// Figure out scrollbar appearance.  @a s is the minimal slider length as well
/// as the scrollbar resolution per @a visible item.
struct scrollbar { long length, start; }
app_compute_scrollbar (struct tab *tab, long visible, long s)
{
	long top = s * tab->item_top, total = s * tab->item_count;
	if (total < visible)
		return (struct scrollbar) { 0, 0 };
	if (visible == 1)
		return (struct scrollbar) { s, 0 };
	if (visible == 2)
		return (struct scrollbar) { s, top >= total / 2 ? s : 0 };

	// Only be at the top or bottom when the top or bottom item can be seen.
	// The algorithm isn't optimal but it's a bitch to get right.
	double available_length = visible - 2 - s + 1;

	double lenf = s + available_length * visible / total, length = 0.;
	long offset = 1 + available_length * top / total + modf (lenf, &length);

	if (top == 0)
		return (struct scrollbar) { length, 0 };
	if (top + visible >= total)
		return (struct scrollbar) { length, visible - length };

	return (struct scrollbar) { length, offset };
}

static struct layout
app_layout_row (struct tab *tab, int item_index)
{
	int row_attrs = (item_index & 1) ? APP_ATTR (ODD) : APP_ATTR (EVEN);

	bool override_colors = true;
	if (item_index == tab->item_selected)
		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_xui.focused
			? APP_ATTR (MULTISELECT) : APP_ATTR (DEFOCUSED);
	else
		override_colors = false;

	// The padding must be added before the recoloring below.
	struct layout l = tab->on_item_layout (item_index);
	struct widget *w = g.ui->padding (0, 0.25, 1);
	LIST_PREPEND (l.head, w);
	app_push (&l, g.ui->padding (0, 0.25, 1));

	// Combine attributes used by the handler with the defaults.
	LIST_FOR_EACH (struct widget, w, l.head)
	{
		chtype *attrs = &w->attrs;
		if (override_colors)
			*attrs = (*attrs & ~(A_COLOR | A_REVERSE)) | row_attrs;
		else if ((*attrs & A_COLOR) && (row_attrs & A_COLOR))
			*attrs |= (row_attrs & ~A_COLOR);
		else
			*attrs |=  row_attrs;
	}
	return l;
}

static void
app_layout_view (struct layout *out, int height)
{
	struct layout l = {};
	struct widget *list = app_push_fill (&l, g.ui->list ());
	list->id = WIDGET_LIST;
	list->height = height;
	list->width = g_xui.width;

	struct tab *tab = g.active_tab;
	if ((int) tab->item_count * g_xui.vunit > list->height)
	{
		struct widget *scrollbar = g.ui->scrollbar (APP_ATTR (SCROLLBAR));
		list->width -= scrollbar->width;
		app_push (&l, scrollbar)->id = WIDGET_SCROLLBAR;
	}

	int to_show = MIN ((int) tab->item_count - tab->item_top,
		ceil ((double) list->height / g_xui.vunit));

	struct layout children = {};
	for (int row = 0; row < to_show; row++)
	{
		int item_index = tab->item_top + row;
		struct layout subl = app_layout_row (tab, item_index);
		// TODO: Change layouting so that we don't need to know list->width.
		app_flush_layout_full (&subl, list->width, &children);
	}
	list->children = children.head;

	app_flush_layout (&l, out);
}

static void
app_layout_mpd_status_playlist (struct layout *l, chtype attrs)
{
	char *songs = (g.playlist.len == 1)
		? xstrdup_printf ("1 song")
		: xstrdup_printf ("%zu songs", g.playlist.len);
	app_push (l, g.ui->label (attrs, songs));
	free (songs);

	int hours   = g.playlist_time / 3600;
	int minutes = g.playlist_time % 3600 / 60;
	if (hours || minutes)
	{
		struct str length = str_make ();
		if (hours == 1)
			str_append_printf (&length, " 1 hour");
		else if (hours)
			str_append_printf (&length, " %d hours", hours);

		if (minutes == 1)
			str_append_printf (&length, " 1 minute");
		else if (minutes)
			str_append_printf (&length, " %d minutes", minutes);

		app_push (l, g.ui->padding (attrs, 1, 1));
		app_push (l, g.ui->label (attrs, length.str + 1));
		str_free (&length);
	}

	const char *task = NULL;
	if (g.poller_curl.registered)
		task = "Downloading...";
	else if (str_map_find (&g.playback_info, "updating_db"))
		task = "Updating database...";

	if (task)
	{
		app_push (l, g.ui->padding (attrs, 1, 1));
		app_push (l, g.ui->label (attrs, task));
	}
}

static void
app_layout_mpd_status (struct layout *out)
{
	struct layout l = {};
	chtype attrs[2] = { APP_ATTR (NORMAL), APP_ATTR (HIGHLIGHT) };
	app_push (&l, g.ui->padding (attrs[0], 0.25, 1));

	if (g.active_tab->item_mark > -1)
	{
		struct tab_range r = tab_selection_range (g.active_tab);
		char *msg = xstrdup_printf (r.from == r.upto
			? "Selected %d item" : "Selected %d items", r.upto - r.from + 1);
		app_push_fill (&l, g.ui->label (attrs[0], msg));
		free (msg);
	}
	else
	{
		app_layout_mpd_status_playlist (&l, attrs[0]);
		l.tail->width = -1;
	}

	const char *s = NULL;
	struct str_map *map = &g.playback_info;
	bool repeat  = (s = str_map_find (map, "repeat"))  && strcmp (s, "0");
	bool random  = (s = str_map_find (map, "random"))  && strcmp (s, "0");
	bool single  = (s = str_map_find (map, "single"))  && strcmp (s, "0");
	bool consume = (s = str_map_find (map, "consume")) && strcmp (s, "0");

	if (g.ui->have_icons || repeat)
	{
		app_push (&l, g.ui->padding (attrs[0], 0.5, 1));
		app_push (&l,
			g.ui->button (attrs[repeat], "repeat", ACTION_MPD_REPEAT));
	}
	if (g.ui->have_icons || random)
	{
		app_push (&l, g.ui->padding (attrs[0], 0.5, 1));
		app_push (&l,
			g.ui->button (attrs[random], "random", ACTION_MPD_RANDOM));
	}
	if (g.ui->have_icons || single)
	{
		app_push (&l, g.ui->padding (attrs[0], 0.5, 1));
		app_push (&l,
			g.ui->button (attrs[single], "single", ACTION_MPD_SINGLE));
	}
	if (g.ui->have_icons || consume)
	{
		app_push (&l, g.ui->padding (attrs[0], 0.5, 1));
		app_push (&l,
			g.ui->button (attrs[consume], "consume", ACTION_MPD_CONSUME));
	}

	app_push (&l, g.ui->padding (attrs[0], 0.25, 1));
	app_flush_layout (&l, out);
}

static void
app_layout_statusbar (struct layout *out)
{
	chtype attrs[2] = { APP_ATTR (NORMAL), APP_ATTR (HIGHLIGHT) };
	app_layout_padding (attrs[0], out);

	struct layout l = {};
	if (g.message)
	{
		app_push (&l, g.ui->padding (attrs[0], 0.25, 1));
		if (!g.message_detail)
			app_push_fill (&l, g.ui->label (attrs[1], g.message));
		else
		{
			app_push (&l, g.ui->label (attrs[1], g.message));
			app_push_fill (&l, g.ui->label (attrs[0], g.message_detail));
		}
		app_push (&l, g.ui->padding (attrs[0], 0.25, 1));

		app_flush_layout (&l, out);
		LIST_FOR_EACH (struct widget, w, l.head)
			w->id = WIDGET_MESSAGE;
	}
	else if (g.editor.line)
	{
		app_push (&l, g.ui->padding (attrs[0], 0.25, 1));
		app_push (&l, g.ui->editor (attrs[1]));
		app_push (&l, g.ui->padding (attrs[0], 0.25, 1));
		app_flush_layout (&l, out);
	}
	else if (g.client.state == MPD_CONNECTED)
		app_layout_mpd_status (out);
	else if (g.client.state == MPD_CONNECTING)
		app_layout_text ("Connecting to MPD...", attrs[0], out);
	else if (g.client.state == MPD_DISCONNECTED)
		app_layout_text ("Disconnected", attrs[0], out);

	app_layout_padding (attrs[0], out);
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

static struct widget *
app_widget_by_id (int id)
{
	LIST_FOR_EACH (struct widget, w, g_xui.widgets)
		if (w->id == id)
			return w;
	return NULL;
}

static int
app_visible_items_height (void)
{
	struct widget *list = app_widget_by_id (WIDGET_LIST);
	hard_assert (list != NULL);

	// The raw number of items that would have fit on the terminal
	return MAX (0, list->height);
}

static int
app_visible_items (void)
{
	return app_visible_items_height () / g_xui.vunit;
}

/// Checks what items are visible and returns if the range was alright
static bool
app_fix_view_range (void)
{
	struct tab *tab = g.active_tab;
	if (tab->item_top < 0)
	{
		tab->item_top = 0;
		return false;
	}

	// If the contents are at least as long as the screen, always fill it
	int max_item_top = (int) tab->item_count - app_visible_items ();
	// But don't let that suggest a negative offset
	max_item_top = MAX (max_item_top, 0);

	if (tab->item_top > max_item_top)
	{
		tab->item_top = max_item_top;
		return false;
	}
	return true;
}

static void
app_layout (void)
{
	struct layout top = {}, bottom = {};
	app_layout_header (&top);
	app_layout_statusbar (&bottom);

	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;

	struct layout widgets = {};
	app_append_layout (&top, &widgets);
	app_layout_view (&widgets, available_height);
	app_append_layout (&bottom, &widgets);
	g_xui.widgets = widgets.head;

	app_fix_view_range();

	curs_set (0);
}

// --- Actions -----------------------------------------------------------------

/// Scroll down (positive) or up (negative) @a n items
static bool
app_scroll (int n)
{
	g.active_tab->item_top += n;
	xui_invalidate ();
	return app_fix_view_range ();
}

static void
app_ensure_selection_visible (void)
{
	struct tab *tab = g.active_tab;
	if (tab->item_selected < 0 || !tab->item_count)
		return;

	int too_high = tab->item_top - tab->item_selected;
	if (too_high > 0)
		app_scroll (-too_high);

	int too_low = tab->item_selected
		- (tab->item_top + app_visible_items () - 1);
	if (too_low > 0)
		app_scroll (too_low);
}

static bool
app_center_cursor (void)
{
	struct tab *tab = g.active_tab;
	if (tab->item_selected < 0 || !tab->item_count)
		return false;

	int offset = tab->item_selected - tab->item_top;
	int target = app_visible_items () / 2;
	app_scroll (offset - target);
	return true;
}

static bool
app_move_selection (int diff)
{
	struct tab *tab = g.active_tab;
	int fixed = tab->item_selected + diff;
	fixed = MIN (fixed, (int) tab->item_count - 1);
	fixed = MAX (fixed, 0);

	bool result = !diff || tab->item_selected != fixed;
	tab->item_selected = fixed;
	xui_invalidate ();

	app_ensure_selection_visible ();
	return result;
}

static void
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);
	xui_invalidate ();
}

static void
app_hide_message (void)
{
	if (!g.message)
		return;

	cstr_set (&g.message, NULL);
	cstr_set (&g.message_detail, NULL);
	poller_timer_reset (&g.message_timer);
	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);
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

static void
app_prepend_tab (struct tab *tab)
{
	LIST_PREPEND (g.tabs, tab);
	xui_invalidate ();
}

static void
app_switch_tab (struct tab *tab)
{
	if (tab == g.active_tab)
		return;

	g.last_tab = g.active_tab;
	g.active_tab = tab;
	xui_invalidate ();
}

static bool
app_goto_tab (int tab_index)
{
	int i = 0;
	LIST_FOR_EACH (struct tab, iter, g.tabs)
		if (i++ == tab_index)
		{
			app_switch_tab (iter);
			return true;
		}
	return false;
}

// --- Actions -----------------------------------------------------------------

static int
action_resolve (const char *name)
{
	for (int i = 0; i < ACTION_USER_0; i++)
		if (!strcasecmp_ascii (g_action_names[i], name))
			return i;

	// We could put this lookup first, and accordingly adjust
	// app_init_bindings() to do action_resolve(action_name(action)),
	// however the ability to override internal actions seems pointless.
	for (size_t i = 0; i < g.action_names.len; i++)
		if (!strcasecmp_ascii (g.action_names.vector[i], name))
			return ACTION_USER_0 + i;
	return -1;
}

static const char *
action_name (enum action action)
{
	if (action < ACTION_USER_0)
		return g_action_names[action];

	size_t user_action = action - ACTION_USER_0;
	hard_assert (user_action < g.action_names.len);
	return g.action_names.vector[user_action];
}

static const char *
action_description (enum action action)
{
	if (action < ACTION_USER_0)
		return g_action_descriptions[action];

	size_t user_action = action - ACTION_USER_0;
	hard_assert (user_action < g.action_descriptions.len);
	return g.action_descriptions.vector[user_action];
}

static const char *
action_command (enum action action)
{
	if (action < ACTION_USER_0)
		return NULL;

	size_t user_action = action - ACTION_USER_0;
	hard_assert (user_action < g.action_commands.len);
	return g.action_commands.vector[user_action];
}

// --- User input handling -----------------------------------------------------

static void
mpd_client_vsend_command (struct mpd_client *self, va_list ap)
{
	struct strv v = strv_make ();
	const char *command;
	while ((command = va_arg (ap, const char *)))
		strv_append (&v, command);
	mpd_client_send_commandv (self, v.vector);
	strv_free (&v);
}

/// Send a command to MPD without caring about the response
static bool mpd_client_send_simple (struct mpd_client *self, ...)
	ATTRIBUTE_SENTINEL;

static void
mpd_on_simple_response (const struct mpd_response *response,
	const struct strv *data, void *user_data)
{
	(void) data;
	(void) user_data;

	if (!response->success)
		print_error ("%s: %s", "command failed", response->message_text);
}

static bool
mpd_client_send_simple (struct mpd_client *self, ...)
{
	if (self->state != MPD_CONNECTED)
		return false;

	va_list ap;
	va_start (ap, self);
	mpd_client_vsend_command (self, ap);
	va_end (ap);

	mpd_client_add_task (self, mpd_on_simple_response, NULL);
	mpd_client_idle (self, 0);
	return true;
}

#define MPD_SIMPLE(...) \
	mpd_client_send_simple (&g.client, __VA_ARGS__, NULL)

static bool
app_setvol (int value)
{
	char *volume = xstrdup_printf ("%d", MAX (0, MIN (100, value)));
	bool result = g.volume >= 0 && MPD_SIMPLE ("setvol", volume);
	free (volume);
	return result;
}

static void
app_on_mpd_command_editor_end (bool confirmed)
{
	struct mpd_client *c = &g.client;
	if (!confirmed)
		return;

	size_t len;
	char *u8 = (char *) u32_to_u8 (g.editor.line, g.editor.len + 1, NULL, &len);
	mpd_client_send_command_raw (c, u8);
	free (u8);

	mpd_client_add_task (c, mpd_on_simple_response, NULL);
	mpd_client_idle (c, 0);
}

static size_t
incremental_search_match (const ucs4_t *needle, size_t len,
	const ucs4_t *chars, size_t chars_len)
{
	// XXX: this is slow and simplistic, but unistring is awkward to use
	size_t best = 0;
	for (size_t start = 0; start < chars_len; start++)
	{
		size_t i = 0;
		for (; i < len && start + i < chars_len; i++)
			if (uc_tolower (needle[i]) != uc_tolower (chars[start + i]))
				break;
		best = MAX (best, i);
	}
	return best;
}

static void
incremental_search_on_changed (void)
{
	struct tab *tab = g.active_tab;
	if (!tab->item_count)
		return;

	size_t best = 0, current = 0, index = MAX (tab->item_selected, 0), i = 0;
	while (i++ < tab->item_count)
	{
		struct str s = str_make ();
		LIST_FOR_EACH (struct widget, w, tab->on_item_layout (index).head)
		{
			str_append (&s, w->text);
			widget_destroy (w);
		}

		size_t len;
		ucs4_t *text = u8_to_u32 ((const uint8_t *) s.str, s.len, NULL, &len);
		str_free (&s);
		current = incremental_search_match
			(g.editor.line, g.editor.len, text, len);
		free (text);
		if (best < current)
		{
			best = current;
			tab->item_selected = index;
			app_move_selection (0);
		}
		index = (index + 1) % tab->item_count;
	}
}

static void
incremental_search_on_end (bool confirmed)
{
	(void) confirmed;
	// Required callback, nothing to do here.
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

static bool
run_command (const char *command, struct str *output, struct error **e)
{
	char *adjusted = xstrdup_printf ("2>&1 %s", command);
	print_debug ("running command: %s", adjusted);

	FILE *fp = popen (adjusted, "r");
	free (adjusted);
	if (!fp)
		return error_set (e, "%s", strerror (errno));

	char buf[BUFSIZ];
	size_t len;
	while ((len = fread (buf, 1, sizeof buf, fp)) == sizeof buf)
		str_append_data (output, buf, len);
	str_append_data (output, buf, len);

	int status = pclose (fp);
	if (status < 0)
		return error_set (e, "%s", strerror (errno));
	if (WIFEXITED (status) && WEXITSTATUS (status))
		return error_set (e, "exit status %d", WEXITSTATUS (status));
	if (WIFSIGNALED (status))
		return error_set (e, "terminated on signal %d", WTERMSIG (status));
	if (WIFSTOPPED (status))
		return error_set (e, "stopped on signal %d", WSTOPSIG (status));
	return true;
}

static bool
app_process_action_command (enum action action)
{
	const char *command = action_command (action);
	if (!command)
		return false;

	struct str output = str_make ();
	struct error *error = NULL;
	(void) run_command (command, &output, &error);
	str_enforce_utf8 (&output);

	struct strv lines = strv_make ();
	cstr_split (output.str, "\r\n", false, &lines);
	str_free (&output);
	while (lines.len && !*lines.vector[lines.len - 1])
		free (strv_steal (&lines, lines.len - 1));
	for (size_t i = 0; i < lines.len; i++)
		print_debug ("output: %s", lines.vector[i]);
	strv_free (&lines);

	if (error)
	{
		print_error ("\"%s\": %s", action_description (action), error->message);
		error_free (error);
	}
	return true;
}

static bool
app_mpd_toggle (const char *name)
{
	const char *s = str_map_find (&g.playback_info, name);
	bool value = s && strcmp (s, "0");
	return MPD_SIMPLE (name, value ? "0" : "1");
}

static bool
app_process_action (enum action action)
{
	// First let the tab try to handle this
	struct tab *tab = g.active_tab;
	if (tab->on_action && tab->on_action (action))
	{
		xui_invalidate ();
		return true;
	}

	switch (action)
	{
	case ACTION_NONE:
		return true;
	case ACTION_QUIT:
		app_quit ();
		return true;
	case ACTION_REDRAW:
		clear ();
		xui_invalidate ();
		return true;

	case ACTION_ABORT:
		// It is a pseudomode, avoid surprising the user
		if (tab->item_mark > -1)
		{
			tab->item_mark = -1;
			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;
		xui_invalidate ();
		app_hide_message ();
		return true;

	case ACTION_MULTISELECT:
		if (!tab->can_multiselect
		 || !tab->item_count || tab->item_selected < 0)
			return false;

		xui_invalidate ();
		if (tab->item_mark > -1)
			tab->item_mark = -1;
		else
			tab->item_mark = tab->item_selected;
		return true;
	case ACTION_INCREMENTAL_SEARCH:
		line_editor_start (&g.editor, '/');
		g.editor.on_changed = incremental_search_on_changed;
		g.editor.on_end = incremental_search_on_end;
		xui_invalidate ();
		app_hide_message ();
		return true;

	case ACTION_TAB_LAST:
		if (!g.last_tab)
			return false;
		app_switch_tab (g.last_tab);
		return true;
	case ACTION_TAB_HELP:
		app_switch_tab (g.help_tab);
		return true;
	case ACTION_TAB_PREVIOUS:
		if (g.active_tab == g.help_tab)
			return false;
		if (!g.active_tab->prev)
			app_switch_tab (g.help_tab);
		else
			app_switch_tab (g.active_tab->prev);
		return true;
	case ACTION_TAB_NEXT:
		if (g.active_tab == g.help_tab)
			app_switch_tab (g.tabs);
		else if (g.active_tab->next)
			app_switch_tab (g.active_tab->next);
		else
			return false;
		return true;

	case ACTION_MPD_TOGGLE:
		if (g.state == PLAYER_PLAYING)  return MPD_SIMPLE ("pause", "1");
		if (g.state == PLAYER_PAUSED)   return MPD_SIMPLE ("pause", "0");
		return MPD_SIMPLE ("play");
	case ACTION_MPD_STOP:               return MPD_SIMPLE ("stop");
	case ACTION_MPD_PREVIOUS:           return MPD_SIMPLE ("previous");
	case ACTION_MPD_NEXT:               return MPD_SIMPLE ("next");
	case ACTION_MPD_FORWARD:            return MPD_SIMPLE ("seekcur", "+10");
	case ACTION_MPD_BACKWARD:           return MPD_SIMPLE ("seekcur", "-10");
	case ACTION_MPD_REPEAT:             return app_mpd_toggle ("repeat");
	case ACTION_MPD_RANDOM:             return app_mpd_toggle ("random");
	case ACTION_MPD_SINGLE:             return app_mpd_toggle ("single");
	case ACTION_MPD_CONSUME:            return app_mpd_toggle ("consume");
	case ACTION_MPD_UPDATE_DB:          return MPD_SIMPLE ("update");

	case ACTION_MPD_VOLUME_UP:          return app_setvol (g.volume + 5);
	case ACTION_MPD_VOLUME_DOWN:        return app_setvol (g.volume - 5);

#ifdef WITH_PULSE
	case ACTION_PULSE_VOLUME_UP:        return pulse_volume_set (&g.pulse, +5);
	case ACTION_PULSE_VOLUME_DOWN:      return pulse_volume_set (&g.pulse, -5);
	case ACTION_PULSE_MUTE:             return pulse_volume_mute (&g.pulse);
#endif  // WITH_PULSE

		// XXX: these two should rather be parametrized
	case ACTION_SCROLL_UP:              return app_scroll (-3);
	case ACTION_SCROLL_DOWN:            return app_scroll (+3);
	case ACTION_CENTER_CURSOR:          return app_center_cursor ();

	case ACTION_GOTO_TOP:
		if (tab->item_count)
		{
			g.active_tab->item_selected = 0;
			app_ensure_selection_visible ();
			xui_invalidate ();
		}
		return true;
	case ACTION_GOTO_BOTTOM:
		if (tab->item_count)
		{
			g.active_tab->item_selected =
				MAX (0, (int) g.active_tab->item_count - 1);
			app_ensure_selection_visible ();
			xui_invalidate ();
		}
		return true;

	case ACTION_GOTO_ITEM_PREVIOUS:     return app_move_selection (-1);
	case ACTION_GOTO_ITEM_NEXT:         return app_move_selection (1);

	case ACTION_GOTO_PAGE_PREVIOUS:
		app_scroll (-app_visible_items ());
		return app_move_selection (-app_visible_items ());
	case ACTION_GOTO_PAGE_NEXT:
		app_scroll (app_visible_items ());
		return app_move_selection (app_visible_items ());

	case ACTION_GOTO_VIEW_TOP:
		g.active_tab->item_selected = g.active_tab->item_top;
		return app_move_selection (0);
	case ACTION_GOTO_VIEW_CENTER:
		g.active_tab->item_selected = g.active_tab->item_top;
		return app_move_selection (MAX (0, app_visible_items () / 2 - 1));
	case ACTION_GOTO_VIEW_BOTTOM:
		g.active_tab->item_selected = g.active_tab->item_top;
		return app_move_selection (MAX (0, app_visible_items () - 1));

	default:
		if (app_process_action_command (action))
			return true;

		print_error ("\"%s\" is not allowed here", action_description (action));
		return false;
	}
}

static bool
app_editor_process_action (enum action action)
{
	xui_invalidate ();
	switch (action)
	{
	case ACTION_ABORT:
		line_editor_abort (&g.editor, false);
		g.editor.on_end = NULL;
		return true;
	case ACTION_EDITOR_CONFIRM:
		line_editor_abort (&g.editor, true);
		g.editor.on_end = NULL;
		return true;
	default:
		print_error ("\"%s\" is not allowed here", action_description (action));
		return false;

	case ACTION_EDITOR_B_CHAR:
		return line_editor_action (&g.editor, LINE_EDITOR_B_CHAR);
	case ACTION_EDITOR_F_CHAR:
		return line_editor_action (&g.editor, LINE_EDITOR_F_CHAR);
	case ACTION_EDITOR_B_WORD:
		return line_editor_action (&g.editor, LINE_EDITOR_B_WORD);
	case ACTION_EDITOR_F_WORD:
		return line_editor_action (&g.editor, LINE_EDITOR_F_WORD);
	case ACTION_EDITOR_HOME:
		return line_editor_action (&g.editor, LINE_EDITOR_HOME);
	case ACTION_EDITOR_END:
		return line_editor_action (&g.editor, LINE_EDITOR_END);

	case ACTION_EDITOR_UPCASE_WORD:
		return line_editor_action (&g.editor, LINE_EDITOR_UPCASE_WORD);
	case ACTION_EDITOR_DOWNCASE_WORD:
		return line_editor_action (&g.editor, LINE_EDITOR_DOWNCASE_WORD);
	case ACTION_EDITOR_CAPITALIZE_WORD:
		return line_editor_action (&g.editor, LINE_EDITOR_CAPITALIZE_WORD);

	case ACTION_EDITOR_B_DELETE:
		return line_editor_action (&g.editor, LINE_EDITOR_B_DELETE);
	case ACTION_EDITOR_F_DELETE:
		return line_editor_action (&g.editor, LINE_EDITOR_F_DELETE);
	case ACTION_EDITOR_B_KILL_WORD:
		return line_editor_action (&g.editor, LINE_EDITOR_B_KILL_WORD);
	case ACTION_EDITOR_B_KILL_LINE:
		return line_editor_action (&g.editor, LINE_EDITOR_B_KILL_LINE);
	case ACTION_EDITOR_F_KILL_LINE:
		return line_editor_action (&g.editor, LINE_EDITOR_F_KILL_LINE);
	}
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

// Carefully chosen to limit the possibility of ever hitting termo keymods.
enum { APP_KEYMOD_DOUBLE_CLICK = 1 << 15 };

static bool
app_process_left_mouse_click (struct widget *w, int x, int y, int modifiers)
{
	switch (w->id)
	{
	case WIDGET_BUTTON:
		app_process_action (w->userdata);
		break;
	case WIDGET_GAUGE:
	{
		// TODO: We should avoid queuing up too many.
		float position = (float) x / w->width;
		if (g.song_duration >= 1)
		{
			char *where = xstrdup_printf ("%f", position * g.song_duration);
			MPD_SIMPLE ("seekcur", where);
			free (where);
		}
		break;
	}
	case WIDGET_TAB:
	{
		struct tab *tab = g.help_tab;
		int i = 0;
		LIST_FOR_EACH (struct tab, iter, g.tabs)
			if (++i == w->userdata)
				tab = iter;

		app_switch_tab (tab);
		break;
	}
	case WIDGET_LIST:
	{
		struct tab *tab = g.active_tab;
		int row_index = y / g_xui.vunit;
		if (row_index < 0
		 || row_index >= (int) tab->item_count - tab->item_top)
			return false;

		if (!(modifiers & TERMO_KEYMOD_SHIFT))
			tab->item_mark = -1;
		else if (!tab->can_multiselect || tab->item_selected < 0)
			return false;
		else if (tab->item_mark < 0)
			tab->item_mark = tab->item_selected;

		tab->item_selected = row_index + tab->item_top;
		app_ensure_selection_visible ();
		xui_invalidate ();

		if (modifiers & APP_KEYMOD_DOUBLE_CLICK)
			app_process_action (ACTION_CHOOSE);
		break;
	}
	case WIDGET_SCROLLBAR:
	{
		struct tab *tab = g.active_tab;
		int visible_items = app_visible_items ();
		tab->item_top = (double) y / w->height
			* (int) tab->item_count - visible_items / 2;
		xui_invalidate ();
		app_fix_view_range ();
		break;
	}
	case WIDGET_MESSAGE:
		app_hide_message ();
	}
	return true;
}

static bool
app_process_mouse (termo_mouse_event_t type, int x, int y, int button,
	int modifiers)
{
	// XXX: Terminals don't let us know which button has been released,
	//   so we can't press buttons at that point.  We'd need a special "click"
	//   event handler that could be handled better under X11.
	if (type == TERMO_MOUSE_RELEASE)
	{
		g.ui_dragging = WIDGET_NONE;
		return true;
	}

	if (type == TERMO_MOUSE_DRAG)
	{
		if (g.ui_dragging != WIDGET_GAUGE
		 && g.ui_dragging != WIDGET_SCROLLBAR)
			return true;

		struct widget *target = app_widget_by_id (g.ui_dragging);
		x -= target->x;
		y -= target->y;
		return app_process_left_mouse_click (target, x, y, modifiers);
	}

	if (g.editor.line)
	{
		line_editor_abort (&g.editor, false);
		xui_invalidate ();
	}

	struct widget *target = NULL;
	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;
	if (!target)
		return false;

	x -= target->x;
	y -= target->y;
	switch (button)
	{
	case 1:
		g.ui_dragging = target->id;
		return app_process_left_mouse_click (target, x, y, modifiers);
	case 4:
		switch (target->id)
		{
		case WIDGET_LIST:
			return app_process_action (ACTION_SCROLL_UP);
		case WIDGET_VOLUME:
			return app_process_action (
#ifdef WITH_PULSE
				g.pulse_control_requested ? ACTION_PULSE_VOLUME_UP :
#endif  // WITH_PULSE
				ACTION_MPD_VOLUME_UP);
		case WIDGET_GAUGE:
			return app_process_action (ACTION_MPD_FORWARD);
		}
		break;
	case 5:
		switch (target->id)
		{
		case WIDGET_LIST:
			return app_process_action (ACTION_SCROLL_DOWN);
		case WIDGET_VOLUME:
			return app_process_action (
#ifdef WITH_PULSE
				g.pulse_control_requested ? ACTION_PULSE_VOLUME_DOWN :
#endif  // WITH_PULSE
				ACTION_MPD_VOLUME_DOWN);
		case WIDGET_GAUGE:
			return app_process_action (ACTION_MPD_BACKWARD);
		}
		break;
	}
	return false;
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

static struct binding
{
	termo_key_t decoded;                ///< Decoded key definition
	enum action action;                 ///< Action to take
	int order;                          ///< Order for stable sorting
}
*g_normal_keys, *g_editor_keys;
static size_t g_normal_keys_len, g_editor_keys_len;

static struct binding_default
{
	const char *key;                    ///< Key definition
	enum action action;                 ///< Action to take
}
g_normal_defaults[] =
{
	{ "q",          ACTION_QUIT               },
	{ "C-l",        ACTION_REDRAW             },
	{ "Escape",     ACTION_ABORT              },
	{ "M-Tab",      ACTION_TAB_LAST           },
	{ "F1",         ACTION_TAB_HELP           },
	{ "S-Tab",      ACTION_TAB_PREVIOUS       },
	{ "Tab",        ACTION_TAB_NEXT           },
	{ "C-Left",     ACTION_TAB_PREVIOUS       },
	{ "C-Right",    ACTION_TAB_NEXT           },
	{ "C-PageUp",   ACTION_TAB_PREVIOUS       },
	{ "C-PageDown", ACTION_TAB_NEXT           },

	{ "o",          ACTION_GOTO_PLAYING       },
	{ "Home",       ACTION_GOTO_TOP           },
	{ "End",        ACTION_GOTO_BOTTOM        },
	{ "M-<",        ACTION_GOTO_TOP           },
	{ "M->",        ACTION_GOTO_BOTTOM        },
	{ "g",          ACTION_GOTO_TOP           },
	{ "G",          ACTION_GOTO_BOTTOM        },
	{ "S-Up",       ACTION_MOVE_UP            },
	{ "S-Down",     ACTION_MOVE_DOWN          },
	{ "Up",         ACTION_GOTO_ITEM_PREVIOUS },
	{ "Down",       ACTION_GOTO_ITEM_NEXT     },
	{ "k",          ACTION_GOTO_ITEM_PREVIOUS },
	{ "j",          ACTION_GOTO_ITEM_NEXT     },
	{ "PageUp",     ACTION_GOTO_PAGE_PREVIOUS },
	{ "PageDown",   ACTION_GOTO_PAGE_NEXT     },
	{ "C-p",        ACTION_GOTO_ITEM_PREVIOUS },
	{ "C-n",        ACTION_GOTO_ITEM_NEXT     },
	{ "C-b",        ACTION_GOTO_PAGE_PREVIOUS },
	{ "C-f",        ACTION_GOTO_PAGE_NEXT     },
	{ "C-y",        ACTION_SCROLL_UP          },
	{ "C-e",        ACTION_SCROLL_DOWN        },
	{ "z",          ACTION_CENTER_CURSOR      },

	{ "H",          ACTION_GOTO_VIEW_TOP      },
	{ "M",          ACTION_GOTO_VIEW_CENTER   },
	{ "L",          ACTION_GOTO_VIEW_BOTTOM   },

	// Not sure how to set these up, they're pretty arbitrary so far
	{ "Enter",      ACTION_CHOOSE             },
	{ "Delete",     ACTION_DELETE             },
	{ "d",          ACTION_DELETE             },
	{ "?",          ACTION_DESCRIBE           },
	{ "M-Up",       ACTION_UP                 },
	{ "Backspace",  ACTION_UP                 },
	{ "v",          ACTION_MULTISELECT        },
	{ "C-s",        ACTION_INCREMENTAL_SEARCH },
	{ "/",          ACTION_MPD_SEARCH         },
	{ "a",          ACTION_MPD_ADD            },
	{ "r",          ACTION_MPD_REPLACE        },
	{ ":",          ACTION_MPD_COMMAND        },

	{ "<",          ACTION_MPD_PREVIOUS       },
	{ ">",          ACTION_MPD_NEXT           },
	{ "Left",       ACTION_MPD_PREVIOUS       },
	{ "Right",      ACTION_MPD_NEXT           },
	{ "M-Left",     ACTION_MPD_BACKWARD       },
	{ "M-Right",    ACTION_MPD_FORWARD        },
	{ "h",          ACTION_MPD_PREVIOUS       },
	{ "l",          ACTION_MPD_NEXT           },
	{ "Space",      ACTION_MPD_TOGGLE         },
	{ "C-Space",    ACTION_MPD_STOP           },
	{ "u",          ACTION_MPD_UPDATE_DB      },
	{ "+",          ACTION_MPD_VOLUME_UP      },
	{ "-",          ACTION_MPD_VOLUME_DOWN    },
},
g_editor_defaults[] =
{
	{ "C-g",        ACTION_ABORT              },
	{ "Escape",     ACTION_ABORT              },
	{ "Enter",      ACTION_EDITOR_CONFIRM     },

	{ "Left",       ACTION_EDITOR_B_CHAR      },
	{ "Right",      ACTION_EDITOR_F_CHAR      },
	{ "C-b",        ACTION_EDITOR_B_CHAR      },
	{ "C-f",        ACTION_EDITOR_F_CHAR      },
	{ "M-b",        ACTION_EDITOR_B_WORD      },
	{ "M-f",        ACTION_EDITOR_F_WORD      },
	{ "Home",       ACTION_EDITOR_HOME        },
	{ "End",        ACTION_EDITOR_END         },
	{ "C-a",        ACTION_EDITOR_HOME        },
	{ "C-e",        ACTION_EDITOR_END         },

	{ "M-u",        ACTION_EDITOR_UPCASE_WORD     },
	{ "M-l",        ACTION_EDITOR_DOWNCASE_WORD   },
	{ "M-c",        ACTION_EDITOR_CAPITALIZE_WORD },

	{ "C-h",        ACTION_EDITOR_B_DELETE    },
	{ "DEL",        ACTION_EDITOR_B_DELETE    },
	{ "Backspace",  ACTION_EDITOR_B_DELETE    },
	{ "C-d",        ACTION_EDITOR_F_DELETE    },
	{ "Delete",     ACTION_EDITOR_F_DELETE    },
	{ "C-u",        ACTION_EDITOR_B_KILL_LINE },
	{ "C-k",        ACTION_EDITOR_F_KILL_LINE },
	{ "C-w",        ACTION_EDITOR_B_KILL_WORD },
};

static int
app_binding_cmp (const void *a, const void *b)
{
	const struct binding *aa = a, *bb = b;
	int cmp = termo_keycmp (g_xui.tk, &aa->decoded, &bb->decoded);
	return cmp ? cmp : bb->order - aa->order;
}

static bool
app_next_binding (struct str_map_iter *iter, termo_key_t *key, int *action)
{
	struct config_item *v;
	while ((v = str_map_iter_next (iter)))
	{
		*action = ACTION_NONE;
		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)
			return true;
		else if (v->type != CONFIG_ITEM_STRING)
			print_error ("%s: bindings must be strings", iter->link->key);
		else if ((*action = action_resolve (v->value.string.str)) >= 0)
			return true;
		else
			print_error ("%s: unknown action: %s",
				iter->link->key, v->value.string.str);
	}
	return false;
}

static struct binding *
app_init_bindings (const char *keymap,
	struct binding_default *defaults, size_t defaults_len, size_t *result_len)
{
	ARRAY (struct binding, a)
	ARRAY_INIT_SIZED (a, defaults_len);

	// Order for stable sorting
	size_t order = 0;

	termo_key_t decoded;
	for (size_t i = 0; i < defaults_len; i++)
	{
		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++ };
	}

	struct config_item *root = config_item_get (g.config.root, keymap, NULL);
	if (root && root->type == CONFIG_ITEM_OBJECT)
	{
		struct str_map_iter iter = str_map_iter_make (&root->value.object);
		ARRAY_RESERVE (a, iter.map->len);

		int action;
		while (app_next_binding (&iter, &decoded, &action))
			a[a_len++] = (struct binding) { decoded, action, order++ };
	}

	// Use the helper field to use the last mappings of identical bindings
	size_t out = 0;
	qsort (a, a_len, sizeof *a, app_binding_cmp);
	for (size_t in = 0; in < a_len; in++)
	{
		a[in].order = 0;
		if (!out
		 || termo_keycmp (g_xui.tk, &a[in].decoded, &a[out - 1].decoded))
			a[out++] = a[in];
	}

	*result_len = out;
	return a;
}

static char *
app_strfkey (const termo_key_t *key)
{
	// For display purposes, this is highly desirable
	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_xui.tk, &fixed);
	termo_set_flags (g_xui.tk, flags);

	char buf[16] = "";
	termo_strfkey_utf8 (g_xui.tk,
		buf, sizeof buf, &fixed, TERMO_FORMAT_ALTISMETA);
	return xstrdup (buf);
}

static bool
app_process_termo_event (termo_key_t *event)
{
	char *formatted = app_strfkey (event);
	print_debug ("%s", formatted);
	free (formatted);

	bool handled = false;
	if ((handled = event->type == TERMO_TYPE_FOCUS))
	{
		xui_invalidate ();
		// Senseless fall-through
	}

	struct binding dummy = { *event, 0, 0 }, *binding;
	if (g.editor.line)
	{
		if (event->type == TERMO_TYPE_KEY
		 || event->type == TERMO_TYPE_FUNCTION
		 || event->type == TERMO_TYPE_KEYSYM)
			app_hide_message ();

		if ((binding = bsearch (&dummy, g_editor_keys, g_editor_keys_len,
			sizeof *binding, app_binding_cmp)))
			return app_editor_process_action (binding->action);
		if (event->type != TERMO_TYPE_KEY || event->modifiers != 0)
			return handled;

		line_editor_insert (&g.editor, event->code.codepoint);
		xui_invalidate ();
		return true;
	}
	if ((binding = bsearch (&dummy, g_normal_keys, g_normal_keys_len,
		sizeof *binding, app_binding_cmp)))
		return app_process_action (binding->action);

	// TODO: parametrize actions, put this among other bindings
	if (!(event->modifiers & ~TERMO_KEYMOD_ALT)
	 && event->code.codepoint >= '0'
	 && event->code.codepoint <= '9')
	{
		int n = event->code.codepoint - '0';
		if (app_goto_tab ((n == 0 ? 10 : n) - 1))
			return true;
	}
	return handled;
}

// --- Current tab -------------------------------------------------------------

static struct tab g_current_tab;

static struct layout
current_tab_on_item_layout (size_t item_index)
{
	// TODO: configurable output, maybe dynamically sized columns
	compact_map_t map = item_list_get (&g.playlist, item_index);
	const char *artist = compact_map_find (map, "artist");
	const char *title  = compact_map_find (map, "title");

	chtype attrs = (int) item_index == g.song ? A_BOLD : 0;
	struct layout l = {};
	if (artist && title)
	{
		char *joined = xstrdup_printf ("%s - %s", artist, title);
		app_push_fill (&l, g.ui->label (attrs, joined));
		free (joined);
	}
	else
		app_push_fill (&l, g.ui->label (attrs, compact_map_find (map, "file")));

	int duration = -1;
	mpd_read_time (compact_map_find (map, "duration"), &duration, NULL);
	mpd_read_time (compact_map_find (map, "time"),     &duration, NULL);

	char *s = duration < 0 ? xstrdup ("-") : app_time_string (duration);
	app_push (&l, g.ui->padding (attrs, 1, 1));
	app_push (&l, g.ui->label (attrs, s));
	free (s);

	return l;
}

static void
mpd_on_move_response (const struct mpd_response *response,
	const struct strv *data, void *user_data)
{
	(void) data;

	*(bool *) user_data = false;
	if (!response->success)
		print_error ("%s: %s", "command failed", response->message_text);
}

static void
current_tab_move (int from, int to)
{
	compact_map_t map;
	const char *id;
	if (!(map = item_list_get (&g.playlist, from))
	 || !(id = compact_map_find (map, "id")))
		return;

	char *target_str = xstrdup_printf ("%d", to);
	mpd_client_send_command (&g.client, "moveid", id, target_str, NULL);
	free (target_str);
}

static bool
current_tab_move_selection (int diff)
{
	static bool already_moving;
	if (already_moving || diff == 0)
		return true;

	struct mpd_client *c = &g.client;
	if (c->state != MPD_CONNECTED)
		return false;

	struct tab *tab = &g_current_tab;
	struct tab_range range = tab_selection_range (tab);
	if (range.from + diff < 0
	 || range.upto + diff >= (int) tab->item_count)
		return false;

	mpd_client_list_begin (c);
	if (diff < 0)
		for (int i = range.from; i <= range.upto; i++)
			current_tab_move (i, i + diff);
	else
		for (int i = range.upto; i >= range.from; i--)
			current_tab_move (i, i + diff);
	mpd_client_list_end (c);

	mpd_client_add_task (c, mpd_on_move_response, &already_moving);
	mpd_client_idle (c, 0);
	return already_moving = true;
}

static bool
current_tab_on_action (enum action action)
{
	struct tab *tab = &g_current_tab;
	compact_map_t map = item_list_get (&g.playlist, tab->item_selected);
	switch (action)
	{
		const char *id;
	case ACTION_GOTO_PLAYING:
		if (g.song < 0 || (size_t) g.song >= tab->item_count)
			return false;

		tab->item_selected = g.song;
		app_ensure_selection_visible ();
		return true;
	case ACTION_MOVE_UP:
		return current_tab_move_selection (-1);
	case ACTION_MOVE_DOWN:
		return current_tab_move_selection (+1);
	case ACTION_CHOOSE:
		tab->item_mark = -1;
		return map && (id = compact_map_find (map, "id"))
			&& MPD_SIMPLE ("playid", id);
	case ACTION_DESCRIBE:
		if (!map || !(id = compact_map_find (map, "file")))
			return false;

		app_show_message (xstrdup ("Path: "), xstrdup (id));
		return true;
	case ACTION_DELETE:
	{
		struct mpd_client *c = &g.client;
		struct tab_range range = tab_selection_range (tab);
		if (range.from < 0 || c->state != MPD_CONNECTED)
			return false;

		mpd_client_list_begin (c);
		for (int i = range.from; i <= range.upto; i++)
		{
			if ((map = item_list_get (&g.playlist, i))
			 && (id = compact_map_find (map, "id")))
				mpd_client_send_command (c, "deleteid", id, NULL);
		}
		mpd_client_list_end (c);
		mpd_client_add_task (c, mpd_on_simple_response, NULL);
		mpd_client_idle (c, 0);
		return true;
	}
	default:
		return false;
	}
}

static void
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);
	xui_invalidate ();
}

static struct tab *
current_tab_init (void)
{
	struct tab *super = &g_current_tab;
	tab_init (super, "Current");
	super->can_multiselect = true;
	super->on_action = current_tab_on_action;
	super->on_item_layout = current_tab_on_item_layout;
	return super;
}

// --- Library tab -------------------------------------------------------------

struct library_level
{
	LIST_HEADER (struct library_level)

	int item_top;                       ///< Stored state
	int item_selected;                  ///< Stored state
	char path[];                        ///< Path of the level
};

enum
{
	// This list is also ordered by ASCII and important for sorting

	LIBRARY_ROOT     = '/',             ///< Root entry
	LIBRARY_UP       = '^',             ///< Upper directory
	LIBRARY_DIR      = 'd',             ///< Directory
	LIBRARY_FILE     = 'f',             ///< File
	LIBRARY_PLAYLIST = 'p',             ///< Playlist (unsupported)
};

struct library_tab_item
{
	int type;                           ///< Type of the item
	int duration;                       ///< Duration or -1 if N/A or unknown
	char *name;                         ///< Visible name
	const char *path;                   ///< MPD path (follows the name)
};

static struct
{
	struct tab super;                   ///< Parent class
	struct str path;                    ///< Current path
	struct library_level *above;        ///< Upper levels

	/// Current items
	ARRAY (struct library_tab_item, items)

	bool searching;                     ///< Search mode is active
}
g_library_tab;

static void
library_tab_add (int type, int duration, const char *name, const char *path)
{
	// Slightly reduce memory overhead while retaining friendly access
	size_t name_len = strlen (name), path_len = strlen (path);
	char *combined = xmalloc (++name_len + ++path_len);

	ARRAY_RESERVE (g_library_tab.items, 1);
	g_library_tab.items[g_library_tab.items_len++] = (struct library_tab_item)
	{
		.type = type,
		.duration = duration,
		.name = memcpy (combined, name, name_len),
		.path = memcpy (combined + name_len, path, path_len),
	};
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

static struct layout
library_tab_on_item_layout (size_t item_index)
{
	hard_assert (item_index < g_library_tab.items_len);

	struct library_tab_item *x = &g_library_tab.items[item_index];
	const char *prefix, *name;
	switch (x->type)
	{
	case LIBRARY_ROOT: prefix = "/"; name = "";      break;
	case LIBRARY_UP:   prefix = "/"; name = "..";    break;
	case LIBRARY_DIR:  prefix = "/"; name = x->name; break;
	case LIBRARY_FILE: prefix = " "; name = x->name; break;
	default:           hard_assert (!"invalid item type");
	}

	chtype attrs = x->type != LIBRARY_FILE ? APP_ATTR (DIRECTORY) : 0;
	struct layout l = {};

	app_push (&l, g.ui->label (attrs, prefix));
	app_push_fill (&l, g.ui->label (attrs, name));

	if (x->duration >= 0)
	{
		char *s = app_time_string (x->duration);
		app_push (&l, g.ui->padding (0, 1, 1));
		app_push (&l, g.ui->label (attrs, s));
		free (s);
	}
	return l;
}

static char
library_tab_header_type (const char *key)
{
	if (!strcasecmp_ascii (key, "file"))      return LIBRARY_FILE;
	if (!strcasecmp_ascii (key, "directory")) return LIBRARY_DIR;
	if (!strcasecmp_ascii (key, "playlist"))  return LIBRARY_PLAYLIST;
	return 0;
}

static void
library_tab_chunk (char type, const char *path, struct str_map *map)
{
	// CUE files appear once as a directory and another time as a playlist,
	// just skip them entirely
	if (type == LIBRARY_PLAYLIST)
		return;

	const char *artist = str_map_find (map, "artist");
	const char *title  = str_map_find (map, "title");
	char *name = (artist && title)
		? xstrdup_printf ("%s - %s", artist, title)
		: xstrdup (xbasename (path));

	int duration = -1;
	mpd_read_time (str_map_find (map, "duration"), &duration, NULL);
	mpd_read_time (str_map_find (map, "time"),     &duration, NULL);
	library_tab_add (type, duration, name, path);
	free (name);
}

static int
library_tab_compare (struct library_tab_item *a, struct library_tab_item *b)
{
	if (a->type != b->type)
		return a->type - b->type;

	return app_casecmp ((uint8_t *) a->path, (uint8_t *) b->path);
}

static char *
library_tab_parent (void)
{
	struct str *path = &g_library_tab.path;
	if (!path->len)
		return NULL;

	char *last_slash;
	if ((last_slash = strrchr (path->str, '/')))
		return xstrndup (path->str, last_slash - path->str);
	return xstrdup ("");
}

static bool
library_tab_is_above (const char *above, const char *subdir)
{
	size_t above_len = strlen (above);
	if (strncmp (above, subdir, above_len))
		return false;
	// The root is an empty string and is above anything other than itself
	return subdir[above_len] == '/' || (*subdir && !*above);
}

static void
library_tab_change_level (const char *new_path)
{
	struct str *path = &g_library_tab.path;
	if (!strcmp (path->str, new_path))
		return;

	struct library_level *above;
	if (library_tab_is_above (path->str, new_path))
	{
		above = xcalloc (1, sizeof *above + path->len + 1);
		above->item_top = g_library_tab.super.item_top;
		above->item_selected = g_library_tab.super.item_selected;
		memcpy (above->path, path->str, path->len);
		LIST_PREPEND (g_library_tab.above, above);

		// Select the ".." entry to reflect Norton Commander
		g_library_tab.super.item_top = 0;
		g_library_tab.super.item_selected = 1;
	}
	else while ((above = g_library_tab.above)
		&& !library_tab_is_above (above->path, new_path))
	{
		if (!strcmp (above->path, new_path))
		{
			g_library_tab.super.item_top = above->item_top;
			g_library_tab.super.item_selected = above->item_selected;
		}
		g_library_tab.above = above->next;
		free (above);
	}

	str_reset (path);
	str_append (path, new_path);

	cstr_set (&g_library_tab.super.header, NULL);
	g_library_tab.super.item_mark = -1;

	if (path->len)
		g_library_tab.super.header = xstrdup_printf ("/%s", path->str);
}

static void
library_tab_reset (void)
{
	for (size_t i = 0; i < g_library_tab.items_len; i++)
		free (g_library_tab.items[i].name);
	free (g_library_tab.items);
	ARRAY_INIT (g_library_tab.items);
}

static void
library_tab_load_data (const struct strv *data)
{
	library_tab_reset ();

	char *parent = library_tab_parent ();
	if (parent)
	{
		library_tab_add (LIBRARY_ROOT, -1, "", "");
		library_tab_add (LIBRARY_UP, -1, "", parent);
		free (parent);
	}

	struct str_map map = str_map_make (NULL);
	map.key_xfrm = tolower_ascii_strxfrm;

	char *key, *value, type;
	for (size_t i = data->len; i--; )
		if (!(key = mpd_parse_kv (data->vector[i], &value)))
			continue;
		else if (!(type = library_tab_header_type (key)))
			str_map_set (&map, key, value);
		else
		{
			library_tab_chunk (type, value, &map);
			str_map_clear (&map);
		}
	str_map_free (&map);

	struct library_tab_item *items = g_library_tab.items;
	size_t len = g_library_tab.super.item_count = g_library_tab.items_len;
	qsort (items, len, sizeof *items,
		(int (*) (const void *, const void *)) library_tab_compare);

	// XXX: this unmarks even if just the database updates
	g_library_tab.super.item_mark = -1;

	// Don't force the selection visible when there's no need to touch it
	if (g_library_tab.super.item_selected >= (int) len)
		app_move_selection (0);

	xui_invalidate ();
}

static void
library_tab_on_data (const struct mpd_response *response,
	const struct strv *data, void *user_data)
{
	char *new_path = user_data;
	if (!response->success)
	{
		print_error ("cannot read directory: %s", response->message_text);
		free (new_path);
		return;
	}

	g_library_tab.searching = false;
	library_tab_change_level (new_path);
	free (new_path);

	library_tab_load_data (data);
}

static void
library_tab_reload (const char *new_path)
{
	if (!new_path && g_library_tab.searching)
		return;  // TODO: perhaps we should call search_on_changed()

	char *path = new_path
		? xstrdup (new_path)
		: xstrdup (g_library_tab.path.str);

	struct mpd_client *c = &g.client;
	mpd_client_send_command (c, "lsinfo", *path ? path : "/", NULL);
	mpd_client_add_task (c, library_tab_on_data, path);
	mpd_client_idle (c, 0);
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

static void
library_tab_on_search_data (const struct mpd_response *response,
	const struct strv *data, void *user_data)
{
	char *filter = user_data;
	if (!g_library_tab.searching)
		goto out;

	if (!response->success)
		print_error ("cannot search: %s", response->message_text);
	else
	{
		cstr_set (&g_library_tab.super.header,
			xstrdup_printf ("%s: %s", "Global search", filter));
		library_tab_load_data (data);
	}

out:
	free (filter);
}

static char *
mpd_quoted_filter_string (const char *value)
{
	struct str quoted = str_make ();
	str_append_c (&quoted, '\'');
	for (const char *p = value; *p; p++)
	{
		if (mpd_client_must_escape_in_quote (*p))
			str_append_c (&quoted, '\\');
		str_append_c (&quoted, *p);
	}
	str_append_c (&quoted, '\'');
	return str_steal (&quoted);
}

static void
search_on_changed (void)
{
	struct mpd_client *c = &g.client;
	if (c->state != MPD_CONNECTED)
		return;

	size_t len;
	char *u8 = (char *) u32_to_u8 (g.editor.line, g.editor.len + 1, NULL, &len);
	mpd_client_list_begin (c);
	mpd_client_send_command (c, "search", "any", u8, NULL);

	// Just tag search doesn't consider filenames.
	// Older MPD can do `search any X file X` but without the negation,
	// which is necessary to avoid duplicates.  Neither syntax supports OR.
	// XXX: We should parse this, but it's probably not going to reach 100 soon,
	//   and it is not really documented what this should even look like.
	if (strcmp (c->got_hello, "0.21.") > 1)
	{
		char *quoted = mpd_quoted_filter_string (u8);
		char *expression = xstrdup_printf ("((!(any contains %s)) AND "
			"(file contains %s))", quoted, quoted);
		mpd_client_send_command (c, "search", expression, NULL);
		free (expression);
		free (quoted);
	}

	mpd_client_list_end (c);
	mpd_client_add_task (c, library_tab_on_search_data, u8);
	mpd_client_idle (c, 0);
}

static void
search_on_end (bool confirmed)
{
	if (!confirmed)
		library_tab_reload (g_library_tab.above->path);
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

static bool
library_tab_is_range_playable (struct tab_range range)
{
	for (int i = range.from; i <= range.upto; i++)
	{
		struct library_tab_item *x = &g_library_tab.items[i];
		if (x->type == LIBRARY_DIR || x->type == LIBRARY_FILE)
			return true;
	}
	return false;
}

static bool
library_tab_on_action (enum action action)
{
	struct mpd_client *c = &g.client;
	if (c->state != MPD_CONNECTED)
		return false;

	struct tab *tab = &g_library_tab.super;
	struct tab_range range = tab_selection_range (tab);
	if (range.from < 0)
		return false;

	struct library_tab_item *x = &g_library_tab.items[range.from];
	switch (action)
	{
	case ACTION_CHOOSE:
		// I can't think of a reasonable way of handling that
		if (range.from != range.upto)
			break;

		switch (x->type)
		{
		case LIBRARY_ROOT:
		case LIBRARY_UP:
		case LIBRARY_DIR:  library_tab_reload (x->path); break;
		case LIBRARY_FILE: MPD_SIMPLE ("add", x->path);  break;
		default:           hard_assert (!"invalid item type");
		}
		tab->item_mark = -1;
		return true;
	case ACTION_DESCRIBE:
		if (!*x->path)
			break;

		app_show_message (xstrdup ("Path: "), xstrdup (x->path));
		return true;
	case ACTION_UP:
	{
		char *parent = library_tab_parent ();
		if (parent)
		{
			library_tab_reload (parent);
			free (parent);
		}
		return parent != NULL;
	}
	case ACTION_MPD_SEARCH:
	{
		line_editor_start (&g.editor, '/');
		g.editor.on_changed = search_on_changed;
		g.editor.on_end = search_on_end;

		// We just need to be deeper but not match anything real,
		// in order to keep the rest of the codebase functional as-is
		if (!g_library_tab.searching)
		{
			char *fake_subdir = xstrdup_printf ("%s/", g_library_tab.path.str);
			library_tab_change_level (fake_subdir);
			free (fake_subdir);
		}

		cstr_set (&tab->header, xstrdup_printf ("Global search"));
		g_library_tab.searching = true;

		// Since we've already changed the header, empty the list,
		// although to be consistent we should also ask to search for "",
		// which dumps the database
		struct strv empty = strv_make ();
		library_tab_load_data (&empty);
		strv_free (&empty);

		xui_invalidate ();
		return true;
	}
	case ACTION_MPD_ADD:
		if (!library_tab_is_range_playable (range))
			break;

		for (int i = range.from; i <= range.upto; i++)
		{
			struct library_tab_item *x = &g_library_tab.items[i];
			if (x->type == LIBRARY_DIR || x->type == LIBRARY_FILE)
				MPD_SIMPLE ("add", x->path);
		}
		tab->item_mark = -1;
		return true;
	case ACTION_MPD_REPLACE:
		if (!library_tab_is_range_playable (range))
			break;

		// Clears the playlist (which stops playback), add what user wanted
		// to replace it with, and eventually restore playback;
		// I can't think of a reliable alternative that omits the "play"
		mpd_client_list_begin (c);

		mpd_client_send_command (c, "clear", NULL);
		for (int i = range.from; i <= range.upto; i++)
		{
			struct library_tab_item *x = &g_library_tab.items[i];
			if (x->type == LIBRARY_DIR || x->type == LIBRARY_FILE)
				mpd_client_send_command (c, "add", x->path, NULL);
		}
		if (g.state == PLAYER_PLAYING)
			mpd_client_send_command (c, "play", NULL);

		mpd_client_list_end (c);
		mpd_client_add_task (c, mpd_on_simple_response, NULL);
		mpd_client_idle (c, 0);
		tab->item_mark = -1;
		return true;
	default:
		break;
	}
	return false;
}

static struct tab *
library_tab_init (void)
{
	g_library_tab.path = str_make ();
	// g_library_tab.items is fine with zero initialisation

	struct tab *super = &g_library_tab.super;
	tab_init (super, "Library");
	super->can_multiselect = true;
	super->on_action = library_tab_on_action;
	super->on_item_layout = library_tab_on_item_layout;
	return super;
}

// --- Streams -----------------------------------------------------------------

// MPD can only parse m3u8 playlists, and only when it feels like doing so

struct stream_tab_task
{
	struct poller_curl_task curl;       ///< Superclass
	struct str data;                    ///< Downloaded data
	bool replace;                       ///< Should playlist be replaced?
	struct curl_slist *alias_ok;
};

static bool
is_content_type (const char *content_type,
	const char *expected_type, const char *expected_subtype)
{
	char *type = NULL, *subtype = NULL;
	bool result = http_parse_media_type (content_type, &type, &subtype, NULL)
		&& !strcasecmp_ascii (type, expected_type)
		&& !strcasecmp_ascii (subtype, expected_subtype);
	free (type);
	free (subtype);
	return result;
}

static void
streams_tab_filter (char *line, regex_t *re, struct strv *out)
{
	regmatch_t groups[2];
	if (regexec (re, line, 2, groups, 0) == REG_NOMATCH)
		return;

	// It may happen that playlist files contain useless, invalid quotes,
	// let's be liberal in what we accept
	regoff_t start = groups[1].rm_so, end = groups[1].rm_eo;
	while (end > start + 1 && line[start] == '"' && line[end - 1] == '"')
	{
		start++;
		end--;
	}

	char *target = xstrndup (line + start, end - start);
	if (utf8_validate (target, end - start))
		strv_append_owned (out, target);
	else
	{
		strv_append_owned (out, latin1_to_utf8 (target));
		free (target);
	}
}

static void
streams_tab_parse_playlist (const char *playlist, const char *content_type,
	struct strv *out)
{
	// We accept a lot of very broken stuff because this is the real world
	struct strv lines = strv_make ();
	cstr_split (playlist, "\r\n", true, &lines);

	// Since this excludes '"', it should even work for XMLs (w/o entities)
	const char *extract_re =
		"(https?://([][a-z0-9._~:/?#@!$&'()*+,;=-]|%[a-f0-9]{2})+)";
	if ((lines.len && !strcasecmp_ascii (lines.vector[0], "[playlist]"))
	 || (content_type && is_content_type (content_type, "audio", "x-scpls")))
		extract_re = "^File[^=]*=(.+)";
	else if ((lines.len && !strcasecmp_ascii (lines.vector[0], "#EXTM3U"))
	 || (content_type && is_content_type (content_type, "audio", "mpegurl"))
	 || (content_type && is_content_type (content_type, "audio", "x-mpegurl")))
		// This could be "^([^#].*)", however 1. we would need to resolve
		// relative URIs, and 2. relative URIs probably mean a Media Playlist,
		// which must be passed to MPD.  The better thing to do here would be to
		// reject anything with EXT-X-TARGETDURATION, and to resolve the URIs.
		extract_re = "^(https?://.+)";

	regex_t *re = regex_compile (extract_re, REG_EXTENDED, NULL);
	hard_assert (re != NULL);
	for (size_t i = 0; i < lines.len; i++)
		streams_tab_filter (lines.vector[i], re, out);
	regex_free (re);
	strv_free (&lines);
}

static bool
streams_tab_extract_links (struct str *data, const char *content_type,
	struct strv *out)
{
	// Since playlists are also "audio/*", this seems like a sane thing to do
	for (size_t i = 0; i < data->len; i++)
	{
		uint8_t c = data->str[i];
		if (iscntrl_ascii (c) & (c != '\t') & (c != '\r') & (c != '\n'))
			return false;
	}

	streams_tab_parse_playlist (data->str, content_type, out);
	return out->len != 0;
}

static void
streams_tab_task_finalize (struct stream_tab_task *self)
{
	curl_easy_cleanup (self->curl.easy);
	curl_slist_free_all (self->alias_ok);
	str_free (&self->data);
	free (self);
}

static void
streams_tab_task_dispose (struct stream_tab_task *self)
{
	hard_assert (poller_curl_remove (&g.poller_curl, self->curl.easy, NULL));
	streams_tab_task_finalize (self);
}

static void
streams_tab_on_downloaded (CURLMsg *msg, struct poller_curl_task *task)
{
	struct stream_tab_task *self =
		CONTAINER_OF (task, struct stream_tab_task, curl);

	if (msg->data.result
	 && msg->data.result != CURLE_WRITE_ERROR)
	{
		cstr_uncapitalize (self->curl.curl_error);
		print_error ("%s", self->curl.curl_error);
		goto dispose;
	}

	struct mpd_client *c = &g.client;
	if (c->state != MPD_CONNECTED)
		goto dispose;

	CURL *easy = msg->easy_handle;
	CURLcode res;

	long code;
	char *type, *uri;
	if ((res = curl_easy_getinfo (easy, CURLINFO_RESPONSE_CODE, &code))
	 || (res = curl_easy_getinfo (easy, CURLINFO_CONTENT_TYPE, &type))
	 || (res = curl_easy_getinfo (easy, CURLINFO_EFFECTIVE_URL, &uri)))
	{
		print_error ("%s: %s",
			"cURL info retrieval failed", curl_easy_strerror (res));
		goto dispose;
	}
	// cURL is not willing to parse the ICY header, the code is zero then
	if (code && code != 200)
	{
		print_error ("%s: %ld", "unexpected HTTP response", code);
		goto dispose;
	}

	mpd_client_list_begin (c);
	if (self->replace)
		mpd_client_send_command (c, "clear", NULL);

	struct strv links = strv_make ();
	if (!streams_tab_extract_links (&self->data, type, &links))
		strv_append (&links, uri);
	for (size_t i = 0; i < links.len; i++)
		mpd_client_send_command (c, "add", links.vector[i], NULL);
	if (self->replace && g.state == PLAYER_PLAYING)
		mpd_client_send_command (c, "play", NULL);

	strv_free (&links);
	mpd_client_list_end (c);
	mpd_client_add_task (c, mpd_on_simple_response, NULL);
	mpd_client_idle (c, 0);

dispose:
	streams_tab_task_dispose (self);
}

static size_t
write_callback (char *ptr, size_t size, size_t nmemb, void *user_data)
{
	struct str *buf = user_data;
	str_append_data (buf, ptr, size * nmemb);

	// Invoke CURLE_WRITE_ERROR when we've received enough data for a playlist
	if (buf->len >= (1 << 16))
		return 0;

	return size * nmemb;
}

static bool
streams_tab_process (const char *uri, bool replace, struct error **e)
{
	// TODO: streams_tab_task_dispose() on that running task
	if (g.poller_curl.registered)
	{
		print_error ("waiting for the last stream to time out");
		return false;
	}

	struct stream_tab_task *task = xcalloc (1, sizeof *task);
	hard_assert (poller_curl_spawn (&task->curl, NULL));

	CURL *easy = task->curl.easy;
	task->data = str_make ();
	task->replace = replace;
	task->alias_ok = curl_slist_append (NULL, "ICY 200 OK");

	CURLcode res;
	if ((res = curl_easy_setopt (easy, CURLOPT_FOLLOWLOCATION, 1L))
	 || (res = curl_easy_setopt (easy, CURLOPT_NOPROGRESS,     1L))
	 || (res = curl_easy_setopt (easy, CURLOPT_TIMEOUT,        10L))
	// Not checking anything, we just want some data, any data
	 || (res = curl_easy_setopt (easy, CURLOPT_SSL_VERIFYPEER, 0L))
	 || (res = curl_easy_setopt (easy, CURLOPT_SSL_VERIFYHOST, 0L))
	 || (res = curl_easy_setopt (easy, CURLOPT_URL,            uri))
	 || (res = curl_easy_setopt (easy, CURLOPT_HTTP200ALIASES, task->alias_ok))

	 || (res = curl_easy_setopt (easy, CURLOPT_VERBOSE, (long) g_debug_mode))
	 || (res = curl_easy_setopt (easy, CURLOPT_DEBUGFUNCTION, print_curl_debug))
	 || (res = curl_easy_setopt (easy, CURLOPT_WRITEDATA, &task->data))
	 || (res = curl_easy_setopt (easy, CURLOPT_WRITEFUNCTION, write_callback)))
	{
		error_set (e, "%s: %s", "cURL setup failed", curl_easy_strerror (res));
		streams_tab_task_finalize (task);
		return false;
	}

	task->curl.on_done = streams_tab_on_downloaded;
	hard_assert (poller_curl_add (&g.poller_curl, task->curl.easy, NULL));
	return true;
}

static bool
streams_tab_on_action (enum action action)
{
	struct tab *tab = g.active_tab;
	if (tab->item_selected < 0 || !tab->item_count)
		return false;

	// For simplicity the URL is the string following the stream name
	const char *uri = 1 + strchr (g.streams.vector[tab->item_selected], 0);

	struct error *e = NULL;
	switch (action)
	{
	case ACTION_MPD_REPLACE:
		streams_tab_process (uri, true,  &e);
		break;
	case ACTION_CHOOSE:
	case ACTION_MPD_ADD:
		streams_tab_process (uri, false, &e);
		break;
	case ACTION_DESCRIBE:
		app_show_message (xstrdup (uri), NULL);
		break;
	default:
		return false;
	}
	if (e)
	{
		print_error ("%s", e->message);
		error_free (e);
	}
	return true;
}

static struct layout
streams_tab_on_item_layout (size_t item_index)
{
	struct layout l = {};
	app_push_fill (&l, g.ui->label (0, g.streams.vector[item_index]));
	return l;
}

static struct tab *
streams_tab_init (void)
{
	static struct tab super;
	tab_init (&super, "Streams");
	super.on_action = streams_tab_on_action;
	super.on_item_layout = streams_tab_on_item_layout;
	super.item_count = g.streams.len;
	return &super;
}

// --- Info tab ----------------------------------------------------------------

struct info_tab_plugin
{
	LIST_HEADER (struct info_tab_plugin)

	char *path;                         ///< Filesystem path to plugin
	char *description;                  ///< What the plugin does
};

static struct info_tab_plugin *
info_tab_plugin_load (const char *path)
{
	// Shell quoting is less annoying than process management.
	struct str escaped = str_make ();
	shell_quote (path, &escaped);

	struct str description = str_make ();
	struct error *error = NULL;
	(void) run_command (escaped.str, &description, &error);
	str_free (&escaped);
	if (error)
	{
		print_error ("%s: %s", path, error->message);
		error_free (error);
		str_free (&description);
		return NULL;
	}

	char *newline = strpbrk (description.str, "\r\n");
	if (newline)
	{
		description.len = newline - description.str;
		*newline = '\0';
	}
	str_enforce_utf8 (&description);
	if (!description.len)
	{
		print_error ("%s: %s", path, "missing description");
		str_free (&description);
		return NULL;
	}

	struct info_tab_plugin *plugin = xcalloc (1, sizeof *plugin);
	plugin->path = xstrdup (path);
	plugin->description = str_steal (&description);
	return plugin;
}

static void
info_tab_plugin_load_dir (struct str_map *basename_to_path, const char *dirname)
{
	DIR *dir = opendir (dirname);
	if (!dir)
	{
		print_debug ("opendir: %s: %s", dirname, strerror (errno));
		return;
	}

	struct dirent *entry = NULL;
	while ((entry = readdir (dir)))
	{
		struct stat st = {};
		char *path = xstrdup_printf ("%s/%s", dirname, entry->d_name);
		if (stat (path, &st) || !S_ISREG (st.st_mode))
		{
			free (path);
			continue;
		}

		// Empty files silently erase formerly found basenames.
		if (!st.st_size)
			cstr_set (&path, NULL);

		str_map_set (basename_to_path, entry->d_name, path);
	}
	closedir (dir);
}

static int
strv_sort_cb (const void *a, const void *b)
{
	return strcmp (*(const char **) a, *(const char **) b);
}

static struct info_tab_plugin *
info_tab_plugin_load_all (void)
{
	struct str_map basename_to_path = str_map_make (free);
	struct strv paths = strv_make ();
	get_xdg_data_dirs (&paths);
	strv_append (&paths, PROJECT_DATADIR);
	for (size_t i = paths.len; i--; )
	{
		char *dirname =
			xstrdup_printf ("%s/" PROGRAM_NAME "/info", paths.vector[i]);
		info_tab_plugin_load_dir (&basename_to_path, dirname);
		free (dirname);
	}
	strv_free (&paths);

	struct strv sorted = strv_make ();
	struct str_map_iter iter = str_map_iter_make (&basename_to_path);
	while (str_map_iter_next (&iter))
		strv_append (&sorted, iter.link->key);
	qsort (sorted.vector, sorted.len, sizeof *sorted.vector, strv_sort_cb);

	struct info_tab_plugin *result = NULL;
	for (size_t i = sorted.len; i--; )
	{
		const char *path = str_map_find (&basename_to_path, sorted.vector[i]);
		struct info_tab_plugin *plugin = info_tab_plugin_load (path);
		if (plugin)
			LIST_PREPEND (result, plugin);
	}
	str_map_free (&basename_to_path);
	strv_free (&sorted);
	return result;
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

struct info_tab_item
{
	char *prefix;                       ///< Fixed-width prefix column or NULL
	char *text;                         ///< Text or NULL
	bool formatted;                     ///< Interpret inline formatting marks?
	struct info_tab_plugin *plugin;     ///< Activatable plugin
};

static void
info_tab_item_free (struct info_tab_item *self)
{
	cstr_set (&self->prefix, NULL);
	cstr_set (&self->text, NULL);
}

static struct
{
	struct tab super;                   ///< Parent class
	struct info_tab_item *items;        ///< Items array
	size_t items_alloc;                 ///< How many items are allocated

	struct info_tab_plugin *plugins;    ///< Plugins

	int plugin_songid;                  ///< Song ID or -1
	pid_t plugin_pid;                   ///< Running plugin's process ID or -1
	int plugin_stdout;                  ///< pid != -1: read end of stdout
	struct poller_fd plugin_event;      ///< pid != -1: stdout is readable
	struct str plugin_output;           ///< pid != -1: buffer, otherwise result
}
g_info_tab;

static chtype
info_tab_format_decode_toggle (char c)
{
	switch (c)
	{
	case '\x01':
		return A_BOLD;
	case '\x02':
#ifdef A_ITALIC
		return A_ITALIC;
#else
		return A_UNDERLINE;
#endif
	default:
		return 0;
	}
}

static void
info_tab_format (struct layout *l, const char *text)
{
	chtype attrs = 0;
	for (const char *p = text; *p; p++)
	{
		chtype toggled = info_tab_format_decode_toggle (*p);
		if (!toggled)
			continue;

		if (p != text)
		{
			char *slice = xstrndup (text, p - text);
			app_push (l, g.ui->label (attrs, slice));
			free (slice);
		}

		attrs ^= toggled;
		text = p + 1;
	}
	if (*text)
		app_push (l, g.ui->label (attrs, text));
}

static struct layout
info_tab_on_item_layout (size_t item_index)
{
	struct info_tab_item *item = &g_info_tab.items[item_index];
	struct layout l = {};
	if (item->prefix)
	{
		char *prefix = xstrdup_printf ("%s:", item->prefix);
		app_push (&l, g.ui->label (A_BOLD, prefix))
			->width = 8 * g_xui.hunit;
		app_push (&l, g.ui->padding (0, 0.5, 1));
	}

	if (item->plugin)
		app_push (&l, g.ui->label (A_BOLD, item->plugin->description));
	else if (!item->text || !*item->text)
		app_push (&l, g.ui->padding (0, 1, 1));
	else if (item->formatted)
		info_tab_format (&l, item->text);
	else
		app_push (&l, g.ui->label (0, item->text));

	if (l.tail)
		l.tail->width = -1;
	return l;
}

static struct info_tab_item *
info_tab_prepare (void)
{
	if (g_info_tab.super.item_count == g_info_tab.items_alloc)
		g_info_tab.items = xreallocarray (g_info_tab.items,
			sizeof *g_info_tab.items, (g_info_tab.items_alloc <<= 1));

	struct info_tab_item *item =
		&g_info_tab.items[g_info_tab.super.item_count++];
	memset (item, 0, sizeof *item);
	return item;
}

static void
info_tab_add (compact_map_t data, const char *field)
{
	struct info_tab_item *item = info_tab_prepare ();
	item->prefix = xstrdup (field);
	item->text = xstrdup0 (compact_map_find (data, field));
}

static void
info_tab_update (void)
{
	while (g_info_tab.super.item_count)
		info_tab_item_free (&g_info_tab.items[--g_info_tab.super.item_count]);

	compact_map_t map = item_list_get (&g.playlist, g.song);
	if (!map)
		return;

	info_tab_add (map, "Title");
	info_tab_add (map, "Artist");
	info_tab_add (map, "Album");
	info_tab_add (map, "Track");
	info_tab_add (map, "Genre");
	// We actually receive it as "file", but the key is also used for display
	info_tab_add (map, "File");

	if (g_info_tab.plugins)
	{
		(void) info_tab_prepare ();
		LIST_FOR_EACH (struct info_tab_plugin, plugin, g_info_tab.plugins)
			info_tab_prepare ()->plugin = plugin;
	}

	if (g_info_tab.plugin_pid != -1)
	{
		(void) info_tab_prepare ();
		info_tab_prepare ()->text = xstrdup ("Processing...");
		return;
	}

	const char *songid = compact_map_find (map, "Id");
	if (songid && atoi (songid) == g_info_tab.plugin_songid
	 && g_info_tab.plugin_output.len)
	{
		struct strv lines = strv_make ();
		cstr_split (g_info_tab.plugin_output.str, "\r\n", false, &lines);

		(void) info_tab_prepare ();
		for (size_t i = 0; i < lines.len; i++)
		{
			struct info_tab_item *item = info_tab_prepare ();
			item->formatted = true;
			item->text = lines.vector[i];
		}
		free (lines.vector);
	}
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

static void
info_tab_plugin_abort (void)
{
	if (g_info_tab.plugin_pid == -1)
		return;

	// XXX: our methods of killing are very crude, we hope to improve;
	//   at least install a SIGCHLD handler to collect zombies
	(void) kill (-g_info_tab.plugin_pid, SIGTERM);

	int status = 0;
	while (waitpid (g_info_tab.plugin_pid, &status, WNOHANG) == -1
		&& errno == EINTR)
		;
	if (WIFEXITED (status) && WEXITSTATUS (status) != EXIT_SUCCESS)
		print_error ("plugin reported failure");

	g_info_tab.plugin_pid = -1;
	poller_fd_reset (&g_info_tab.plugin_event);
	xclose (g_info_tab.plugin_stdout);
	g_info_tab.plugin_stdout = -1;
}

static void
info_tab_on_plugin_stdout (const struct pollfd *fd, void *user_data)
{
	(void) user_data;

	struct str *buf = &g_info_tab.plugin_output;
	switch (socket_io_try_read (fd->fd, buf))
	{
	case SOCKET_IO_OK:
		str_enforce_utf8 (buf);
		return;
	case SOCKET_IO_ERROR:
		print_error ("error reading from plugin: %s", strerror (errno));
		// Fall-through
	case SOCKET_IO_EOF:
		info_tab_plugin_abort ();
		info_tab_update ();
		xui_invalidate ();
	}
}

static void
info_tab_plugin_run (struct info_tab_plugin *plugin, compact_map_t map)
{
	info_tab_plugin_abort ();
	if (!map)
		return;

	const char *songid = compact_map_find (map, "Id");
	const char *title  = compact_map_find (map, "Title");
	const char *artist = compact_map_find (map, "Artist");
	const char *album  = compact_map_find (map, "Album");
	if (!songid || !title || !artist)
	{
		print_error ("unknown song title or artist");
		return;
	}

	int stdout_pipe[2];
	if (pipe (stdout_pipe))
	{
		print_error ("%s: %s", "pipe", strerror (errno));
		return;
	}

	enum { READ, WRITE };
	set_cloexec (stdout_pipe[READ]);
	set_cloexec (stdout_pipe[WRITE]);

	const char *argv[] =
		{ xbasename (plugin->path), title, artist, album, NULL };

	pid_t child = fork ();
	switch (child)
	{
	case -1:
		print_error ("%s: %s", "fork", strerror (errno));
		xclose (stdout_pipe[READ]);
		xclose (stdout_pipe[WRITE]);
		return;
	case 0:
		if (setpgid (0, 0) == -1 || !freopen ("/dev/null", "r", stdin)
		 || dup2 (stdout_pipe[WRITE], STDOUT_FILENO) == -1
		 || dup2 (stdout_pipe[WRITE], STDERR_FILENO) == -1)
			_exit (EXIT_FAILURE);

		signal (SIGPIPE, SIG_DFL);

		(void) execv (plugin->path, (char **) argv);
		fprintf (stderr, "%s\n", strerror (errno));
		_exit (EXIT_FAILURE);
	default:
		// Resolve the race, even though it isn't critical for us
		(void) setpgid (child, child);

		g_info_tab.plugin_songid = atoi (songid);
		g_info_tab.plugin_pid = child;
		set_blocking ((g_info_tab.plugin_stdout = stdout_pipe[READ]), false);
		xclose (stdout_pipe[WRITE]);

		struct poller_fd *event = &g_info_tab.plugin_event;
		*event = poller_fd_make (&g.poller, g_info_tab.plugin_stdout);
		event->dispatcher = info_tab_on_plugin_stdout;
		str_reset (&g_info_tab.plugin_output);
		poller_fd_set (&g_info_tab.plugin_event, POLLIN);
	}
}

static bool
info_tab_on_action (enum action action)
{
	struct tab *tab = g.active_tab;
	if (tab->item_selected < 0
	 || tab->item_selected >= (int) tab->item_count)
		return false;

	struct info_tab_item *item = &g_info_tab.items[tab->item_selected];
	if (!item->plugin)
		return false;

	switch (action)
	{
	case ACTION_DESCRIBE:
		app_show_message (xstrdup ("Path: "), xstrdup (item->plugin->path));
		return true;
	case ACTION_CHOOSE:
		info_tab_plugin_run (item->plugin, item_list_get (&g.playlist, g.song));
		info_tab_update ();
		xui_invalidate ();
		return true;
	default:
		return false;
	}
}

static struct tab *
info_tab_init (void)
{
	g_info_tab.items =
		xcalloc ((g_info_tab.items_alloc = 16), sizeof *g_info_tab.items);

	g_info_tab.plugins = info_tab_plugin_load_all ();
	g_info_tab.plugin_songid = -1;
	g_info_tab.plugin_pid = -1;
	g_info_tab.plugin_stdout = -1;
	g_info_tab.plugin_output = str_make ();

	struct tab *super = &g_info_tab.super;
	tab_init (super, "Info");
	super->on_action = info_tab_on_action;
	super->on_item_layout = info_tab_on_item_layout;
	return super;
}

// --- Help tab ----------------------------------------------------------------

static struct
{
	struct tab super;                   ///< Parent class
	ARRAY (enum action, actions)        ///< Actions for content
	struct strv lines;                  ///< Visible content
}
g_help_tab;

static bool
help_tab_on_action (enum action action)
{
	struct tab *tab = &g_help_tab.super;
	if (tab->item_selected < 0
	 || tab->item_selected >= (int) g_help_tab.actions_len)
		return false;

	enum action a = g_help_tab.actions[tab->item_selected];
	if (!a)
		return false;

	if (action == ACTION_DESCRIBE)
	{
		app_show_message (xstrdup ("Configuration name: "),
			xstrdup (action_name (a)));
		return true;
	}
	if (action != ACTION_CHOOSE || a == ACTION_CHOOSE /* avoid recursion */)
		return false;

	// XXX: We can't propagate failure to ring the terminal/X11 bell, but we
	//   don't want to let our caller show a bad "can't do that" message either.
	return app_process_action (a), true;
}

static void
help_tab_assign_action (enum action action)
{
	hard_assert (g_help_tab.lines.len > g_help_tab.actions_len);

	size_t to_push = g_help_tab.lines.len - g_help_tab.actions_len;
	ARRAY_RESERVE (g_help_tab.actions, to_push);
	for (size_t i = 1; i < to_push; i++)
		g_help_tab.actions[g_help_tab.actions_len++] = ACTION_NONE;
	g_help_tab.actions[g_help_tab.actions_len++] = action;
}

static void
help_tab_group (struct binding *keys, size_t len, struct strv *out,
	bool bound[], size_t action_count)
{
	for (enum action i = 0; i < action_count; i++)
	{
		struct strv ass = strv_make ();
		for (size_t k = 0; k < len; k++)
			if (keys[k].action == i)
				strv_append_owned (&ass, app_strfkey (&keys[k].decoded));
		if (ass.len)
		{
			char *joined = strv_join (&ass, ", ");
			strv_append_owned (out, xstrdup_printf
				("  %s%c%s", action_description (i), 0, joined));
			free (joined);

			bound[i] = true;
			help_tab_assign_action (i);
		}
		strv_free (&ass);
	}
}

static void
help_tab_unbound (struct strv *out, bool bound[], size_t action_count)
{
	for (enum action i = 0; i < action_count; i++)
		if (!bound[i])
		{
			strv_append_owned (out,
				xstrdup_printf ("  %s%c", action_description (i), 0));
			help_tab_assign_action (i);
		}
}

static struct layout
help_tab_on_item_layout (size_t item_index)
{
	hard_assert (item_index < g_help_tab.lines.len);
	const char *line = g_help_tab.lines.vector[item_index];

	struct layout l = {};
	app_push_fill (&l, g.ui->label (*line == ' ' ? 0 : A_BOLD, line));

	const char *definition = strchr (line, 0) + 1;
	if (*line == ' ' && *definition)
	{
		app_push (&l, g.ui->padding (0, 0.5, 1));
		app_push_fill (&l, g.ui->label (0, definition));
	}
	return l;
}

static struct tab *
help_tab_init (void)
{
	ARRAY_INIT (g_help_tab.actions);
	struct strv *lines = &g_help_tab.lines;
	*lines = strv_make ();

	size_t bound_len = ACTION_USER_0 + g.action_names.len;
	bool *bound = xcalloc (bound_len, sizeof *bound);
	bound[ACTION_NONE] = true;

	strv_append (lines, "Normal mode actions");
	help_tab_group (g_normal_keys, g_normal_keys_len, lines, bound, bound_len);
	strv_append (lines, "");

	strv_append (lines, "Editor mode actions");
	help_tab_group (g_editor_keys, g_editor_keys_len, lines, bound, bound_len);
	strv_append (lines, "");

	bool have_unbound = false;
	for (size_t i = 0; i < bound_len; i++)
		if (!bound[i])
			have_unbound = true;

	if (have_unbound)
	{
		strv_append (lines, "Unbound actions");
		help_tab_unbound (lines, bound, bound_len);
		strv_append (lines, "");
	}
	free (bound);

	struct tab *super = &g_help_tab.super;
	tab_init (super, "Help");
	super->on_action = help_tab_on_action;
	super->on_item_layout = help_tab_on_item_layout;
	super->item_count = lines->len;
	return super;
}

// --- Debug tab ---------------------------------------------------------------

struct debug_item
{
	char *text;                         ///< Logged line
	int64_t timestamp;                  ///< Timestamp
	chtype attrs;                       ///< Line attributes
};

static struct
{
	struct tab super;                   ///< Parent class
	ARRAY (struct debug_item, items)    ///< Items
	bool active;                        ///< The tab is present
}
g_debug_tab;

static struct layout
debug_tab_on_item_layout (size_t item_index)
{
	hard_assert (item_index < g_debug_tab.items_len);
	struct debug_item *item = &g_debug_tab.items[item_index];

	char buf[16];
	struct tm tm;
	time_t when = item->timestamp / 1000;
	strftime (buf, sizeof buf, "%T", localtime_r (&when, &tm));

	char *prefix = xstrdup_printf
		("%s.%03d", buf, (int) (item->timestamp % 1000));

	struct layout l = {};
	app_push (&l, g.ui->label (0, prefix));
	app_push (&l, g.ui->padding (item->attrs, 0.5, 1));
	app_push_fill (&l, g.ui->label (item->attrs, item->text));
	free (prefix);
	return l;
}

static void
debug_tab_push (char *message, chtype attrs)
{
	ARRAY_RESERVE (g_debug_tab.items, 1);
	struct debug_item *item = &g_debug_tab.items[g_debug_tab.items_len++];
	g_debug_tab.super.item_count = g_debug_tab.items_len;
	item->text = message;
	item->attrs = attrs;
	item->timestamp = clock_msec (CLOCK_REALTIME);

	xui_invalidate ();
}

static struct tab *
debug_tab_init (void)
{
	ARRAY_INIT (g_debug_tab.items);
	g_debug_tab.active = true;

	struct tab *super = &g_debug_tab.super;
	tab_init (super, "Debug");
	super->on_item_layout = debug_tab_on_item_layout;
	return super;
}

// --- Spectrum analyser -------------------------------------------------------

#ifdef WITH_FFTW

static void
spectrum_redraw (void)
{
	// A full refresh would be too computationally expensive,
	// let's hack around it in this case
	struct widget *spectrum = app_widget_by_id (WIDGET_SPECTRUM);
	if (spectrum)
		spectrum->on_render (spectrum);

	poller_idle_set (&g_xui.flip_event);
}

// When any problem occurs with the FIFO, we'll just give up on it completely
static void
spectrum_discard_fifo (void)
{
	if (g.spectrum_fd != -1)
	{
		poller_fd_reset (&g.spectrum_event);
		xclose (g.spectrum_fd);
		g.spectrum_fd = -1;

		spectrum_free (&g.spectrum);
		xui_invalidate ();
	}
}

static void
spectrum_on_fifo_readable (const struct pollfd *pfd, void *user_data)
{
	(void) user_data;
	struct spectrum *s = &g.spectrum;

	bool update = false;
	ssize_t n;
restart:
	while ((n = read (pfd->fd,
		s->buffer + s->buffer_len, s->buffer_size - s->buffer_len)) > 0)
		if ((s->buffer_len += n) == s->buffer_size)
		{
			update = true;
			spectrum_sample (s);
			s->buffer_len = 0;
		}

	if (!n)
		spectrum_discard_fifo ();
	else if (errno == EINTR)
		goto restart;
	else if (errno != EAGAIN)
	{
		print_error ("spectrum: %s", strerror (errno));
		spectrum_discard_fifo ();
	}
	else if (update)
		spectrum_redraw ();
}

// When playback is stopped, we need to feed the analyser some zeroes ourselves.
// We could also just hide it.  Hard to say which is simpler or better.
static void
spectrum_clear (void)
{
	if (g.spectrum_fd != -1)
	{
		struct spectrum *s = &g.spectrum;
		memset (s->buffer, 0, s->buffer_size);
		spectrum_sample (s);
		spectrum_sample (s);
		s->buffer_len = 0;

		spectrum_redraw ();
	}
}

static void
spectrum_setup_fifo (void)
{
	const char *spectrum_path =
		get_config_string (g.config.root, "settings.spectrum_path");
	const char *spectrum_format =
		get_config_string (g.config.root, "settings.spectrum_format");
	struct config_item *spectrum_bars =
		config_item_get (g.config.root, "settings.spectrum_bars", NULL);
	struct config_item *spectrum_fps =
		config_item_get (g.config.root, "settings.spectrum_fps", NULL);
	if (!spectrum_path)
		return;

	struct error *e = NULL;
	char *path = resolve_filename
		(spectrum_path, resolve_relative_config_filename);

	if (!path)
		print_error ("spectrum: %s", "FIFO path could not be resolved");
	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))
	{
		print_error ("spectrum: %s", e->message);
		error_free (e);
	}
	else if ((g.spectrum_fd = open (path, O_RDONLY | O_NONBLOCK)) == -1)
	{
		print_error ("spectrum: %s: %s", path, strerror (errno));
		spectrum_free (&g.spectrum);
	}
	else
	{
		g.spectrum_event = poller_fd_make (&g.poller, g.spectrum_fd);
		g.spectrum_event.dispatcher = spectrum_on_fifo_readable;
		poller_fd_set (&g.spectrum_event, POLLIN);
	}

	free (path);
}

#else  // ! WITH_FFTW
#define spectrum_setup_fifo()   BLOCK_START BLOCK_END
#define spectrum_clear()        BLOCK_START BLOCK_END
#define spectrum_discard_fifo() BLOCK_START BLOCK_END
#endif  // ! WITH_FFTW

// --- PulseAudio --------------------------------------------------------------

#ifdef WITH_PULSE

static bool
mpd_find_output (const struct strv *data, const char *wanted)
{
	// The plugin field is new in MPD 0.21, by default take any output
	unsigned long n, accept = 1;
	for (size_t i = data->len; i--; )
	{
		char *key, *value;
		if (!(key = mpd_parse_kv (data->vector[i], &value)))
			continue;

		if (!strcasecmp_ascii (key, "outputid"))
		{
			if (accept)
				return true;

			accept = 1;
		}
		else if (!strcasecmp_ascii (key, "plugin"))
			accept &= !strcmp (value, wanted);
		else if (!strcasecmp_ascii (key, "outputenabled")
			&& xstrtoul (&n, value, 10))
			accept &= n == 1;
	}
	return false;
}

static void
mpd_on_outputs_response (const struct mpd_response *response,
	const struct strv *data, void *user_data)
{
	(void) user_data;

	// TODO: check whether an action is actually necessary
	pulse_free (&g.pulse);
	if (response->success && !mpd_find_output (data, "pulse"))
		print_debug ("MPD has no PulseAudio output to control");
	else
	{
		pulse_init (&g.pulse, &g.poller);
		g.pulse.on_update = xui_invalidate;
	}

	xui_invalidate ();
}

static void
pulse_update (void)
{
	struct mpd_client *c = &g.client;
	if (!g.pulse_control_requested)
		return;

	// The read permission is sufficient for this command
	mpd_client_send_command (c, "outputs", NULL);
	mpd_client_add_task (c, mpd_on_outputs_response, NULL);
	mpd_client_idle (c, 0);
}

static void
pulse_disable (void)
{
	pulse_free (&g.pulse);
	xui_invalidate ();
}

#else  // ! WITH_PULSE
#define pulse_update()  BLOCK_START BLOCK_END
#define pulse_disable() BLOCK_START BLOCK_END
#endif  // ! WITH_PULSE

// --- MPD interface -----------------------------------------------------------

static void
mpd_update_playlist_time (void)
{
	g.playlist_time = 0;

	// It would also be possible to retrieve this from "stats" -> "playtime"
	unsigned long n;
	for (size_t i = 0; i < g.playlist.len; i++)
	{
		compact_map_t map = item_list_get (&g.playlist, i);
		const char *time = compact_map_find (map, "time");
		if (time && xstrtoul (&n, time, 10))
			g.playlist_time += n;
	}
}

static void
mpd_set_elapsed_timer (int msec_past_second)
{
	int delay_msec = 1000 - msec_past_second;  // Until the next round second
	if (!g.elapsed_poll)
	{
		poller_timer_set (&g.elapsed_event, delay_msec);
		// Remember when the last round second was, relative to monotonic time
		g.elapsed_since = clock_msec (CLOCK_BEST) - msec_past_second;
		return;
	}

	// We may receive an earlier time, this seems to compensate for it well
	// (I haven't seen it trigger more than 50ms too early)
	delay_msec += 100;

	// When playback stalls, avoid busy looping with the server
	int elapsed_msec = g.song_elapsed * 1000 + msec_past_second;
	if (elapsed_msec == g.elapsed_since)
		delay_msec = MAX (delay_msec, 500);

	// In polling mode, we're interested in progress rather than stability.
	// We can reuse both the poller_timer struct and the timestamp field.
	poller_timer_set (&g.elapsed_event, delay_msec);
	g.elapsed_since = elapsed_msec;
}

static void
mpd_update_playback_state (void)
{
	struct str_map *map = &g.playback_info;
	g.song_elapsed = g.song_duration = g.volume = g.song = -1;
	uint32_t last_playlist_version = g.playlist_version;
	g.playlist_version = 0;

	const char *state;
	g.state = PLAYER_STOPPED;
	if ((state = str_map_find (map, "state")))
	{
		if (!strcmp (state, "play"))   g.state = PLAYER_PLAYING;
		if (!strcmp (state, "pause"))  g.state = PLAYER_PAUSED;
	}
	if (g.state == PLAYER_STOPPED)
	{
		spectrum_clear ();
	}

	// Values in "time" are always rounded.  "elapsed", introduced in MPD 0.16,
	// is in millisecond precision and "duration" as well, starting with 0.20.
	// Prefer the more precise values but use what we have.
	const char *time     = str_map_find (map, "time");
	const char *elapsed  = str_map_find (map, "elapsed");
	const char *duration = str_map_find (map, "duration");

	struct strv fields = strv_make ();
	if (time)
	{
		cstr_split (time, ":", false, &fields);
		if (fields.len >= 1 && !elapsed)   elapsed  = fields.vector[0];
		if (fields.len >= 2 && !duration)  duration = fields.vector[1];
	}

	int msec_past_second = 0;
	mpd_read_time (elapsed,  &g.song_elapsed,  &msec_past_second);
	mpd_read_time (duration, &g.song_duration, NULL);
	strv_free (&fields);

	poller_timer_reset (&g.elapsed_event);
	if (g.state == PLAYER_PLAYING)
		mpd_set_elapsed_timer (msec_past_second);
	else
		g.elapsed_since = -1;

	// The server sends -1 when nothing is being played right now
	unsigned long n;
	if (xstrtoul_map (map, "volume",   &n))  g.volume           = n;

	if (xstrtoul_map (map, "playlist", &n))  g.playlist_version = n;
	if (xstrtoul_map (map, "song",     &n))  g.song             = n;

	if (g.playlist_version != last_playlist_version)
		mpd_update_playlist_time ();

	xui_invalidate ();
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

static void
mpd_process_info_data (const struct strv *data)
{
	struct str_map *map = &g.playback_info;

	// First there's the status, followed by playlist items chunked by "file"
	unsigned long n; char *key, *value;
	for (size_t i = 0; i < data->len - 1 && data->vector[i]; i++)
	{
		if (!(key = mpd_parse_kv (data->vector[i], &value)))
			continue;
		if (!strcasecmp_ascii (key, "playlistlength")
			&& xstrtoul (&n, value, 10))
			item_list_resize (&g.playlist, n);
		str_map_set (map, key, xstrdup (value));
	}

	// It's much better to process the playlist from the back
	struct str_map item = str_map_make (NULL);
	item.key_xfrm = tolower_ascii_strxfrm;
	for (size_t i = data->len - 1; i-- && data->vector[i]; )
	{
		if (!(key = mpd_parse_kv (data->vector[i], &value)))
			continue;
		str_map_set (&item, key, value);
		if (!strcasecmp_ascii (key, "file"))
		{
			if (xstrtoul_map (&item, "pos", &n))
				item_list_set (&g.playlist, n, &item);
			str_map_clear (&item);
		}
	}
	str_map_free (&item);
}

/// Find a song by its id in the current playlist.  Expensive, rarely called.
static ssize_t
mpd_find_pos_of_id (const char *desired_id)
{
	compact_map_t map;
	const char *id;
	for (size_t i = 0; i < g.playlist.len; i++)
	{
		if ((map = item_list_get (&g.playlist, i))
		 && (id = compact_map_find (map, "id"))
		 && !strcmp (id, desired_id))
			return i;
	}
	return -1;
}

static const char *
mpd_id_of_pos (int pos)
{
	compact_map_t map = item_list_get (&g.playlist, pos);
	return map ? compact_map_find (map,  "id") : NULL;
}

static void
mpd_process_info (const struct strv *data)
{
	struct tab *tab = &g_current_tab;
	char *prev_sel_id  = xstrdup0 (mpd_id_of_pos (tab->item_selected));
	char *prev_mark_id = xstrdup0 (mpd_id_of_pos (tab->item_mark));
	char *fallback_id  = NULL;

	struct tab_range r = tab_selection_range (g.active_tab);
	if (r.upto >= 0)
	{
		if (!(fallback_id = xstrdup0 (mpd_id_of_pos (r.upto + 1))))
			fallback_id = xstrdup0 (mpd_id_of_pos (r.from - 1));
	}

	mpd_process_info_data (data);

	const char *sel_id  = mpd_id_of_pos (tab->item_selected);
	const char *mark_id = mpd_id_of_pos (tab->item_mark);

	if (prev_mark_id && (!mark_id || strcmp (prev_mark_id, mark_id)))
		tab->item_mark = mpd_find_pos_of_id (prev_mark_id);
	if (prev_sel_id  && (!sel_id  || strcmp (prev_sel_id,  sel_id)))
	{
		if ((tab->item_selected = mpd_find_pos_of_id (prev_sel_id)) < 0)
		{
			tab->item_mark = -1;
			if (fallback_id)
				tab->item_selected = mpd_find_pos_of_id (fallback_id);
		}
		app_move_selection (0);
	}

	free (prev_sel_id);
	free (prev_mark_id);
	free (fallback_id);
}

static void
mpd_on_info_response (const struct mpd_response *response,
	const struct strv *data, void *user_data)
{
	(void) user_data;

	// TODO: preset an error player state?
	str_map_clear (&g.playback_info);
	if (!response->success)
		print_error ("%s: %s",
			"MPD status retrieval failed", response->message_text);
	else if (!data->len)
		print_debug ("empty MPD status response");
	else
		mpd_process_info (data);

	mpd_update_playback_state ();
	current_tab_update ();
	info_tab_update ();
}

static void
mpd_on_elapsed_time_tick (void *user_data)
{
	(void) user_data;

	// Compute how much time has elapsed since the last round second
	int64_t diff_msec = clock_msec (CLOCK_BEST) - g.elapsed_since;
	int elapsed_sec = diff_msec / 1000;
	int elapsed_msec = diff_msec % 1000;

	g.song_elapsed += elapsed_sec;
	g.elapsed_since += elapsed_sec * 1000;

	// Try to get called on the next round second of playback
	poller_timer_set (&g.elapsed_event, 1000 - elapsed_msec);

	xui_invalidate ();
}

static void
mpd_request_info (void)
{
	struct mpd_client *c = &g.client;

	mpd_client_list_ok_begin (c);
	mpd_client_send_command (c, "status", NULL);
	char *last_version = xstrdup_printf ("%" PRIu32, g.playlist_version);
	mpd_client_send_command (c, "plchanges", last_version, NULL);
	free (last_version);
	mpd_client_list_end (c);
	mpd_client_add_task (c, mpd_on_info_response, NULL);
	mpd_client_idle (c, 0);
}

static void
mpd_on_elapsed_time_tick_poll (void *user_data)
{
	(void) user_data;

	// As soon as the reply arrives, we (may) set the timer again
	mpd_request_info ();
}

static void
mpd_on_events (unsigned subsystems, void *user_data)
{
	(void) user_data;
	struct mpd_client *c = &g.client;

	if (subsystems & MPD_SUBSYSTEM_DATABASE)
		library_tab_reload (NULL);
	if (subsystems & MPD_SUBSYSTEM_OUTPUT)
		pulse_update ();

	if (subsystems & (MPD_SUBSYSTEM_PLAYER | MPD_SUBSYSTEM_OPTIONS
		| MPD_SUBSYSTEM_PLAYLIST | MPD_SUBSYSTEM_MIXER | MPD_SUBSYSTEM_UPDATE))
		mpd_request_info ();
	else
		mpd_client_idle (c, 0);
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

static void
mpd_queue_reconnect (void)
{
	poller_timer_set (&g.connect_event, 5 * 1000);
}

// On an error, MPD discards the rest of our enqueuing commands--work it around
static void mpd_enqueue_step (size_t start_offset);

static void
mpd_on_enqueue_response (const struct mpd_response *response,
	const struct strv *data, void *user_data)
{
	(void) data;
	intptr_t start_offset = (intptr_t) user_data;

	if (response->success)
		strv_reset (&g.enqueue);
	else
	{
		// Their addition may also overflow, but YOLO
		hard_assert (start_offset >= 0 && response->list_offset >= 0);

		print_error ("%s: %s", response->message_text,
			g.enqueue.vector[start_offset + response->list_offset]);
		mpd_enqueue_step (start_offset + response->list_offset + 1);
	}
}

static void
mpd_enqueue_step (size_t start_offset)
{
	struct mpd_client *c = &g.client;
	if (start_offset >= g.enqueue.len)
	{
		strv_reset (&g.enqueue);
		return;
	}

	// TODO: might want to consider using addid and autoplaying
	mpd_client_list_begin (c);
	for (size_t i = start_offset; i < g.enqueue.len; i++)
		mpd_client_send_command (c, "add", g.enqueue.vector[i], NULL);
	mpd_client_list_end (c);
	mpd_client_add_task (c, mpd_on_enqueue_response, (void *) start_offset);
	mpd_client_idle (c, 0);
}

static void
mpd_on_ready (void)
{
	mpd_request_info ();
	library_tab_reload (NULL);
	spectrum_setup_fifo ();
	pulse_update ();
	mpd_enqueue_step (0);
}

static void
mpd_on_password_response (const struct mpd_response *response,
	const struct strv *data, void *user_data)
{
	(void) data;
	(void) user_data;
	struct mpd_client *c = &g.client;

	if (response->success)
		mpd_on_ready ();
	else
	{
		print_error ("%s: %s",
			"MPD authentication failed", response->message_text);
		mpd_client_send_command (c, "close", NULL);
	}
}

static void
mpd_on_connected (void *user_data)
{
	(void) user_data;
	struct mpd_client *c = &g.client;

	const char *password =
		get_config_string (g.config.root, "settings.password");
	if (password)
	{
		mpd_client_send_command (c, "password", password, NULL);
		mpd_client_add_task (c, mpd_on_password_response, NULL);
	}
	else
		mpd_on_ready ();
}

static void
mpd_on_failure (void *user_data)
{
	(void) user_data;
	// This is also triggered both by a failed connect and a clean disconnect
	print_debug ("connection to MPD failed");
	mpd_queue_reconnect ();

	str_map_clear (&g.playback_info);
	item_list_resize (&g.playlist, 0);

	mpd_update_playback_state ();
	current_tab_update ();
	info_tab_update ();

	spectrum_discard_fifo ();
	pulse_disable ();
}

static void
mpd_on_io_hook (void *user_data, bool outgoing, const char *line)
{
	(void) user_data;
	if (outgoing)
		debug_tab_push (xstrdup_printf ("<< %s", line), APP_ATTR (OUTGOING));
	else
		debug_tab_push (xstrdup_printf (">> %s", line), APP_ATTR (INCOMING));
}

static void
app_on_reconnect (void *user_data)
{
	(void) user_data;

	struct mpd_client *c = &g.client;
	c->on_failure   = mpd_on_failure;
	c->on_connected = mpd_on_connected;
	c->on_event     = mpd_on_events;

	if (g_debug_mode)
		c->on_io_hook = mpd_on_io_hook;

	// We accept hostname/IPv4/IPv6 in pseudo-URL format, as well as sockets
	char *address = xstrdup (get_config_string (g.config.root,
		"settings.address")), *p = address, *host = address, *port = "6600";

	// Unwrap IPv6 addresses in format_host_port_pair() format
	char *right_bracket = strchr (p, ']');
	if (p[0] == '[' && right_bracket)
	{
		*right_bracket = '\0';
		host = p + 1;
		p = right_bracket + 1;
	}

	char *colon = strchr (p, ':');
	if (colon)
	{
		*colon = '\0';
		port = colon + 1;
	}

	struct error *e = NULL;
	if (!mpd_client_connect (c, host, port, &e))
	{
		print_error ("%s: %s", "cannot connect to MPD", e->message);
		error_free (e);
		mpd_queue_reconnect ();
	}
	free (address);
	xui_invalidate ();
}

// --- TUI ---------------------------------------------------------------------

static struct widget *
tui_make_button (chtype attrs, const char *label, enum action a)
{
	struct widget *w = tui_make_label (attrs, 0, label);
	w->id = WIDGET_BUTTON;
	w->userdata = a;
	return w;
}

static void
tui_render_gauge (struct widget *self)
{
	struct row_buffer buf = row_buffer_make ();
	if (g.state == PLAYER_STOPPED || g.song_elapsed < 0 || g.song_duration < 1)
		goto out;

	float ratio = (float) g.song_elapsed / g.song_duration;
	if (ratio < 0) ratio = 0;
	if (ratio > 1) ratio = 1;

	// Always compute it in exactly eight times the resolution,
	// because sometimes Unicode is even useful
	int len_left = ratio * self->width * 8 + 0.5;

	static const char *partials[] = { " ", "▏", "▎", "▍", "▌", "▋", "▊", "▉" };
	int remainder = len_left % 8;
	len_left /= 8;

	const char *partial = NULL;
	if (g.use_partial_boxes)
		partial = partials[remainder];
	else
		len_left += remainder >= (int) 4;

	int len_right = self->width - len_left;
	row_buffer_space (&buf, len_left, APP_ATTR (ELAPSED));
	if (partial && len_right-- > 0)
		row_buffer_append (&buf, partial, APP_ATTR (REMAINS));
	row_buffer_space (&buf, len_right, APP_ATTR (REMAINS));

out:
	tui_flush_buffer (self, &buf);
}

// TODO: Perhaps it should save the number within.
static struct widget *
tui_make_gauge (chtype attrs)
{
	struct widget *w = xcalloc (1, sizeof *w + 1);
	w->on_render = tui_render_gauge;
	w->attrs = attrs;
	w->width = -1;
	w->height = 1;
	return w;
}

static void
tui_render_spectrum (struct widget *self)
{
	// Don't mess up the line editor caret, when it's shown
	int last_x, last_y;
	getyx (stdscr, last_y, last_x);

	struct row_buffer buf = row_buffer_make ();
#ifdef WITH_FFTW
	row_buffer_append (&buf, g.spectrum.rendered, self->attrs);
#endif   // WITH_FFTW
	tui_flush_buffer (self, &buf);

	move (last_y, last_x);
}

static struct widget *
tui_make_spectrum (chtype attrs, int width)
{
	struct widget *w = xcalloc (1, sizeof *w + 1);
	w->on_render = tui_render_spectrum;
	w->attrs = attrs;
	w->width = width;
	w->height = 1;
	return w;
}

static void
tui_render_scrollbar (struct widget *self)
{
	// This assumes that we can write to the one-before-last column,
	// i.e. that it's not covered by any double-wide character (and that
	// ncurses comes to the right results when counting characters).
	struct tab *tab = g.active_tab;
	int visible_items = app_visible_items ();

	hard_assert (tab->item_count != 0);
	if (!g.use_partial_boxes)
	{
		struct scrollbar bar = app_compute_scrollbar (tab, visible_items, 1);
		for (int row = 0; row < visible_items; row++)
		{
			move (self->y + row, self->x);
			if (row < bar.start || row >= bar.start + bar.length)
				addch (' ' | self->attrs);
			else
				addch (' ' | self->attrs | A_REVERSE);
		}
		return;
	}

	struct scrollbar bar = app_compute_scrollbar (tab, visible_items * 8, 8);
	bar.length += bar.start;

	int start_part = bar.start  % 8; bar.start  /= 8;
	int end_part   = bar.length % 8; bar.length /= 8;

	// Even with this, the solid part must be at least one character high
	static const char *partials[] = { "█", "▇", "▆", "▅", "▄", "▃", "▂", "▁" };

	for (int row = 0; row < visible_items; row++)
	{
		chtype attrs = self->attrs;
		if (row > bar.start && row <= bar.length)
			attrs ^= A_REVERSE;

		const char *c = " ";
		if (row == bar.start)  c = partials[start_part];
		if (row == bar.length) c = partials[end_part];

		move (self->y + row, self->x);

		struct row_buffer buf = row_buffer_make ();
		row_buffer_append (&buf, c, attrs);
		row_buffer_flush (&buf);
		row_buffer_free (&buf);
	}
}

static struct widget *
tui_make_scrollbar (chtype attrs)
{
	struct widget *w = xcalloc (1, sizeof *w + 1);
	w->on_render = tui_render_scrollbar;
	w->attrs = attrs;
	w->width = 1;
	return 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;
	return w;
}

static void
tui_render_editor (struct widget *self)
{
	struct row_buffer buf = row_buffer_make ();
	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);
}

static struct widget *
tui_make_editor (chtype attrs)
{
	// TODO: This should ideally measure the text, and copy it to w->text.
	struct widget *w = xcalloc (1, sizeof *w + 1);
	w->on_render = tui_render_editor;
	w->attrs = attrs;
	w->width = -1;
	w->height = 1;
	return w;
}

static struct app_ui app_tui_ui =
{
	.padding     = tui_make_padding,
	.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,
};

// --- X11 ---------------------------------------------------------------------

#ifdef WITH_X11

// 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->userdata);
	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_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;
	for (size_t i = 0; i < total; i++)
		if (icon[i].x != INFINITY)
		{
			p->x = x + icon[i].x / 20.0 * self->width;
			p->y = y + icon[i].y / 20.0 * self->width;
			p++;
		}
		else if (p != buffer)
		{
			XRenderCompositeDoublePoly (g_xui.dpy, PictOpOver,
				source, g_xui.x11_pixmap_picture, format,
				0, 0, 0, 0, buffer, p - buffer, EvenOddRule);
			p = buffer;
		}
	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, 0, label);
	w->id = WIDGET_BUTTON;
	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_xui.vunit;
		w->width = w->height * 3 / 4;
	}
	return w;
}

static void
x11_render_gauge (struct widget *self)
{
	x11_render_padding (self);
	if (g.state == PLAYER_STOPPED || g.song_elapsed < 0 || g.song_duration < 1)
		return;

	int part = (float) g.song_elapsed / g.song_duration * self->width;
	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_xui.dpy, PictOpSrc, g_xui.x11_pixmap_picture,
		x11_bg_attrs (APP_ATTR (REMAINS)),
		self->x + part,
		self->y + self->height / 8,
		self->width - part,
		self->height * 3 / 4);
}

// TODO: Perhaps it should save the number within.
static struct widget *
x11_make_gauge (chtype attrs)
{
	struct widget *w = xcalloc (1, sizeof *w + 1);
	w->on_render = x11_render_gauge;
	w->attrs = attrs;
	w->width = -1;
	w->height = g_xui.vunit;
	return w;
}

static void
x11_render_spectrum (struct widget *self)
{
	x11_render_padding (self);

#ifdef WITH_FFTW
	XRectangle rectangles[g.spectrum.bars];
	int step = self->width / N_ELEMENTS (rectangles);
	for (int i = 0; i < g.spectrum.bars; i++)
	{
		int height = round ((self->height - 2) * g.spectrum.spectrum[i]);
		rectangles[i] = (XRectangle)
		{
			self->x + i * step,
			self->y + self->height - 1 - height,
			step,
			height,
		};
	}

	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_xui.x11_clip, g_xui.x11_clip);
}

static struct widget *
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_xui.vunit / 2;
	w->height = g_xui.vunit;
	return w;
}

static void
x11_render_scrollbar (struct widget *self)
{
	x11_render_padding (self);

	struct tab *tab = g.active_tab;
	struct scrollbar bar =
		app_compute_scrollbar (tab, app_visible_items_height (), g_xui.vunit);

	XRenderFillRectangle (g_xui.dpy, PictOpSrc, g_xui.x11_pixmap_picture,
		x11_fg_attrs (self->attrs),
		self->x,
		self->y + bar.start,
		self->width,
		bar.length);
}

static struct widget *
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_xui.vunit / 2;
	return w;
}

static struct widget *
x11_make_list (void)
{
	struct widget *w = xcalloc (1, sizeof *w + 1);
	w->on_render = x11_render_padding;
	return w;
}

static void
x11_render_editor (struct widget *self)
{
	x11_render_padding (self);

	struct x11_font *font = x11_widget_font (self);
	XftColor color = { .color = *x11_fg (self) };

	// A simplistic adaptation of tui_render_editor() follows.
	const struct line_editor *e = &g.editor;
	int x = self->x;
	if (e->prompt)
	{
		hard_assert (e->prompt < 127);
		x += x11_font_draw (font, &color, x, self->y,
			(char[2]) { e->prompt, 0 }) + g_xui.vunit / 4;
	}

	// TODO: Make this scroll around the caret, and fade like labels.
	size_t len;
	ucs4_t *buf = xcalloc (e->len + 1, sizeof *buf);
	u32_cpy (buf, e->line, e->point);
	char *a = (char *) u32_to_u8 (buf, u32_strlen (buf) + 1, NULL, &len);
	u32_cpy (buf, e->line + e->point, e->len - e->point + 1);
	char *b = (char *) u32_to_u8 (buf, u32_strlen (buf) + 1, NULL, &len);
	free (buf);

	x += x11_font_draw (font, &color, x, self->y, a);
	int caret = x;
	x += x11_font_draw (font, &color, x, self->y, b);
	free (a);
	free (b);

	XRenderFillRectangle (g_xui.dpy, PictOpSrc, g_xui.x11_pixmap_picture,
		&color.color, caret, self->y, 2, self->height);
}

static struct widget *
x11_make_editor (chtype attrs)
{
	// TODO: This should ideally measure the text, and copy it to w->text.
	struct widget *w = xcalloc (1, sizeof *w + 1);
	w->on_render = x11_render_editor;
	w->attrs = attrs;
	w->width = -1;
	w->height = g_xui.vunit;
	return w;
}

static struct app_ui app_x11_ui =
{
	.padding     = x11_make_padding,
	.label       = app_make_label,
	.button      = x11_make_button,
	.gauge       = x11_make_gauge,
	.spectrum    = x11_make_spectrum,
	.scrollbar   = x11_make_scrollbar,
	.list        = x11_make_list,
	.editor      = x11_make_editor,

	.have_icons  = true,
};

#endif  // WITH_X11

// --- Signals -----------------------------------------------------------------

static int g_signal_pipe[2];            ///< A pipe used to signal... signals

/// Program termination has been requested by a signal
static volatile sig_atomic_t g_termination_requested;
/// The window has changed in size
static volatile sig_atomic_t g_winch_received;

static void
signals_postpone_handling (char id)
{
	int original_errno = errno;
	if (write (g_signal_pipe[1], &id, 1) == -1)
		soft_assert (errno == EAGAIN);
	errno = original_errno;
}

static void
signals_superhandler (int signum)
{
	switch (signum)
	{
	case SIGWINCH:
		g_winch_received = true;
		signals_postpone_handling ('w');
		break;
	case SIGINT:
	case SIGTERM:
		g_termination_requested = true;
		signals_postpone_handling ('t');
		break;
	default:
		hard_assert (!"unhandled signal");
	}
}

static void
signals_setup_handlers (void)
{
	if (pipe (g_signal_pipe) == -1)
		exit_fatal ("%s: %s", "pipe", strerror (errno));

	set_cloexec (g_signal_pipe[0]);
	set_cloexec (g_signal_pipe[1]);

	// So that the pipe cannot overflow; it would make write() block within
	// the signal handler, which is something we really don't want to happen.
	// The same holds true for read().
	set_blocking (g_signal_pipe[0], false);
	set_blocking (g_signal_pipe[1], false);

	signal (SIGPIPE, SIG_IGN);

	struct sigaction sa;
	sa.sa_flags = SA_RESTART;
	sa.sa_handler = signals_superhandler;
	sigemptyset (&sa.sa_mask);

	if (sigaction (SIGWINCH, &sa, NULL) == -1
	 || sigaction (SIGINT,   &sa, NULL) == -1
	 || sigaction (SIGTERM,  &sa, NULL) == -1)
		exit_fatal ("sigaction: %s", strerror (errno));
}

// --- Initialisation, event handling ------------------------------------------

static bool g_verbose_mode = false;

static void
app_on_signal_pipe_readable (const struct pollfd *fd, void *user_data)
{
	(void) user_data;

	char id = 0;
	(void) read (fd->fd, &id, 1);

	if (g_termination_requested && !g.quitting)
		app_quit ();

	// It would be awkward to set up SIGWINCH conditionally,
	// so have it as a handler within UIs.
	if (g_winch_received)
	{
		g_winch_received = false;
		if (g_xui.ui->winch)
			g_xui.ui->winch ();
	}
}

static void
app_on_message_timer (void *user_data)
{
	(void) user_data;

	app_hide_message ();
}

static void
app_log_handler (void *user_data, const char *quote, const char *fmt,
	va_list ap)
{
	// We certainly don't want to end up in a possibly infinite recursion
	static bool in_processing;
	if (in_processing)
		return;

	in_processing = true;

	struct str message = str_make ();
	str_append (&message, quote);
	size_t quote_len = message.len;
	str_append_vprintf (&message, fmt, ap);

	// Show it prettified to the user, then maybe log it elsewhere as well.
	// TODO: Review locale encoding vs UTF-8 in the entire program.
	message.str[0] = toupper_ascii (message.str[0]);
	app_show_message (xstrndup (message.str, quote_len),
		xstrdup (message.str + quote_len));

	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),
			user_data == NULL ? 0 : g.attrs[(intptr_t) user_data].attrs);
	str_free (&message);

	in_processing = false;
}

static void
app_init_poller_events (void)
{
	g.signal_event = poller_fd_make (&g.poller, g_signal_pipe[0]);
	g.signal_event.dispatcher = app_on_signal_pipe_readable;
	poller_fd_set (&g.signal_event, POLLIN);

	g.message_timer = poller_timer_make (&g.poller);
	g.message_timer.dispatcher = app_on_message_timer;

	g.connect_event = poller_timer_make (&g.poller);
	g.connect_event.dispatcher = app_on_reconnect;
	poller_timer_set (&g.connect_event, 0);

	g.elapsed_event = poller_timer_make (&g.poller);
	g.elapsed_event.dispatcher = g.elapsed_poll
		? mpd_on_elapsed_time_tick_poll
		: mpd_on_elapsed_time_tick;
}

static void
app_init_ui (bool requested_x11)
{
	xui_preinit ();

	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
app_init_enqueue (char *argv[], int argc)
{
	// TODO: MPD is unwilling to play directories, so perhaps recurse ourselves
	char cwd[4096] = "";
	for (int i = 0; i < argc; i++)
	{
		// This is a super-trivial method of URL detection, however anything
		// contaning the scheme and authority delimiters in a sequence is most
		// certainly not a filesystem path, and thus it will work as expected.
		// Error handling may be done by MPD.
		const char *path_or_URL = argv[i];
		if (*path_or_URL == '/' || strstr (path_or_URL, "://"))
			strv_append (&g.enqueue, path_or_URL);
		else if (!*cwd && !getcwd (cwd, sizeof cwd))
			exit_fatal ("getcwd: %s", strerror (errno));
		else
			strv_append_owned (&g.enqueue,
				xstrdup_printf ("%s/%s", cwd, path_or_URL));
	}
}

int
main (int argc, char *argv[])
{
	static const struct opt opts[] =
	{
		{ 'd', "debug", NULL, 0, "run in debug mode" },
#ifdef WITH_X11
		{ 'x', "x11", NULL, 0, "use X11 even when run from a terminal" },
#endif  // WITH_X11
		{ 'h', "help", NULL, 0, "display this help and exit" },
		{ 'v', "verbose", NULL, 0, "log messages on standard error" },
		{ 'V', "version", NULL, 0, "output version information and exit" },
		{ 0, NULL, NULL, 0, NULL }
	};

	bool requested_x11 = false;
	struct opt_handler oh
		= opt_handler_make (argc, argv, opts, "[URL | PATH]...", "MPD client.");

	int c;
	while ((c = opt_handler_get (&oh)) != -1)
	switch (c)
	{
	case 'd':
		g_debug_mode = true;
		break;
	case 'x':
		requested_x11 = true;
		break;
	case 'v':
		g_verbose_mode = true;
		break;
	case 'h':
		opt_handler_usage (&oh, stdout);
		exit (EXIT_SUCCESS);
	case 'V':
		printf (PROGRAM_NAME " " PROGRAM_VERSION "\n");
		exit (EXIT_SUCCESS);
	default:
		print_error ("wrong options");
		opt_handler_usage (&oh, stderr);
		exit (EXIT_FAILURE);
	}

	argc -= optind;
	argv += optind;
	opt_handler_free (&oh);

	// We only need to convert to and from the terminal encoding
	if (!setlocale (LC_CTYPE, ""))
		print_warning ("failed to set the locale");

	app_init_context ();
	app_init_enqueue (argv, argc);
	app_load_configuration ();
	signals_setup_handlers ();
	app_init_poller_events ();
	app_init_ui (requested_x11);

	if (g_debug_mode)
		app_prepend_tab (debug_tab_init ());

	// Redirect all messages from liberty to a special tab so they're not lost
	g_log_message_real = app_log_handler;

	app_prepend_tab (info_tab_init ());
	if (g.streams.len)
		app_prepend_tab (streams_tab_init ());
	app_prepend_tab (library_tab_init ());
	app_prepend_tab (current_tab_init ());
	app_switch_tab ((g.help_tab = help_tab_init ()));

	// TODO: the help tab should be the default for new users only,
	//   so provide a configuration option to flip this
	if (argc)
		app_switch_tab (&g_current_tab);

	g.polling = true;
	while (g.polling)
		poller_run (&g.poller);

	xui_stop ();
	g_log_message_real = log_message_stdio;
	app_free_context ();
	return 0;
}