19 Commits

Author SHA1 Message Date
0335443b22 Bump version, update NEWS 2020-11-05 01:47:18 +01:00
70ff29e3d5 Add a real manual page
Closes #3
2020-11-05 01:47:06 +01:00
ba122b7672 Minor clarifications 2020-11-05 01:47:05 +01:00
456fab5b11 CMakeLists.txt: install the contrib directory 2020-11-05 01:47:05 +01:00
f4999a63a5 CMakeLists.txt: make this build in OpenBSD 2020-10-29 18:14:41 +01:00
33b4976d7a CMakeLists.txt: omit end{if,foreach} expressions
Their usefulness was almost negative.
2020-10-29 18:14:41 +01:00
df82357cfd Bump minimum CMake version to 3.0
A nice, round number.  This allows us to remove some boilerplate.
2020-10-29 18:14:41 +01:00
bd5152a9e7 Bump termo
This allows us to get rid of a compiler flag.
2020-10-29 18:14:40 +01:00
322a60aa39 Bump liberty 2020-10-29 18:14:40 +01:00
e86f4b6908 Comment the "poll_elapsed_time" option 2020-10-24 15:44:12 +02:00
26b6b1f902 Show song duration in the library
Ideally we'd make columns configurable, which isn't trivial.

This brings the "Current" and "Library" tabs closer together.

Closes #2
2020-10-24 14:58:53 +02:00
8121046be6 Skip playlists in lsinfo responses
Instead of merging the fields into other items.
2020-10-24 14:58:48 +02:00
0dc29a3e2d Refactor the library tab, track duration
The `struct strv` was clunky, it's better to store items
directly in the format we use for all processing.
The additional memory cost is negligible.
2020-10-24 14:55:25 +02:00
791c000791 Use '-' instead of '?' for unknown duration
It is less distracting.

Also use mpd_read_time() and load "duration" as well.
This value isn't rounded to whole seconds, so we load
it before "time" as a fail-safe measure.
2020-10-24 14:54:17 +02:00
c0119027b1 Improve the MPD time parser
- reject negative values, which strtoul() happily accepts
 - deal with an arbitrary number of decimal digits
 - don't return milliseconds when we fail to parse seconds
2020-10-24 14:54:12 +02:00
3934d9b1f9 Bind M-Up to the "up" action
Taken from Windows Explorer, which abandoned the Backspace binding.
2020-10-23 03:33:26 +02:00
2d3909fdd1 Cleanup
No functional change.
2020-10-23 02:57:34 +02:00
b6ce8a0913 Avoid jumping around in polling mode
While still avoiding busy loops.

It works well enough to enable this by default.

Closes #1
2020-10-23 02:42:18 +02:00
9928eca274 Add a comment and update another one 2020-10-18 21:09:03 +02:00
8 changed files with 322 additions and 187 deletions

View File

