Count elapsed seconds
This commit is contained in:
parent
767e87bd3f
commit
6bfa754f4d
211
nncmpp.c
211
nncmpp.c
|
@ -70,7 +70,7 @@
|
||||||
|
|
||||||
#define CTRL_KEY(x) ((x) - 'A' + 1)
|
#define CTRL_KEY(x) ((x) - 'A' + 1)
|
||||||
|
|
||||||
#define APP_TITLE PROGRAM_NAME " " ///< Left top corner
|
#define APP_TITLE PROGRAM_NAME ///< Left top corner
|
||||||
|
|
||||||
// --- Utilities ---------------------------------------------------------------
|
// --- Utilities ---------------------------------------------------------------
|
||||||
|
|
||||||
|
@ -101,17 +101,19 @@ update_curses_terminal_size (void)
|
||||||
// global namespace and makes it harder to distinguish what functions relate to.
|
// global namespace and makes it harder to distinguish what functions relate to.
|
||||||
|
|
||||||
// Avoiding colours in the defaults here in order to support dumb terminals
|
// Avoiding colours in the defaults here in order to support dumb terminals
|
||||||
// TODO: we also want a different "highlighted" attribute for the top part
|
|
||||||
// -> then we have to do attrset(0)
|
|
||||||
// TODO: add two attributes for the gauge
|
|
||||||
// -> we should also make it possible to set characters for both parts
|
|
||||||
// TODO: create another attribute for selected items
|
|
||||||
#define ATTRIBUTE_TABLE(XX) \
|
#define ATTRIBUTE_TABLE(XX) \
|
||||||
XX( TOP, "top", -1, -1, 0 ) \
|
XX( HEADER, "header", -1, -1, 0 ) \
|
||||||
XX( HEADER, "header", -1, -1, A_REVERSE ) \
|
XX( HIGHLIGHT, "highlight", -1, -1, A_BOLD ) \
|
||||||
XX( ACTIVE, "header_active", -1, -1, A_UNDERLINE ) \
|
\
|
||||||
|
XX( ELAPSED, "elapsed", -1, -1, A_REVERSE ) \
|
||||||
|
XX( REMAINS, "remains", -1, -1, A_UNDERLINE ) \
|
||||||
|
\
|
||||||
|
XX( TAB_BAR, "tab_bar", -1, -1, A_REVERSE ) \
|
||||||
|
XX( TAB_ACTIVE, "tab_active", -1, -1, A_UNDERLINE ) \
|
||||||
|
\
|
||||||
XX( EVEN, "even", -1, -1, 0 ) \
|
XX( EVEN, "even", -1, -1, 0 ) \
|
||||||
XX( ODD, "odd", -1, -1, 0 )
|
XX( ODD, "odd", -1, -1, 0 ) \
|
||||||
|
XX( SELECTION, "selection", -1, -1, A_REVERSE )
|
||||||
|
|
||||||
enum
|
enum
|
||||||
{
|
{
|
||||||
|
@ -202,11 +204,11 @@ static struct app_context
|
||||||
enum player_state state; ///< Player state
|
enum player_state state; ///< Player state
|
||||||
struct str_map song_info; ///< Current song info
|
struct str_map song_info; ///< Current song info
|
||||||
|
|
||||||
// FIXME: this is doomed to drift unless we use POSIX CLOCK_MONOTONIC
|
|
||||||
struct poller_timer elapsed_event; ///< Seconds elapsed event
|
struct poller_timer elapsed_event; ///< Seconds elapsed event
|
||||||
// TODO: initialize these to -1
|
// TODO: initialize these to -1
|
||||||
int song_elapsed; ///< Song elapsed in seconds
|
int song_elapsed; ///< Song elapsed in seconds
|
||||||
int song_duration; ///< Song duration in seconds
|
int song_duration; ///< Song duration in seconds
|
||||||
|
int volume; ///< Current volume
|
||||||
|
|
||||||
// Data:
|
// Data:
|
||||||
|
|
||||||
|
@ -690,9 +692,35 @@ app_next_row (chtype attrs)
|
||||||
mvwhline (stdscr, g_ctx.list_offset++, 0, ' ' | attrs, COLS);
|
mvwhline (stdscr, g_ctx.list_offset++, 0, ' ' | attrs, COLS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static size_t
|
||||||
|
app_write_time (int seconds, chtype attrs)
|
||||||
|
{
|
||||||
|
int minutes = seconds / 60; seconds %= 60;
|
||||||
|
int hours = minutes / 60; hours %= 60;
|
||||||
|
|
||||||
|
struct str s;
|
||||||
|
str_init (&s);
|
||||||
|
|
||||||
|
if (hours)
|
||||||
|
{
|
||||||
|
str_append_printf (&s, "%d:", hours);
|
||||||
|
str_append_printf (&s, "%02d:", minutes);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
str_append_printf (&s, "%d:", minutes);
|
||||||
|
|
||||||
|
str_append_printf (&s, "%02d", seconds);
|
||||||
|
size_t result = app_write_utf8 (s.str, attrs, -1);
|
||||||
|
str_free (&s);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
app_redraw_status (void)
|
app_redraw_status (void)
|
||||||
{
|
{
|
||||||
|
chtype normal = APP_ATTR (HEADER);
|
||||||
|
chtype highlight = APP_ATTR (HIGHLIGHT);
|
||||||
|
|
||||||
if (g_ctx.state == PLAYER_STOPPED)
|
if (g_ctx.state == PLAYER_STOPPED)
|
||||||
goto line;
|
goto line;
|
||||||
|
|
||||||
|
@ -706,15 +734,22 @@ app_redraw_status (void)
|
||||||
|| (title = str_map_find (map, "name"))
|
|| (title = str_map_find (map, "name"))
|
||||||
|| (title = str_map_find (map, "file")))
|
|| (title = str_map_find (map, "file")))
|
||||||
{
|
{
|
||||||
app_next_row (0);
|
app_next_row (normal);
|
||||||
app_write_utf8 (title, A_BOLD, COLS);
|
|
||||||
|
struct row_buffer buf;
|
||||||
|
row_buffer_init (&buf);
|
||||||
|
row_buffer_append (&buf, title, highlight);
|
||||||
|
if (buf.total_width > COLS)
|
||||||
|
row_buffer_ellipsis (&buf, COLS, highlight);
|
||||||
|
row_buffer_flush (&buf);
|
||||||
|
row_buffer_free (&buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
char *artist = str_map_find (map, "artist");
|
char *artist = str_map_find (map, "artist");
|
||||||
char *album = str_map_find (map, "album");
|
char *album = str_map_find (map, "album");
|
||||||
if (artist || album)
|
if (artist || album)
|
||||||
{
|
{
|
||||||
app_next_row (0);
|
app_next_row (normal);
|
||||||
|
|
||||||
struct row_buffer buf;
|
struct row_buffer buf;
|
||||||
row_buffer_init (&buf);
|
row_buffer_init (&buf);
|
||||||
|
@ -722,45 +757,76 @@ app_redraw_status (void)
|
||||||
bool first = true;
|
bool first = true;
|
||||||
if (artist)
|
if (artist)
|
||||||
{
|
{
|
||||||
if (!first) row_buffer_append (&buf, " ", 0);
|
if (!first) row_buffer_append (&buf, " ", normal);
|
||||||
row_buffer_append (&buf, "by ", 0);
|
row_buffer_append (&buf, "by ", normal);
|
||||||
row_buffer_append (&buf, artist, A_BOLD);
|
row_buffer_append (&buf, artist, highlight);
|
||||||
first = false;
|
first = false;
|
||||||
}
|
}
|
||||||
if (album)
|
if (album)
|
||||||
{
|
{
|
||||||
if (!first) row_buffer_append (&buf, " ", 0);
|
if (!first) row_buffer_append (&buf, " ", normal);
|
||||||
row_buffer_append (&buf, "from ", 0);
|
row_buffer_append (&buf, "from ", normal);
|
||||||
row_buffer_append (&buf, album, A_BOLD);
|
row_buffer_append (&buf, album, highlight);
|
||||||
first = false;
|
first = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (buf.total_width > COLS)
|
if (buf.total_width > COLS)
|
||||||
row_buffer_ellipsis (&buf, COLS, 0);
|
row_buffer_ellipsis (&buf, COLS, normal);
|
||||||
row_buffer_flush (&buf);
|
row_buffer_flush (&buf);
|
||||||
row_buffer_free (&buf);
|
row_buffer_free (&buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
line:
|
line:
|
||||||
app_next_row (0);
|
app_next_row (normal);
|
||||||
|
|
||||||
bool stopped = g_ctx.state == PLAYER_STOPPED;
|
bool stopped = g_ctx.state == PLAYER_STOPPED;
|
||||||
app_write_utf8 ("<< ", stopped ? 0 : A_BOLD, -1);
|
chtype active = stopped ? normal : highlight;
|
||||||
|
|
||||||
|
// TODO: we desperately need some better abstractions for this;
|
||||||
|
// at minimum we need to count characters already written
|
||||||
|
app_write_utf8 ("<<", active, -1);
|
||||||
|
addch (' ' | normal);
|
||||||
|
|
||||||
if (g_ctx.state == PLAYER_PLAYING)
|
if (g_ctx.state == PLAYER_PLAYING)
|
||||||
app_write_utf8 ("|| ", A_BOLD, -1);
|
app_write_utf8 ("||", highlight, -1);
|
||||||
else
|
else
|
||||||
app_write_utf8 ("|> ", A_BOLD, -1);
|
app_write_utf8 ("|>", highlight, -1);
|
||||||
|
addch (' ' | normal);
|
||||||
|
|
||||||
app_write_utf8 ("[] ", stopped ? 0 : A_BOLD, -1);
|
app_write_utf8 ("[]", active, -1);
|
||||||
app_write_utf8 (">> ", stopped ? 0 : A_BOLD, -1);
|
addch (' ' | normal);
|
||||||
|
app_write_utf8 (">>", active, -1);
|
||||||
|
addch (' ' | normal);
|
||||||
|
addch (' ' | normal);
|
||||||
|
|
||||||
if (stopped)
|
if (stopped)
|
||||||
app_write_utf8 ("Stopped", 0, COLS);
|
app_write_utf8 ("Stopped", normal, COLS);
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// TODO: convert and display "song_elapsed / song_duration"
|
// TODO: convert the display to minutes
|
||||||
// TODO: display a gauge representing the same information
|
if (g_ctx.song_elapsed >= 0)
|
||||||
|
{
|
||||||
|
app_write_time (g_ctx.song_elapsed, normal);
|
||||||
|
addch (' ' | normal);
|
||||||
|
}
|
||||||
|
if (g_ctx.song_duration >= 0)
|
||||||
|
{
|
||||||
|
addch ('/' | normal);
|
||||||
|
addch (' ' | normal);
|
||||||
|
app_write_time (g_ctx.song_duration, normal);
|
||||||
|
addch (' ' | normal);
|
||||||
|
}
|
||||||
|
addch (' ' | normal);
|
||||||
|
|
||||||
|
if (g_ctx.song_elapsed >= 0 && g_ctx.song_duration >= 0)
|
||||||
|
{
|
||||||
|
// TODO: display a gauge representing the same information,
|
||||||
|
// attributes BAR_ELAPSED and BAR_REMAINS
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// TODO: just fill the space
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: append the volume value if available
|
// TODO: append the volume value if available
|
||||||
|
@ -772,32 +838,35 @@ app_redraw_top (void)
|
||||||
// TODO: when it changes from the previous value, fix the selection
|
// TODO: when it changes from the previous value, fix the selection
|
||||||
g_ctx.list_offset = 0;
|
g_ctx.list_offset = 0;
|
||||||
|
|
||||||
attrset (APP_ATTR (TOP));
|
attrset (0);
|
||||||
switch (g_ctx.client.state)
|
switch (g_ctx.client.state)
|
||||||
{
|
{
|
||||||
case MPD_CONNECTED:
|
case MPD_CONNECTED:
|
||||||
app_redraw_status ();
|
app_redraw_status ();
|
||||||
break;
|
break;
|
||||||
case MPD_CONNECTING:
|
case MPD_CONNECTING:
|
||||||
app_next_row (0);
|
app_next_row (APP_ATTR (HEADER));
|
||||||
app_write_utf8 ("Connecting to MPD...", 0, COLS);
|
app_write_utf8 ("Connecting to MPD...", APP_ATTR (HEADER), COLS);
|
||||||
break;
|
break;
|
||||||
case MPD_DISCONNECTED:
|
case MPD_DISCONNECTED:
|
||||||
app_next_row (0);
|
app_next_row (APP_ATTR (HEADER));
|
||||||
app_write_utf8 ("Disconnected", 0, COLS);
|
app_write_utf8 ("Disconnected", APP_ATTR (HEADER), COLS);
|
||||||
}
|
}
|
||||||
|
|
||||||
attrset (APP_ATTR (HEADER));
|
attrset (APP_ATTR (TAB_BAR));
|
||||||
app_next_row (0);
|
app_next_row (0);
|
||||||
// TODO: render this with APP_ATTR (ACTIVE) when the help tab is selected;
|
// TODO: render this with APP_ATTR (TAB_ACTIVE) when the help tab is selected;
|
||||||
// ...maybe the help tab should not even be on the list?
|
// ...maybe the help tab should not even be on the list?
|
||||||
size_t indent = app_write_utf8 (APP_TITLE, A_BOLD, -1);
|
size_t indent = app_write_utf8 (APP_TITLE, 0, -1);
|
||||||
|
|
||||||
|
addch (' ');
|
||||||
|
indent++;
|
||||||
|
|
||||||
attrset (0);
|
attrset (0);
|
||||||
LIST_FOR_EACH (struct tab, it, g_ctx.tabs)
|
LIST_FOR_EACH (struct tab, it, g_ctx.tabs)
|
||||||
{
|
{
|
||||||
indent += app_write_utf8 (it->name,
|
indent += app_write_utf8 (it->name,
|
||||||
it == g_ctx.active_tab ? APP_ATTR (ACTIVE) : APP_ATTR (HEADER),
|
it == g_ctx.active_tab ? APP_ATTR (TAB_ACTIVE) : APP_ATTR (TAB_BAR),
|
||||||
MIN (COLS - indent, it->name_width));
|
MIN (COLS - indent, it->name_width));
|
||||||
}
|
}
|
||||||
refresh ();
|
refresh ();
|
||||||
|
@ -815,10 +884,10 @@ app_redraw_view (void)
|
||||||
(int) tab->item_count - tab->item_top);
|
(int) tab->item_count - tab->item_top);
|
||||||
for (int row_index = 0; row_index < to_show; row_index++)
|
for (int row_index = 0; row_index < to_show; row_index++)
|
||||||
{
|
{
|
||||||
unsigned item_index = tab->item_top + row_index;
|
int item_index = tab->item_top + row_index;
|
||||||
int row_attrs = (item_index & 1) ? APP_ATTR (ODD) : APP_ATTR (EVEN);
|
int row_attrs = (item_index & 1) ? APP_ATTR (ODD) : APP_ATTR (EVEN);
|
||||||
if ((int) item_index == tab->item_selected)
|
if (item_index == tab->item_selected)
|
||||||
row_attrs |= A_REVERSE;
|
row_attrs = APP_ATTR (SELECTION);
|
||||||
|
|
||||||
attrset (row_attrs);
|
attrset (row_attrs);
|
||||||
|
|
||||||
|
@ -1246,11 +1315,18 @@ mpd_on_info_response (const struct mpd_response *response,
|
||||||
const struct str_vector *data, void *user_data)
|
const struct str_vector *data, void *user_data)
|
||||||
{
|
{
|
||||||
(void) user_data;
|
(void) user_data;
|
||||||
|
|
||||||
|
// TODO: do this also on disconnect
|
||||||
|
g_ctx.song_elapsed = -1;
|
||||||
|
g_ctx.song_duration = -1;
|
||||||
|
g_ctx.volume = -1;
|
||||||
|
str_map_free (&g_ctx.song_info);
|
||||||
|
poller_timer_reset (&g_ctx.elapsed_event);
|
||||||
|
|
||||||
if (!response->success)
|
if (!response->success)
|
||||||
{
|
{
|
||||||
print_debug ("%s: %s",
|
print_debug ("%s: %s",
|
||||||
"retrieving MPD info failed", response->message_text);
|
"retrieving MPD info failed", response->message_text);
|
||||||
// TODO: invalidate any song-related data
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1270,23 +1346,53 @@ mpd_on_info_response (const struct mpd_response *response,
|
||||||
// Note that we may receive a "time" field twice, however the right one
|
// Note that we may receive a "time" field twice, however the right one
|
||||||
// wins here due to the order we send the commands in
|
// wins here due to the order we send the commands in
|
||||||
char *time = str_map_find (&map, "time");
|
char *time = str_map_find (&map, "time");
|
||||||
|
char *duration = str_map_find (&map, "duration");
|
||||||
if (time)
|
if (time)
|
||||||
{
|
{
|
||||||
char *colon = strchr (time, ':');
|
char *colon = strchr (time, ':');
|
||||||
// TODO: split "time" at ':' -> elapsed seconds, total seconds;
|
if (colon)
|
||||||
// if there's no colon, use "duration"
|
{
|
||||||
|
*colon = '\0';
|
||||||
|
duration = colon + 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: if we're playing, parse the "elapsed" value and use it
|
unsigned long tmp;
|
||||||
// to set a timer for status updates (cancel the timer at the start
|
if (time && xstrtoul (&tmp, time, 10))
|
||||||
// of the info callback and upon disconnect)
|
g_ctx.song_elapsed = tmp;
|
||||||
|
if (duration && xstrtoul (&tmp, duration, 10))
|
||||||
|
g_ctx.song_duration = tmp;
|
||||||
|
|
||||||
// TODO: "volume" is a string, parse it nonetheless so that we can later
|
// TODO: use "time" as a fallback (no milliseconds there)
|
||||||
// tell MPD to change it
|
char *elapsed = str_map_find (&map, "elapsed");
|
||||||
|
if (elapsed && g_ctx.state == PLAYER_PLAYING)
|
||||||
|
{
|
||||||
|
// TODO: parse the "elapsed" value and use it
|
||||||
|
char *period = strchr (elapsed, '.');
|
||||||
|
if (period && xstrtoul (&tmp, period + 1, 10))
|
||||||
|
{
|
||||||
|
// TODO: initialize the timer and create a callback
|
||||||
|
poller_timer_set (&g_ctx.elapsed_event, 1000 - tmp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
char *volume = str_map_find (&map, "volume");
|
||||||
|
if (volume && xstrtoul (&tmp, volume, 10))
|
||||||
|
g_ctx.volume = tmp;
|
||||||
|
|
||||||
str_map_free (&g_ctx.song_info);
|
|
||||||
g_ctx.song_info = map;
|
g_ctx.song_info = map;
|
||||||
|
app_redraw ();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
mpd_on_tick (void *user_data)
|
||||||
|
{
|
||||||
|
(void) user_data;
|
||||||
|
// FIXME: this is doomed to drift unless we use POSIX CLOCK_MONOTONIC
|
||||||
|
poller_timer_set (&g_ctx.elapsed_event, 1000);
|
||||||
|
|
||||||
|
g_ctx.song_elapsed++;
|
||||||
|
// TODO: try to be more efficient in the redrawing procedures
|
||||||
app_redraw ();
|
app_redraw ();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1521,6 +1627,9 @@ app_init_poller_events (void)
|
||||||
poller_timer_init (&g_ctx.reconnect_event, &g_ctx.poller);
|
poller_timer_init (&g_ctx.reconnect_event, &g_ctx.poller);
|
||||||
g_ctx.reconnect_event.dispatcher = app_on_reconnect;
|
g_ctx.reconnect_event.dispatcher = app_on_reconnect;
|
||||||
poller_timer_set (&g_ctx.reconnect_event, 0);
|
poller_timer_set (&g_ctx.reconnect_event, 0);
|
||||||
|
|
||||||
|
poller_timer_init (&g_ctx.elapsed_event, &g_ctx.poller);
|
||||||
|
g_ctx.elapsed_event.dispatcher = mpd_on_tick;
|
||||||
}
|
}
|
||||||
|
|
||||||
int
|
int
|
||||||
|
|
Loading…
Reference in New Issue