9 Commits

Author SHA1 Message Date
15e583beb2 Bump version to 0.9 2018-11-02 21:50:36 +01:00
cdb86652b9 Fix unmarking behaviour, cleanup 2018-10-29 15:04:22 +01:00
cbdec0552d Allow moving multiple items in the Current tab 2018-10-29 14:45:25 +01:00
2cd100af7a Remove an outdated comment 2018-10-29 13:44:43 +01:00
44ebc3591e Make holding Shift+Up/Down behave better 2018-10-29 13:42:39 +01:00
0691c533b4 Update selection on playlist changes 2018-10-29 13:22:56 +01:00
6298235e22 Add actions for repeat/random/single/consume
Now the user can at least toggle them from the help tab,
or even bind them as necessary.
2018-10-29 09:58:43 +01:00
841e2f79c0 Make help tab items actionable 2018-10-29 09:46:45 +01:00
5ade0f082e Show unbound actions in help 2018-10-29 09:19:34 +01:00
3 changed files with 233 additions and 59 deletions

View File

@@ -9,7 +9,7 @@ endif ("${CMAKE_C_COMPILER_ID}" MATCHES "GNU" OR CMAKE_COMPILER_IS_GNUCC)
# Version # Version
set (project_VERSION_MAJOR "0") set (project_VERSION_MAJOR "0")
set (project_VERSION_MINOR "1") set (project_VERSION_MINOR "9")
set (project_VERSION_PATCH "0") set (project_VERSION_PATCH "0")
set (project_VERSION "${project_VERSION_MAJOR}") set (project_VERSION "${project_VERSION_MAJOR}")

4
NEWS Normal file
View File

@@ -0,0 +1,4 @@
0.9.0 (2018-11-02)
* Initial release

286
nncmpp.c
View File