@@ -1,20 +1,11 @@
project (nncmpp C)
cmake_minimum_required (VERSION 2.8.5)
cmake_minimum_required (VERSION 3.0)
project (nncmpp VERSION 1.0.0 LANGUAGES C)
# Moar warnings
if ("${CMAKE_C_COMPILER_ID}" MATCHES "GNU" OR CMAKE_COMPILER_IS_GNUCC)
set (wdisabled "-Wno-unused-function -Wno-implicit-fallthrough")
set (wdisabled "-Wno-unused-function")
set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -std=gnu99 -Wall -Wextra ${wdisabled}")
endif ("${CMAKE_C_COMPILER_ID}" MATCHES "GNU" OR CMAKE_COMPILER_IS_GNUCC)
# Version
set (project_VERSION_MAJOR "0")
set (project_VERSION_MINOR "9")
set (project_VERSION_PATCH "0")
set (project_VERSION "${project_VERSION_MAJOR}")
set (project_VERSION "${project_VERSION}.${project_VERSION_MINOR}")
set (project_VERSION "${project_VERSION}.${project_VERSION_PATCH}")
endif ()
# For custom modules
set (CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/liberty/cmake)
@@ -35,7 +26,7 @@ if (USE_SYSTEM_TERMO)
if (NOT Termo_FOUND)
message (FATAL_ERROR "System termo library not found")
endif (NOT Termo_FOUND)
else (USE_SYSTEM_TERMO)
else ()
add_subdirectory (termo EXCLUDE_FROM_ALL)
# We don't have many good choices when we don't want to install it and want
# to support older versions of CMake; this is a relatively clean approach
@@ -45,7 +36,7 @@ else (USE_SYSTEM_TERMO)
get_directory_property (Termo_INCLUDE_DIRS
DIRECTORY termo INCLUDE_DIRECTORIES)
set (Termo_LIBRARIES termo-static)
endif (USE_SYSTEM_TERMO)
endif ()
include_directories (${Unistring_INCLUDE_DIRS}
${Ncursesw_INCLUDE_DIRS} ${Termo_INCLUDE_DIRS} ${curl_INCLUDE_DIRS})
@@ -56,6 +47,12 @@ include (CheckFunctionExists)
set (CMAKE_REQUIRED_LIBRARIES ${Ncursesw_LIBRARIES})
CHECK_FUNCTION_EXISTS ("resizeterm" HAVE_RESIZETERM)
if ("${CMAKE_SYSTEM_NAME}" MATCHES "BSD")
# Need this for SIGWINCH in FreeBSD and OpenBSD respectively;
# our POSIX version macros make it undefined
add_definitions (-D__BSD_VISIBLE=1 -D_BSD_SOURCE=1)
endif ()
# Generate a configuration file
configure_file (${PROJECT_SOURCE_DIR}/config.h.in
${PROJECT_BINARY_DIR}/config.h)
@@ -71,22 +68,25 @@ add_threads (${PROJECT_NAME})
include (GNUInstallDirs)
install (TARGETS ${PROJECT_NAME} DESTINATION ${CMAKE_INSTALL_BINDIR})
install (FILES LICENSE DESTINATION ${CMAKE_INSTALL_DOCDIR})
install (DIRECTORY contrib DESTINATION ${CMAKE_INSTALL_DATADIR}/${PROJECT_NAME})
# Generate documentation from program help
find_program (HELP2MAN_EXECUTABLE help2man)
if (NOT HELP2MAN_EXECUTABLE)
message (FATAL_ERROR "help2man not found")
endif (NOT HELP2MAN_EXECUTABLE)
# Generate documentation from text markup
find_program (ASCIIDOCTOR_EXECUTABLE asciidoctor)
if (NOT ASCIIDOCTOR_EXECUTABLE)
message (FATAL_ERROR "asciidoctor not found")
endif ()
foreach (page ${PROJECT_NAME})
set (page_output "${PROJECT_BINARY_DIR}/${page}.1")
list (APPEND project_MAN_PAGES "${page_output}")
add_custom_command (OUTPUT ${page_output}
COMMAND ${HELP2MAN_EXECUTABLE} -N
"${PROJECT_BINARY_DIR}/${page}" -o ${page_output}
DEPENDS ${page}
COMMAND ${ASCIIDOCTOR_EXECUTABLE} -b manpage
-a release-version=${PROJECT_VERSION}
"${PROJECT_SOURCE_DIR}/${page}.adoc"
-o "${page_output}"
DEPENDS ${page}.adoc
COMMENT "Generating man page for ${page}" VERBATIM)
endforeach (page)
endforeach ()
add_custom_target (docs ALL DEPENDS ${project_MAN_PAGES})
@@ -94,23 +94,20 @@ foreach (page ${project_MAN_PAGES})
string (REGEX MATCH "\\.([0-9])$" manpage_suffix "${page}")
install (FILES "${page}"
DESTINATION "${CMAKE_INSTALL_MANDIR}/man${CMAKE_MATCH_1}")
endforeach (page)
endforeach ()
# CPack
set (CPACK_PACKAGE_DESCRIPTION_SUMMARY "MPD client")
set (CPACK_PACKAGE_VENDOR "Premysl Eric Janouch")
set (CPACK_PACKAGE_CONTACT "Přemysl Eric Janouch <p@janouch.name>")
set (CPACK_RESOURCE_FILE_LICENSE "${PROJECT_SOURCE_DIR}/LICENSE")
set (CPACK_PACKAGE_VERSION_MAJOR ${project_VERSION_MAJOR})
set (CPACK_PACKAGE_VERSION_MINOR ${project_VERSION_MINOR})
set (CPACK_PACKAGE_VERSION_PATCH ${project_VERSION_PATCH})
set (CPACK_GENERATOR "TGZ;ZIP")
set (CPACK_PACKAGE_FILE_NAME
"${PROJECT_NAME}-${project_VERSION}-${CMAKE_SYSTEM_NAME}-${CMAKE_SYSTEM_PROCESSOR}")
set (CPACK_PACKAGE_INSTALL_DIRECTORY "${PROJECT_NAME}-${project_VERSION}")
"${PROJECT_NAME}-${PROJECT_VERSION}-${CMAKE_SYSTEM_NAME}-${CMAKE_SYSTEM_PROCESSOR}")
set (CPACK_PACKAGE_INSTALL_DIRECTORY "${PROJECT_NAME}-${PROJECT_VERSION}")
set (CPACK_SOURCE_GENERATOR "TGZ;ZIP")
set (CPACK_SOURCE_IGNORE_FILES "/\\\\.git;/build;/CMakeLists.txt.user")
set (CPACK_SOURCE_PACKAGE_FILE_NAME "${PROJECT_NAME}-${project_VERSION}")
set (CPACK_SOURCE_PACKAGE_FILE_NAME "${PROJECT_NAME}-${PROJECT_VERSION}")
set (CPACK_SET_DESTDIR TRUE)
include (CPack)

