aboutsummaryrefslogtreecommitdiff
path: root/nncmpp.c
diff options
context:
space:
mode:
Diffstat (limited to 'nncmpp.c')
-rw-r--r--nncmpp.c3484
1 files changed, 2539 insertions, 945 deletions
diff --git a/nncmpp.c b/nncmpp.c
index 8677635..0ee6796 100644
--- a/nncmpp.c
+++ b/nncmpp.c
@@ -1,7 +1,7 @@
/*
* nncmpp -- the MPD client you never knew you needed
*
- * Copyright (c) 2016 - 2021, Přemysl Eric Janouch <p@janouch.name>
+ * Copyright (c) 2016 - 2023, 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.
@@ -28,7 +28,7 @@
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_UNDERLINE ) \
+ XX( TAB_ACTIVE, tab_active, -1, -1, A_BOLD ) \
/* Listview */ \
XX( HEADER, header, -1, -1, A_UNDERLINE ) \
XX( EVEN, even, -1, -1, 0 ) \
@@ -70,69 +70,54 @@ enum
#define LIBERTY_WANT_PROTO_HTTP
#define LIBERTY_WANT_PROTO_MPD
#include "liberty/liberty.c"
-#include "liberty/liberty-tui.c"
-#define HAVE_LIBERTY
-#include "line-editor.c"
+#ifdef WITH_X11
+#define LIBERTY_XUI_WANT_X11
+#endif // WITH_X11
+#include "liberty/liberty-xui.c"
-#include <math.h>
+#include <dirent.h>
#include <locale.h>
-#include <termios.h>
-#ifndef TIOCGWINSZ
-#include <sys/ioctl.h>
-#endif // ! TIOCGWINSZ
-
-// ncurses is notoriously retarded for input handling, we need something
-// different if only to receive mouse events reliably.
-//
-// 2021 update: ncurses is mostly reliable now, though rxvt-unicode only
-// supports the 1006 mode that ncurses also supports mode starting with 9.25.
-
-#include "termo.h"
+#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 ---------------------------------------------------------------
-// The standard endwin/refresh sequence makes the terminal flicker
static void
-update_curses_terminal_size (void)
+shell_quote (const char *str, struct str *output)
{
-#if defined (HAVE_RESIZETERM) && defined (TIOCGWINSZ)
- struct winsize size;
- if (!ioctl (STDOUT_FILENO, TIOCGWINSZ, (char *) &size))
+ // See SUSv3 Shell and Utilities, 2.2.3 Double-Quotes
+ str_append_c (output, '"');
+ for (const char *p = str; *p; p++)
{
- char *row = getenv ("LINES");
- char *col = getenv ("COLUMNS");
- unsigned long tmp;
- resizeterm (
- (row && xstrtoul (&tmp, row, 10)) ? tmp : size.ws_row,
- (col && xstrtoul (&tmp, col, 10)) ? tmp : size.ws_col);
+ if (strchr ("`$\"\\", *p))
+ str_append_c (output, '\\');
+ str_append_c (output, *p);
}
-#else // HAVE_RESIZETERM && TIOCGWINSZ
- endwin ();
- refresh ();
-#endif // HAVE_RESIZETERM && TIOCGWINSZ
-}
-
-static int64_t
-clock_msec (clockid_t clock)
-{
- struct timespec tp;
- hard_assert (clock_gettime (clock, &tp) != -1);
- return (int64_t) tp.tv_sec * 1000 + (int64_t) tp.tv_nsec / 1000000;
+ str_append_c (output, '"');
}
static bool
@@ -149,6 +134,8 @@ xbasename (const char *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)
{
@@ -168,6 +155,18 @@ latin1_to_utf8 (const char *latin1)
}
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]))
@@ -275,6 +274,10 @@ struct poller_curl
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
@@ -317,6 +320,8 @@ poller_curl_on_socket_action (CURL *easy, curl_socket_t s, int what,
struct poller_curl_fd *fd;
if (!(fd = socket_data))
{
+ set_cloexec (s);
+
fd = xmalloc (sizeof *fd);
LIST_PREPEND (self->fds, fd);
@@ -383,6 +388,7 @@ poller_curl_init (struct poller_curl *self, struct poller *poller,
|| (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));
}
@@ -443,6 +449,7 @@ poller_curl_add (struct poller_curl *self, CURL *easy, struct error **e)
// "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;
}
@@ -452,6 +459,7 @@ 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;
}
@@ -586,7 +594,8 @@ struct spectrum
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 *spectrum; ///< String buffer for the "render"
+ 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
@@ -708,7 +717,7 @@ spectrum_sample (struct spectrum *s)
}
int last_bin = 0;
- char *p = s->spectrum;
+ char *p = s->rendered;
for (int bar = 0; bar < s->bars; bar++)
{
int top_bin = s->top_bins[bar];
@@ -726,14 +735,19 @@ spectrum_sample (struct spectrum *s)
db = 0;
// Assuming decibels are always negative (i.e., properly normalized).
- // The division defines the cutoff: 9 * 7 = 63 dB of range.
+ // 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, struct error **e)
+spectrum_init (struct spectrum *s, char *format, int bars, int fps,
+ struct error **e)
{
errno = 0;
@@ -793,13 +807,15 @@ spectrum_init (struct spectrum *s, char *format, int bars, struct error **e)
// 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.)
+ // (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->spectrum = xcalloc (sizeof *s->spectrum, s->bars * 3 + 1);
+ s->rendered = xcalloc (sizeof *s->rendered, s->bars * 3 + 1);
+ s->spectrum = xcalloc (sizeof *s->spectrum, s->bars);
s->top_bins = xcalloc (sizeof *s->top_bins, s->bars);
for (int bar = 0; bar < s->bars; bar++)
{
@@ -807,8 +823,7 @@ spectrum_init (struct spectrum *s, char *format, int bars, struct error **e)
s->top_bins[bar] = MIN (top_bin, used_bins);
}
- // Limit updates to 30 times per second to limit CPU load
- s->samples = s->sampling_rate / s->bins * 2 / 30;
+ s->samples = s->sampling_rate / s->bins * 2 / MAX (fps, 1);
if (s->samples < 1)
s->samples = 1;
@@ -851,7 +866,12 @@ spectrum_free (struct spectrum *s)
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);
@@ -861,45 +881,318 @@ spectrum_free (struct spectrum *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
+// 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're not using any TUI framework (which are mostly a lost cause to me
-// in the post-Unicode era and not worth pursuing), and the code would get
-// bloated and incomprehensible fast. We mostly rely on "row_buffer" to write
-// text from left to right row after row while keeping track of cells.
+// 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;
-enum action;
/// Try to handle an action in the tab
typedef bool (*tab_action_fn) (enum action action);
-/// Draw an item to the screen using the row buffer API
-typedef void (*tab_item_draw_fn)
- (size_t item_index, struct row_buffer *buffer, int width);
+/// 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
- size_t name_width; ///< Visible width of the name
-
char *header; ///< The header, should there be any
// Implementation:
tab_action_fn on_action; ///< User action handler callback
- tab_item_draw_fn on_item_draw; ///< Item draw callback
+ tab_item_layout_fn on_item_layout; ///< Item layout callback
// Provided by tab owner:
@@ -926,6 +1219,7 @@ 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
@@ -934,6 +1228,7 @@ static struct app_context
struct poller_timer message_timer; ///< Message timeout
char *message; ///< Message to show in the statusbar
+ char *message_detail; ///< Non-emphasized part
// Connection:
@@ -947,7 +1242,6 @@ static struct app_context
int64_t elapsed_since; ///< Last tick ts or last elapsed time
bool elapsed_poll; ///< Poll MPD for the elapsed time?
- // TODO: initialize these to -1
int song; ///< Current song index
int song_elapsed; ///< Song elapsed in seconds
int song_duration; ///< Song duration in seconds
@@ -961,38 +1255,34 @@ static struct app_context
struct config config; ///< Program configuration
struct strv streams; ///< List of "name NUL URI NUL"
+ struct strv enqueue; ///< Items to enqueue once connected
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
- // Emulated widgets:
-
- int header_height; ///< Height of the header
+ // User interface:
- int tabs_offset; ///< Offset to tabs or -1
- int controls_offset; ///< Offset to player controls or -1
- int gauge_offset; ///< Offset to the gauge or -1
- int gauge_width; ///< Width of the gauge, if present
+ 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)
- int spectrum_column, spectrum_row; ///< Position for fast refresh
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
- struct poller_idle refresh_event; ///< Refresh the screen
// Terminal:
- termo_t *tk; ///< termo handle
- struct poller_timer tk_timer; ///< termo timeout timer
- bool locale_is_utf8; ///< The locale is Unicode
bool use_partial_boxes; ///< Use Unicode box drawing chars
- bool focused; ///< Whether the terminal has focus
struct attrs attrs[ATTRIBUTE_COUNT];
}
@@ -1010,9 +1300,6 @@ tab_init (struct tab *self, const char *name)
// Add some padding for decorative purposes
self->name = xstrdup_printf (" %s ", name);
- // Assuming tab names are pure ASCII, otherwise this would be inaccurate
- // and we'd need to filter it first to replace invalid chars with '?'
- self->name_width = u8_strwidth ((uint8_t *) self->name, locale_charset ());
self->item_selected = 0;
self->item_mark = -1;
}
@@ -1043,6 +1330,13 @@ on_poll_elapsed_time_changed (struct config_item *item)
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 struct config_schema g_config_settings[] =
{
{ .name = "address",
@@ -1054,6 +1348,7 @@ static struct config_schema g_config_settings[] =
.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 },
@@ -1072,8 +1367,27 @@ static struct config_schema g_config_settings[] =
.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
@@ -1216,74 +1530,46 @@ app_init_attributes (void)
#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.playlist = item_list_make ();
+ g.enqueue = strv_make ();
g.playback_info = str_map_make (free);
g.playback_info.key_xfrm = tolower_ascii_strxfrm;
#ifdef WITH_FFTW
g.spectrum_fd = -1;
- g.spectrum_row = g.spectrum_column = -1;
#endif // WITH_FFTW
- // This is also approximately what libunistring does internally,
- // since the locale name is canonicalized by locale_charset().
- // Note that non-Unicode locales are handled pretty inefficiently.
- g.locale_is_utf8 = !strcasecmp_ascii (locale_charset (), "UTF-8");
-
- // It doesn't work 100% (e.g. incompatible with undelining in urxvt)
- // TODO: make this configurable
- g.use_partial_boxes = g.locale_is_utf8;
-
- // Presumably, although not necessarily; unsure if queryable at all
- g.focused = true;
+#ifdef WITH_PULSE
+ pulse_init (&g.pulse, NULL);
+#endif // WITH_PULSE
app_init_attributes ();
}
static void
-app_init_terminal (void)
-{
- TERMO_CHECK_VERSION;
- if (!(g.tk = termo_new (STDIN_FILENO, NULL, 0)))
- abort ();
- if (!initscr () || nonl () == ERR)
- abort ();
-
- // By default we don't use any colors so they're not required...
- if (start_color () == ERR
- || use_default_colors () == ERR
- || COLOR_PAIRS <= ATTRIBUTE_COUNT)
- return;
-
- for (int a = 0; a < ATTRIBUTE_COUNT; a++)
- {
- // ...thus we can reset back to defaults even after initializing some
- if (g.attrs[a].fg >= COLORS || g.attrs[a].fg < -1
- || g.attrs[a].bg >= COLORS || g.attrs[a].bg < -1)
- {
- app_init_attributes ();
- return;
- }
-
- init_pair (a + 1, g.attrs[a].fg, g.attrs[a].bg);
- g.attrs[a].attrs |= COLOR_PAIR (a + 1);
- }
-}
-
-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);
item_list_free (&g.playlist);
#ifdef WITH_FFTW
@@ -1295,14 +1581,17 @@ app_free_context (void)
}
#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);
-
- if (g.tk)
- termo_destroy (g.tk);
+ free (g.message_detail);
}
static void
@@ -1315,92 +1604,179 @@ app_quit (void)
g.polling = false;
}
-static bool
-app_is_character_in_locale (ucs4_t ch)
+// --- Layouting ---------------------------------------------------------------
+
+static void
+app_append_layout (struct layout *l, struct layout *dest)
{
- // Avoid the overhead joined with calling iconv() for all characters.
- if (g.locale_is_utf8)
- return true;
+ 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);
- // The library really creates a new conversion object every single time
- // and doesn't provide any smarter APIs. Luckily, most users use UTF-8.
- size_t len;
- char *tmp = u32_conv_to_encoding (locale_charset (), iconveh_error,
- &ch, 1, NULL, NULL, &len);
- if (!tmp)
- return false;
- free (tmp);
- return true;
-}
+ last->next = l->head;
+ l->head->prev = last;
+ dest->tail = l->tail;
+ }
-// --- Rendering ---------------------------------------------------------------
+ *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_invalidate (void)
+app_flush_layout_full (struct layout *l, int width, struct layout *dest)
{
- poller_idle_set (&g.refresh_event);
+ 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_buffer (struct row_buffer *buf, int width, chtype attrs)
+app_flush_layout (struct layout *l, struct layout *out)
{
- row_buffer_align (buf, width, attrs);
- row_buffer_flush (buf);
- row_buffer_free (buf);
+ app_flush_layout_full (l, g_xui.width, out);
}
-/// Write the given UTF-8 string padded with spaces.
-/// @param[in] attrs Text attributes for the text, including padding.
-static void
-app_write_line (const char *str, chtype attrs)
+static struct widget *
+app_push (struct layout *l, struct widget *w)
{
- struct row_buffer buf = row_buffer_make ();
- row_buffer_append (&buf, str, attrs);
- app_flush_buffer (&buf, COLS, attrs);
+ 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_flush_header (struct row_buffer *buf, chtype attrs)
+app_layout_text (const char *str, chtype attrs, struct layout *out)
{
- move (g.header_height++, 0);
- app_flush_buffer (buf, COLS, attrs);
+ 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_draw_song_info (void)
+app_layout_song_info (struct layout *out)
{
compact_map_t map;
if (!(map = item_list_get (&g.playlist, g.song)))
return;
- chtype attr_normal = APP_ATTR (NORMAL);
- chtype attr_highlight = APP_ATTR (HIGHLIGHT);
+ chtype attrs[2] = { APP_ATTR (NORMAL), APP_ATTR (HIGHLIGHT) };
- char *title;
+ // 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 = compact_map_find (map, "name"))
- || (title = compact_map_find (map, "file")))
+ || (title = name)
+ || (title = subroot_basename)
+ || (title = file))
{
- struct row_buffer buf = row_buffer_make ();
- row_buffer_append (&buf, title, attr_highlight);
- app_flush_header (&buf, attr_normal);
+ 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)
- return;
+ 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));
+ }
- struct row_buffer buf = row_buffer_make ();
- if (artist)
- row_buffer_append_args (&buf, " by " + !buf.total_width, attr_normal,
- artist, attr_highlight, NULL);
- if (album)
- row_buffer_append_args (&buf, " from " + !buf.total_width, attr_normal,
- album, attr_highlight, NULL);
- app_flush_header (&buf, attr_normal);
+ 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 *
@@ -1420,179 +1796,147 @@ app_time_string (int seconds)
}
static void
-app_write_time (struct row_buffer *buf, int seconds, chtype attrs)
+app_layout_status (struct layout *out)
{
- char *s = app_time_string (seconds);
- row_buffer_append (buf, s, attrs);
- free (s);
-}
-
-static void
-app_write_gauge (struct row_buffer *buf, float ratio, int width)
-{
- if (ratio < 0) ratio = 0;
- if (ratio > 1) ratio = 1;
-
- // Always compute it in exactly eight times the resolution,
- // because sometimes Unicode is even useful
- int len_left = ratio * width * 8 + 0.5;
-
- static const char *partials[] = { " ", "▏", "▎", "▍", "▌", "▋", "▊", "▉" };
- int remainder = len_left % 8;
- len_left /= 8;
-
- const char *partial = NULL;
- if (g.use_partial_boxes)
- partial = partials[remainder];
- else
- len_left += remainder >= (int) 4;
-
- int len_right = width - len_left;
- row_buffer_space (buf, len_left, APP_ATTR (ELAPSED));
- if (partial && len_right-- > 0)
- row_buffer_append (buf, partial, APP_ATTR (REMAINS));
- row_buffer_space (buf, len_right, APP_ATTR (REMAINS));
-}
-
-static void
-app_draw_status (void)
-{
- if (g.state != PLAYER_STOPPED)
- app_draw_song_info ();
-
- chtype attr_normal = APP_ATTR (NORMAL);
- chtype attr_highlight = APP_ATTR (HIGHLIGHT);
-
- struct row_buffer buf = row_buffer_make ();
bool stopped = g.state == PLAYER_STOPPED;
- chtype attr_song_action = stopped ? attr_normal : attr_highlight;
+ 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 ? "||" : "|>";
- row_buffer_append_args (&buf,
- "<<", attr_song_action, " ", attr_normal,
- toggle, attr_highlight, " ", attr_normal,
- "[]", attr_song_action, " ", attr_normal,
- ">>", attr_song_action, " ", attr_normal,
- NULL);
+ 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)
- row_buffer_append (&buf, "Stopped", attr_normal);
+ app_push_fill (&l, g.ui->label (attrs[0], "Stopped"));
else
{
if (g.song_elapsed >= 0)
{
- app_write_time (&buf, g.song_elapsed, attr_normal);
- row_buffer_append (&buf, " ", attr_normal);
+ char *s = app_time_string (g.song_elapsed);
+ app_push (&l, g.ui->label (attrs[0], s));
+ free (s);
}
if (g.song_duration >= 1)
{
- row_buffer_append (&buf, "/ ", attr_normal);
- app_write_time (&buf, g.song_duration, attr_normal);
- row_buffer_append (&buf, " ", attr_normal);
+ 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);
}
- row_buffer_append (&buf, " ", attr_normal);
- }
- // It gets a bit complicated due to the only right-aligned item on the row
- char *volume = NULL;
- int remaining = COLS - buf.total_width;
- if (g.volume >= 0)
- {
- volume = xstrdup_printf (" %3d%%", g.volume);
- remaining -= strlen (volume);
+ app_push (&l, g.ui->padding (attrs[0], 1, 1));
}
- if (!stopped && g.song_elapsed >= 0 && g.song_duration >= 1
- && remaining > 0)
+ struct str volume = str_make ();
+#ifdef WITH_PULSE
+ if (g.pulse_control_requested)
{
- g.gauge_offset = buf.total_width;
- g.gauge_width = remaining;
- app_write_gauge (&buf,
- (float) g.song_elapsed / g.song_duration, remaining);
+ 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
- row_buffer_space (&buf, remaining, attr_normal);
+#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)
+ if (volume.len)
{
- row_buffer_append (&buf, volume, attr_normal);
- free (volume);
+ app_push (&l, g.ui->padding (attrs[0], 1, 1));
+ app_push (&l, g.ui->label (attrs[0], volume.str))
+ ->id = WIDGET_VOLUME;
}
- g.controls_offset = g.header_height;
- app_flush_header (&buf, attr_normal);
+ str_free (&volume);
+
+ app_push (&l, g.ui->padding (attrs[0], 0.25, 1));
+ app_flush_layout (&l, out);
}
static void
-app_draw_header (void)
+app_layout_tabs (struct layout *out)
{
- g.header_height = 0;
-
- g.tabs_offset = -1;
- g.controls_offset = -1;
- g.gauge_offset = -1;
- g.gauge_width = 0;
-
- switch (g.client.state)
- {
- case MPD_CONNECTED:
- app_draw_status ();
- break;
- case MPD_CONNECTING:
- move (g.header_height++, 0);
- app_write_line ("Connecting to MPD...", APP_ATTR (NORMAL));
- break;
- case MPD_DISCONNECTED:
- move (g.header_height++, 0);
- app_write_line ("Disconnected", APP_ATTR (NORMAL));
- }
-
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
- struct row_buffer buf = row_buffer_make ();
- row_buffer_append (&buf, APP_TITLE, attrs[g.active_tab == g.help_tab]);
- row_buffer_append (&buf, " ", attrs[false]);
+ 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;
- g.tabs_offset = g.header_height;
+ // 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)
- row_buffer_append (&buf, iter->name, attrs[iter == g.active_tab]);
+ {
+ 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)
{
- // Find some space and remember where it was, for fast refreshes
- row_buffer_ellipsis (&buf, COLS - g.spectrum.bars - 1);
- row_buffer_align (&buf, COLS - g.spectrum.bars, attrs[false]);
- g.spectrum_row = g.header_height;
- g.spectrum_column = buf.total_width;
-
- row_buffer_append (&buf, g.spectrum.spectrum, attrs[false]);
+ app_push (&l, g.ui->spectrum (attrs[0], g.spectrum.bars))
+ ->id = WIDGET_SPECTRUM;
}
#endif // WITH_FFTW
- app_flush_header (&buf, attrs[false]);
-
- const char *header = g.active_tab->header;
- if (header)
- {
- buf = row_buffer_make ();
- row_buffer_append (&buf, header, APP_ATTR (HEADER));
- app_flush_header (&buf, APP_ATTR (HEADER));
- }
+ app_flush_layout (&l, out);
}
-static int
-app_fitting_items (void)
+static void
+app_layout_padding (chtype attrs, struct layout *out)
{
- // The raw number of items that would have fit on the terminal
- return LINES - g.header_height - 1 /* status bar */;
+ struct layout l = {};
+ app_push_fill (&l, g.ui->padding (attrs, 0, 0.125));
+ app_flush_layout (&l, out);
}
-static int
-app_visible_items (void)
+static void
+app_layout_header (struct layout *out)
{
- return MAX (0, app_fitting_items ());
+ 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
@@ -1600,7 +1944,7 @@ app_visible_items (void)
struct scrollbar { long length, start; }
app_compute_scrollbar (struct tab *tab, long visible, long s)
{
- long top = tab->item_top, total = tab->item_count;
+ long top = s * tab->item_top, total = s * tab->item_count;
if (total < visible)
return (struct scrollbar) { 0, 0 };
if (visible == 1)
@@ -1610,7 +1954,7 @@ app_compute_scrollbar (struct tab *tab, long visible, long s)
// 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 = s * visible - 2 - s + 1;
+ 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);
@@ -1618,215 +1962,247 @@ app_compute_scrollbar (struct tab *tab, long visible, long s)
if (top == 0)
return (struct scrollbar) { length, 0 };
if (top + visible >= total)
- return (struct scrollbar) { length, s * visible - length };
+ return (struct scrollbar) { length, visible - length };
return (struct scrollbar) { length, offset };
}
-static void
-app_draw_scrollbar (void)
+static struct layout
+app_layout_row (struct tab *tab, int item_index)
{
- // This assumes that we can write to the one-before-last column,
- // i.e. that it's not covered by any double-wide character (and that
- // ncurses comes to the right results when counting characters).
- //
- // We could also precompute the scrollbar and append it to each row
- // as we render them, plus all the unoccupied rows.
- struct tab *tab = g.active_tab;
- int visible_items = app_visible_items ();
-
- hard_assert (tab->item_count != 0);
- if (!g.use_partial_boxes)
- {
- struct scrollbar bar = app_compute_scrollbar (tab, visible_items, 1);
- for (int row = 0; row < visible_items; row++)
- {
- move (g.header_height + row, COLS - 1);
- if (row < bar.start || row >= bar.start + bar.length)
- addch (' ' | APP_ATTR (SCROLLBAR));
- else
- addch (' ' | APP_ATTR (SCROLLBAR) | A_REVERSE);
- }
- return;
- }
-
- struct scrollbar bar = app_compute_scrollbar (tab, visible_items, 8);
- bar.length += bar.start;
+ int row_attrs = (item_index & 1) ? APP_ATTR (ODD) : APP_ATTR (EVEN);
- int start_part = bar.start % 8; bar.start /= 8;
- int end_part = bar.length % 8; bar.length /= 8;
+ 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;
- // Even with this, the solid part must be at least one character high
- static const char *partials[] = { "█", "▇", "▆", "▅", "▄", "▃", "▂", "▁" };
+ // 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));
- for (int row = 0; row < visible_items; row++)
+ // Combine attributes used by the handler with the defaults.
+ LIST_FOR_EACH (struct widget, w, l.head)
{
- chtype attrs = APP_ATTR (SCROLLBAR);
- if (row > bar.start && row <= bar.length)
- attrs ^= A_REVERSE;
-
- const char *c = " ";
- if (row == bar.start) c = partials[start_part];
- if (row == bar.length) c = partials[end_part];
-
- move (g.header_height + row, COLS - 1);
-
- struct row_buffer buf = row_buffer_make ();
- row_buffer_append (&buf, c, attrs);
- row_buffer_flush (&buf);
- row_buffer_free (&buf);
+ 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_draw_view (void)
+app_layout_view (struct layout *out, int height)
{
- move (g.header_height, 0);
- clrtobot ();
+ 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;
- bool want_scrollbar = (int) tab->item_count > app_visible_items ();
- int view_width = COLS - want_scrollbar;
+ 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));
- int to_show =
- MIN (app_fitting_items (), (int) tab->item_count - tab->item_top);
+ struct layout children = {};
for (int row = 0; row < to_show; row++)
{
int item_index = tab->item_top + row;
- 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.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.focused
- ? APP_ATTR (MULTISELECT) : APP_ATTR (DEFOCUSED);
- else
- override_colors = false;
-
- struct row_buffer buf = row_buffer_make ();
- tab->on_item_draw (item_index, &buf, view_width);
-
- // Combine attributes used by the handler with the defaults.
- // Avoiding attrset() because of row_buffer_flush().
- for (size_t i = 0; i < buf.chars_len; i++)
- {
- chtype *attrs = &buf.chars[i].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;
- }
-
- move (g.header_height + row, 0);
- app_flush_buffer (&buf, view_width, row_attrs);
+ 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;
- if (want_scrollbar)
- app_draw_scrollbar ();
+ app_flush_layout (&l, out);
}
static void
-app_write_mpd_status_playlist (struct row_buffer *buf)
+app_layout_mpd_status_playlist (struct layout *l, chtype attrs)
{
- struct str stats = str_make ();
- if (g.playlist.len == 1)
- str_append_printf (&stats, "1 song ");
- else
- str_append_printf (&stats, "%zu songs ", g.playlist.len);
+ 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)
{
- str_append_c (&stats, ' ');
-
+ struct str length = str_make ();
if (hours == 1)
- str_append_printf (&stats, " 1 hour");
+ str_append_printf (&length, " 1 hour");
else if (hours)
- str_append_printf (&stats, " %d hours", hours);
+ str_append_printf (&length, " %d hours", hours);
if (minutes == 1)
- str_append_printf (&stats, " 1 minute");
+ str_append_printf (&length, " 1 minute");
else if (minutes)
- str_append_printf (&stats, " %d minutes", 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));
}
- row_buffer_append (buf, stats.str, APP_ATTR (NORMAL));
- str_free (&stats);
}
static void
-app_write_mpd_status (struct row_buffer *buf)
+app_layout_mpd_status (struct layout *out)
{
- struct str_map *map = &g.playback_info;
+ 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);
- row_buffer_append (buf, msg, APP_ATTR (HIGHLIGHT));
+ app_push_fill (&l, g.ui->label (attrs[0], msg));
free (msg);
}
- else if (str_map_find (map, "updating_db"))
- row_buffer_append (buf, "Updating database...", APP_ATTR (NORMAL));
else
- app_write_mpd_status_playlist (buf);
+ {
+ app_layout_mpd_status_playlist (&l, attrs[0]);
+ l.tail->width = -1;
+ }
- const char *s;
+ 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");
- struct row_buffer right = row_buffer_make ();
- chtype a[2] = { APP_ATTR (NORMAL), APP_ATTR (HIGHLIGHT) };
- if (repeat) row_buffer_append_args (&right,
- " ", APP_ATTR (NORMAL), "repeat", a[repeat], NULL);
- if (random) row_buffer_append_args (&right,
- " ", APP_ATTR (NORMAL), "random", a[random], NULL);
- if (single) row_buffer_append_args (&right,
- " ", APP_ATTR (NORMAL), "single", a[single], NULL);
- if (consume) row_buffer_append_args (&right,
- " ", APP_ATTR (NORMAL), "consume", a[consume], NULL);
+ 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));
+ }
- row_buffer_space (buf,
- MAX (0, COLS - buf->total_width - right.total_width),
- APP_ATTR (NORMAL));
- row_buffer_append_buffer (buf, &right);
- row_buffer_free (&right);
+ app_push (&l, g.ui->padding (attrs[0], 0.25, 1));
+ app_flush_layout (&l, out);
}
static void
-app_draw_statusbar (void)
+app_layout_statusbar (struct layout *out)
{
- int caret = -1;
+ chtype attrs[2] = { APP_ATTR (NORMAL), APP_ATTR (HIGHLIGHT) };
+ app_layout_padding (attrs[0], out);
- struct row_buffer buf = row_buffer_make ();
+ struct layout l = {};
if (g.message)
- row_buffer_append (&buf, g.message, APP_ATTR (HIGHLIGHT));
- else if (g.editor.line)
- caret = line_editor_write (&g.editor, &buf, COLS, APP_ATTR (HIGHLIGHT));
- else if (g.client.state == MPD_CONNECTED)
- app_write_mpd_status (&buf);
-
- move (LINES - 1, 0);
- app_flush_buffer (&buf, COLS, APP_ATTR (NORMAL));
+ {
+ 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));
- curs_set (0);
- if (caret != -1)
+ app_flush_layout (&l, out);
+ LIST_FOR_EACH (struct widget, w, l.head)
+ w->id = WIDGET_MESSAGE;
+ }
+ else if (g.editor.line)
{
- move (LINES - 1, caret);
- curs_set (1);
+ 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)
@@ -1852,17 +2228,27 @@ app_fix_view_range (void)
}
static void
-app_on_refresh (void *user_data)
+app_layout (void)
{
- (void) user_data;
- poller_idle_reset (&g.refresh_event);
+ struct layout top = {}, bottom = {};
+ app_layout_header (&top);
+ app_layout_statusbar (&bottom);
+
+ int available_height = g_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_draw_header ();
app_fix_view_range();
- app_draw_view ();
- app_draw_statusbar ();
- refresh ();
+ curs_set (0);
}
// --- Actions -----------------------------------------------------------------
@@ -1872,7 +2258,7 @@ static bool
app_scroll (int n)
{
g.active_tab->item_top += n;
- app_invalidate ();
+ xui_invalidate ();
return app_fix_view_range ();
}
@@ -1894,6 +2280,19 @@ app_ensure_selection_visible (void)
}
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;
@@ -1903,19 +2302,52 @@ app_move_selection (int diff)
bool result = !diff || tab->item_selected != fixed;
tab->item_selected = fixed;
- app_invalidate ();
+ xui_invalidate ();
app_ensure_selection_visible ();
return result;
}
+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);
- app_invalidate ();
+ xui_invalidate ();
}
static void
@@ -1926,7 +2358,7 @@ app_switch_tab (struct tab *tab)
g.last_tab = g.active_tab;
g.active_tab = tab;
- app_invalidate ();
+ xui_invalidate ();
}
static bool
@@ -1944,105 +2376,12 @@ app_goto_tab (int tab_index)
// --- Actions -----------------------------------------------------------------
-#define ACTIONS(XX) \
- XX( NONE, Do nothing ) \
- \
- XX( QUIT, Quit ) \
- XX( REDRAW, Redraw screen ) \
- XX( TAB_HELP, Switch to help tab ) \
- XX( TAB_LAST, Switch to last tab ) \
- XX( TAB_PREVIOUS, Switch to previous tab ) \
- XX( TAB_NEXT, Switch to next tab ) \
- \
- XX( MPD_TOGGLE, Toggle play/pause ) \
- XX( MPD_STOP, Stop playback ) \
- XX( MPD_PREVIOUS, Previous song ) \
- XX( MPD_NEXT, Next song ) \
- XX( MPD_BACKWARD, Seek backwards ) \
- XX( MPD_FORWARD, Seek forwards ) \
- XX( MPD_VOLUME_UP, Increase volume ) \
- XX( MPD_VOLUME_DOWN, Decrease volume ) \
- \
- XX( MPD_SEARCH, Global search ) \
- XX( MPD_ADD, Add selection to playlist ) \
- XX( MPD_REPLACE, Replace playlist ) \
- XX( MPD_REPEAT, Toggle repeat ) \
- XX( MPD_RANDOM, Toggle random playback ) \
- XX( MPD_SINGLE, Toggle single song playback ) \
- XX( MPD_CONSUME, Toggle consume ) \
- XX( MPD_UPDATE_DB, Update MPD database ) \
- XX( MPD_COMMAND, Send raw command to MPD ) \
- \
- XX( CHOOSE, Choose item ) \
- XX( DELETE, Delete item ) \
- XX( UP, Go up a level ) \
- XX( MULTISELECT, Toggle multiselect ) \
- \
- XX( SCROLL_UP, Scroll up ) \
- XX( SCROLL_DOWN, Scroll down ) \
- XX( MOVE_UP, Move selection up ) \
- XX( MOVE_DOWN, Move selection down ) \
- \
- XX( GOTO_TOP, Go to top ) \
- XX( GOTO_BOTTOM, Go to bottom ) \
- XX( GOTO_ITEM_PREVIOUS, Go to previous item ) \
- XX( GOTO_ITEM_NEXT, Go to next item ) \
- XX( GOTO_PAGE_PREVIOUS, Go to previous page ) \
- XX( GOTO_PAGE_NEXT, Go to next page ) \
- \
- XX( GOTO_VIEW_TOP, Select top item ) \
- XX( GOTO_VIEW_CENTER, Select center item ) \
- XX( GOTO_VIEW_BOTTOM, Select bottom item ) \
- \
- XX( EDITOR_CONFIRM, Confirm input ) \
- \
- XX( EDITOR_B_CHAR, Go back a character ) \
- XX( EDITOR_F_CHAR, Go forward a character ) \
- XX( EDITOR_B_WORD, Go back a word ) \
- XX( EDITOR_F_WORD, Go forward a word ) \
- XX( EDITOR_HOME, Go to start of line ) \
- XX( EDITOR_END, Go to end of line ) \
- \
- XX( EDITOR_B_DELETE, Delete last character ) \
- XX( EDITOR_F_DELETE, Delete next character ) \
- XX( EDITOR_B_KILL_WORD, Delete last word ) \
- XX( EDITOR_B_KILL_LINE, Delete everything up to BOL ) \
- XX( EDITOR_F_KILL_LINE, Delete everything up to EOL )
-
-enum action
-{
-#define XX(name, description) ACTION_ ## name,
- ACTIONS (XX)
-#undef XX
- ACTION_COUNT
-};
-
-static struct action_info
-{
- const char *name; ///< Name for user bindings
- const char *description; ///< Human-readable description
-}
-g_actions[] =
-{
-#define XX(name, description) { #name, #description },
- ACTIONS (XX)
-#undef XX
-};
-
-/// Accept a more human format of action-name instead of ACTION_NAME
-static int action_toupper (int c) { return c == '-' ? '_' : toupper_ascii (c); }
-
static int
action_resolve (const char *name)
{
- const unsigned char *s = (const unsigned char *) name;
for (int i = 0; i < ACTION_COUNT; i++)
- {
- const char *target = g_actions[i].name;
- for (size_t k = 0; action_toupper (s[k]) == target[k]; k++)
- if (!s[k] && !target[k])
- return i;
- }
+ if (!strcasecmp_ascii (g_action_names[i], name))
+ return i;
return -1;
}
@@ -2103,7 +2442,7 @@ app_setvol (int value)
}
static void
-app_on_editor_end (bool confirmed)
+app_on_mpd_command_editor_end (bool confirmed)
{
struct mpd_client *c = &g.client;
if (!confirmed)
@@ -2118,6 +2457,63 @@ app_on_editor_end (bool confirmed)
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
@@ -2135,7 +2531,7 @@ app_process_action (enum action action)
struct tab *tab = g.active_tab;
if (tab->on_action && tab->on_action (action))
{
- app_invalidate ();
+ xui_invalidate ();
return true;
}
@@ -2144,26 +2540,31 @@ app_process_action (enum action action)
case ACTION_NONE:
return true;
case ACTION_QUIT:
+ app_quit ();
+ return true;
+ case ACTION_REDRAW:
+ clear ();
+ xui_invalidate ();
+ return true;
+
+ case ACTION_ABORT:
// It is a pseudomode, avoid surprising the user
if (tab->item_mark > -1)
{
tab->item_mark = -1;
- app_invalidate ();
+ xui_invalidate ();
return true;
}
-
- app_quit ();
- return true;
- case ACTION_REDRAW:
- clear ();
- app_invalidate ();
- return true;
+ return false;
case ACTION_MPD_COMMAND:
line_editor_start (&g.editor, ':');
- g.editor.on_end = app_on_editor_end;
- app_invalidate ();
+ g.editor.on_end = app_on_mpd_command_editor_end;
+ xui_invalidate ();
+ app_hide_message ();
return true;
default:
+ print_error ("\"%s\" is not allowed here",
+ g_action_descriptions[action]);
return false;
case ACTION_MULTISELECT:
@@ -2171,12 +2572,19 @@ app_process_action (enum action action)
|| !tab->item_count || tab->item_selected < 0)
return false;
- app_invalidate ();
+ xui_invalidate ();
if (tab->item_mark > -1)
tab->item_mark = -1;
else
tab->item_mark = tab->item_selected;
return true;
+ case ACTION_INCREMENTAL_SEARCH:
+ line_editor_start (&g.editor, '/');
+ g.editor.on_changed = incremental_search_on_changed;
+ g.editor.on_end = incremental_search_on_end;
+ xui_invalidate ();
+ app_hide_message ();
+ return true;
case ACTION_TAB_LAST:
if (!g.last_tab)
@@ -2218,19 +2626,26 @@ app_process_action (enum action action)
case ACTION_MPD_CONSUME: return app_mpd_toggle ("consume");
case ACTION_MPD_UPDATE_DB: return MPD_SIMPLE ("update");
- case ACTION_MPD_VOLUME_UP: return app_setvol (g.volume + 10);
- case ACTION_MPD_VOLUME_DOWN: return app_setvol (g.volume - 10);
+ case ACTION_MPD_VOLUME_UP: return app_setvol (g.volume + 5);
+ case ACTION_MPD_VOLUME_DOWN: return app_setvol (g.volume - 5);
- // XXX: these should rather be parametrized
+#ifdef WITH_PULSE
+ case ACTION_PULSE_VOLUME_UP: return pulse_volume_set (&g.pulse, +5);
+ case ACTION_PULSE_VOLUME_DOWN: return pulse_volume_set (&g.pulse, -5);
+ case ACTION_PULSE_MUTE: return pulse_volume_mute (&g.pulse);
+#endif // WITH_PULSE
+
+ // XXX: these two should rather be parametrized
case ACTION_SCROLL_UP: return app_scroll (-3);
- case ACTION_SCROLL_DOWN: return app_scroll (3);
+ case ACTION_SCROLL_DOWN: return app_scroll (+3);
+ case ACTION_CENTER_CURSOR: return app_center_cursor ();
case ACTION_GOTO_TOP:
if (tab->item_count)
{
g.active_tab->item_selected = 0;
app_ensure_selection_visible ();
- app_invalidate ();
+ xui_invalidate ();
}
return true;
case ACTION_GOTO_BOTTOM:
@@ -2239,7 +2654,7 @@ app_process_action (enum action action)
g.active_tab->item_selected =
MAX (0, (int) g.active_tab->item_count - 1);
app_ensure_selection_visible ();
- app_invalidate ();
+ xui_invalidate ();
}
return true;
@@ -2269,10 +2684,10 @@ app_process_action (enum action action)
static bool
app_editor_process_action (enum action action)
{
- app_invalidate ();
+ xui_invalidate ();
switch (action)
{
- case ACTION_QUIT:
+ case ACTION_ABORT:
line_editor_abort (&g.editor, false);
g.editor.on_end = NULL;
return true;
@@ -2281,6 +2696,8 @@ app_editor_process_action (enum action action)
g.editor.on_end = NULL;
return true;
default:
+ print_error ("\"%s\" is not allowed here",
+ g_action_descriptions[action]);
return false;
case ACTION_EDITOR_B_CHAR:
@@ -2296,6 +2713,13 @@ app_editor_process_action (enum action action)
case ACTION_EDITOR_END:
return line_editor_action (&g.editor, LINE_EDITOR_END);
+ case ACTION_EDITOR_UPCASE_WORD:
+ return line_editor_action (&g.editor, LINE_EDITOR_UPCASE_WORD);
+ case ACTION_EDITOR_DOWNCASE_WORD:
+ return line_editor_action (&g.editor, LINE_EDITOR_DOWNCASE_WORD);
+ case ACTION_EDITOR_CAPITALIZE_WORD:
+ return line_editor_action (&g.editor, LINE_EDITOR_CAPITALIZE_WORD);
+
case ACTION_EDITOR_B_DELETE:
return line_editor_action (&g.editor, LINE_EDITOR_B_DELETE);
case ACTION_EDITOR_F_DELETE:
@@ -2311,93 +2735,156 @@ app_editor_process_action (enum action action)
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+// Carefully chosen to limit the possibility of ever hitting termo keymods.
+enum { APP_KEYMOD_DOUBLE_CLICK = 1 << 15 };
+
static bool
-app_process_left_mouse_click (int line, int column, bool double_click)
+app_process_left_mouse_click (struct widget *w, int x, int y, int modifiers)
{
- if (line == g.controls_offset)
+ switch (w->id)
{
- // XXX: there could be a push_widget(buf, text, attrs, handler)
- // function to help with this but it might not be worth it
- enum action action = ACTION_NONE;
- if (column >= 0 && column <= 1) action = ACTION_MPD_PREVIOUS;
- if (column >= 3 && column <= 4) action = ACTION_MPD_TOGGLE;
- if (column >= 6 && column <= 7) action = ACTION_MPD_STOP;
- if (column >= 9 && column <= 10) action = ACTION_MPD_NEXT;
-
- if (action)
- return app_process_action (action);
-
- int gauge_offset = column - g.gauge_offset;
- if (g.gauge_offset < 0
- || gauge_offset < 0 || gauge_offset >= g.gauge_width)
- return false;
-
- float position = (float) gauge_offset / g.gauge_width;
+ case WIDGET_BUTTON:
+ app_process_action (w->userdata);
+ break;
+ case WIDGET_GAUGE:
+ {
+ // TODO: We should avoid queuing up too many.
+ float position = (float) x / w->width;
if (g.song_duration >= 1)
{
char *where = xstrdup_printf ("%f", position * g.song_duration);
MPD_SIMPLE ("seekcur", where);
free (where);
}
+ break;
}
- else if (line == g.tabs_offset)
+ case WIDGET_TAB:
{
- struct tab *winner = NULL;
- int indent = strlen (APP_TITLE);
- if (column < indent)
- {
- app_switch_tab (g.help_tab);
- return true;
- }
- for (struct tab *iter = g.tabs; !winner && iter; iter = iter->next)
- {
- if (column < (indent += iter->name_width))
- winner = iter;
- }
- if (!winner)
- return false;
+ struct tab *tab = g.help_tab;
+ int i = 0;
+ LIST_FOR_EACH (struct tab, iter, g.tabs)
+ if (++i == w->userdata)
+ tab = iter;
- app_switch_tab (winner);
+ app_switch_tab (tab);
+ break;
}
- else if (line >= g.header_height)
+ case WIDGET_LIST:
{
struct tab *tab = g.active_tab;
- int row_index = line - g.header_height;
+ int row_index = y / g_xui.vunit;
if (row_index < 0
|| row_index >= (int) tab->item_count - tab->item_top)
return false;
- // TODO: handle the scrollbar a bit better than this
- int visible_items = app_visible_items ();
- if ((int) tab->item_count > visible_items && column == COLS - 1)
- tab->item_top = (float) row_index / visible_items
- * (int) tab->item_count - visible_items / 2;
- else
- tab->item_selected = row_index + tab->item_top;
- app_invalidate ();
+ if (!(modifiers & TERMO_KEYMOD_SHIFT))
+ tab->item_mark = -1;
+ else if (!tab->can_multiselect || tab->item_selected < 0)
+ return false;
+ else if (tab->item_mark < 0)
+ tab->item_mark = tab->item_selected;
+
+ tab->item_selected = row_index + tab->item_top;
+ app_ensure_selection_visible ();
+ xui_invalidate ();
- if (double_click)
+ if (modifiers & APP_KEYMOD_DOUBLE_CLICK)
app_process_action (ACTION_CHOOSE);
+ break;
+ }
+ case WIDGET_SCROLLBAR:
+ {
+ struct tab *tab = g.active_tab;
+ int visible_items = app_visible_items ();
+ tab->item_top = (double) y / w->height
+ * (int) tab->item_count - visible_items / 2;
+ xui_invalidate ();
+ app_fix_view_range ();
+ break;
+ }
+ case WIDGET_MESSAGE:
+ app_hide_message ();
}
return true;
}
static bool
-app_process_mouse (termo_mouse_event_t type, int line, int column, int button,
- bool double_click)
+app_process_mouse (termo_mouse_event_t type, int x, int y, int button,
+ int modifiers)
{
- if (type != TERMO_MOUSE_PRESS)
+ // XXX: Terminals don't let us know which button has been released,
+ // so we can't press buttons at that point. We'd need a special "click"
+ // event handler that could be handled better under X11.
+ if (type == TERMO_MOUSE_RELEASE)
+ {
+ g.ui_dragging = WIDGET_NONE;
return true;
+ }
+
+ if (type == TERMO_MOUSE_DRAG)
+ {
+ if (g.ui_dragging != WIDGET_GAUGE
+ && g.ui_dragging != WIDGET_SCROLLBAR)
+ return true;
+
+ struct widget *target = app_widget_by_id (g.ui_dragging);
+ x -= target->x;
+ y -= target->y;
+ return app_process_left_mouse_click (target, x, y, modifiers);
+ }
if (g.editor.line)
+ {
line_editor_abort (&g.editor, false);
+ xui_invalidate ();
+ }
- if (button == 1)
- return app_process_left_mouse_click (line, column, double_click);
- else if (button == 4)
- return app_process_action (ACTION_SCROLL_UP);
- else if (button == 5)
- return app_process_action (ACTION_SCROLL_DOWN);
+ struct widget *target = NULL;
+ LIST_FOR_EACH (struct widget, w, g_xui.widgets)
+ if (x >= w->x && x < w->x + w->width
+ && y >= w->y && y < w->y + w->height)
+ target = w;
+ if (!target)
+ return false;
+
+ x -= target->x;
+ y -= target->y;
+ switch (button)
+ {
+ case 1:
+ g.ui_dragging = target->id;
+ return app_process_left_mouse_click (target, x, y, modifiers);
+ case 4:
+ switch (target->id)
+ {
+ case WIDGET_LIST:
+ return app_process_action (ACTION_SCROLL_UP);
+ case WIDGET_VOLUME:
+ return app_process_action (
+#ifdef WITH_PULSE
+ g.pulse_control_requested ? ACTION_PULSE_VOLUME_UP :
+#endif // WITH_PULSE
+ ACTION_MPD_VOLUME_UP);
+ case WIDGET_GAUGE:
+ return app_process_action (ACTION_MPD_FORWARD);
+ }
+ break;
+ case 5:
+ switch (target->id)
+ {
+ case WIDGET_LIST:
+ return app_process_action (ACTION_SCROLL_DOWN);
+ case WIDGET_VOLUME:
+ return app_process_action (
+#ifdef WITH_PULSE
+ g.pulse_control_requested ? ACTION_PULSE_VOLUME_DOWN :
+#endif // WITH_PULSE
+ ACTION_MPD_VOLUME_DOWN);
+ case WIDGET_GAUGE:
+ return app_process_action (ACTION_MPD_BACKWARD);
+ }
+ break;
+ }
return false;
}
@@ -2419,16 +2906,19 @@ static struct binding_default
}
g_normal_defaults[] =
{
- { "Escape", ACTION_QUIT },
{ "q", ACTION_QUIT },
{ "C-l", ACTION_REDRAW },
+ { "Escape", ACTION_ABORT },
{ "M-Tab", ACTION_TAB_LAST },
{ "F1", ACTION_TAB_HELP },
+ { "S-Tab", ACTION_TAB_PREVIOUS },
+ { "Tab", ACTION_TAB_NEXT },
{ "C-Left", ACTION_TAB_PREVIOUS },
{ "C-Right", ACTION_TAB_NEXT },
{ "C-PageUp", ACTION_TAB_PREVIOUS },
{ "C-PageDown", ACTION_TAB_NEXT },
+ { "o", ACTION_GOTO_PLAYING },
{ "Home", ACTION_GOTO_TOP },
{ "End", ACTION_GOTO_BOTTOM },
{ "M-<", ACTION_GOTO_TOP },
@@ -2449,6 +2939,7 @@ g_normal_defaults[] =
{ "C-f", ACTION_GOTO_PAGE_NEXT },
{ "C-y", ACTION_SCROLL_UP },
{ "C-e", ACTION_SCROLL_DOWN },
+ { "z", ACTION_CENTER_CURSOR },
{ "H", ACTION_GOTO_VIEW_TOP },
{ "M", ACTION_GOTO_VIEW_CENTER },
@@ -2458,9 +2949,11 @@ g_normal_defaults[] =
{ "Enter", ACTION_CHOOSE },
{ "Delete", ACTION_DELETE },
{ "d", ACTION_DELETE },
+ { "?", ACTION_DESCRIBE },
{ "M-Up", ACTION_UP },
{ "Backspace", ACTION_UP },
{ "v", ACTION_MULTISELECT },
+ { "C-s", ACTION_INCREMENTAL_SEARCH },
{ "/", ACTION_MPD_SEARCH },
{ "a", ACTION_MPD_ADD },
{ "r", ACTION_MPD_REPLACE },
@@ -2477,11 +2970,15 @@ g_normal_defaults[] =
{ "Space", ACTION_MPD_TOGGLE },
{ "C-Space", ACTION_MPD_STOP },
{ "u", ACTION_MPD_UPDATE_DB },
- { "M-PageUp", ACTION_MPD_VOLUME_UP },
- { "M-PageDown", ACTION_MPD_VOLUME_DOWN },
+ { "+", ACTION_MPD_VOLUME_UP },
+ { "-", ACTION_MPD_VOLUME_DOWN },
},
g_editor_defaults[] =
{
+ { "C-g", ACTION_ABORT },
+ { "Escape", ACTION_ABORT },
+ { "Enter", ACTION_EDITOR_CONFIRM },
+
{ "Left", ACTION_EDITOR_B_CHAR },
{ "Right", ACTION_EDITOR_F_CHAR },
{ "C-b", ACTION_EDITOR_B_CHAR },
@@ -2493,6 +2990,10 @@ g_editor_defaults[] =
{ "C-a", ACTION_EDITOR_HOME },
{ "C-e", ACTION_EDITOR_END },
+ { "M-u", ACTION_EDITOR_UPCASE_WORD },
+ { "M-l", ACTION_EDITOR_DOWNCASE_WORD },
+ { "M-c", ACTION_EDITOR_CAPITALIZE_WORD },
+
{ "C-h", ACTION_EDITOR_B_DELETE },
{ "DEL", ACTION_EDITOR_B_DELETE },
{ "Backspace", ACTION_EDITOR_B_DELETE },
@@ -2501,17 +3002,13 @@ g_editor_defaults[] =
{ "C-u", ACTION_EDITOR_B_KILL_LINE },
{ "C-k", ACTION_EDITOR_F_KILL_LINE },
{ "C-w", ACTION_EDITOR_B_KILL_WORD },
-
- { "C-g", ACTION_QUIT },
- { "Escape", ACTION_QUIT },
- { "Enter", ACTION_EDITOR_CONFIRM },
};
static int
app_binding_cmp (const void *a, const void *b)
{
const struct binding *aa = a, *bb = b;
- int cmp = termo_keycmp (g.tk, &aa->decoded, &bb->decoded);
+ int cmp = termo_keycmp (g_xui.tk, &aa->decoded, &bb->decoded);
return cmp ? cmp : bb->order - aa->order;
}
@@ -2522,7 +3019,7 @@ app_next_binding (struct str_map_iter *iter, termo_key_t *key, int *action)
while ((v = str_map_iter_next (iter)))
{
*action = ACTION_NONE;
- if (*termo_strpkey_utf8 (g.tk,
+ if (*termo_strpkey_utf8 (g_xui.tk,
iter->link->key, key, TERMO_FORMAT_ALTISMETA))
print_error ("%s: invalid binding", iter->link->key);
else if (v->type == CONFIG_ITEM_NULL)
@@ -2551,7 +3048,7 @@ app_init_bindings (const char *keymap,
termo_key_t decoded;
for (size_t i = 0; i < defaults_len; i++)
{
- hard_assert (!*termo_strpkey_utf8 (g.tk,
+ hard_assert (!*termo_strpkey_utf8 (g_xui.tk,
defaults[i].key, &decoded, TERMO_FORMAT_ALTISMETA));
a[a_len++] = (struct binding) { decoded, defaults[i].action, order++ };
}
@@ -2573,7 +3070,8 @@ app_init_bindings (const char *keymap,
for (size_t in = 0; in < a_len; in++)
{
a[in].order = 0;
- if (!out || termo_keycmp (g.tk, &a[in].decoded, &a[out - 1].decoded))
+ if (!out
+ || termo_keycmp (g_xui.tk, &a[in].decoded, &a[out - 1].decoded))
a[out++] = a[in];
}
@@ -2581,26 +3079,52 @@ app_init_bindings (const char *keymap,
return a;
}
+static char *
+app_strfkey (const termo_key_t *key)
+{
+ // For display purposes, this is highly desirable
+ int flags = termo_get_flags (g_xui.tk);
+ termo_set_flags (g_xui.tk, flags | TERMO_FLAG_SPACESYMBOL);
+ termo_key_t fixed = *key;
+ termo_canonicalise (g_xui.tk, &fixed);
+ termo_set_flags (g_xui.tk, flags);
+
+ char buf[16] = "";
+ termo_strfkey_utf8 (g_xui.tk,
+ buf, sizeof buf, &fixed, TERMO_FORMAT_ALTISMETA);
+ return xstrdup (buf);
+}
+
static bool
app_process_termo_event (termo_key_t *event)
{
- if (event->type == TERMO_TYPE_FOCUS)
+ char *formatted = app_strfkey (event);
+ print_debug ("%s", formatted);
+ free (formatted);
+
+ bool handled = false;
+ if ((handled = event->type == TERMO_TYPE_FOCUS))
{
- g.focused = !!event->code.focused;
- app_invalidate ();
+ xui_invalidate ();
+ // Senseless fall-through
}
struct binding dummy = { *event, 0, 0 }, *binding;
if (g.editor.line)
{
+ if (event->type == TERMO_TYPE_KEY
+ || event->type == TERMO_TYPE_FUNCTION
+ || event->type == TERMO_TYPE_KEYSYM)
+ app_hide_message ();
+
if ((binding = bsearch (&dummy, g_editor_keys, g_editor_keys_len,
sizeof *binding, app_binding_cmp)))
return app_editor_process_action (binding->action);
if (event->type != TERMO_TYPE_KEY || event->modifiers != 0)
- return false;
+ return handled;
line_editor_insert (&g.editor, event->code.codepoint);
- app_invalidate ();
+ xui_invalidate ();
return true;
}
if ((binding = bsearch (&dummy, g_normal_keys, g_normal_keys_len,
@@ -2616,18 +3140,15 @@ app_process_termo_event (termo_key_t *event)
if (app_goto_tab ((n == 0 ? 10 : n) - 1))
return true;
}
- return false;
+ return handled;
}
// --- Current tab -------------------------------------------------------------
static struct tab g_current_tab;
-#define DURATION_MAX_LEN (1 /*separator */ + 2 /* h */ + 3 /* m */+ 3 /* s */)
-
-static void
-current_tab_on_item_draw (size_t item_index, struct row_buffer *buffer,
- int width)
+static struct layout
+current_tab_on_item_layout (size_t item_index)
{
// TODO: configurable output, maybe dynamically sized columns
compact_map_t map = item_list_get (&g.playlist, item_index);
@@ -2635,23 +3156,26 @@ current_tab_on_item_draw (size_t item_index, struct row_buffer *buffer,
const char *title = compact_map_find (map, "title");
chtype attrs = (int) item_index == g.song ? A_BOLD : 0;
+ struct layout l = {};
if (artist && title)
- row_buffer_append_args (buffer,
- artist, attrs, " - ", attrs, title, attrs, NULL);
+ {
+ char *joined = xstrdup_printf ("%s - %s", artist, title);
+ app_push_fill (&l, g.ui->label (attrs, joined));
+ free (joined);
+ }
else
- row_buffer_append (buffer, compact_map_find (map, "file"), attrs);
-
- row_buffer_align (buffer, width - DURATION_MAX_LEN, attrs);
+ app_push_fill (&l, g.ui->label (attrs, compact_map_find (map, "file")));
int duration = -1;
mpd_read_time (compact_map_find (map, "duration"), &duration, NULL);
mpd_read_time (compact_map_find (map, "time"), &duration, NULL);
char *s = duration < 0 ? xstrdup ("-") : app_time_string (duration);
- char *right_aligned = xstrdup_printf ("%*s", DURATION_MAX_LEN, s);
- row_buffer_append (buffer, right_aligned, attrs);
- free (right_aligned);
+ app_push (&l, g.ui->padding (attrs, 1, 1));
+ app_push (&l, g.ui->label (attrs, s));
free (s);
+
+ return l;
}
static void
@@ -2718,6 +3242,13 @@ current_tab_on_action (enum action action)
switch (action)
{
const char *id;
+ case ACTION_GOTO_PLAYING:
+ if (g.song < 0 || (size_t) g.song >= tab->item_count)
+ return false;
+
+ tab->item_selected = g.song;
+ app_ensure_selection_visible ();
+ return true;
case ACTION_MOVE_UP:
return current_tab_move_selection (-1);
case ACTION_MOVE_DOWN:
@@ -2726,6 +3257,12 @@ current_tab_on_action (enum action action)
tab->item_mark = -1;
return map && (id = compact_map_find (map, "id"))
&& MPD_SIMPLE ("playid", id);
+ case ACTION_DESCRIBE:
+ if (!map || !(id = compact_map_find (map, "file")))
+ return false;
+
+ app_show_message (xstrdup ("Path: "), xstrdup (id));
+ return true;
case ACTION_DELETE:
{
struct mpd_client *c = &g.client;
@@ -2756,7 +3293,7 @@ current_tab_update (void)
g_current_tab.item_count = g.playlist.len;
g_current_tab.item_mark =
MIN ((int) g.playlist.len - 1, g_current_tab.item_mark);
- app_invalidate ();
+ xui_invalidate ();
}
static struct tab *
@@ -2766,7 +3303,7 @@ current_tab_init (void)
tab_init (super, "Current");
super->can_multiselect = true;
super->on_action = current_tab_on_action;
- super->on_item_draw = current_tab_on_item_draw;
+ super->on_item_layout = current_tab_on_item_layout;
return super;
}
@@ -2832,11 +3369,9 @@ library_tab_add (int type, int duration, const char *name, const char *path)
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-static void
-library_tab_on_item_draw (size_t item_index, struct row_buffer *buffer,
- int width)
+static struct layout
+library_tab_on_item_layout (size_t item_index)
{
- (void) width;
hard_assert (item_index < g_library_tab.items_len);
struct library_tab_item *x = &g_library_tab.items[item_index];
@@ -2849,15 +3384,21 @@ library_tab_on_item_draw (size_t item_index, struct row_buffer *buffer,
case LIBRARY_FILE: prefix = " "; name = x->name; break;
default: hard_assert (!"invalid item type");
}
+
chtype attrs = x->type != LIBRARY_FILE ? APP_ATTR (DIRECTORY) : 0;
- row_buffer_append_args (buffer, prefix, attrs, name, attrs, NULL);
- if (x->duration < 0)
- return;
+ struct layout l = {};
- char *s = app_time_string (x->duration);
- row_buffer_align (buffer, width - 2 /* gap */ - strlen (s), 0);
- row_buffer_append_args (buffer, " " /* gap */, 0, s, 0, NULL);
- free (s);
+ app_push (&l, g.ui->label (attrs, prefix));
+ app_push_fill (&l, g.ui->label (attrs, name));
+
+ if (x->duration >= 0)
+ {
+ char *s = app_time_string (x->duration);
+ app_push (&l, g.ui->padding (0, 1, 1));
+ app_push (&l, g.ui->label (attrs, s));
+ free (s);
+ }
+ return l;
}
static char
@@ -3014,7 +3555,7 @@ library_tab_load_data (const struct strv *data)
if (g_library_tab.super.item_selected >= (int) len)
app_move_selection (0);
- app_invalidate ();
+ xui_invalidate ();
}
static void
@@ -3136,6 +3677,12 @@ library_tab_on_action (enum action action)
}
tab->item_mark = -1;
return true;
+ case ACTION_DESCRIBE:
+ if (!*x->path)
+ break;
+
+ app_show_message (xstrdup ("Path: "), xstrdup (x->path));
+ return true;
case ACTION_UP:
{
char *parent = library_tab_parent ();
@@ -3171,7 +3718,7 @@ library_tab_on_action (enum action action)
library_tab_load_data (&empty);
strv_free (&empty);
- app_invalidate ();
+ xui_invalidate ();
return true;
}
case ACTION_MPD_ADD:
@@ -3226,7 +3773,7 @@ library_tab_init (void)
tab_init (super, "Library");
super->can_multiselect = true;
super->on_action = library_tab_on_action;
- super->on_item_draw = library_tab_on_item_draw;
+ super->on_item_layout = library_tab_on_item_layout;
return super;
}
@@ -3238,8 +3785,8 @@ struct stream_tab_task
{
struct poller_curl_task curl; ///< Superclass
struct str data; ///< Downloaded data
- bool polling; ///< Still downloading
bool replace; ///< Should playlist be replaced?
+ struct curl_slist *alias_ok;
};
static bool
@@ -3296,8 +3843,13 @@ streams_tab_parse_playlist (const char *playlist, const char *content_type,
|| (content_type && is_content_type (content_type, "audio", "x-scpls")))
extract_re = "^File[^=]*=(.+)";
else if ((lines.len && !strcasecmp_ascii (lines.vector[0], "#EXTM3U"))
+ || (content_type && is_content_type (content_type, "audio", "mpegurl"))
|| (content_type && is_content_type (content_type, "audio", "x-mpegurl")))
- extract_re = "^([^#].*)";
+ // This could be "^([^#].*)", however 1. we would need to resolve
+ // relative URIs, and 2. relative URIs probably mean a Media Playlist,
+ // which must be passed to MPD. The better thing to do here would be to
+ // reject anything with EXT-X-TARGETDURATION, and to resolve the URIs.
+ extract_re = "^(https?://.+)";
regex_t *re = regex_compile (extract_re, REG_EXTENDED, NULL);
hard_assert (re != NULL);
@@ -3320,7 +3872,23 @@ streams_tab_extract_links (struct str *data, const char *content_type,
}
streams_tab_parse_playlist (data->str, content_type, out);
- return true;
+ return out->len != 0;
+}
+
+static void
+streams_tab_task_finalize (struct stream_tab_task *self)
+{
+ curl_easy_cleanup (self->curl.easy);
+ curl_slist_free_all (self->alias_ok);
+ str_free (&self->data);
+ free (self);
+}
+
+static void
+streams_tab_task_dispose (struct stream_tab_task *self)
+{
+ hard_assert (poller_curl_remove (&g.poller_curl, self->curl.easy, NULL));
+ streams_tab_task_finalize (self);
}
static void
@@ -3328,19 +3896,18 @@ streams_tab_on_downloaded (CURLMsg *msg, struct poller_curl_task *task)
{
struct stream_tab_task *self =
CONTAINER_OF (task, struct stream_tab_task, curl);
- self->polling = false;
if (msg->data.result
&& msg->data.result != CURLE_WRITE_ERROR)
{
cstr_uncapitalize (self->curl.curl_error);
print_error ("%s", self->curl.curl_error);
- return;
+ goto dispose;
}
struct mpd_client *c = &g.client;
if (c->state != MPD_CONNECTED)
- return;
+ goto dispose;
CURL *easy = msg->easy_handle;
CURLcode res;
@@ -3353,13 +3920,13 @@ streams_tab_on_downloaded (CURLMsg *msg, struct poller_curl_task *task)
{
print_error ("%s: %s",
"cURL info retrieval failed", curl_easy_strerror (res));
- return;
+ goto dispose;
}
// cURL is not willing to parse the ICY header, the code is zero then
if (code && code != 200)
{
print_error ("%s: %ld", "unexpected HTTP response", code);
- return;
+ goto dispose;
}
mpd_client_list_begin (c);
@@ -3378,6 +3945,9 @@ streams_tab_on_downloaded (CURLMsg *msg, struct poller_curl_task *task)
mpd_client_list_end (c);
mpd_client_add_task (c, mpd_on_simple_response, NULL);
mpd_client_idle (c, 0);
+
+dispose:
+ streams_tab_task_dispose (self);
}
static size_t
@@ -3396,60 +3966,44 @@ write_callback (char *ptr, size_t size, size_t nmemb, void *user_data)
static bool
streams_tab_process (const char *uri, bool replace, struct error **e)
{
- struct poller poller;
- poller_init (&poller);
-
- struct poller_curl pc;
- hard_assert (poller_curl_init (&pc, &poller, NULL));
- struct stream_tab_task task;
- hard_assert (poller_curl_spawn (&task.curl, NULL));
+ // TODO: streams_tab_task_dispose() on that running task
+ if (g.poller_curl.registered)
+ {
+ print_error ("waiting for the last stream to time out");
+ return false;
+ }
- CURL *easy = task.curl.easy;
- task.data = str_make ();
- task.replace = replace;
- bool result = false;
+ struct stream_tab_task *task = xcalloc (1, sizeof *task);
+ hard_assert (poller_curl_spawn (&task->curl, NULL));
- struct curl_slist *ok_headers = curl_slist_append (NULL, "ICY 200 OK");
+ CURL *easy = task->curl.easy;
+ task->data = str_make ();
+ task->replace = replace;
+ task->alias_ok = curl_slist_append (NULL, "ICY 200 OK");
CURLcode res;
if ((res = curl_easy_setopt (easy, CURLOPT_FOLLOWLOCATION, 1L))
|| (res = curl_easy_setopt (easy, CURLOPT_NOPROGRESS, 1L))
- // TODO: make the timeout a bit larger once we're asynchronous
- || (res = curl_easy_setopt (easy, CURLOPT_TIMEOUT, 5L))
+ || (res = curl_easy_setopt (easy, CURLOPT_TIMEOUT, 10L))
// Not checking anything, we just want some data, any data
|| (res = curl_easy_setopt (easy, CURLOPT_SSL_VERIFYPEER, 0L))
|| (res = curl_easy_setopt (easy, CURLOPT_SSL_VERIFYHOST, 0L))
|| (res = curl_easy_setopt (easy, CURLOPT_URL, uri))
- || (res = curl_easy_setopt (easy, CURLOPT_HTTP200ALIASES, ok_headers))
+ || (res = curl_easy_setopt (easy, CURLOPT_HTTP200ALIASES, task->alias_ok))
|| (res = curl_easy_setopt (easy, CURLOPT_VERBOSE, (long) g_debug_mode))
|| (res = curl_easy_setopt (easy, CURLOPT_DEBUGFUNCTION, print_curl_debug))
- || (res = curl_easy_setopt (easy, CURLOPT_WRITEDATA, &task.data))
+ || (res = curl_easy_setopt (easy, CURLOPT_WRITEDATA, &task->data))
|| (res = curl_easy_setopt (easy, CURLOPT_WRITEFUNCTION, write_callback)))
{
error_set (e, "%s: %s", "cURL setup failed", curl_easy_strerror (res));
- goto error;
+ streams_tab_task_finalize (task);
+ return false;
}
- task.curl.on_done = streams_tab_on_downloaded;
- hard_assert (poller_curl_add (&pc, task.curl.easy, NULL));
-
- // TODO: don't run a subloop, run the task fully asynchronously
- task.polling = true;
- while (task.polling)
- poller_run (&poller);
-
- hard_assert (poller_curl_remove (&pc, task.curl.easy, NULL));
- result = true;
-
-error:
- curl_easy_cleanup (task.curl.easy);
- curl_slist_free_all (ok_headers);
- str_free (&task.data);
- poller_curl_free (&pc);
-
- poller_free (&poller);
- return result;
+ task->curl.on_done = streams_tab_on_downloaded;
+ hard_assert (poller_curl_add (&g.poller_curl, task->curl.easy, NULL));
+ return true;
}
static bool
@@ -3472,6 +4026,9 @@ streams_tab_on_action (enum action action)
case ACTION_MPD_ADD:
streams_tab_process (uri, false, &e);
break;
+ case ACTION_DESCRIBE:
+ app_show_message (xstrdup (uri), NULL);
+ break;
default:
return false;
}
@@ -3483,12 +4040,12 @@ streams_tab_on_action (enum action action)
return true;
}
-static void
-streams_tab_on_item_draw (size_t item_index, struct row_buffer *buffer,
- int width)
+static struct layout
+streams_tab_on_item_layout (size_t item_index)
{
- (void) width;
- row_buffer_append (buffer, g.streams.vector[item_index], 0);
+ struct layout l = {};
+ app_push_fill (&l, g.ui->label (0, g.streams.vector[item_index]));
+ return l;
}
static struct tab *
@@ -3497,80 +4054,468 @@ streams_tab_init (void)
static struct tab super;
tab_init (&super, "Streams");
super.on_action = streams_tab_on_action;
- super.on_item_draw = streams_tab_on_item_draw;
+ super.on_item_layout = streams_tab_on_item_layout;
super.item_count = g.streams.len;
return &super;
}
// --- Info tab ----------------------------------------------------------------
+struct info_tab_plugin
+{
+ LIST_HEADER (struct info_tab_plugin)
+
+ char *path; ///< Filesystem path to plugin
+ char *description; ///< What the plugin does
+};
+
+static struct info_tab_plugin *
+info_tab_plugin_load (const char *path)
+{
+ // Shell quoting is less annoying than process management.
+ struct str escaped = str_make ();
+ shell_quote (path, &escaped);
+ FILE *fp = popen (escaped.str, "r");
+ str_free (&escaped);
+ if (!fp)
+ {
+ print_error ("%s: %s", path, strerror (errno));
+ return NULL;
+ }
+
+ struct str description = str_make ();
+ char buf[BUFSIZ];
+ size_t len;
+ while ((len = fread (buf, 1, sizeof buf, fp)) == sizeof buf)
+ str_append_data (&description, buf, len);
+ str_append_data (&description, buf, len);
+ if (pclose (fp))
+ {
+ str_free (&description);
+ print_error ("%s: %s", path, strerror (errno));
+ return NULL;
+ }
+
+ char *newline = strpbrk (description.str, "\r\n");
+ if (newline)
+ {
+ description.len = newline - description.str;
+ *newline = '\0';
+ }
+ str_enforce_utf8 (&description);
+ if (!description.len)
+ {
+ str_free (&description);
+ print_error ("%s: %s", path, "missing description");
+ return NULL;
+ }
+
+ struct info_tab_plugin *plugin = xcalloc (1, sizeof *plugin);
+ plugin->path = xstrdup (path);
+ plugin->description = str_steal (&description);
+ return plugin;
+}
+
+static void
+info_tab_plugin_load_dir (struct str_map *basename_to_path, const char *dirname)
+{
+ DIR *dir = opendir (dirname);
+ if (!dir)
+ {
+ print_debug ("opendir: %s: %s", dirname, strerror (errno));
+ return;
+ }
+
+ struct dirent *entry = NULL;
+ while ((entry = readdir (dir)))
+ {
+ struct stat st = {};
+ char *path = xstrdup_printf ("%s/%s", dirname, entry->d_name);
+ if (stat (path, &st) || !S_ISREG (st.st_mode))
+ {
+ free (path);
+ continue;
+ }
+
+ // Empty files silently erase formerly found basenames.
+ if (!st.st_size)
+ cstr_set (&path, NULL);
+
+ str_map_set (basename_to_path, entry->d_name, path);
+ }
+ closedir (dir);
+}
+
+static int
+strv_sort_cb (const void *a, const void *b)
+{
+ return strcmp (*(const char **) a, *(const char **) b);
+}
+
+static struct info_tab_plugin *
+info_tab_plugin_load_all (void)
+{
+ struct str_map basename_to_path = str_map_make (free);
+ struct strv paths = strv_make ();
+ get_xdg_data_dirs (&paths);
+ strv_append (&paths, PROJECT_DATADIR);
+ for (size_t i = paths.len; i--; )
+ {
+ char *dirname =
+ xstrdup_printf ("%s/" PROGRAM_NAME "/info", paths.vector[i]);
+ info_tab_plugin_load_dir (&basename_to_path, dirname);
+ free (dirname);
+ }
+ strv_free (&paths);
+
+ struct strv sorted = strv_make ();
+ struct str_map_iter iter = str_map_iter_make (&basename_to_path);
+ while (str_map_iter_next (&iter))
+ strv_append (&sorted, iter.link->key);
+ qsort (sorted.vector, sorted.len, sizeof *sorted.vector, strv_sort_cb);
+
+ struct info_tab_plugin *result = NULL;
+ for (size_t i = sorted.len; i--; )
+ {
+ const char *path = str_map_find (&basename_to_path, sorted.vector[i]);
+ struct info_tab_plugin *plugin = info_tab_plugin_load (path);
+ if (plugin)
+ LIST_PREPEND (result, plugin);
+ }
+ str_map_free (&basename_to_path);
+ strv_free (&sorted);
+ return result;
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+struct info_tab_item
+{
+ char *prefix; ///< Fixed-width prefix column or NULL
+ char *text; ///< Text or NULL
+ bool formatted; ///< Interpret inline formatting marks?
+ struct info_tab_plugin *plugin; ///< Activatable plugin
+};
+
+static void
+info_tab_item_free (struct info_tab_item *self)
+{
+ cstr_set (&self->prefix, NULL);
+ cstr_set (&self->text, NULL);
+}
+
static struct
{
struct tab super; ///< Parent class
- struct strv keys; ///< Data keys
- struct strv values; ///< Data values
+ struct info_tab_item *items; ///< Items array
+ size_t items_alloc; ///< How many items are allocated
+
+ struct info_tab_plugin *plugins; ///< Plugins
+
+ int plugin_songid; ///< Song ID or -1
+ pid_t plugin_pid; ///< Running plugin's process ID or -1
+ int plugin_stdout; ///< pid != -1: read end of stdout
+ struct poller_fd plugin_event; ///< pid != -1: stdout is readable
+ struct str plugin_output; ///< pid != -1: buffer, otherwise result
}
g_info_tab;
+static chtype
+info_tab_format_decode_toggle (char c)
+{
+ switch (c)
+ {
+ case '\x01':
+ return A_BOLD;
+ case '\x02':
+#ifdef A_ITALIC
+ return A_ITALIC;
+#else
+ return A_UNDERLINE;
+#endif
+ default:
+ return 0;
+ }
+}
+
static void
-info_tab_on_item_draw (size_t item_index, struct row_buffer *buffer, int width)
+info_tab_format (struct layout *l, const char *text)
{
- (void) width;
+ chtype attrs = 0;
+ for (const char *p = text; *p; p++)
+ {
+ chtype toggled = info_tab_format_decode_toggle (*p);
+ if (!toggled)
+ continue;
+
+ if (p != text)
+ {
+ char *slice = xstrndup (text, p - text);
+ app_push (l, g.ui->label (attrs, slice));
+ free (slice);
+ }
+
+ attrs ^= toggled;
+ text = p + 1;
+ }
+ if (*text)
+ app_push (l, g.ui->label (attrs, text));
+}
+
+static struct layout
+info_tab_on_item_layout (size_t item_index)
+{
+ struct info_tab_item *item = &g_info_tab.items[item_index];
+ struct layout l = {};
+ if (item->prefix)
+ {
+ char *prefix = xstrdup_printf ("%s:", item->prefix);
+ app_push (&l, g.ui->label (A_BOLD, prefix))
+ ->width = 8 * g_xui.hunit;
+ app_push (&l, g.ui->padding (0, 0.5, 1));
+ }
- // It looks like we could do with a generic list structure that just
- // stores formatted row_buffers. Let's see for other tabs:
- // - Current -- unusable, has dynamic column alignment
- // - Library -- could work for the "icons"
- // - Streams -- useless
- // - Debug -- it'd take up considerably more space
- // However so far we're only showing show key-value pairs.
+ if (item->plugin)
+ app_push (&l, g.ui->label (A_BOLD, item->plugin->description));
+ else if (!item->text || !*item->text)
+ app_push (&l, g.ui->padding (0, 1, 1));
+ else if (item->formatted)
+ info_tab_format (&l, item->text);
+ else
+ app_push (&l, g.ui->label (0, item->text));
- row_buffer_append_args (buffer,
- g_info_tab.keys.vector[item_index], A_BOLD, ":", A_BOLD, NULL);
- row_buffer_space (buffer, 8 - buffer->total_width, 0);
- row_buffer_append (buffer, g_info_tab.values.vector[item_index], 0);
+ if (l.tail)
+ l.tail->width = -1;
+ return l;
+}
+
+static struct info_tab_item *
+info_tab_prepare (void)
+{
+ if (g_info_tab.super.item_count == g_info_tab.items_alloc)
+ g_info_tab.items = xreallocarray (g_info_tab.items,
+ sizeof *g_info_tab.items, (g_info_tab.items_alloc <<= 1));
+
+ struct info_tab_item *item =
+ &g_info_tab.items[g_info_tab.super.item_count++];
+ memset (item, 0, sizeof *item);
+ return item;
}
static void
info_tab_add (compact_map_t data, const char *field)
{
- const char *value = compact_map_find (data, field);
- if (!value) value = "";
-
- strv_append (&g_info_tab.keys, field);
- strv_append (&g_info_tab.values, value);
- g_info_tab.super.item_count++;
+ struct info_tab_item *item = info_tab_prepare ();
+ item->prefix = xstrdup (field);
+ item->text = xstrdup0 (compact_map_find (data, field));
}
static void
info_tab_update (void)
{
- strv_reset (&g_info_tab.keys);
- strv_reset (&g_info_tab.values);
- g_info_tab.super.item_count = 0;
+ while (g_info_tab.super.item_count)
+ info_tab_item_free (&g_info_tab.items[--g_info_tab.super.item_count]);
- compact_map_t map;
- if ((map = item_list_get (&g.playlist, g.song)))
+ compact_map_t map = item_list_get (&g.playlist, g.song);
+ if (!map)
+ return;
+
+ info_tab_add (map, "Title");
+ info_tab_add (map, "Artist");
+ info_tab_add (map, "Album");
+ info_tab_add (map, "Track");
+ info_tab_add (map, "Genre");
+ // We actually receive it as "file", but the key is also used for display
+ info_tab_add (map, "File");
+
+ if (g_info_tab.plugins)
+ {
+ (void) info_tab_prepare ();
+ LIST_FOR_EACH (struct info_tab_plugin, plugin, g_info_tab.plugins)
+ info_tab_prepare ()->plugin = plugin;
+ }
+
+ if (g_info_tab.plugin_pid != -1)
+ {
+ (void) info_tab_prepare ();
+ info_tab_prepare ()->text = xstrdup ("Processing...");
+ return;
+ }
+
+ const char *songid = compact_map_find (map, "Id");
+ if (songid && atoi (songid) == g_info_tab.plugin_songid
+ && g_info_tab.plugin_output.len)
+ {
+ struct strv lines = strv_make ();
+ cstr_split (g_info_tab.plugin_output.str, "\r\n", false, &lines);
+
+ (void) info_tab_prepare ();
+ for (size_t i = 0; i < lines.len; i++)
+ {
+ struct info_tab_item *item = info_tab_prepare ();
+ item->formatted = true;
+ item->text = lines.vector[i];
+ }
+ free (lines.vector);
+ }
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static void
+info_tab_plugin_abort (void)
+{
+ if (g_info_tab.plugin_pid == -1)
+ return;
+
+ // XXX: our methods of killing are very crude, we hope to improve;
+ // at least install a SIGCHLD handler to collect zombies
+ (void) kill (-g_info_tab.plugin_pid, SIGTERM);
+
+ int status = 0;
+ while (waitpid (g_info_tab.plugin_pid, &status, WNOHANG) == -1
+ && errno == EINTR)
+ ;
+ if (WIFEXITED (status) && WEXITSTATUS (status) != EXIT_SUCCESS)
+ print_error ("plugin reported failure");
+
+ g_info_tab.plugin_pid = -1;
+ poller_fd_reset (&g_info_tab.plugin_event);
+ xclose (g_info_tab.plugin_stdout);
+ g_info_tab.plugin_stdout = -1;
+}
+
+static void
+info_tab_on_plugin_stdout (const struct pollfd *fd, void *user_data)
+{
+ (void) user_data;
+
+ struct str *buf = &g_info_tab.plugin_output;
+ switch (socket_io_try_read (fd->fd, buf))
{
- info_tab_add (map, "Title");
- info_tab_add (map, "Artist");
- info_tab_add (map, "Album");
- info_tab_add (map, "Track");
- info_tab_add (map, "Genre");
- // Yes, it is "file", but this is also for display
- info_tab_add (map, "File");
+ case SOCKET_IO_OK:
+ str_enforce_utf8 (buf);
+ return;
+ case SOCKET_IO_ERROR:
+ print_error ("error reading from plugin: %s", strerror (errno));
+ // Fall-through
+ case SOCKET_IO_EOF:
+ info_tab_plugin_abort ();
+ info_tab_update ();
+ xui_invalidate ();
+ }
+}
+
+static void
+info_tab_plugin_run (struct info_tab_plugin *plugin, compact_map_t map)
+{
+ info_tab_plugin_abort ();
+ if (!map)
+ return;
+
+ const char *songid = compact_map_find (map, "Id");
+ const char *title = compact_map_find (map, "Title");
+ const char *artist = compact_map_find (map, "Artist");
+ const char *album = compact_map_find (map, "Album");
+ if (!songid || !title || !artist)
+ {
+ print_error ("unknown song title or artist");
+ return;
+ }
+
+ int stdout_pipe[2];
+ if (pipe (stdout_pipe))
+ {
+ print_error ("%s: %s", "pipe", strerror (errno));
+ return;
+ }
+
+ enum { READ, WRITE };
+ set_cloexec (stdout_pipe[READ]);
+ set_cloexec (stdout_pipe[WRITE]);
+
+ const char *argv[] =
+ { xbasename (plugin->path), title, artist, album, NULL };
+
+ pid_t child = fork ();
+ switch (child)
+ {
+ case -1:
+ print_error ("%s: %s", "fork", strerror (errno));
+ xclose (stdout_pipe[READ]);
+ xclose (stdout_pipe[WRITE]);
+ return;
+ case 0:
+ if (setpgid (0, 0) == -1 || !freopen ("/dev/null", "r", stdin)
+ || dup2 (stdout_pipe[WRITE], STDOUT_FILENO) == -1
+ || dup2 (stdout_pipe[WRITE], STDERR_FILENO) == -1)
+ _exit (EXIT_FAILURE);
+
+ signal (SIGPIPE, SIG_DFL);
+
+ (void) execv (plugin->path, (char **) argv);
+ fprintf (stderr, "%s\n", strerror (errno));
+ _exit (EXIT_FAILURE);
+ default:
+ // Resolve the race, even though it isn't critical for us
+ (void) setpgid (child, child);
+
+ g_info_tab.plugin_songid = atoi (songid);
+ g_info_tab.plugin_pid = child;
+ set_blocking ((g_info_tab.plugin_stdout = stdout_pipe[READ]), false);
+ xclose (stdout_pipe[WRITE]);
+
+ struct poller_fd *event = &g_info_tab.plugin_event;
+ *event = poller_fd_make (&g.poller, g_info_tab.plugin_stdout);
+ event->dispatcher = info_tab_on_plugin_stdout;
+ str_reset (&g_info_tab.plugin_output);
+ poller_fd_set (&g_info_tab.plugin_event, POLLIN);
+ }
+}
+
+static bool
+info_tab_on_action (enum action action)
+{
+ struct tab *tab = g.active_tab;
+ if (tab->item_selected < 0
+ || tab->item_selected >= (int) tab->item_count)
+ return false;
+
+ struct info_tab_item *item = &g_info_tab.items[tab->item_selected];
+ if (!item->plugin)
+ return false;
+
+ switch (action)
+ {
+ case ACTION_DESCRIBE:
+ app_show_message (xstrdup ("Path: "), xstrdup (item->plugin->path));
+ return true;
+ case ACTION_CHOOSE:
+ info_tab_plugin_run (item->plugin, item_list_get (&g.playlist, g.song));
+ info_tab_update ();
+ xui_invalidate ();
+ return true;
+ default:
+ return false;
}
}
static struct tab *
info_tab_init (void)
{
- g_info_tab.keys = strv_make ();
- g_info_tab.values = strv_make ();
+ g_info_tab.items =
+ xcalloc ((g_info_tab.items_alloc = 16), sizeof *g_info_tab.items);
+
+ g_info_tab.plugins = info_tab_plugin_load_all ();
+ g_info_tab.plugin_songid = -1;
+ g_info_tab.plugin_pid = -1;
+ g_info_tab.plugin_stdout = -1;
+ g_info_tab.plugin_output = str_make ();
struct tab *super = &g_info_tab.super;
tab_init (super, "Info");
- super->on_item_draw = info_tab_on_item_draw;
+ super->on_action = info_tab_on_action;
+ super->on_item_layout = info_tab_on_item_layout;
return super;
}
@@ -3589,29 +4534,25 @@ help_tab_on_action (enum action action)
{
struct tab *tab = &g_help_tab.super;
if (tab->item_selected < 0
- || tab->item_selected >= (int) g_help_tab.actions_len
- || action != ACTION_CHOOSE)
+ || tab->item_selected >= (int) g_help_tab.actions_len)
return false;
- action = g_help_tab.actions[tab->item_selected];
- return action != ACTION_NONE
- && action != ACTION_CHOOSE // avoid recursion
- && app_process_action (action);
-}
+ enum action a = g_help_tab.actions[tab->item_selected];
+ if (!a)
+ return false;
-static void
-help_tab_strfkey (const termo_key_t *key, struct strv *out)
-{
- // For display purposes, this is highly desirable
- int flags = termo_get_flags (g.tk);
- termo_set_flags (g.tk, flags | TERMO_FLAG_SPACESYMBOL);
- termo_key_t fixed = *key;
- termo_canonicalise (g.tk, &fixed);
- termo_set_flags (g.tk, flags);
+ if (action == ACTION_DESCRIBE)
+ {
+ app_show_message (xstrdup ("Configuration name: "),
+ xstrdup (g_action_names[a]));
+ return true;
+ }
+ if (action != ACTION_CHOOSE || a == ACTION_CHOOSE /* avoid recursion */)
+ return false;
- char buf[16];
- termo_strfkey_utf8 (g.tk, buf, sizeof buf, &fixed, TERMO_FORMAT_ALTISMETA);
- strv_append (out, buf);
+ // XXX: We can't propagate failure to ring the terminal/X11 bell, but we
+ // don't want to let our caller show a bad "can't do that" message either.
+ return app_process_action (a), true;
}
static void
@@ -3635,12 +4576,12 @@ help_tab_group (struct binding *keys, size_t len, struct strv *out,
struct strv ass = strv_make ();
for (size_t k = 0; k < len; k++)
if (keys[k].action == i)
- help_tab_strfkey (&keys[k].decoded, &ass);
+ strv_append_owned (&ass, app_strfkey (&keys[k].decoded));
if (ass.len)
{
char *joined = strv_join (&ass, ", ");
strv_append_owned (out, xstrdup_printf
- (" %-30s %s", g_actions[i].description, joined));
+ (" %s%c%s", g_action_descriptions[i], 0, joined));
free (joined);
bound[i] = true;
@@ -3657,19 +4598,27 @@ help_tab_unbound (struct strv *out, bool bound[ACTION_COUNT])
if (!bound[i])
{
strv_append_owned (out,
- xstrdup_printf (" %-30s", g_actions[i].description));
+ xstrdup_printf (" %s%c", g_action_descriptions[i], 0));
help_tab_assign_action (i);
}
}
-static void
-help_tab_on_item_draw (size_t item_index, struct row_buffer *buffer, int width)
+static struct layout
+help_tab_on_item_layout (size_t item_index)
{
- (void) width;
-
hard_assert (item_index < g_help_tab.lines.len);
const char *line = g_help_tab.lines.vector[item_index];
- row_buffer_append (buffer, line, *line == ' ' ? 0 : A_BOLD);
+
+ struct layout l = {};
+ app_push_fill (&l, g.ui->label (*line == ' ' ? 0 : A_BOLD, line));
+
+ const char *definition = strchr (line, 0) + 1;
+ if (*line == ' ' && *definition)
+ {
+ app_push (&l, g.ui->padding (0, 0.5, 1));
+ app_push_fill (&l, g.ui->label (0, definition));
+ }
+ return l;
}
static struct tab *
@@ -3704,7 +4653,7 @@ help_tab_init (void)
struct tab *super = &g_help_tab.super;
tab_init (super, "Help");
super->on_action = help_tab_on_action;
- super->on_item_draw = help_tab_on_item_draw;
+ super->on_item_layout = help_tab_on_item_layout;
super->item_count = lines->len;
return super;
}
@@ -3726,8 +4675,8 @@ static struct
}
g_debug_tab;
-static void
-debug_tab_on_item_draw (size_t item_index, struct row_buffer *buffer, int width)
+static struct layout
+debug_tab_on_item_layout (size_t item_index)
{
hard_assert (item_index < g_debug_tab.items_len);
struct debug_item *item = &g_debug_tab.items[item_index];
@@ -3739,14 +4688,13 @@ debug_tab_on_item_draw (size_t item_index, struct row_buffer *buffer, int width)
char *prefix = xstrdup_printf
("%s.%03d", buf, (int) (item->timestamp % 1000));
- row_buffer_append (buffer, prefix, 0);
- free (prefix);
-
- row_buffer_append (buffer, " ", item->attrs);
- row_buffer_append (buffer, item->text, item->attrs);
- // We override the formatting including colors -- do it for the whole line
- row_buffer_align (buffer, width, item->attrs);
+ struct layout l = {};
+ app_push (&l, g.ui->label (0, prefix));
+ app_push (&l, g.ui->padding (item->attrs, 0.5, 1));
+ app_push_fill (&l, g.ui->label (item->attrs, item->text));
+ free (prefix);
+ return l;
}
static void
@@ -3759,7 +4707,7 @@ debug_tab_push (char *message, chtype attrs)
item->attrs = attrs;
item->timestamp = clock_msec (CLOCK_REALTIME);
- app_invalidate ();
+ xui_invalidate ();
}
static struct tab *
@@ -3770,7 +4718,7 @@ debug_tab_init (void)
struct tab *super = &g_debug_tab.super;
tab_init (super, "Debug");
- super->on_item_draw = debug_tab_on_item_draw;
+ super->on_item_layout = debug_tab_on_item_layout;
return super;
}
@@ -3783,15 +4731,11 @@ spectrum_redraw (void)
{
// A full refresh would be too computationally expensive,
// let's hack around it in this case
- if (g.spectrum_row != -1)
- {
- attrset (APP_ATTR (TAB_BAR));
- mvaddstr (g.spectrum_row, g.spectrum_column, g.spectrum.spectrum);
- attrset (0);
- refresh ();
- }
- else
- app_invalidate ();
+ struct widget *spectrum = app_widget_by_id (WIDGET_SPECTRUM);
+ if (spectrum)
+ spectrum->on_render (spectrum);
+
+ poller_idle_set (&g_xui.flip_event);
}
// When any problem occurs with the FIFO, we'll just give up on it completely
@@ -3805,8 +4749,7 @@ spectrum_discard_fifo (void)
g.spectrum_fd = -1;
spectrum_free (&g.spectrum);
- g.spectrum_row = g.spectrum_column = -1;
- app_invalidate ();
+ xui_invalidate ();
}
}
@@ -3867,6 +4810,8 @@ spectrum_setup_fifo (void)
get_config_string (g.config.root, "settings.spectrum_format");
struct config_item *spectrum_bars =
config_item_get (g.config.root, "settings.spectrum_bars", NULL);
+ struct config_item *spectrum_fps =
+ config_item_get (g.config.root, "settings.spectrum_fps", NULL);
if (!spectrum_path)
return;
@@ -3876,10 +4821,10 @@ spectrum_setup_fifo (void)
if (!path)
print_error ("spectrum: %s", "FIFO path could not be resolved");
- else if (!g.locale_is_utf8)
+ else if (!g_xui.locale_is_utf8)
print_error ("spectrum: %s", "UTF-8 locale required");
- else if (!spectrum_init (&g.spectrum,
- (char *) spectrum_format, spectrum_bars->value.integer, &e))
+ else if (!spectrum_init (&g.spectrum, (char *) spectrum_format,
+ spectrum_bars->value.integer, spectrum_fps->value.integer, &e))
{
print_error ("spectrum: %s", e->message);
error_free (e);
@@ -3900,11 +4845,86 @@ spectrum_setup_fifo (void)
}
#else // ! WITH_FFTW
-#define spectrum_setup_fifo()
-#define spectrum_clear()
-#define spectrum_discard_fifo()
+#define spectrum_setup_fifo() BLOCK_START BLOCK_END
+#define spectrum_clear() BLOCK_START BLOCK_END
+#define spectrum_discard_fifo() BLOCK_START BLOCK_END
#endif // ! WITH_FFTW
+// --- PulseAudio --------------------------------------------------------------
+
+#ifdef WITH_PULSE
+
+static bool
+mpd_find_output (const struct strv *data, const char *wanted)
+{
+ // The plugin field is new in MPD 0.21, by default take any output
+ unsigned long n, accept = 1;
+ for (size_t i = data->len; i--; )
+ {
+ char *key, *value;
+ if (!(key = mpd_parse_kv (data->vector[i], &value)))
+ continue;
+
+ if (!strcasecmp_ascii (key, "outputid"))
+ {
+ if (accept)
+ return true;
+
+ accept = 1;
+ }
+ else if (!strcasecmp_ascii (key, "plugin"))
+ accept &= !strcmp (value, wanted);
+ else if (!strcasecmp_ascii (key, "outputenabled")
+ && xstrtoul (&n, value, 10))
+ accept &= n == 1;
+ }
+ return false;
+}
+
+static void
+mpd_on_outputs_response (const struct mpd_response *response,
+ const struct strv *data, void *user_data)
+{
+ (void) user_data;
+
+ // TODO: check whether an action is actually necessary
+ pulse_free (&g.pulse);
+ if (response->success && !mpd_find_output (data, "pulse"))
+ print_debug ("MPD has no PulseAudio output to control");
+ else
+ {
+ pulse_init (&g.pulse, &g.poller);
+ g.pulse.on_update = xui_invalidate;
+ }
+
+ xui_invalidate ();
+}
+
+static void
+pulse_update (void)
+{
+ struct mpd_client *c = &g.client;
+ if (!g.pulse_control_requested)
+ return;
+
+ // The read permission is sufficient for this command
+ mpd_client_send_command (c, "outputs", NULL);
+ mpd_client_add_task (c, mpd_on_outputs_response, NULL);
+ mpd_client_idle (c, 0);
+}
+
+static void
+pulse_disable (void)
+{
+ pulse_free (&g.pulse);
+ xui_invalidate ();
+}
+
+#else // ! WITH_PULSE
+#define pulse_update() BLOCK_START BLOCK_END
+#define pulse_disable() BLOCK_START BLOCK_END
+#endif // ! WITH_PULSE
+
// --- MPD interface -----------------------------------------------------------
static void
@@ -4006,7 +5026,7 @@ mpd_update_playback_state (void)
if (g.playlist_version != last_playlist_version)
mpd_update_playlist_time ();
- app_invalidate ();
+ xui_invalidate ();
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@@ -4062,7 +5082,7 @@ mpd_find_pos_of_id (const char *desired_id)
return -1;
}
-static char *
+static const char *
mpd_id_of_pos (int pos)
{
compact_map_t map = item_list_get (&g.playlist, pos);
@@ -4072,29 +5092,39 @@ mpd_id_of_pos (int pos)
static void
mpd_process_info (const struct strv *data)
{
- int *selected = &g_current_tab.item_selected;
- int *marked = &g_current_tab.item_mark;
- char *prev_sel_id = mpd_id_of_pos (*selected);
- char *prev_mark_id = mpd_id_of_pos (*marked);
- if (prev_sel_id) prev_sel_id = xstrdup (prev_sel_id);
- if (prev_mark_id) prev_mark_id = xstrdup (prev_mark_id);
+ struct tab *tab = &g_current_tab;
+ char *prev_sel_id = xstrdup0 (mpd_id_of_pos (tab->item_selected));
+ char *prev_mark_id = xstrdup0 (mpd_id_of_pos (tab->item_mark));
+ char *fallback_id = NULL;
+
+ struct tab_range r = tab_selection_range (g.active_tab);
+ if (r.upto >= 0)
+ {
+ if (!(fallback_id = xstrdup0 (mpd_id_of_pos (r.upto + 1))))
+ fallback_id = xstrdup0 (mpd_id_of_pos (r.from - 1));
+ }
mpd_process_info_data (data);
- const char *sel_id = mpd_id_of_pos (*selected);
- const char *mark_id = mpd_id_of_pos (*marked);
+ const char *sel_id = mpd_id_of_pos (tab->item_selected);
+ const char *mark_id = mpd_id_of_pos (tab->item_mark);
if (prev_mark_id && (!mark_id || strcmp (prev_mark_id, mark_id)))
- *marked = mpd_find_pos_of_id (prev_mark_id);
+ tab->item_mark = mpd_find_pos_of_id (prev_mark_id);
if (prev_sel_id && (!sel_id || strcmp (prev_sel_id, sel_id)))
{
- if ((*selected = mpd_find_pos_of_id (prev_sel_id)) < 0)
- *marked = -1;
+ if ((tab->item_selected = mpd_find_pos_of_id (prev_sel_id)) < 0)
+ {
+ tab->item_mark = -1;
+ if (fallback_id)
+ tab->item_selected = mpd_find_pos_of_id (fallback_id);
+ }
app_move_selection (0);
}
free (prev_sel_id);
free (prev_mark_id);
+ free (fallback_id);
}
static void
@@ -4134,7 +5164,7 @@ mpd_on_elapsed_time_tick (void *user_data)
// Try to get called on the next round second of playback
poller_timer_set (&g.elapsed_event, 1000 - elapsed_msec);
- app_invalidate ();
+ xui_invalidate ();
}
static void
@@ -4169,6 +5199,8 @@ mpd_on_events (unsigned subsystems, void *user_data)
if (subsystems & MPD_SUBSYSTEM_DATABASE)
library_tab_reload (NULL);
+ if (subsystems & MPD_SUBSYSTEM_OUTPUT)
+ pulse_update ();
if (subsystems & (MPD_SUBSYSTEM_PLAYER | MPD_SUBSYSTEM_OPTIONS
| MPD_SUBSYSTEM_PLAYLIST | MPD_SUBSYSTEM_MIXER | MPD_SUBSYSTEM_UPDATE))
@@ -4185,6 +5217,58 @@ mpd_queue_reconnect (void)
poller_timer_set (&g.connect_event, 5 * 1000);
}
+// On an error, MPD discards the rest of our enqueuing commands--work it around
+static void mpd_enqueue_step (size_t start_offset);
+
+static void
+mpd_on_enqueue_response (const struct mpd_response *response,
+ const struct strv *data, void *user_data)
+{
+ (void) data;
+ intptr_t start_offset = (intptr_t) user_data;
+
+ if (response->success)
+ strv_reset (&g.enqueue);
+ else
+ {
+ // Their addition may also overflow, but YOLO
+ hard_assert (start_offset >= 0 && response->list_offset >= 0);
+
+ print_error ("%s: %s", response->message_text,
+ g.enqueue.vector[start_offset + response->list_offset]);
+ mpd_enqueue_step (start_offset + response->list_offset + 1);
+ }
+}
+
+static void
+mpd_enqueue_step (size_t start_offset)
+{
+ struct mpd_client *c = &g.client;
+ if (start_offset >= g.enqueue.len)
+ {
+ strv_reset (&g.enqueue);
+ return;
+ }
+
+ // TODO: might want to consider using addid and autoplaying
+ mpd_client_list_begin (c);
+ for (size_t i = start_offset; i < g.enqueue.len; i++)
+ mpd_client_send_command (c, "add", g.enqueue.vector[i], NULL);
+ mpd_client_list_end (c);
+ mpd_client_add_task (c, mpd_on_enqueue_response, (void *) start_offset);
+ mpd_client_idle (c, 0);
+}
+
+static void
+mpd_on_ready (void)
+{
+ mpd_request_info ();
+ library_tab_reload (NULL);
+ spectrum_setup_fifo ();
+ pulse_update ();
+ mpd_enqueue_step (0);
+}
+
static void
mpd_on_password_response (const struct mpd_response *response,
const struct strv *data, void *user_data)
@@ -4194,10 +5278,7 @@ mpd_on_password_response (const struct mpd_response *response,
struct mpd_client *c = &g.client;
if (response->success)
- {
- mpd_request_info ();
- library_tab_reload (NULL);
- }
+ mpd_on_ready ();
else
{
print_error ("%s: %s",
@@ -4220,12 +5301,7 @@ mpd_on_connected (void *user_data)
mpd_client_add_task (c, mpd_on_password_response, NULL);
}
else
- {
- mpd_request_info ();
- library_tab_reload (NULL);
- }
-
- spectrum_setup_fifo ();
+ mpd_on_ready ();
}
static void
@@ -4244,6 +5320,7 @@ mpd_on_failure (void *user_data)
info_tab_update ();
spectrum_discard_fifo ();
+ pulse_disable ();
}
static void
@@ -4297,8 +5374,544 @@ app_on_reconnect (void *user_data)
mpd_queue_reconnect ();
}
free (address);
+ xui_invalidate ();
+}
+
+// --- TUI ---------------------------------------------------------------------
+
+static struct widget *
+tui_make_button (chtype attrs, const char *label, enum action a)
+{
+ struct widget *w = tui_make_label (attrs, 0, label);
+ w->id = WIDGET_BUTTON;
+ w->userdata = a;
+ return w;
+}
+
+static void
+tui_render_gauge (struct widget *self)
+{
+ struct row_buffer buf = row_buffer_make ();
+ if (g.state == PLAYER_STOPPED || g.song_elapsed < 0 || g.song_duration < 1)
+ goto out;
+
+ float ratio = (float) g.song_elapsed / g.song_duration;
+ if (ratio < 0) ratio = 0;
+ if (ratio > 1) ratio = 1;
+
+ // Always compute it in exactly eight times the resolution,
+ // because sometimes Unicode is even useful
+ int len_left = ratio * self->width * 8 + 0.5;
+
+ static const char *partials[] = { " ", "▏", "▎", "▍", "▌", "▋", "▊", "▉" };
+ int remainder = len_left % 8;
+ len_left /= 8;
+
+ const char *partial = NULL;
+ if (g.use_partial_boxes)
+ partial = partials[remainder];
+ else
+ len_left += remainder >= (int) 4;
+
+ int len_right = self->width - len_left;
+ row_buffer_space (&buf, len_left, APP_ATTR (ELAPSED));
+ if (partial && len_right-- > 0)
+ row_buffer_append (&buf, partial, APP_ATTR (REMAINS));
+ row_buffer_space (&buf, len_right, APP_ATTR (REMAINS));
+
+out:
+ tui_flush_buffer (self, &buf);
+}
+
+// TODO: Perhaps it should save the number within.
+static struct widget *
+tui_make_gauge (chtype attrs)
+{
+ struct widget *w = xcalloc (1, sizeof *w + 1);
+ w->on_render = tui_render_gauge;
+ w->attrs = attrs;
+ w->width = -1;
+ w->height = 1;
+ return w;
+}
+
+static void
+tui_render_spectrum (struct widget *self)
+{
+ // Don't mess up the line editor caret, when it's shown
+ int last_x, last_y;
+ getyx (stdscr, last_y, last_x);
+
+ struct row_buffer buf = row_buffer_make ();
+#ifdef WITH_FFTW
+ row_buffer_append (&buf, g.spectrum.rendered, self->attrs);
+#endif // WITH_FFTW
+ tui_flush_buffer (self, &buf);
+
+ move (last_y, last_x);
+}
+
+static struct widget *
+tui_make_spectrum (chtype attrs, int width)
+{
+ struct widget *w = xcalloc (1, sizeof *w + 1);
+ w->on_render = tui_render_spectrum;
+ w->attrs = attrs;
+ w->width = width;
+ w->height = 1;
+ return w;
+}
+
+static void
+tui_render_scrollbar (struct widget *self)
+{
+ // This assumes that we can write to the one-before-last column,
+ // i.e. that it's not covered by any double-wide character (and that
+ // ncurses comes to the right results when counting characters).
+ struct tab *tab = g.active_tab;
+ int visible_items = app_visible_items ();
+
+ hard_assert (tab->item_count != 0);
+ if (!g.use_partial_boxes)
+ {
+ struct scrollbar bar = app_compute_scrollbar (tab, visible_items, 1);
+ for (int row = 0; row < visible_items; row++)
+ {
+ move (self->y + row, self->x);
+ if (row < bar.start || row >= bar.start + bar.length)
+ addch (' ' | self->attrs);
+ else
+ addch (' ' | self->attrs | A_REVERSE);
+ }
+ return;
+ }
+
+ struct scrollbar bar = app_compute_scrollbar (tab, visible_items * 8, 8);
+ bar.length += bar.start;
+
+ int start_part = bar.start % 8; bar.start /= 8;
+ int end_part = bar.length % 8; bar.length /= 8;
+
+ // Even with this, the solid part must be at least one character high
+ static const char *partials[] = { "█", "▇", "▆", "▅", "▄", "▃", "▂", "▁" };
+
+ for (int row = 0; row < visible_items; row++)
+ {
+ chtype attrs = self->attrs;
+ if (row > bar.start && row <= bar.length)
+ attrs ^= A_REVERSE;
+
+ const char *c = " ";
+ if (row == bar.start) c = partials[start_part];
+ if (row == bar.length) c = partials[end_part];
+
+ move (self->y + row, self->x);
+
+ struct row_buffer buf = row_buffer_make ();
+ row_buffer_append (&buf, c, attrs);
+ row_buffer_flush (&buf);
+ row_buffer_free (&buf);
+ }
+}
+
+static struct widget *
+tui_make_scrollbar (chtype attrs)
+{
+ struct widget *w = xcalloc (1, sizeof *w + 1);
+ w->on_render = tui_render_scrollbar;
+ w->attrs = attrs;
+ w->width = 1;
+ return w;
+}
+
+static struct widget *
+tui_make_list (void)
+{
+ struct widget *w = xcalloc (1, sizeof *w + 1);
+ w->width = -1;
+ w->height = g.active_tab->item_count;
+ return w;
+}
+
+static void
+tui_render_editor (struct widget *self)
+{
+ struct row_buffer buf = row_buffer_make ();
+ const struct line_editor *e = &g.editor;
+ int width = self->width;
+ if (e->prompt)
+ {
+ hard_assert (e->prompt < 127);
+ row_buffer_append_c (&buf, e->prompt, self->attrs);
+ width--;
+ }
+
+ int following = 0;
+ for (size_t i = e->point; i < e->len; i++)
+ following += e->w[i];
+
+ int preceding = 0;
+ size_t start = e->point;
+ while (start && preceding < width / 2)
+ preceding += e->w[--start];
+
+ // There can be one extra space at the end of the line but this way we
+ // don't need to care about non-spacing marks following full-width chars
+ while (start && width - preceding - following > 2 /* widest char */)
+ preceding += e->w[--start];
+
+ // XXX: we should also show < > indicators for overflow but it'd probably
+ // considerably complicate this algorithm
+ for (; start < e->len; start++)
+ row_buffer_append_c (&buf, e->line[start], self->attrs);
+ tui_flush_buffer (self, &buf);
+
+ // FIXME: This should be at the end of of tui_render().
+ int caret = !!e->prompt + preceding;
+ move (self->y, self->x + caret);
+ curs_set (1);
+}
+
+static struct widget *
+tui_make_editor (chtype attrs)
+{
+ // TODO: This should ideally measure the text, and copy it to w->text.
+ struct widget *w = xcalloc (1, sizeof *w + 1);
+ w->on_render = tui_render_editor;
+ w->attrs = attrs;
+ w->width = -1;
+ w->height = 1;
+ return w;
+}
+
+static struct app_ui app_tui_ui =
+{
+ .padding = tui_make_padding,
+ .label = app_make_label,
+ .button = tui_make_button,
+ .gauge = tui_make_gauge,
+ .spectrum = tui_make_spectrum,
+ .scrollbar = tui_make_scrollbar,
+ .list = tui_make_list,
+ .editor = tui_make_editor,
+};
+
+// --- X11 ---------------------------------------------------------------------
+
+#ifdef WITH_X11
+
+// On a 20x20 raster to make it feasible to design on paper.
+#define X11_STOP {INFINITY, INFINITY}
+static const XPointDouble
+ x11_icon_previous[] =
+ {
+ {10, 0}, {0, 10}, {10, 20}, X11_STOP,
+ {20, 0}, {10, 10}, {20, 20}, X11_STOP, X11_STOP,
+ },
+ x11_icon_pause[] =
+ {
+ {1, 0}, {7, 0}, {7, 20}, {1, 20}, X11_STOP,
+ {13, 0}, {19, 0}, {19, 20}, {13, 20}, X11_STOP, X11_STOP,
+ },
+ x11_icon_play[] =
+ {
+ {0, 0}, {20, 10}, {0, 20}, X11_STOP, X11_STOP,
+ },
+ x11_icon_stop[] =
+ {
+ {0, 0}, {20, 0}, {20, 20}, {0, 20}, X11_STOP, X11_STOP,
+ },
+ x11_icon_next[] =
+ {
+ {0, 0}, {10, 10}, {0, 20}, X11_STOP,
+ {10, 0}, {20, 10}, {10, 20}, X11_STOP, X11_STOP,
+ },
+ x11_icon_repeat[] =
+ {
+ {0, 12}, {0, 6}, {3, 3}, {13, 3}, {13, 0}, {20, 4.5},
+ {13, 9}, {13, 6}, {3, 6}, {3, 10}, X11_STOP,
+ {0, 15.5}, {7, 11}, {7, 14}, {17, 14}, {17, 10}, {20, 8},
+ {20, 14}, {17, 17}, {7, 17}, {7, 20}, X11_STOP, X11_STOP,
+ },
+ x11_icon_random[] =
+ {
+ {0, 6}, {0, 3}, {5, 3}, {6, 4.5}, {4, 7.5}, {3, 6}, X11_STOP,
+ {9, 15.5}, {11, 12.5}, {12, 14}, {13, 14}, {13, 11}, {20, 15.5},
+ {13, 20}, {13, 17}, {10, 17}, X11_STOP,
+ {0, 17}, {0, 14}, {3, 14}, {10, 3}, {13, 3}, {13, 0}, {20, 4.5},
+ {13, 9}, {13, 6}, {12, 6}, {5, 17}, X11_STOP, X11_STOP,
+ },
+ x11_icon_single[] =
+ {
+ {7, 6}, {7, 4}, {9, 2}, {12, 2}, {12, 15}, {14, 15}, {14, 18},
+ {7, 18}, {7, 15}, {9, 15}, {9, 6}, X11_STOP, X11_STOP,
+ },
+ x11_icon_consume[] =
+ {
+ {0, 13}, {0, 7}, {4, 3}, {10, 3}, {14, 7}, {5, 10}, {14, 13},
+ {10, 17}, {4, 17}, X11_STOP,
+ {16, 12}, {16, 8}, {20, 8}, {20, 12}, X11_STOP, X11_STOP,
+ };
+
+static const XPointDouble *
+x11_icon_for_action (enum action action)
+{
+ switch (action)
+ {
+ case ACTION_MPD_PREVIOUS:
+ return x11_icon_previous;
+ case ACTION_MPD_TOGGLE:
+ return g.state == PLAYER_PLAYING ? x11_icon_pause : x11_icon_play;
+ case ACTION_MPD_STOP:
+ return x11_icon_stop;
+ case ACTION_MPD_NEXT:
+ return x11_icon_next;
+ case ACTION_MPD_REPEAT:
+ return x11_icon_repeat;
+ case ACTION_MPD_RANDOM:
+ return x11_icon_random;
+ case ACTION_MPD_SINGLE:
+ return x11_icon_single;
+ case ACTION_MPD_CONSUME:
+ return x11_icon_consume;
+ default:
+ return NULL;
+ }
+}
+
+static void
+x11_render_button (struct widget *self)
+{
+ x11_render_padding (self);
+
+ const XPointDouble *icon = x11_icon_for_action (self->userdata);
+ if (!icon)
+ {
+ x11_render_label (self);
+ return;
+ }
+
+ size_t total = 0;
+ for (size_t i = 0; icon[i].x != INFINITY || icon[i - 1].x != INFINITY; i++)
+ total++;
+
+ // TODO: There should be an attribute for buttons, to handle this better.
+ XRenderColor color = *x11_fg (self);
+ if (!(self->attrs & A_BOLD))
+ {
+ color.alpha /= 2;
+ color.red /= 2;
+ color.green /= 2;
+ color.blue /= 2;
+ }
+
+ Picture source = XRenderCreateSolidFill (g_xui.dpy, &color);
+ const XRenderPictFormat *format
+ = XRenderFindStandardFormat (g_xui.dpy, PictStandardA8);
+
+ int x = self->x, y = self->y + (self->height - self->width) / 2;
+ XPointDouble buffer[total], *p = buffer;
+ for (size_t i = 0; i < total; i++)
+ if (icon[i].x != INFINITY)
+ {
+ p->x = x + icon[i].x / 20.0 * self->width;
+ p->y = y + icon[i].y / 20.0 * self->width;
+ p++;
+ }
+ else if (p != buffer)
+ {
+ XRenderCompositeDoublePoly (g_xui.dpy, PictOpOver,
+ source, g_xui.x11_pixmap_picture, format,
+ 0, 0, 0, 0, buffer, p - buffer, EvenOddRule);
+ p = buffer;
+ }
+ XRenderFreePicture (g_xui.dpy, source);
+}
+
+static struct widget *
+x11_make_button (chtype attrs, const char *label, enum action a)
+{
+ struct widget *w = x11_make_label (attrs, 0, label);
+ w->id = WIDGET_BUTTON;
+ w->userdata = a;
+
+ if (x11_icon_for_action (a))
+ {
+ w->on_render = x11_render_button;
+
+ // It should be padded by the caller horizontally.
+ w->height = g_xui.vunit;
+ w->width = w->height * 3 / 4;
+ }
+ return w;
+}
+
+static void
+x11_render_gauge (struct widget *self)
+{
+ x11_render_padding (self);
+ if (g.state == PLAYER_STOPPED || g.song_elapsed < 0 || g.song_duration < 1)
+ return;
+
+ int part = (float) g.song_elapsed / g.song_duration * self->width;
+ XRenderFillRectangle (g_xui.dpy, PictOpSrc, g_xui.x11_pixmap_picture,
+ x11_bg_attrs (APP_ATTR (ELAPSED)),
+ self->x,
+ self->y + self->height / 8,
+ part,
+ self->height * 3 / 4);
+ XRenderFillRectangle (g_xui.dpy, PictOpSrc, g_xui.x11_pixmap_picture,
+ x11_bg_attrs (APP_ATTR (REMAINS)),
+ self->x + part,
+ self->y + self->height / 8,
+ self->width - part,
+ self->height * 3 / 4);
+}
+
+// TODO: Perhaps it should save the number within.
+static struct widget *
+x11_make_gauge (chtype attrs)
+{
+ struct widget *w = xcalloc (1, sizeof *w + 1);
+ w->on_render = x11_render_gauge;
+ w->attrs = attrs;
+ w->width = -1;
+ w->height = g_xui.vunit;
+ return w;
+}
+
+static void
+x11_render_spectrum (struct widget *self)
+{
+ x11_render_padding (self);
+
+#ifdef WITH_FFTW
+ XRectangle rectangles[g.spectrum.bars];
+ int step = self->width / N_ELEMENTS (rectangles);
+ for (int i = 0; i < g.spectrum.bars; i++)
+ {
+ int height = round ((self->height - 2) * g.spectrum.spectrum[i]);
+ rectangles[i] = (XRectangle)
+ {
+ self->x + i * step,
+ self->y + self->height - 1 - height,
+ step,
+ height,
+ };
+ }
+
+ XRenderFillRectangles (g_xui.dpy, PictOpSrc, g_xui.x11_pixmap_picture,
+ x11_fg (self), rectangles, N_ELEMENTS (rectangles));
+#endif // WITH_FFTW
+
+ // Enable the spectrum_redraw() hack.
+ XRectangle r = { self->x, self->y, self->width, self->height };
+ XUnionRectWithRegion (&r, g_xui.x11_clip, g_xui.x11_clip);
+}
+
+static struct widget *
+x11_make_spectrum (chtype attrs, int width)
+{
+ struct widget *w = xcalloc (1, sizeof *w + 1);
+ w->on_render = x11_render_spectrum;
+ w->attrs = attrs;
+ w->width = width * g_xui.vunit / 2;
+ w->height = g_xui.vunit;
+ return w;
+}
+
+static void
+x11_render_scrollbar (struct widget *self)
+{
+ x11_render_padding (self);
+
+ struct tab *tab = g.active_tab;
+ struct scrollbar bar =
+ app_compute_scrollbar (tab, app_visible_items_height (), g_xui.vunit);
+
+ XRenderFillRectangle (g_xui.dpy, PictOpSrc, g_xui.x11_pixmap_picture,
+ x11_fg_attrs (self->attrs),
+ self->x,
+ self->y + bar.start,
+ self->width,
+ bar.length);
+}
+
+static struct widget *
+x11_make_scrollbar (chtype attrs)
+{
+ struct widget *w = xcalloc (1, sizeof *w + 1);
+ w->on_render = x11_render_scrollbar;
+ w->attrs = attrs;
+ w->width = g_xui.vunit / 2;
+ return w;
+}
+
+static struct widget *
+x11_make_list (void)
+{
+ struct widget *w = xcalloc (1, sizeof *w + 1);
+ w->on_render = x11_render_padding;
+ return w;
+}
+
+static void
+x11_render_editor (struct widget *self)
+{
+ x11_render_padding (self);
+
+ XftFont *font = x11_widget_font (self)->list->font;
+ XftColor color = { .color = *x11_fg (self) };
+
+ // A simplistic adaptation of line_editor_write() follows.
+ int x = self->x, y = self->y + font->ascent;
+ XGlyphInfo extents = {};
+ if (g.editor.prompt)
+ {
+ FT_UInt i = XftCharIndex (g_xui.dpy, font, g.editor.prompt);
+ XftDrawGlyphs (g_xui.xft_draw, &color, font, x, y, &i, 1);
+ XftGlyphExtents (g_xui.dpy, font, &i, 1, &extents);
+ x += extents.xOff + g_xui.vunit / 4;
+ }
+
+ // TODO: Adapt x11_font_{hadvance,draw}().
+ // TODO: Make this scroll around the caret, and fade like labels.
+ XftDrawString32 (g_xui.xft_draw, &color, font, x, y,
+ g.editor.line, g.editor.len);
+
+ XftTextExtents32 (g_xui.dpy, font, g.editor.line, g.editor.point, &extents);
+ XRenderFillRectangle (g_xui.dpy, PictOpSrc, g_xui.x11_pixmap_picture,
+ &color.color, x + extents.xOff, self->y, 2, self->height);
+}
+
+static struct widget *
+x11_make_editor (chtype attrs)
+{
+ // TODO: This should ideally measure the text, and copy it to w->text.
+ struct widget *w = xcalloc (1, sizeof *w + 1);
+ w->on_render = x11_render_editor;
+ w->attrs = attrs;
+ w->width = -1;
+ w->height = g_xui.vunit;
+ return w;
}
+static struct app_ui app_x11_ui =
+{
+ .padding = x11_make_padding,
+ .label = app_make_label,
+ .button = x11_make_button,
+ .gauge = x11_make_gauge,
+ .spectrum = x11_make_spectrum,
+ .scrollbar = x11_make_scrollbar,
+ .list = x11_make_list,
+ .editor = x11_make_editor,
+
+ .have_icons = true,
+};
+
+#endif // WITH_X11
+
// --- Signals -----------------------------------------------------------------
static int g_signal_pipe[2]; ///< A pipe used to signal... signals
@@ -4366,72 +5979,7 @@ signals_setup_handlers (void)
// --- Initialisation, event handling ------------------------------------------
-static void
-app_on_tty_event (termo_key_t *event, int64_t event_ts)
-{
- // Simple double click detection via release--press delay, only a bit
- // complicated by the fact that we don't know what's being released
- static termo_key_t last_event;
- static int64_t last_event_ts;
- static int last_button;
-
- int y, x, button, y_last, x_last;
- termo_mouse_event_t type, type_last;
- if (termo_interpret_mouse (g.tk, event, &type, &button, &y, &x))
- {
- bool double_click = termo_interpret_mouse
- (g.tk, &last_event, &type_last, NULL, &y_last, &x_last)
- && event_ts - last_event_ts < 500
- && type_last == TERMO_MOUSE_RELEASE && type == TERMO_MOUSE_PRESS
- && y_last == y && x_last == x && last_button == button;
- if (!app_process_mouse (type, y, x, button, double_click))
- beep ();
-
- // Prevent interpreting triple clicks as two double clicks
- if (double_click)
- last_button = 0;
- else if (type == TERMO_MOUSE_PRESS)
- last_button = button;
- }
- else if (!app_process_termo_event (event))
- beep ();
-
- last_event = *event;
- last_event_ts = event_ts;
-}
-
-static void
-app_on_tty_readable (const struct pollfd *fd, void *user_data)
-{
- (void) user_data;
- if (fd->revents & ~(POLLIN | POLLHUP | POLLERR))
- print_debug ("fd %d: unexpected revents: %d", fd->fd, fd->revents);
-
- poller_timer_reset (&g.tk_timer);
- termo_advisereadable (g.tk);
-
- termo_key_t event;
- int64_t event_ts = clock_msec (CLOCK_BEST);
- termo_result_t res;
- while ((res = termo_getkey (g.tk, &event)) == TERMO_RES_KEY)
- app_on_tty_event (&event, event_ts);
-
- if (res == TERMO_RES_AGAIN)
- poller_timer_set (&g.tk_timer, termo_get_waittime (g.tk));
- else if (res == TERMO_RES_ERROR || res == TERMO_RES_EOF)
- app_quit ();
-}
-
-static void
-app_on_key_timer (void *user_data)
-{
- (void) user_data;
-
- termo_key_t event;
- if (termo_getkey_force (g.tk, &event) == TERMO_RES_KEY)
- if (!app_process_termo_event (&event))
- beep ();
-}
+static bool g_verbose_mode = false;
static void
app_on_signal_pipe_readable (const struct pollfd *fd, void *user_data)
@@ -4444,11 +5992,13 @@ app_on_signal_pipe_readable (const struct pollfd *fd, void *user_data)
if (g_termination_requested && !g.quitting)
app_quit ();
+ // It would be awkward to set up SIGWINCH conditionally,
+ // so have it as a handler within UIs.
if (g_winch_received)
{
g_winch_received = false;
- update_curses_terminal_size ();
- app_invalidate ();
+ if (g_xui.ui->winch)
+ g_xui.ui->winch ();
}
}
@@ -4457,8 +6007,7 @@ app_on_message_timer (void *user_data)
{
(void) user_data;
- cstr_set (&g.message, NULL);
- app_invalidate ();
+ app_hide_message ();
}
static void
@@ -4474,21 +6023,20 @@ app_log_handler (void *user_data, const char *quote, const char *fmt,
struct str message = str_make ();
str_append (&message, quote);
+ size_t quote_len = message.len;
str_append_vprintf (&message, fmt, ap);
- // If the standard error output isn't redirected, try our best at showing
- // the message to the user
- if (!isatty (STDERR_FILENO))
+ // Show it prettified to the user, then maybe log it elsewhere as well.
+ // TODO: Review locale encoding vs UTF-8 in the entire program.
+ message.str[0] = toupper_ascii (message.str[0]);
+ app_show_message (xstrndup (message.str, quote_len),
+ xstrdup (message.str + quote_len));
+
+ if (g_verbose_mode && (g_xui.ui != &tui_ui || !isatty (STDERR_FILENO)))
fprintf (stderr, "%s\n", message.str);
- else if (g_debug_tab.active)
+ if (g_debug_tab.active)
debug_tab_push (str_steal (&message),
user_data == NULL ? 0 : g.attrs[(intptr_t) user_data].attrs);
- else
- {
- cstr_set (&g.message, xstrdup (message.str));
- app_invalidate ();
- poller_timer_set (&g.message_timer, 5000);
- }
str_free (&message);
in_processing = false;
@@ -4501,16 +6049,9 @@ app_init_poller_events (void)
g.signal_event.dispatcher = app_on_signal_pipe_readable;
poller_fd_set (&g.signal_event, POLLIN);
- g.tty_event = poller_fd_make (&g.poller, STDIN_FILENO);
- g.tty_event.dispatcher = app_on_tty_readable;
- poller_fd_set (&g.tty_event, POLLIN);
-
g.message_timer = poller_timer_make (&g.poller);
g.message_timer.dispatcher = app_on_message_timer;
- g.tk_timer = poller_timer_make (&g.poller);
- g.tk_timer.dispatcher = app_on_key_timer;
-
g.connect_event = poller_timer_make (&g.poller);
g.connect_event.dispatcher = app_on_reconnect;
poller_timer_set (&g.connect_event, 0);
@@ -4519,9 +6060,56 @@ app_init_poller_events (void)
g.elapsed_event.dispatcher = g.elapsed_poll
? mpd_on_elapsed_time_tick_poll
: mpd_on_elapsed_time_tick;
+}
- g.refresh_event = poller_idle_make (&g.poller);
- g.refresh_event.dispatcher = app_on_refresh;
+static void
+app_init_ui (bool requested_x11)
+{
+ xui_preinit ();
+
+ g_normal_keys = app_init_bindings ("normal",
+ g_normal_defaults, N_ELEMENTS (g_normal_defaults), &g_normal_keys_len);
+ g_editor_keys = app_init_bindings ("editor",
+ g_editor_defaults, N_ELEMENTS (g_editor_defaults), &g_editor_keys_len);
+
+ // It doesn't work 100% (e.g. incompatible with undelining in urxvt)
+ // TODO: make this configurable
+ g.use_partial_boxes = g_xui.locale_is_utf8;
+
+#ifdef WITH_X11
+ g_xui.x11_fontname = get_config_string (g.config.root, "settings.x11_font");
+#endif // WITH_X11
+
+ xui_start (&g.poller, requested_x11, g.attrs, N_ELEMENTS (g.attrs));
+
+#ifdef WITH_X11
+ if (g_xui.ui == &x11_ui)
+ g.ui = &app_x11_ui;
+ else
+#endif // WITH_X11
+ g.ui = &app_tui_ui;
+}
+
+static void
+app_init_enqueue (char *argv[], int argc)
+{
+ // TODO: MPD is unwilling to play directories, so perhaps recurse ourselves
+ char cwd[4096] = "";
+ for (int i = 0; i < argc; i++)
+ {
+ // This is a super-trivial method of URL detection, however anything
+ // contaning the scheme and authority delimiters in a sequence is most
+ // certainly not a filesystem path, and thus it will work as expected.
+ // Error handling may be done by MPD.
+ const char *path_or_URL = argv[i];
+ if (*path_or_URL == '/' || strstr (path_or_URL, "://"))
+ strv_append (&g.enqueue, path_or_URL);
+ else if (!*cwd && !getcwd (cwd, sizeof cwd))
+ exit_fatal ("getcwd: %s", strerror (errno));
+ else
+ strv_append_owned (&g.enqueue,
+ xstrdup_printf ("%s/%s", cwd, path_or_URL));
+ }
}
int
@@ -4530,13 +6118,18 @@ main (int argc, char *argv[])
static const struct opt opts[] =
{
{ 'd', "debug", NULL, 0, "run in debug mode" },
+#ifdef WITH_X11
+ { 'x', "x11", NULL, 0, "use X11 even when run from a terminal" },
+#endif // WITH_X11
{ 'h', "help", NULL, 0, "display this help and exit" },
+ { 'v', "verbose", NULL, 0, "log messages on standard error" },
{ 'V', "version", NULL, 0, "output version information and exit" },
{ 0, NULL, NULL, 0, NULL }
};
- struct opt_handler oh =
- opt_handler_make (argc, argv, opts, NULL, "Terminal-based MPD client.");
+ bool requested_x11 = false;
+ struct opt_handler oh
+ = opt_handler_make (argc, argv, opts, "[URL | PATH]...", "MPD client.");
int c;
while ((c = opt_handler_get (&oh)) != -1)
@@ -4545,6 +6138,12 @@ main (int argc, char *argv[])
case 'd':
g_debug_mode = true;
break;
+ case 'x':
+ requested_x11 = true;
+ break;
+ case 'v':
+ g_verbose_mode = true;
+ break;
case 'h':
opt_handler_usage (&oh, stdout);
exit (EXIT_SUCCESS);
@@ -4559,12 +6158,6 @@ main (int argc, char *argv[])
argc -= optind;
argv += optind;
-
- if (argc)
- {
- opt_handler_usage (&oh, stderr);
- exit (EXIT_FAILURE);
- }
opt_handler_free (&oh);
// We only need to convert to and from the terminal encoding
@@ -4572,15 +6165,11 @@ main (int argc, char *argv[])
print_warning ("failed to set the locale");
app_init_context ();
+ app_init_enqueue (argv, argc);
app_load_configuration ();
- app_init_terminal ();
signals_setup_handlers ();
app_init_poller_events ();
-
- g_normal_keys = app_init_bindings ("normal",
- g_normal_defaults, N_ELEMENTS (g_normal_defaults), &g_normal_keys_len);
- g_editor_keys = app_init_bindings ("editor",
- g_editor_defaults, N_ELEMENTS (g_editor_defaults), &g_editor_keys_len);
+ app_init_ui (requested_x11);
if (g_debug_mode)
app_prepend_tab (debug_tab_init ());
@@ -4595,11 +6184,16 @@ main (int argc, char *argv[])
app_prepend_tab (current_tab_init ());
app_switch_tab ((g.help_tab = help_tab_init ()));
+ // TODO: the help tab should be the default for new users only,
+ // so provide a configuration option to flip this
+ if (argc)
+ app_switch_tab (&g_current_tab);
+
g.polling = true;
while (g.polling)
poller_run (&g.poller);
- endwin ();
+ xui_stop ();
g_log_message_real = log_message_stdio;
app_free_context ();
return 0;