@@ -1366,7 +1366,6 @@ app_write_mpd_status (struct row_buffer *buf)
bool single = (s = str_map_find (map, "single")) && 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"); bool consume = (s = str_map_find (map, "consume")) && strcmp (s, "0");
// TODO: remove the conditionals once we make them clickable
struct row_buffer right = row_buffer_make (); struct row_buffer right = row_buffer_make ();
chtype a[2] = { APP_ATTR (NORMAL), APP_ATTR (HIGHLIGHT) }; chtype a[2] = { APP_ATTR (NORMAL), APP_ATTR (HIGHLIGHT) };
if (repeat) row_buffer_append_args (&right, if (repeat) row_buffer_append_args (&right,
@@ -1548,6 +1547,10 @@ app_goto_tab (int tab_index)
XX( MPD_SEARCH, "Global search" ) \ XX( MPD_SEARCH, "Global search" ) \
XX( MPD_ADD, "Add selection to playlist" ) \ XX( MPD_ADD, "Add selection to playlist" ) \
XX( MPD_REPLACE, "Replace 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_UPDATE_DB, "Update MPD database" ) \
XX( MPD_COMMAND, "Send raw command to MPD" ) \ XX( MPD_COMMAND, "Send raw command to MPD" ) \
\ \
@@ -1698,6 +1701,14 @@ app_on_editor_end (bool confirmed)
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static bool
app_mpd_toggle (const char *name)
{
const char *s = str_map_find (&g.playback_info, name);
bool value = s && strcmp (s, "0");
return MPD_SIMPLE (name, value ? "0" : "1");
}
static bool static bool
app_process_action (enum action action) app_process_action (enum action action)
{ {
@@ -1705,7 +1716,6 @@ app_process_action (enum action action)
struct tab *tab = g.active_tab; struct tab *tab = g.active_tab;
if (tab->on_action && tab->on_action (action)) if (tab->on_action && tab->on_action (action))
{ {
tab->item_mark = -1;
app_invalidate (); app_invalidate ();
return true; return true;
} }
@@ -1767,6 +1777,10 @@ app_process_action (enum action action)
case ACTION_MPD_NEXT: return MPD_SIMPLE ("next"); case ACTION_MPD_NEXT: return MPD_SIMPLE ("next");
case ACTION_MPD_FORWARD: return MPD_SIMPLE ("seekcur", "+10"); case ACTION_MPD_FORWARD: return MPD_SIMPLE ("seekcur", "+10");
case ACTION_MPD_BACKWARD: return MPD_SIMPLE ("seekcur", "-10"); case ACTION_MPD_BACKWARD: return MPD_SIMPLE ("seekcur", "-10");
case ACTION_MPD_REPEAT: return app_mpd_toggle ("repeat");
case ACTION_MPD_RANDOM: return app_mpd_toggle ("random");
case ACTION_MPD_SINGLE: return app_mpd_toggle ("single");
case ACTION_MPD_CONSUME: return app_mpd_toggle ("consume");
case ACTION_MPD_UPDATE_DB: return MPD_SIMPLE ("update"); case ACTION_MPD_UPDATE_DB: return MPD_SIMPLE ("update");
case ACTION_MPD_VOLUME_UP: return app_setvol (g.volume + 10); case ACTION_MPD_VOLUME_UP: return app_setvol (g.volume + 10);
@@ -2193,52 +2207,82 @@ current_tab_on_item_draw (size_t item_index, struct row_buffer *buffer,
free (s); free (s);
} }
static bool static void
current_tab_move_song (const char *id, int diff) mpd_on_move_response (const struct mpd_response *response,
const struct strv *data, void *user_data)
{ {
(void) data;
*(bool *) user_data = false;
if (!response->success)
print_error ("%s: %s", "command failed", response->message_text);
}
static void
current_tab_move (int from, int to)
{
compact_map_t map;
const char *id;
if (!(map = item_list_get (&g.playlist, from))
|| !(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);
free (target_str);
}
static bool
current_tab_move_selection (int diff)
{
static bool already_moving;
if (already_moving || diff == 0)
return true;
struct mpd_client *c = &g.client; struct mpd_client *c = &g.client;
int target = g_current_tab.item_selected + diff; if (c->state != MPD_CONNECTED)
if (c->state != MPD_CONNECTED || target < 0)
return false; return false;
char *target_str = xstrdup_printf ("%d", target); struct tab *tab = &g_current_tab;
mpd_client_send_command (c, "moveid", id, target_str, NULL); struct tab_range range = tab_selection_range (tab);
free (target_str); if (range.from + diff < 0
// TODO: we should create a cancellable action waiting for the move to || range.upto + diff >= (int) tab->item_count)
// finish, so that holding Shift-arrows works as expected. return false;
mpd_client_add_task (c, mpd_on_simple_response, NULL);
mpd_client_idle (c, 0);
// XXX: this behaves a bit erratically, as even if we waited for mpd_client_list_begin (c);
// a confirmation from the daemon, it would precede the playlist update if (diff < 0)
g_current_tab.item_selected = target; for (int i = range.from; i <= range.upto; i++)
app_move_selection (0); current_tab_move (i, i + diff);
return true; else
for (int i = range.upto; i >= range.from; i--)
current_tab_move (i, i + diff);
mpd_client_list_end (c);
mpd_client_add_task (c, mpd_on_move_response, &already_moving);
mpd_client_idle (c, 0);
return already_moving = true;
} }
static bool static bool
current_tab_on_action (enum action action) current_tab_on_action (enum action action)
{ {
struct tab *self = g.active_tab; struct tab *tab = &g_current_tab;
struct tab_range range = tab_selection_range (self); compact_map_t map = item_list_get (&g.playlist, tab->item_selected);
compact_map_t map = item_list_get (&g.playlist, self->item_selected);
const char *id;
if (!map || !(id = compact_map_find (map, "id")))
return false;
switch (action) switch (action)
{ {
// TODO: make this block more than just multiselect-tolerant const char *id;
case ACTION_MOVE_UP: return current_tab_move_song (id, -1); case ACTION_MOVE_UP:
case ACTION_MOVE_DOWN: return current_tab_move_song (id, 1); return current_tab_move_selection (-1);
case ACTION_MOVE_DOWN:
return current_tab_move_selection (+1);
case ACTION_CHOOSE: case ACTION_CHOOSE:
return MPD_SIMPLE ("playid", id); tab->item_mark = -1;
return map && (id = compact_map_find (map, "id"))
&& MPD_SIMPLE ("playid", id);
case ACTION_DELETE: case ACTION_DELETE:
{ {
struct mpd_client *c = &g.client; struct mpd_client *c = &g.client;
struct tab_range range = tab_selection_range (tab);
if (range.from < 0 || c->state != MPD_CONNECTED) if (range.from < 0 || c->state != MPD_CONNECTED)
return false; return false;
@@ -2255,15 +2299,16 @@ current_tab_on_action (enum action action)
return true; return true;
} }
default: default:
break;
}
return false; return false;
} }
}
static void static void
current_tab_update (void) current_tab_update (void)
{ {
g_current_tab.item_count = g.playlist.len; 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 (); app_invalidate ();
} }
@@ -2450,6 +2495,7 @@ library_tab_change_level (const char *new_path)
free (g_library_tab.super.header); free (g_library_tab.super.header);
g_library_tab.super.header = NULL; g_library_tab.super.header = NULL;
g_library_tab.super.item_mark = -1;
if (path->len) if (path->len)
g_library_tab.super.header = xstrdup_printf ("/%s", path->str); g_library_tab.super.header = xstrdup_printf ("/%s", path->str);
@@ -2593,8 +2639,12 @@ static bool
library_tab_on_action (enum action action) library_tab_on_action (enum action action)
{ {
struct mpd_client *c = &g.client; struct mpd_client *c = &g.client;
struct tab_range range = tab_selection_range (&g_library_tab.super); if (c->state != MPD_CONNECTED)
if (range.from < 0 || c->state != MPD_CONNECTED) return false;
struct tab *tab = &g_library_tab.super;
struct tab_range range = tab_selection_range (tab);
if (range.from < 0)
return false; return false;
struct library_tab_item x = struct library_tab_item x =
@@ -2615,6 +2665,7 @@ library_tab_on_action (enum action action)
case LIBRARY_FILE: MPD_SIMPLE ("add", x.path); break; case LIBRARY_FILE: MPD_SIMPLE ("add", x.path); break;
default: hard_assert (!"invalid item type"); default: hard_assert (!"invalid item type");
} }
tab->item_mark = -1;
return true; return true;
case ACTION_UP: case ACTION_UP:
{ {
@@ -2641,8 +2692,8 @@ library_tab_on_action (enum action action)
free (fake_subdir); free (fake_subdir);
} }
free (g_library_tab.super.header); free (tab->header);
g_library_tab.super.header = xstrdup_printf ("Global search"); tab->header = xstrdup_printf ("Global search");
g_library_tab.searching = true; g_library_tab.searching = true;
// Since we've already changed the header, empty the list, // Since we've already changed the header, empty the list,
@@ -2666,6 +2717,7 @@ library_tab_on_action (enum action action)
if (x.type == LIBRARY_DIR || x.type == LIBRARY_FILE) if (x.type == LIBRARY_DIR || x.type == LIBRARY_FILE)
MPD_SIMPLE ("add", x.path); MPD_SIMPLE ("add", x.path);
} }
tab->item_mark = -1;
return true; return true;
case ACTION_MPD_REPLACE: case ACTION_MPD_REPLACE:
if (!library_tab_is_range_playable (range)) if (!library_tab_is_range_playable (range))
@@ -2690,6 +2742,7 @@ library_tab_on_action (enum action action)
mpd_client_list_end (c); mpd_client_list_end (c);
mpd_client_add_task (c, mpd_on_simple_response, NULL); mpd_client_add_task (c, mpd_on_simple_response, NULL);
mpd_client_idle (c, 0); mpd_client_idle (c, 0);
tab->item_mark = -1;
return true; return true;
default: default:
break; break;
@@ -2932,12 +2985,12 @@ error:
static bool static bool
streams_tab_on_action (enum action action) streams_tab_on_action (enum action action)
{ {
struct tab *self = g.active_tab; struct tab *tab = g.active_tab;
if (self->item_selected < 0 || !self->item_count) if (tab->item_selected < 0 || !tab->item_count)
return false; return false;
// For simplicity the URL is the string following the stream name // For simplicity the URL is the string following the stream name
const char *uri = 1 + strchr (g.streams.vector[self->item_selected], 0); const char *uri = 1 + strchr (g.streams.vector[tab->item_selected], 0);
struct error *e = NULL; struct error *e = NULL;
switch (action) switch (action)
@@ -3053,7 +3106,28 @@ info_tab_init (void)
// --- Help tab ---------------------------------------------------------------- // --- Help tab ----------------------------------------------------------------
static struct strv g_help_tab_lines; static struct
{
struct tab super; ///< Parent class
ARRAY (enum action, actions) ///< Actions for content
struct strv lines; ///< Visible content
}
g_help_tab;
static bool
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)
return false;
action = g_help_tab.actions[tab->item_selected];
return action != ACTION_NONE
&& action != ACTION_CHOOSE // avoid recursion
&& app_process_action (action);
}
static void static void
help_tab_strfkey (const termo_key_t *key, struct strv *out) help_tab_strfkey (const termo_key_t *key, struct strv *out)
@@ -3071,7 +3145,20 @@ help_tab_strfkey (const termo_key_t *key, struct strv *out)
} }
static void static void
help_tab_group (struct binding *keys, size_t len, struct strv *out) help_tab_assign_action (enum action action)
{
hard_assert (g_help_tab.lines.len > g_help_tab.actions_len);
size_t to_push = g_help_tab.lines.len - g_help_tab.actions_len;
ARRAY_RESERVE (g_help_tab.actions, to_push);
for (size_t i = 1; i < to_push; i++)
g_help_tab.actions[g_help_tab.actions_len++] = ACTION_NONE;
g_help_tab.actions[g_help_tab.actions_len++] = action;
}
static void
help_tab_group (struct binding *keys, size_t len, struct strv *out,
bool bound[ACTION_COUNT])
{ {
for (enum action i = 0; i < ACTION_COUNT; i++) for (enum action i = 0; i < ACTION_COUNT; i++)
{ {
@@ -3085,39 +3172,71 @@ help_tab_group (struct binding *keys, size_t len, struct strv *out)
strv_append_owned (out, xstrdup_printf strv_append_owned (out, xstrdup_printf
(" %-30s %s", g_actions[i].description, joined)); (" %-30s %s", g_actions[i].description, joined));
free (joined); free (joined);
bound[i] = true;
help_tab_assign_action (i);
} }
strv_free (&ass); strv_free (&ass);
} }
} }
static void
help_tab_unbound (struct strv *out, bool bound[ACTION_COUNT])
{
for (enum action i = 0; i < ACTION_COUNT; i++)
if (!bound[i])
{
strv_append_owned (out,
xstrdup_printf (" %-30s", g_actions[i].description));
help_tab_assign_action (i);
}
}
static void static void
help_tab_on_item_draw (size_t item_index, struct row_buffer *buffer, int width) help_tab_on_item_draw (size_t item_index, struct row_buffer *buffer, int width)
{ {
(void) width; (void) width;
hard_assert (item_index < g_help_tab_lines.len); hard_assert (item_index < g_help_tab.lines.len);
const char *line = g_help_tab_lines.vector[item_index]; const char *line = g_help_tab.lines.vector[item_index];
row_buffer_append (buffer, line, *line == ' ' ? 0 : A_BOLD); row_buffer_append (buffer, line, *line == ' ' ? 0 : A_BOLD);
} }
static struct tab * static struct tab *
help_tab_init (void) help_tab_init (void)
{ {
g_help_tab_lines = strv_make (); ARRAY_INIT (g_help_tab.actions);
struct strv *lines = &g_help_tab.lines;
*lines = strv_make ();
strv_append (&g_help_tab_lines, "Normal mode actions"); bool bound[ACTION_COUNT] = { [ACTION_NONE] = true };
help_tab_group (g_normal_keys, g_normal_keys_len, &g_help_tab_lines);
strv_append (&g_help_tab_lines, "");
strv_append (&g_help_tab_lines, "Editor mode actions"); strv_append (lines, "Normal mode actions");
help_tab_group (g_editor_keys, g_editor_keys_len, &g_help_tab_lines); help_tab_group (g_normal_keys, g_normal_keys_len, lines, bound);
strv_append (&g_help_tab_lines, ""); strv_append (lines, "");
static struct tab super; strv_append (lines, "Editor mode actions");
tab_init (&super, "Help"); help_tab_group (g_editor_keys, g_editor_keys_len, lines, bound);
super.on_item_draw = help_tab_on_item_draw; strv_append (lines, "");
super.item_count = g_help_tab_lines.len;
return &super; bool have_unbound = false;
for (enum action i = 0; i < ACTION_COUNT; i++)
if (!bound[i])
have_unbound = true;
if (have_unbound)
{
strv_append (lines, "Unbound actions");
help_tab_unbound (lines, bound);
strv_append (lines, "");
}
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->item_count = lines->len;
return super;
} }
// --- Debug tab --------------------------------------------------------------- // --- Debug tab ---------------------------------------------------------------
@@ -3282,7 +3401,7 @@ mpd_update_playback_state (void)
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void static void
mpd_process_info (const struct strv *data) mpd_process_info_data (const struct strv *data)
{ {
struct str_map *map = &g.playback_info; struct str_map *map = &g.playback_info;
@@ -3316,6 +3435,57 @@ mpd_process_info (const struct strv *data)
str_map_free (&item); str_map_free (&item);
} }
/// Find a song by its id in the current playlist. Expensive, rarely called.
static ssize_t
mpd_find_pos_of_id (const char *desired_id)
{
compact_map_t map;
const char *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))
return i;
}
return -1;
}
static char *
mpd_id_of_pos (int pos)
{
compact_map_t map = item_list_get (&g.playlist, pos);
return map ? compact_map_find (map, "id") : NULL;
}
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);
mpd_process_info_data (data);
const char *sel_id = mpd_id_of_pos (*selected);
const char *mark_id = mpd_id_of_pos (*marked);
if (prev_mark_id && (!mark_id || strcmp (prev_mark_id, mark_id)))
*marked = 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;
app_move_selection (0);
}
free (prev_sel_id);
free (prev_mark_id);
}
static void static void
mpd_on_info_response (const struct mpd_response *response, mpd_on_info_response (const struct mpd_response *response,
const struct strv *data, void *user_data) const struct strv *data, void *user_data)