22
NEWS
View File

@@ -1,3 +1,25 @@
1.0.0 (2020-11-05)
* Coming with a real manual page instead of a help2man-generated stub
* Added a mode to poll MPD for the elapsed time, enabled by default,
fixing two cases of improper tracking
* Started showing song duration in the library
* Added C-PgUp/PgDown and C-Left/Right bindings to iterate tabs
* Added VIM-like C-y and C-e bindings for scrolling
* Added Windows Explorer-like M-Up binding to go up a directory
* Worked around a cURL bug crashing the application
* Fixed handling of direct SHOUTcast streams
* Miscellaneous little fixes
0.9.0 (2018-11-02)
* Initial release

View File

@@ -22,9 +22,14 @@ Packages
Regular releases are sporadic. git master should be stable enough. You can get
a package with the latest development version from Archlinux's AUR.
Building and Running
--------------------
Build dependencies: CMake, pkg-config, help2man, liberty (included),
Documentation
-------------
See the link:nncmpp.adoc[man page] for information about usage.
The rest of this README will concern itself with externalities.
Building
--------
Build dependencies: CMake, pkg-config, asciidoctor, liberty (included),
termo (included) +
Runtime dependencies: ncursesw, libunistring, cURL
@@ -43,40 +48,6 @@ Or you can try telling CMake to make a package for you. For Debian it is:
$ cpack -G DEB
# dpkg -i nncmpp-*.deb
Note that for versions of CMake before 2.8.9, you need to prefix `cpack` with
`fakeroot` or file ownership will end up wrong.
Having the program installed, create a configuration file and run it.
Configuration
-------------
Create _~/.config/nncmpp/nncmpp.conf_ with contents like the following:
....
settings = {
address = "localhost:6600"
password = "<your password>"
root = "~/Music"
}
colors = {
normal = ""
highlight = "bold"
elapsed = "reverse"
remains = "ul"
tab_bar = "reverse"
tab_active = "ul"
even = ""
odd = ""
selection = "reverse"
multiselect = "-1 6"
scrollbar = ""
}
streams = {
"dnbradio.com" = "http://www.dnbradio.com/hi.m3u"
"BassDrive.com" = "http://bassdrive.com/v2/streams/BassDrive.pls"
}
....
Terminal caveats
----------------
This application aspires to be as close to a GUI as possible. It expects you

