6391 lines
171 KiB
C
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)
|
|