aboutsummaryrefslogtreecommitdiff
path: root/nncmpp.c
diff options
context:
space:
mode:
Diffstat (limited to 'nncmpp.c')
-rw-r--r--nncmpp.c759
1 files changed, 608 insertions, 151 deletions
diff --git a/nncmpp.c b/nncmpp.c
index 27b3210..4a124a4 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 - 2026, 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.
@@ -74,6 +74,9 @@ enum
#ifdef WITH_X11
#define LIBERTY_XUI_WANT_X11
#endif // WITH_X11
+#ifdef WITH_APPKIT
+#define LIBERTY_XUI_WANT_APPKIT
+#endif // WITH_APPKIT
#include "liberty/liberty-xui.c"
#include <dirent.h>
@@ -522,7 +525,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 +817,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 +842,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 +852,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 +1259,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 +1343,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 +1407,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 +1416,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 +1502,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 +1596,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 +1621,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
@@ -1859,7 +1913,7 @@ app_layout_status (struct layout *out)
if (!stopped && g.song_elapsed >= 0 && g.song_duration >= 1)
app_push (&l, g.ui->gauge (attrs[0]))
- ->id = WIDGET_GAUGE;
+ ->widget_id = WIDGET_GAUGE;
else
app_push_fill (&l, g.ui->padding (attrs[0], 0, 1));
@@ -1867,7 +1921,7 @@ app_layout_status (struct layout *out)
{
app_push (&l, g.ui->padding (attrs[0], 1, 1));
app_push (&l, g.ui->label (attrs[0], volume.str))
- ->id = WIDGET_VOLUME;
+ ->widget_id = WIDGET_VOLUME;
}
str_free (&volume);
@@ -1883,20 +1937,20 @@ app_layout_tabs (struct layout *out)
// The help tab is disguised so that it's not too intruding
app_push (&l, g.ui->padding (attrs[g.active_tab == g.help_tab], 0.25, 1))
- ->id = WIDGET_TAB;
+ ->widget_id = WIDGET_TAB;
app_push (&l, g.ui->label (attrs[g.active_tab == g.help_tab], APP_TITLE))
- ->id = WIDGET_TAB;
+ ->widget_id = WIDGET_TAB;
// XXX: attrs[0]?
app_push (&l, g.ui->padding (attrs[g.active_tab == g.help_tab], 0.5, 1))
- ->id = WIDGET_TAB;
+ ->widget_id = WIDGET_TAB;
int i = 0;
LIST_FOR_EACH (struct tab, iter, g.tabs)
{
struct widget *w = app_push (&l,
g.ui->label (attrs[iter == g.active_tab], iter->name));
- w->id = WIDGET_TAB;
+ w->widget_id = WIDGET_TAB;
w->userdata = ++i;
}
@@ -1907,7 +1961,7 @@ app_layout_tabs (struct layout *out)
if (g.spectrum_fd != -1)
{
app_push (&l, g.ui->spectrum (attrs[0], g.spectrum.bars))
- ->id = WIDGET_SPECTRUM;
+ ->widget_id = WIDGET_SPECTRUM;
}
#endif // WITH_FFTW
@@ -2009,7 +2063,7 @@ app_layout_view (struct layout *out, int height)
{
struct layout l = {};
struct widget *list = app_push_fill (&l, g.ui->list ());
- list->id = WIDGET_LIST;
+ list->widget_id = WIDGET_LIST;
list->height = height;
list->width = g_xui.width;
@@ -2018,7 +2072,7 @@ app_layout_view (struct layout *out, int height)
{
struct widget *scrollbar = g.ui->scrollbar (APP_ATTR (SCROLLBAR));
list->width -= scrollbar->width;
- app_push (&l, scrollbar)->id = WIDGET_SCROLLBAR;
+ app_push (&l, scrollbar)->widget_id = WIDGET_SCROLLBAR;
}
int to_show = MIN ((int) tab->item_count - tab->item_top,
@@ -2157,7 +2211,7 @@ app_layout_statusbar (struct layout *out)
app_flush_layout (&l, out);
LIST_FOR_EACH (struct widget, w, l.head)
- w->id = WIDGET_MESSAGE;
+ w->widget_id = WIDGET_MESSAGE;
}
else if (g.editor.line)
{
@@ -2179,10 +2233,10 @@ app_layout_statusbar (struct layout *out)
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static struct widget *
-app_widget_by_id (int id)
+app_widget_by_id (int widget_id)
{
LIST_FOR_EACH (struct widget, w, g_xui.widgets)
- if (w->id == id)
+ if (w->widget_id == widget_id)
return w;
return NULL;
}
@@ -2379,12 +2433,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 +2611,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 +2714,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 +2825,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 +2850,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:
@@ -2741,7 +2894,7 @@ enum { APP_KEYMOD_DOUBLE_CLICK = 1 << 15 };
static bool
app_process_left_mouse_click (struct widget *w, int x, int y, int modifiers)
{
- switch (w->id)
+ switch (w->widget_id)
{
case WIDGET_BUTTON:
app_process_action (w->userdata);
@@ -2852,10 +3005,10 @@ app_process_mouse (termo_mouse_event_t type, int x, int y, int button,
switch (button)
{
case 1:
- g.ui_dragging = target->id;
+ g.ui_dragging = target->widget_id;
return app_process_left_mouse_click (target, x, y, modifiers);
case 4:
- switch (target->id)
+ switch (target->widget_id)
{
case WIDGET_LIST:
return app_process_action (ACTION_SCROLL_UP);
@@ -2870,7 +3023,7 @@ app_process_mouse (termo_mouse_event_t type, int x, int y, int button,
}
break;
case 5:
- switch (target->id)
+ switch (target->widget_id)
{
case WIDGET_LIST:
return app_process_action (ACTION_SCROLL_DOWN);
@@ -3193,13 +3346,13 @@ static void
current_tab_move (int from, int to)
{
compact_map_t map;
- const char *id;
+ const char *item_id;
if (!(map = item_list_get (&g.playlist, from))
- || !(id = compact_map_find (map, "id")))
+ || !(item_id = compact_map_find (map, "id")))
return;
char *target_str = xstrdup_printf ("%d", to);
- mpd_client_send_command (&g.client, "moveid", id, target_str, NULL);
+ mpd_client_send_command (&g.client, "moveid", item_id, target_str, NULL);
free (target_str);
}
@@ -3241,7 +3394,7 @@ current_tab_on_action (enum action action)
compact_map_t map = item_list_get (&g.playlist, tab->item_selected);
switch (action)
{
- const char *id;
+ const char *item_id;
case ACTION_GOTO_PLAYING:
if (g.song < 0 || (size_t) g.song >= tab->item_count)
return false;
@@ -3255,13 +3408,13 @@ current_tab_on_action (enum action action)
return current_tab_move_selection (+1);
case ACTION_CHOOSE:
tab->item_mark = -1;
- return map && (id = compact_map_find (map, "id"))
- && MPD_SIMPLE ("playid", id);
+ return map && (item_id = compact_map_find (map, "id"))
+ && MPD_SIMPLE ("playid", item_id);
case ACTION_DESCRIBE:
- if (!map || !(id = compact_map_find (map, "file")))
+ if (!map || !(item_id = compact_map_find (map, "file")))
return false;
- app_show_message (xstrdup ("Path: "), xstrdup (id));
+ app_show_message (xstrdup ("Path: "), xstrdup (item_id));
return true;
case ACTION_DELETE:
{
@@ -3274,8 +3427,8 @@ current_tab_on_action (enum action action)
for (int i = range.from; i <= range.upto; i++)
{
if ((map = item_list_get (&g.playlist, i))
- && (id = compact_map_find (map, "id")))
- mpd_client_send_command (c, "deleteid", id, NULL);
+ && (item_id = compact_map_find (map, "id")))
+ mpd_client_send_command (c, "deleteid", item_id, NULL);
}
mpd_client_list_end (c);
mpd_client_add_task (c, mpd_on_simple_response, NULL);
@@ -3599,30 +3752,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 +4033,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 +4062,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 +4265,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 +4287,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 +4726,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 +4751,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 +4763,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 +4774,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 +4810,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");
@@ -5066,12 +5256,12 @@ static ssize_t
mpd_find_pos_of_id (const char *desired_id)
{
compact_map_t map;
- const char *id;
+ const char *item_id;
for (size_t i = 0; i < g.playlist.len; i++)
{
if ((map = item_list_get (&g.playlist, i))
- && (id = compact_map_find (map, "id"))
- && !strcmp (id, desired_id))
+ && (item_id = compact_map_find (map, "id"))
+ && !strcmp (item_id, desired_id))
return i;
}
return -1;
@@ -5378,7 +5568,7 @@ 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->widget_id = WIDGET_BUTTON;
w->userdata = a;
return w;
}
@@ -5591,95 +5781,340 @@ static struct app_ui app_tui_ui =
.editor = tui_make_editor,
};
-// --- X11 ---------------------------------------------------------------------
+// --- Shared GUI Icons --------------------------------------------------------
-#ifdef WITH_X11
+#if defined WITH_X11 || defined WITH_APPKIT
+
+struct app_icon_point
+{
+ double x;
+ double y;
+};
// On a 20x20 raster to make it feasible to design on paper.
-#define X11_STOP {INFINITY, INFINITY}
-static const XPointDouble
- x11_icon_previous[] =
+#define APP_ICON_STOP {INFINITY, INFINITY}
+static const struct app_icon_point
+ app_icon_previous[] =
{
- {10, 0}, {0, 10}, {10, 20}, X11_STOP,
- {20, 0}, {10, 10}, {20, 20}, X11_STOP, X11_STOP,
+ {10, 0}, {0, 10}, {10, 20}, APP_ICON_STOP,
+ {20, 0}, {10, 10}, {20, 20}, APP_ICON_STOP, APP_ICON_STOP,
},
- x11_icon_pause[] =
+ app_icon_pause[] =
{
- {1, 0}, {7, 0}, {7, 20}, {1, 20}, X11_STOP,
- {13, 0}, {19, 0}, {19, 20}, {13, 20}, X11_STOP, X11_STOP,
+ {1, 0}, {7, 0}, {7, 20}, {1, 20}, APP_ICON_STOP,
+ {13, 0}, {19, 0}, {19, 20}, {13, 20}, APP_ICON_STOP, APP_ICON_STOP,
},
- x11_icon_play[] =
+ app_icon_play[] =
{
- {0, 0}, {20, 10}, {0, 20}, X11_STOP, X11_STOP,
+ {0, 0}, {20, 10}, {0, 20}, APP_ICON_STOP, APP_ICON_STOP,
},
- x11_icon_stop[] =
+ app_icon_stop[] =
{
- {0, 0}, {20, 0}, {20, 20}, {0, 20}, X11_STOP, X11_STOP,
+ {0, 0}, {20, 0}, {20, 20}, {0, 20}, APP_ICON_STOP, APP_ICON_STOP,
},
- x11_icon_next[] =
+ app_icon_next[] =
{
- {0, 0}, {10, 10}, {0, 20}, X11_STOP,
- {10, 0}, {20, 10}, {10, 20}, X11_STOP, X11_STOP,
+ {0, 0}, {10, 10}, {0, 20}, APP_ICON_STOP,
+ {10, 0}, {20, 10}, {10, 20}, APP_ICON_STOP, APP_ICON_STOP,
},
- x11_icon_repeat[] =
+ app_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,
+ {13, 9}, {13, 6}, {3, 6}, {3, 10}, APP_ICON_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,
+ {20, 14}, {17, 17}, {7, 17}, {7, 20}, APP_ICON_STOP, APP_ICON_STOP,
},
- x11_icon_random[] =
+ app_icon_random[] =
{
- {0, 6}, {0, 3}, {5, 3}, {6, 4.5}, {4, 7.5}, {3, 6}, X11_STOP,
+ {0, 6}, {0, 3}, {5, 3}, {6, 4.5}, {4, 7.5}, {3, 6}, APP_ICON_STOP,
{9, 15.5}, {11, 12.5}, {12, 14}, {13, 14}, {13, 11}, {20, 15.5},
- {13, 20}, {13, 17}, {10, 17}, X11_STOP,
+ {13, 20}, {13, 17}, {10, 17}, APP_ICON_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,
+ {13, 9}, {13, 6}, {12, 6}, {5, 17}, APP_ICON_STOP, APP_ICON_STOP,
},
- x11_icon_single[] =
+ app_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,
+ {7, 18}, {7, 15}, {9, 15}, {9, 6}, APP_ICON_STOP, APP_ICON_STOP,
},
- x11_icon_consume[] =
+ app_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,
+ {10, 17}, {4, 17}, APP_ICON_STOP,
+ {16, 12}, {16, 8}, {20, 8}, {20, 12}, APP_ICON_STOP, APP_ICON_STOP,
};
-static const XPointDouble *
-x11_icon_for_action (enum action action)
+static const struct app_icon_point *
+app_icon_for_action (enum action action)
{
switch (action)
{
case ACTION_MPD_PREVIOUS:
- return x11_icon_previous;
+ return app_icon_previous;
case ACTION_MPD_TOGGLE:
- return g.state == PLAYER_PLAYING ? x11_icon_pause : x11_icon_play;
+ return g.state == PLAYER_PLAYING ? app_icon_pause : app_icon_play;
case ACTION_MPD_STOP:
- return x11_icon_stop;
+ return app_icon_stop;
case ACTION_MPD_NEXT:
- return x11_icon_next;
+ return app_icon_next;
case ACTION_MPD_REPEAT:
- return x11_icon_repeat;
+ return app_icon_repeat;
case ACTION_MPD_RANDOM:
- return x11_icon_random;
+ return app_icon_random;
case ACTION_MPD_SINGLE:
- return x11_icon_single;
+ return app_icon_single;
case ACTION_MPD_CONSUME:
- return x11_icon_consume;
+ return app_icon_consume;
default:
return NULL;
}
}
+#endif // WITH_X11 || WITH_APPKIT
+
+// --- AppKit ------------------------------------------------------------------
+
+#ifdef WITH_APPKIT
+
+static void
+appkit_render_button (struct widget *self)
+{
+ appkit_render_padding (self);
+
+ const struct app_icon_point *icon = app_icon_for_action (self->userdata);
+ if (!icon)
+ {
+ appkit_render_label (self);
+ return;
+ }
+
+ NSColor *color = appkit_fg (self);
+ if (!(self->attrs & A_BOLD))
+ {
+ CGFloat r = 0, g_ = 0, b = 0, a = 1;
+ NSColor *converted =
+ [color colorUsingColorSpace:[NSColorSpace deviceRGBColorSpace]];
+ if (converted)
+ {
+ [converted getRed:&r green:&g_ blue:&b alpha:&a];
+ color = [NSColor colorWithDeviceRed:r * 0.5
+ green:g_ * 0.5 blue:b * 0.5 alpha:a * 0.5];
+ }
+ }
+
+ [color setFill];
+ NSBezierPath *path = [NSBezierPath bezierPath];
+ [path setWindingRule:NSWindingRuleEvenOdd];
+
+ int x = self->x, y = self->y + (self->height - self->width) / 2;
+ bool started = false;
+ for (size_t i = 0; ; i++)
+ {
+ const struct app_icon_point *p = &icon[i];
+ if (isinf (p->x))
+ {
+ if (!started)
+ break;
+ [path closePath];
+ started = false;
+ continue;
+ }
+
+ NSPoint point = NSMakePoint
+ (x + p->x / 20.0 * self->width, y + p->y / 20.0 * self->width);
+ if (!started)
+ [path moveToPoint:point];
+ else
+ [path lineToPoint:point];
+ started = true;
+ }
+ [path fill];
+}
+
+static struct widget *
+appkit_make_button (chtype attrs, const char *label, enum action a)
+{
+ struct widget *w = appkit_make_label (attrs, 0, label);
+ w->widget_id = WIDGET_BUTTON;
+ w->userdata = a;
+
+ if (app_icon_for_action (a))
+ {
+ w->on_render = appkit_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
+appkit_render_gauge (struct widget *self)
+{
+ appkit_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;
+ [appkit_bg_attrs (APP_ATTR (ELAPSED)) setFill];
+ NSRectFill (NSMakeRect
+ (self->x, self->y + self->height / 8, part, self->height * 3 / 4));
+ [appkit_bg_attrs (APP_ATTR (REMAINS)) setFill];
+ NSRectFill (NSMakeRect (self->x + part, self->y + self->height / 8,
+ self->width - part, self->height * 3 / 4));
+}
+
+static struct widget *
+appkit_make_gauge (chtype attrs)
+{
+ struct widget *w = xcalloc (1, sizeof *w + 1);
+ w->on_render = appkit_render_gauge;
+ w->attrs = attrs;
+ w->width = -1;
+ w->height = g_xui.vunit;
+ return w;
+}
+
+static void
+appkit_render_spectrum (struct widget *self)
+{
+ appkit_render_padding (self);
+
+#ifdef WITH_FFTW
+ int bars = g.spectrum.bars;
+ if (bars < 1)
+ return;
+
+ int step = MAX (1, self->width / bars);
+ [appkit_fg (self) setFill];
+ for (int i = 0; i < bars; i++)
+ {
+ int height = round ((self->height - 2) * g.spectrum.spectrum[i]);
+ NSRectFill (NSMakeRect (self->x + i * step,
+ self->y + self->height - 1 - height,
+ step, height));
+ }
+#endif // WITH_FFTW
+}
+
+static struct widget *
+appkit_make_spectrum (chtype attrs, int width)
+{
+ struct widget *w = xcalloc (1, sizeof *w + 1);
+ w->on_render = appkit_render_spectrum;
+ w->attrs = attrs;
+ w->width = width * g_xui.vunit / 2;
+ w->height = g_xui.vunit;
+ return w;
+}
+
+static void
+appkit_render_scrollbar (struct widget *self)
+{
+ appkit_render_padding (self);
+
+ struct tab *tab = g.active_tab;
+ struct scrollbar bar =
+ app_compute_scrollbar (tab, app_visible_items_height (), g_xui.vunit);
+
+ [appkit_fg_attrs (self->attrs) setFill];
+ NSRectFill (NSMakeRect
+ (self->x, self->y + bar.start, self->width, bar.length));
+}
+
+static struct widget *
+appkit_make_scrollbar (chtype attrs)
+{
+ struct widget *w = xcalloc (1, sizeof *w + 1);
+ w->on_render = appkit_render_scrollbar;
+ w->attrs = attrs;
+ w->width = g_xui.vunit / 2;
+ return w;
+}
+
+static struct widget *
+appkit_make_list (void)
+{
+ struct widget *w = xcalloc (1, sizeof *w + 1);
+ w->on_render = appkit_render_padding;
+ return w;
+}
+
+static void
+appkit_render_editor (struct widget *self)
+{
+ appkit_render_padding (self);
+
+ NSFont *font = appkit_widget_font (self);
+ NSColor *color = appkit_fg (self);
+
+ const struct line_editor *e = &g.editor;
+ int x = self->x;
+ if (e->prompt)
+ {
+ hard_assert (e->prompt < 127);
+ x += appkit_font_draw (font, color, x, self->y,
+ (char[2]) { e->prompt, 0 }, self->width) + g_xui.vunit / 4;
+ }
+
+ 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 += appkit_font_draw (font, color, x, self->y, a,
+ MAX (0, self->width - (x - self->x)));
+ int caret = x;
+ x += appkit_font_draw (font, color, x, self->y, b,
+ MAX (0, self->width - (x - self->x)));
+ free (a);
+ free (b);
+
+ [color setFill];
+ NSRectFill (NSMakeRect (caret, self->y, 2, self->height));
+}
+
+static struct widget *
+appkit_make_editor (chtype attrs)
+{
+ struct widget *w = xcalloc (1, sizeof *w + 1);
+ w->on_render = appkit_render_editor;
+ w->attrs = attrs;
+ w->width = -1;
+ w->height = g_xui.vunit;
+ return w;
+}
+
+static struct app_ui app_appkit_ui =
+{
+ .padding = appkit_make_padding,
+ .label = app_make_label,
+ .button = appkit_make_button,
+ .gauge = appkit_make_gauge,
+ .spectrum = appkit_make_spectrum,
+ .scrollbar = appkit_make_scrollbar,
+ .list = appkit_make_list,
+ .editor = appkit_make_editor,
+
+ .have_icons = true,
+};
+
+#endif // WITH_APPKIT
+
+// --- X11 ---------------------------------------------------------------------
+
+#ifdef WITH_X11
+
static void
x11_render_button (struct widget *self)
{
x11_render_padding (self);
- const XPointDouble *icon = x11_icon_for_action (self->userdata);
+ const struct app_icon_point *icon = app_icon_for_action (self->userdata);
if (!icon)
{
x11_render_label (self);
@@ -5687,8 +6122,15 @@ x11_render_button (struct widget *self)
}
size_t total = 0;
- for (size_t i = 0; icon[i].x != INFINITY || icon[i - 1].x != INFINITY; i++)
- total++;
+ for (; ; total++)
+ {
+ if (isinf (icon[total].x) && isinf (icon[total].y)
+ && isinf (icon[total + 1].x) && isinf (icon[total + 1].y))
+ {
+ total++;
+ break;
+ }
+ }
// TODO: There should be an attribute for buttons, to handle this better.
XRenderColor color = *x11_fg (self);
@@ -5727,10 +6169,10 @@ 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->widget_id = WIDGET_BUTTON;
w->userdata = a;
- if (x11_icon_for_action (a))
+ if (app_icon_for_action (a))
{
w->on_render = x11_render_button;
@@ -5855,28 +6297,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 *
@@ -5917,10 +6367,10 @@ static volatile sig_atomic_t g_termination_requested;
static volatile sig_atomic_t g_winch_received;
static void
-signals_postpone_handling (char id)
+signals_postpone_handling (char signal_id)
{
int original_errno = errno;
- if (write (g_signal_pipe[1], &id, 1) == -1)
+ if (write (g_signal_pipe[1], &signal_id, 1) == -1)
soft_assert (errno == EAGAIN);
errno = original_errno;
}
@@ -5981,8 +6431,8 @@ app_on_signal_pipe_readable (const struct pollfd *fd, void *user_data)
{
(void) user_data;
- char id = 0;
- (void) read (fd->fd, &id, 1);
+ char signal_id = 0;
+ (void) read (fd->fd, &signal_id, 1);
if (g_termination_requested && !g.quitting)
app_quit ();
@@ -6082,6 +6532,11 @@ app_init_ui (bool requested_x11)
g.ui = &app_x11_ui;
else
#endif // WITH_X11
+#ifdef WITH_APPKIT
+ if (g_xui.ui == &appkit_ui)
+ g.ui = &app_appkit_ui;
+ else
+#endif // WITH_APPKIT
g.ui = &app_tui_ui;
}
@@ -6113,9 +6568,9 @@ 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
+#if defined WITH_X11 || defined WITH_APPKIT
+ { 'x', "x11", NULL, 0, "use a graphical frontend even from a terminal" },
+#endif // WITH_X11 || WITH_APPKIT
{ '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" },
@@ -6133,9 +6588,11 @@ main (int argc, char *argv[])
case 'd':
g_debug_mode = true;
break;
+#if defined WITH_X11 || defined WITH_APPKIT
case 'x':
requested_x11 = true;
break;
+#endif // WITH_X11 || WITH_APPKIT
case 'v':
g_verbose_mode = true;
break;