View File

@@ -2,7 +2,7 @@
#define CONFIG_H
#define PROGRAM_NAME "${CMAKE_PROJECT_NAME}"
#define PROGRAM_VERSION "${project_VERSION}"
#define PROGRAM_VERSION "${PROJECT_VERSION}"
#cmakedefine HAVE_RESIZETERM

Submodule liberty updated: e029aae1d3...d71c47f8ce

87
nncmpp.adoc Normal file
View File

@@ -0,0 +1,87 @@
nncmpp(1)
=========
:doctype: manpage
:manmanual: nncmpp Manual
:mansource: nncmpp {release-version}
Name
----
nncmpp - terminal-based MPD client
Synopsis
--------
*nncmpp* [_OPTION_]...
Description
-----------
*nncmpp* is a terminal-based GUI-like MPD client. On start up it will welcome
you with an overview of all key bindings and the actions they're assigned to.
Individual tabs can be switched to either using the mouse or by pressing *M-1*
through *M-9*, corresponding to the order they appear in.
Options
-------
*-d*, *--debug*::
Adds a "Debug" tab showing all MPD communication and other information
that help debug various issues.
*-h*, *--help*::
Display a help message and exit.
*-V*, *--version*::
Output version information and exit.
Configuration
-------------
Unless you run MPD on a remote machine, on an unusual port, or protected by
a password, the client doesn't need a configuration file to work. It is,
however, likely that you'll want to customize the looks or add some streams.
You can start off with the following snippet:
....
settings = {
address = "localhost:6600"
password = "<your password>"
root = "~/Music"
}
colors = {
normal = ""
highlight = "bold"
elapsed = "reverse"
remains = "ul"
tab_bar = "reverse"
tab_active = "ul"
even = ""
odd = ""
selection = "reverse"
multiselect = "-1 6"
scrollbar = ""
}
streams = {
"dnbradio.com" = "http://www.dnbradio.com/hi.m3u"
"BassDrive.com" = "http://bassdrive.com/v2/streams/BassDrive.pls"
}
....
Terminal attributes are accepted in a format similar to that of *git-config*(1),
only named colours aren't supported. The distribution contains example colour
schemes in the _contrib_ directory.
// TODO: it seems like liberty should contain an includable snippet about
// the format, which could form a part of nncmpp.conf(5).
Files
-----
*nncmpp* follows the XDG Base Directory Specification.
_~/.config/nncmpp/nncmpp.conf_::
The configuration file.
Reporting bugs
--------------
Use https://git.janouch.name/p/nncmpp to report bugs, request features,
or submit pull requests.
See also
--------
*mpd*(1)

290
nncmpp.c
View File

