aboutsummaryrefslogtreecommitdiff
path: root/nncmpp.c
diff options
context:
space:
mode:
Diffstat (limited to 'nncmpp.c')
-rw-r--r--nncmpp.c335
1 files changed, 265 insertions, 70 deletions
diff --git a/nncmpp.c b/nncmpp.c
index 27b3210..8f17d8d 100644
--- a/nncmpp.c
+++ b/nncmpp.c
@@ -1,7 +1,7 @@
/*
* nncmpp -- the MPD client you never knew you needed
*
- * Copyright (c) 2016 - 2023, Přemysl Eric Janouch <p@janouch.name>
+ * Copyright (c) 2016 - 2024, Přemysl Eric Janouch <p@janouch.name>
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted.
@@ -522,7 +522,7 @@ static struct item_list
item_list_make (void)
{
struct item_list self = {};
- self.items = xcalloc (sizeof *self.items, (self.alloc = 16));
+ self.items = xcalloc ((self.alloc = 16), sizeof *self.items);
return self;
}
@@ -814,9 +814,9 @@ spectrum_init (struct spectrum *s, char *format, int bars, int fps,
s->useful_bins = s->bins / 2;
int used_bins = necessary_bins / 2;
- 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);
+ s->rendered = xcalloc (s->bars * 3 + 1, sizeof *s->rendered);
+ s->spectrum = xcalloc (s->bars, sizeof *s->spectrum);
+ s->top_bins = xcalloc (s->bars, sizeof *s->top_bins);
for (int bar = 0; bar < s->bars; bar++)
{
int top_bin = floor (pow (used_bins + 1, (bar + 1.) / s->bars)) - 1;
@@ -839,7 +839,7 @@ spectrum_init (struct spectrum *s, char *format, int bars, int fps,
s->buffer = xcalloc (1, s->buffer_size);
// Prepare the window
- s->window = xcalloc (sizeof *s->window, s->bins);
+ s->window = xcalloc (s->bins, sizeof *s->window);
window_hann (s->window, s->bins);
// Multiply by 2 for only using half of the DFT's result, then adjust to
@@ -849,11 +849,11 @@ spectrum_init (struct spectrum *s, char *format, int bars, int fps,
float coherent_gain = window_coherent_gain (s->window, s->bins);
s->accumulator_scale = 2 * 2 / coherent_gain / coherent_gain / s->samples;
- s->data = xcalloc (sizeof *s->data, s->bins);
- s->windowed = fftw_malloc (sizeof *s->windowed * s->bins);
- s->out = fftw_malloc (sizeof *s->out * (s->useful_bins + 1));
+ s->data = xcalloc (s->bins, sizeof *s->data);
+ s->windowed = fftw_malloc (s->bins * sizeof *s->windowed);
+ s->out = fftw_malloc ((s->useful_bins + 1) * sizeof *s->out);
s->p = fftwf_plan_dft_r2c_1d (s->bins, s->windowed, s->out, FFTW_MEASURE);
- s->accumulator = xcalloc (sizeof *s->accumulator, s->useful_bins);
+ s->accumulator = xcalloc (s->useful_bins, sizeof *s->accumulator);
return true;
}
@@ -1256,6 +1256,9 @@ 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 strv action_names; ///< User-defined action names
+ struct strv action_descriptions; ///< User-defined action descriptions
+ struct strv action_commands; ///< User-defined action commands
struct tab *help_tab; ///< Special help tab
struct tab *tabs; ///< All other tabs
@@ -1337,7 +1340,7 @@ on_pulseaudio_changed (struct config_item *item)
g.pulse_control_requested = item->value.boolean;
}
-static struct config_schema g_config_settings[] =
+static const struct config_schema g_config_settings[] =
{
{ .name = "address",
.comment = "Address to connect to the MPD server",
@@ -1401,7 +1404,7 @@ static struct config_schema g_config_settings[] =
{}
};
-static struct config_schema g_config_colors[] =
+static const struct config_schema g_config_colors[] =
{
#define XX(name_, config, fg_, bg_, attrs_) \
{ .name = #config, .type = CONFIG_ITEM_STRING },
@@ -1410,6 +1413,17 @@ static struct config_schema g_config_colors[] =
{}
};
+static const struct config_schema g_config_actions[] =
+{
+ { .name = "description",
+ .comment = "Human-readable description of the action",
+ .type = CONFIG_ITEM_STRING },
+ { .name = "command",
+ .comment = "Shell command to run",
+ .type = CONFIG_ITEM_STRING },
+ {}
+};
+
static const char *
get_config_string (struct config_item *root, const char *key)
{
@@ -1485,12 +1499,43 @@ load_config_streams (struct config_item *subtree, void *user_data)
}
static void
+load_config_actions (struct config_item *subtree, void *user_data)
+{
+ (void) user_data;
+
+ struct str_map_iter iter = str_map_iter_make (&subtree->value.object);
+ while (str_map_iter_next (&iter))
+ strv_append (&g.action_names, iter.link->key);
+ qsort (g.action_names.vector, g.action_names.len,
+ sizeof *g.action_names.vector, strv_sort_utf8_cb);
+
+ for (size_t i = 0; i < g.action_names.len; i++)
+ {
+ const char *name = g.action_names.vector[i];
+ struct config_item *item = config_item_get (subtree, name, NULL);
+ hard_assert (item != NULL);
+ if (item->type != CONFIG_ITEM_OBJECT)
+ exit_fatal ("`%s': invalid user action, expected an object", name);
+
+ config_schema_apply_to_object (g_config_actions, item, NULL);
+ config_schema_call_changed (item);
+
+ const char *description = get_config_string (item, "description");
+ const char *command = get_config_string (item, "command");
+ strv_append (&g.action_descriptions, description ? description : name);
+ strv_append (&g.action_commands, command ? command : "");
+ }
+}
+
+static void
app_load_configuration (void)
{
struct config *config = &g.config;
config_register_module (config, "settings", load_config_settings, NULL);
config_register_module (config, "colors", load_config_colors, NULL);
config_register_module (config, "streams", load_config_streams, NULL);
+ // This must run before bindings are parsed in app_init_ui().
+ config_register_module (config, "actions", load_config_actions, NULL);
// Bootstrap configuration, so that we can access schema items at all
config_load (config, config_item_object ());
@@ -1548,6 +1593,9 @@ app_init_context (void)
g.config = config_make ();
g.streams = strv_make ();
g.enqueue = strv_make ();
+ g.action_names = strv_make ();
+ g.action_descriptions = strv_make ();
+ g.action_commands = strv_make ();
g.playback_info = str_map_make (free);
g.playback_info.key_xfrm = tolower_ascii_strxfrm;
@@ -1570,6 +1618,9 @@ app_free_context (void)
str_map_free (&g.playback_info);
strv_free (&g.streams);
strv_free (&g.enqueue);
+ strv_free (&g.action_names);
+ strv_free (&g.action_descriptions);
+ strv_free (&g.action_commands);
item_list_free (&g.playlist);
#ifdef WITH_FFTW
@@ -2379,12 +2430,52 @@ app_goto_tab (int tab_index)
static int
action_resolve (const char *name)
{
- for (int i = 0; i < ACTION_COUNT; i++)
+ for (int i = 0; i < ACTION_USER_0; i++)
if (!strcasecmp_ascii (g_action_names[i], name))
return i;
+
+ // We could put this lookup first, and accordingly adjust
+ // app_init_bindings() to do action_resolve(action_name(action)),
+ // however the ability to override internal actions seems pointless.
+ for (size_t i = 0; i < g.action_names.len; i++)
+ if (!strcasecmp_ascii (g.action_names.vector[i], name))
+ return ACTION_USER_0 + i;
return -1;
}
+static const char *
+action_name (enum action action)
+{
+ if (action < ACTION_USER_0)
+ return g_action_names[action];
+
+ size_t user_action = action - ACTION_USER_0;
+ hard_assert (user_action < g.action_names.len);
+ return g.action_names.vector[user_action];
+}
+
+static const char *
+action_description (enum action action)
+{
+ if (action < ACTION_USER_0)
+ return g_action_descriptions[action];
+
+ size_t user_action = action - ACTION_USER_0;
+ hard_assert (user_action < g.action_descriptions.len);
+ return g.action_descriptions.vector[user_action];
+}
+
+static const char *
+action_command (enum action action)
+{
+ if (action < ACTION_USER_0)
+ return NULL;
+
+ size_t user_action = action - ACTION_USER_0;
+ hard_assert (user_action < g.action_commands.len);
+ return g.action_commands.vector[user_action];
+}
+
// --- User input handling -----------------------------------------------------
static void
@@ -2517,6 +2608,64 @@ incremental_search_on_end (bool confirmed)
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static bool
+run_command (const char *command, struct str *output, struct error **e)
+{
+ char *adjusted = xstrdup_printf ("2>&1 %s", command);
+ print_debug ("running command: %s", adjusted);
+
+ FILE *fp = popen (adjusted, "r");
+ free (adjusted);
+ if (!fp)
+ return error_set (e, "%s", strerror (errno));
+
+ char buf[BUFSIZ];
+ size_t len;
+ while ((len = fread (buf, 1, sizeof buf, fp)) == sizeof buf)
+ str_append_data (output, buf, len);
+ str_append_data (output, buf, len);
+
+ int status = pclose (fp);
+ if (status < 0)
+ return error_set (e, "%s", strerror (errno));
+ if (WIFEXITED (status) && WEXITSTATUS (status))
+ return error_set (e, "exit status %d", WEXITSTATUS (status));
+ if (WIFSIGNALED (status))
+ return error_set (e, "terminated on signal %d", WTERMSIG (status));
+ if (WIFSTOPPED (status))
+ return error_set (e, "stopped on signal %d", WSTOPSIG (status));
+ return true;
+}
+
+static bool
+app_process_action_command (enum action action)
+{
+ const char *command = action_command (action);
+ if (!command)
+ return false;
+
+ struct str output = str_make ();
+ struct error *error = NULL;
+ (void) run_command (command, &output, &error);
+ str_enforce_utf8 (&output);
+
+ struct strv lines = strv_make ();
+ cstr_split (output.str, "\r\n", false, &lines);
+ str_free (&output);
+ while (lines.len && !*lines.vector[lines.len - 1])
+ free (strv_steal (&lines, lines.len - 1));
+ for (size_t i = 0; i < lines.len; i++)
+ print_debug ("output: %s", lines.vector[i]);
+ strv_free (&lines);
+
+ if (error)
+ {
+ print_error ("\"%s\": %s", action_description (action), error->message);
+ error_free (error);
+ }
+ return true;
+}
+
+static bool
app_mpd_toggle (const char *name)
{
const char *s = str_map_find (&g.playback_info, name);
@@ -2562,10 +2711,6 @@ app_process_action (enum action action)
xui_invalidate ();
app_hide_message ();
return true;
- default:
- print_error ("\"%s\" is not allowed here",
- g_action_descriptions[action]);
- return false;
case ACTION_MULTISELECT:
if (!tab->can_multiselect
@@ -2677,8 +2822,14 @@ app_process_action (enum action action)
case ACTION_GOTO_VIEW_BOTTOM:
g.active_tab->item_selected = g.active_tab->item_top;
return app_move_selection (MAX (0, app_visible_items () - 1));
+
+ default:
+ if (app_process_action_command (action))
+ return true;
+
+ print_error ("\"%s\" is not allowed here", action_description (action));
+ return false;
}
- return false;
}
static bool
@@ -2696,8 +2847,7 @@ 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]);
+ print_error ("\"%s\" is not allowed here", action_description (action));
return false;
case ACTION_EDITOR_B_CHAR:
@@ -3599,30 +3749,67 @@ static void
library_tab_on_search_data (const struct mpd_response *response,
const struct strv *data, void *user_data)
{
- (void) user_data;
+ char *filter = user_data;
if (!g_library_tab.searching)
- return;
+ goto out;
if (!response->success)
- {
print_error ("cannot search: %s", response->message_text);
- return;
+ else
+ {
+ cstr_set (&g_library_tab.super.header,
+ xstrdup_printf ("%s: %s", "Global search", filter));
+ library_tab_load_data (data);
}
- library_tab_load_data (data);
+out:
+ free (filter);
+}
+
+static char *
+mpd_quoted_filter_string (const char *value)
+{
+ struct str quoted = str_make ();
+ str_append_c (&quoted, '\'');
+ for (const char *p = value; *p; p++)
+ {
+ if (mpd_client_must_escape_in_quote (*p))
+ str_append_c (&quoted, '\\');
+ str_append_c (&quoted, *p);
+ }
+ str_append_c (&quoted, '\'');
+ return str_steal (&quoted);
}
static void
search_on_changed (void)
{
struct mpd_client *c = &g.client;
+ if (c->state != MPD_CONNECTED)
+ return;
size_t len;
char *u8 = (char *) u32_to_u8 (g.editor.line, g.editor.len + 1, NULL, &len);
+ mpd_client_list_begin (c);
mpd_client_send_command (c, "search", "any", u8, NULL);
- free (u8);
- mpd_client_add_task (c, library_tab_on_search_data, NULL);
+ // Just tag search doesn't consider filenames.
+ // Older MPD can do `search any X file X` but without the negation,
+ // which is necessary to avoid duplicates. Neither syntax supports OR.
+ // XXX: We should parse this, but it's probably not going to reach 100 soon,
+ // and it is not really documented what this should even look like.
+ if (strcmp (c->got_hello, "0.21.") > 1)
+ {
+ char *quoted = mpd_quoted_filter_string (u8);
+ char *expression = xstrdup_printf ("((!(any contains %s)) AND "
+ "(file contains %s))", quoted, quoted);
+ mpd_client_send_command (c, "search", expression, NULL);
+ free (expression);
+ free (quoted);
+ }
+
+ mpd_client_list_end (c);
+ mpd_client_add_task (c, library_tab_on_search_data, u8);
mpd_client_idle (c, 0);
}
@@ -3843,8 +4030,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);
@@ -3867,7 +4059,7 @@ 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
@@ -4070,24 +4262,16 @@ 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))
+ struct error *error = NULL;
+ (void) run_command (escaped.str, &description, &error);
+ str_free (&escaped);
+ if (error)
{
+ print_error ("%s: %s", path, error->message);
+ error_free (error);
str_free (&description);
- print_error ("%s: %s", path, strerror (errno));
return NULL;
}
@@ -4100,8 +4284,8 @@ info_tab_plugin_load (const char *path)
str_enforce_utf8 (&description);
if (!description.len)
{
- str_free (&description);
print_error ("%s: %s", path, "missing description");
+ str_free (&description);
return NULL;
}
@@ -4539,7 +4723,7 @@ help_tab_on_action (enum action action)
if (action == ACTION_DESCRIBE)
{
app_show_message (xstrdup ("Configuration name: "),
- xstrdup (g_action_names[a]));
+ xstrdup (action_name (a)));
return true;
}
if (action != ACTION_CHOOSE || a == ACTION_CHOOSE /* avoid recursion */)
@@ -4564,9 +4748,9 @@ help_tab_assign_action (enum action action)
static void
help_tab_group (struct binding *keys, size_t len, struct strv *out,
- bool bound[ACTION_COUNT])
+ bool bound[], size_t action_count)
{
- for (enum action i = 0; i < ACTION_COUNT; i++)
+ for (enum action i = 0; i < action_count; i++)
{
struct strv ass = strv_make ();
for (size_t k = 0; k < len; k++)
@@ -4576,7 +4760,7 @@ help_tab_group (struct binding *keys, size_t len, struct strv *out,
{
char *joined = strv_join (&ass, ", ");
strv_append_owned (out, xstrdup_printf
- (" %s%c%s", g_action_descriptions[i], 0, joined));
+ (" %s%c%s", action_description (i), 0, joined));
free (joined);
bound[i] = true;
@@ -4587,13 +4771,13 @@ help_tab_group (struct binding *keys, size_t len, struct strv *out,
}
static void
-help_tab_unbound (struct strv *out, bool bound[ACTION_COUNT])
+help_tab_unbound (struct strv *out, bool bound[], size_t action_count)
{
- for (enum action i = 0; i < ACTION_COUNT; i++)
+ for (enum action i = 0; i < action_count; i++)
if (!bound[i])
{
strv_append_owned (out,
- xstrdup_printf (" %s%c", g_action_descriptions[i], 0));
+ xstrdup_printf (" %s%c", action_description (i), 0));
help_tab_assign_action (i);
}
}
@@ -4623,27 +4807,30 @@ help_tab_init (void)
struct strv *lines = &g_help_tab.lines;
*lines = strv_make ();
- bool bound[ACTION_COUNT] = { [ACTION_NONE] = true };
+ size_t bound_len = ACTION_USER_0 + g.action_names.len;
+ bool *bound = xcalloc (bound_len, sizeof *bound);
+ bound[ACTION_NONE] = true;
strv_append (lines, "Normal mode actions");
- help_tab_group (g_normal_keys, g_normal_keys_len, lines, bound);
+ help_tab_group (g_normal_keys, g_normal_keys_len, lines, bound, bound_len);
strv_append (lines, "");
strv_append (lines, "Editor mode actions");
- help_tab_group (g_editor_keys, g_editor_keys_len, lines, bound);
+ help_tab_group (g_editor_keys, g_editor_keys_len, lines, bound, bound_len);
strv_append (lines, "");
bool have_unbound = false;
- for (enum action i = 0; i < ACTION_COUNT; i++)
+ for (size_t i = 0; i < bound_len; i++)
if (!bound[i])
have_unbound = true;
if (have_unbound)
{
strv_append (lines, "Unbound actions");
- help_tab_unbound (lines, bound);
+ help_tab_unbound (lines, bound, bound_len);
strv_append (lines, "");
}
+ free (bound);
struct tab *super = &g_help_tab.super;
tab_init (super, "Help");
@@ -5855,28 +6042,36 @@ x11_render_editor (struct widget *self)
{
x11_render_padding (self);
- XftFont *font = x11_widget_font (self)->list->font;
+ struct x11_font *font = x11_widget_font (self);
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)
+ // A simplistic adaptation of tui_render_editor() follows.
+ const struct line_editor *e = &g.editor;
+ int x = self->x;
+ if (e->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;
+ hard_assert (e->prompt < 127);
+ x += x11_font_draw (font, &color, x, self->y,
+ (char[2]) { e->prompt, 0 }) + 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);
+ size_t len;
+ ucs4_t *buf = xcalloc (e->len + 1, sizeof *buf);
+ u32_cpy (buf, e->line, e->point);
+ char *a = (char *) u32_to_u8 (buf, u32_strlen (buf) + 1, NULL, &len);
+ u32_cpy (buf, e->line + e->point, e->len - e->point + 1);
+ char *b = (char *) u32_to_u8 (buf, u32_strlen (buf) + 1, NULL, &len);
+ free (buf);
+
+ x += x11_font_draw (font, &color, x, self->y, a);
+ int caret = x;
+ x += x11_font_draw (font, &color, x, self->y, b);
+ free (a);
+ free (b);
- 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);
+ &color.color, caret, self->y, 2, self->height);
}
static struct widget *