nncmpp/nncmpp.c
Přemysl Eric Janouch 8b4300c796
All checks were successful
Alpine 3.20 Success
Arch Linux AUR Success
OpenBSD 7.5 Success
Bump liberty, check the connection while searching
This just prevents immediate assertion failures.
2025-01-08 08:10:19 +01:00

6391 lines
171 KiB
C

/*
* 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)