@@ -82,6 +82,9 @@ enum
// ncurses is notoriously retarded for input handling, we need something
// different if only to receive mouse events reliably.
//
// 2020 update: ncurses is mostly reliable now but rxvt-unicode needs to start
// supporting 1006, or ncurses needs to start supporting the 1015 mode.
#include "termo.h"
@@ -206,6 +209,35 @@ mpd_parse_kv (char *line, char **value)
return key;
}
static void
mpd_read_time (const char *value, int *sec, int *optional_msec)
{
if (!value)
return;
char *end = NULL;
long n = strtol (value, &end, 10);
if (n < 0 || (*end && *end != '.'))
return;
int msec = 0;
if (*end == '.')
{
// In practice, MPD always uses three decimal digits
size_t digits = strspn (++end, "0123456789");
if (end[digits])
return;
if (digits--) msec += (*end++ - '0') * 100;
if (digits--) msec += (*end++ - '0') * 10;
if (digits--) msec += *end++ - '0';
}
*sec = MIN (INT_MAX, n);
if (optional_msec)
*optional_msec = msec;
}
// --- cURL async wrapper ------------------------------------------------------
// You are meant to subclass this structure, no user_data pointers needed
@@ -609,7 +641,7 @@ static struct app_context
struct str_map playback_info; ///< Current song info
struct poller_timer elapsed_event; ///< Seconds elapsed event
int64_t elapsed_since; ///< Time of the last tick
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
@@ -709,14 +741,22 @@ static struct config_schema g_config_settings[] =
{ .name = "password",
.comment = "Password to use for MPD authentication",
.type = CONFIG_ITEM_STRING },
// NOTE: this is unused--in theory we could allow manual metadata adjustment
{ .name = "root",
.comment = "Where all the files MPD is playing are located",
.type = CONFIG_ITEM_STRING },
// 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
// is currently playing, we do not reset g.song_elapsed (we could ask
// for a response which feels racy, or rethink the mechanism there)
{ .name = "poll_elapsed_time",
.comment = "Whether to actively poll MPD for the elapsed time",
.type = CONFIG_ITEM_BOOLEAN,
.on_change = on_poll_elapsed_time_changed,
.default_ = "off" },
.default_ = "on" },
{}
};
@@ -2057,6 +2097,7 @@ g_normal_defaults[] =
{ "Enter", ACTION_CHOOSE },
{ "Delete", ACTION_DELETE },
{ "d", ACTION_DELETE },
{ "M-Up", ACTION_UP },
{ "Backspace", ACTION_UP },
{ "v", ACTION_MULTISELECT },
{ "/", ACTION_MPD_SEARCH },
@@ -2215,13 +2256,13 @@ app_process_termo_event (termo_key_t *event)
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)
{
// TODO: configurable output, maybe dynamically sized columns
int length_len = 1 /*separator */ + 2 /* h */ + 3 /* m */+ 3 /* s */;
compact_map_t map = item_list_get (&g.playlist, item_index);
const char *artist = compact_map_find (map, "artist");
const char *title = compact_map_find (map, "title");
@@ -2233,15 +2274,14 @@ current_tab_on_item_draw (size_t item_index, struct row_buffer *buffer,
else
row_buffer_append (buffer, compact_map_find (map, "file"), attrs);
row_buffer_align (buffer, width - length_len, attrs);
row_buffer_align (buffer, width - DURATION_MAX_LEN, attrs);
char *s = NULL;
unsigned long n;
const char *time = compact_map_find (map, "time");
if (!time || !xstrtoul (&n, time, 10) || !(s = app_time_string (n)))
s = xstrdup ("?");
int duration = -1;
mpd_read_time (compact_map_find (map, "duration"), &duration, NULL);
mpd_read_time (compact_map_find (map, "time"), &duration, NULL);
char *right_aligned = xstrdup_printf ("%*s", length_len, s);
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);
free (s);
@@ -2374,49 +2414,53 @@ struct library_level
char path[]; ///< Path of the level
};
static struct
{
struct tab super; ///< Parent class
struct str path; ///< Current path
struct strv items; ///< Current items (type, name, path)
struct library_level *above; ///< Upper levels
bool searching; ///< Search mode is active
}
g_library_tab;
enum
{
// This list is also ordered by ASCII and important for sorting
LIBRARY_ROOT = '/', ///< Root entry
LIBRARY_UP = '^', ///< Upper directory
LIBRARY_DIR = 'd', ///< Directory
LIBRARY_FILE = 'f' ///< File
LIBRARY_ROOT = '/', ///< Root entry
LIBRARY_UP = '^', ///< Upper directory
LIBRARY_DIR = 'd', ///< Directory
LIBRARY_FILE = 'f', ///< File
LIBRARY_PLAYLIST = 'p', ///< Playlist (unsupported)
};
struct library_tab_item
{
int type; ///< Type of the item
const char *name; ///< Visible name
const char *path; ///< MPD path
int duration; ///< Duration or -1 if N/A or unknown
char *name; ///< Visible name
const char *path; ///< MPD path (follows the name)
};
static void
library_tab_add (int type, const char *name, const char *path)
static struct
{
strv_append_owned (&g_library_tab.items,
xstrdup_printf ("%c%s%c%s", type, name, 0, path));
}
struct tab super; ///< Parent class
struct str path; ///< Current path
struct library_level *above; ///< Upper levels
static struct library_tab_item
library_tab_resolve (const char *raw)
/// Current items
ARRAY (struct library_tab_item, items)
bool searching; ///< Search mode is active
}
g_library_tab;
static void
library_tab_add (int type, int duration, const char *name, const char *path)
{
struct library_tab_item item;
item.type = *raw++;
item.name = raw;
item.path = strchr (raw, '\0') + 1;
return item;
// Slightly reduce memory overhead while retaining friendly access
size_t name_len = strlen (name), path_len = strlen (path);
char *combined = xmalloc (++name_len + ++path_len);
ARRAY_RESERVE (g_library_tab.items, 1);
g_library_tab.items[g_library_tab.items_len++] = (struct library_tab_item)
{
.type = type,
.duration = duration,
.name = memcpy (combined, name, name_len),
.path = memcpy (combined + name_len, path, path_len),
};
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@@ -2426,21 +2470,27 @@ library_tab_on_item_draw (size_t item_index, struct row_buffer *buffer,
int width)
{
(void) width;
hard_assert (item_index < g_library_tab.items.len);
hard_assert (item_index < g_library_tab.items_len);
struct library_tab_item x =
library_tab_resolve (g_library_tab.items.vector[item_index]);
struct library_tab_item *x = &g_library_tab.items[item_index];
const char *prefix, *name;
switch (x.type)
switch (x->type)
{
case LIBRARY_ROOT: prefix = "/"; name = ""; break;
case LIBRARY_UP: prefix = "/"; name = ".."; break;
case LIBRARY_DIR: prefix = "/"; name = x.name; break;
case LIBRARY_FILE: prefix = " "; name = x.name; break;
case LIBRARY_ROOT: prefix = "/"; name = ""; break;
case LIBRARY_UP: prefix = "/"; name = ".."; break;
case LIBRARY_DIR: prefix = "/"; name = x->name; break;
case LIBRARY_FILE: prefix = " "; name = x->name; break;
default: hard_assert (!"invalid item type");
}
chtype attrs = x.type != LIBRARY_FILE ? APP_ATTR (DIRECTORY) : 0;
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;
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);
}
static char
@@ -2448,31 +2498,38 @@ library_tab_header_type (const char *key)
{
if (!strcasecmp_ascii (key, "file")) return LIBRARY_FILE;
if (!strcasecmp_ascii (key, "directory")) return LIBRARY_DIR;
if (!strcasecmp_ascii (key, "playlist")) return LIBRARY_PLAYLIST;
return 0;
}
static void
library_tab_chunk (char type, const char *path, struct str_map *map)
{
// CUE files appear once as a directory and another time as a playlist,
// just skip them entirely
if (type == LIBRARY_PLAYLIST)
return;
const char *artist = str_map_find (map, "artist");
const char *title = str_map_find (map, "title");
char *name = (artist && title)
? xstrdup_printf ("%s - %s", artist, title)
: xstrdup (xbasename (path));
library_tab_add (type, name, path);
int duration = -1;
mpd_read_time (str_map_find (map, "duration"), &duration, NULL);
mpd_read_time (str_map_find (map, "time"), &duration, NULL);
library_tab_add (type, duration, name, path);
free (name);
}
static int
library_tab_compare (char **a, char **b)
library_tab_compare (struct library_tab_item *a, struct library_tab_item *b)
{
struct library_tab_item xa = library_tab_resolve (*a);
struct library_tab_item xb = library_tab_resolve (*b);
if (a->type != b->type)
return a->type - b->type;
if (xa.type != xb.type)
return xa.type - xb.type;
return app_casecmp ((uint8_t *) xa.path, (uint8_t *) xb.path);
return app_casecmp ((uint8_t *) a->path, (uint8_t *) b->path);
}
static char *
@@ -2540,16 +2597,25 @@ library_tab_change_level (const char *new_path)
g_library_tab.super.header = xstrdup_printf ("/%s", path->str);
}
static void
library_tab_reset (void)
{
for (size_t i = 0; i < g_library_tab.items_len; i++)
free (g_library_tab.items[i].name);
free (g_library_tab.items);
ARRAY_INIT (g_library_tab.items);
}
static void
library_tab_load_data (const struct strv *data)
{
strv_reset (&g_library_tab.items);
library_tab_reset ();
char *parent = library_tab_parent ();
if (parent)
{
library_tab_add (LIBRARY_ROOT, "", "");
library_tab_add (LIBRARY_UP, "", parent);
library_tab_add (LIBRARY_ROOT, -1, "", "");
library_tab_add (LIBRARY_UP, -1, "", parent);
free (parent);
}
@@ -2569,16 +2635,16 @@ library_tab_load_data (const struct strv *data)
}
str_map_free (&map);
struct strv *items = &g_library_tab.items;
qsort (items->vector, items->len, sizeof *items->vector,
struct library_tab_item *items = g_library_tab.items;
size_t len = g_library_tab.super.item_count = g_library_tab.items_len;
qsort (items, len, sizeof *items,
(int (*) (const void *, const void *)) library_tab_compare);
g_library_tab.super.item_count = items->len;
// XXX: this unmarks even if just the database updates
g_library_tab.super.item_mark = -1;
// Don't force the selection visible when there's no need to touch it
if (g_library_tab.super.item_selected >= (int) items->len)
if (g_library_tab.super.item_selected >= (int) len)
app_move_selection (0);
app_invalidate ();
@@ -2666,9 +2732,8 @@ library_tab_is_range_playable (struct tab_range range)
{
for (int i = range.from; i <= range.upto; i++)
{
struct library_tab_item x =
library_tab_resolve (g_library_tab.items.vector[i]);
if (x.type == LIBRARY_DIR || x.type == LIBRARY_FILE)
struct library_tab_item *x = &g_library_tab.items[i];
if (x->type == LIBRARY_DIR || x->type == LIBRARY_FILE)
return true;
}
return false;
@@ -2686,9 +2751,7 @@ library_tab_on_action (enum action action)
if (range.from < 0)
return false;
struct library_tab_item x =
library_tab_resolve (g_library_tab.items.vector[range.from]);
struct library_tab_item *x = &g_library_tab.items[range.from];
switch (action)
{
case ACTION_CHOOSE:
@@ -2696,12 +2759,12 @@ library_tab_on_action (enum action action)
if (range.from != range.upto)
break;
switch (x.type)
switch (x->type)
{
case LIBRARY_ROOT:
case LIBRARY_UP:
case LIBRARY_DIR: library_tab_reload (x.path); break;
case LIBRARY_FILE: MPD_SIMPLE ("add", x.path); break;
case LIBRARY_DIR: library_tab_reload (x->path); break;
case LIBRARY_FILE: MPD_SIMPLE ("add", x->path); break;
default: hard_assert (!"invalid item type");
}
tab->item_mark = -1;
@@ -2750,10 +2813,9 @@ library_tab_on_action (enum action action)
for (int i = range.from; i <= range.upto; i++)
{
struct library_tab_item x =
library_tab_resolve (g_library_tab.items.vector[i]);
if (x.type == LIBRARY_DIR || x.type == LIBRARY_FILE)
MPD_SIMPLE ("add", x.path);
struct library_tab_item *x = &g_library_tab.items[i];
if (x->type == LIBRARY_DIR || x->type == LIBRARY_FILE)
MPD_SIMPLE ("add", x->path);
}
tab->item_mark = -1;
return true;
@@ -2769,10 +2831,9 @@ library_tab_on_action (enum action action)
mpd_client_send_command (c, "clear", NULL);
for (int i = range.from; i <= range.upto; i++)
{
struct library_tab_item x =
library_tab_resolve (g_library_tab.items.vector[i]);
if (x.type == LIBRARY_DIR || x.type == LIBRARY_FILE)
mpd_client_send_command (c, "add", x.path, NULL);
struct library_tab_item *x = &g_library_tab.items[i];
if (x->type == LIBRARY_DIR || x->type == LIBRARY_FILE)
mpd_client_send_command (c, "add", x->path, NULL);
}
if (g.state == PLAYER_PLAYING)
mpd_client_send_command (c, "play", NULL);
@@ -2792,7 +2853,7 @@ static struct tab *
library_tab_init (void)
{
g_library_tab.path = str_make ();
g_library_tab.items = strv_make ();
// g_library_tab.items is fine with zero initialisation
struct tab *super = &g_library_tab.super;
tab_init (super, "Library");
@@ -3348,25 +3409,6 @@ debug_tab_init (void)
// --- MPD interface -----------------------------------------------------------
static void
mpd_read_time (const char *value, int *sec, int *optional_msec)
{
if (!value)
return;
char *end, *period = strchr (value, '.');
if (optional_msec && period)
{
unsigned long n = strtoul (period + 1, &end, 10);
if (*end)
return;
*optional_msec = MIN (INT_MAX, n);
}
unsigned long n = strtoul (value, &end, 10);
if (end == period || !*end)
*sec = MIN (INT_MAX, n);
}
static void
mpd_update_playlist_time (void)
{
@@ -3383,6 +3425,33 @@ mpd_update_playlist_time (void)
}
}
static void
mpd_set_elapsed_timer (int msec_past_second)
{
int delay_msec = 1000 - msec_past_second; // Until the next round second
if (!g.elapsed_poll)
{
poller_timer_set (&g.elapsed_event, delay_msec);
// Remember when the last round second was, relative to monotonic time
g.elapsed_since = clock_msec (CLOCK_BEST) - msec_past_second;
return;
}
// We may receive an earlier time, this seems to compensate for it well
// (I haven't seen it trigger more than 50ms too early)
delay_msec += 100;
// When playback stalls, avoid busy looping with the server
int elapsed_msec = g.song_elapsed * 1000 + msec_past_second;
if (elapsed_msec == g.elapsed_since)
delay_msec = MAX (delay_msec, 500);
// In polling mode, we're interested in progress rather than stability.
// We can reuse both the poller_timer struct and the timestamp field.
poller_timer_set (&g.elapsed_event, delay_msec);
g.elapsed_since = elapsed_msec;
}
static void
mpd_update_playback_state (void)
{
@@ -3421,20 +3490,9 @@ mpd_update_playback_state (void)
poller_timer_reset (&g.elapsed_event);
if (g.state == PLAYER_PLAYING)
{
int until_next = 1000 - msec_past_second;
// We could make use of "until_next", however this might create
// an intensive busy loop when playback stalls (typically because of
// some network issues). Half a second will work reasonably well.
if (g.elapsed_poll)
until_next = 500;
// Set a timer for when the next round second of playback happens
poller_timer_set (&g.elapsed_event, until_next);
// Remember when the last round second was, relative to monotonic time
g.elapsed_since = clock_msec (CLOCK_BEST) - msec_past_second;
}
mpd_set_elapsed_timer (msec_past_second);
else
g.elapsed_since = -1;
// The server sends -1 when nothing is being played right now
unsigned long n;
@@ -3972,7 +4030,7 @@ main (int argc, char *argv[])
};
struct opt_handler oh =
opt_handler_make (argc, argv, opts, NULL, "MPD client.");
opt_handler_make (argc, argv, opts, NULL, "Terminal-based MPD client.");
int c;
while ((c = opt_handler_get (&oh)) != -1)

2
termo

Submodule termo updated: 8c4e867760...f7912a8ce7