29 Commits
v0.9 ... v1.0.0

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
6c2ae2f6bb Give up and implement elapsed time polling
Playback may sometimes stall but it won't produce any events.

This popular workaround likes to jump around, though.
It might be a good idea to use some kind of hybrid approach.

Therefore this is disabled by default so far.

Updates #1
2020-10-18 07:28:14 +02:00
b3579d1128 Explain the ticking mechanism
Took time to read.  Also fix an invalid comment.
2020-10-18 05:57:44 +02:00
525e952753 Bump liberty and termo 2020-10-10 21:31:31 +02:00
8707b38c48 Make direct SHOUTcast streams work again
Might be an issue specific to my bbc-on-ice, since we're not asking
for SHOUTcast by including "Icy-MetaData: 1" in request headers
but the proxy always outputs an "ICY 200 OK" header.
2020-10-10 14:48:22 +02:00
7af041ac01 Remove unnecessary quotes from macro definitions
The behaviour is defined by the standard.
2020-09-20 13:18:07 +02:00
1f0cab7cdd Bump liberty 2020-09-07 18:15:39 +02:00
e21699ab47 Support iterating tabs with C-PgUp/Down and C-Left/Right 2020-09-07 18:15:39 +02:00
d124f43cf6 Support vi-like scrolling with C-y and C-e 2020-08-01 14:06:17 +02:00
0e2a050c4f Name change 2020-08-01 14:04:10 +02:00
6c1546e919 Workaround cURL bug 2019-02-24 01:41:15 +01:00
10 changed files with 473 additions and 274 deletions

View File

@@ -1,20 +1,11 @@
project (nncmpp C) cmake_minimum_required (VERSION 3.0)
cmake_minimum_required (VERSION 2.8.5) project (nncmpp VERSION 1.0.0 LANGUAGES C)
# Moar warnings # Moar warnings
if ("${CMAKE_C_COMPILER_ID}" MATCHES "GNU" OR CMAKE_COMPILER_IS_GNUCC) 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}") set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -std=gnu99 -Wall -Wextra ${wdisabled}")
endif ("${CMAKE_C_COMPILER_ID}" MATCHES "GNU" OR CMAKE_COMPILER_IS_GNUCC) endif ()
# 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}")
# For custom modules # For custom modules
set (CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/liberty/cmake) set (CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/liberty/cmake)
@@ -35,7 +26,7 @@ if (USE_SYSTEM_TERMO)
if (NOT Termo_FOUND) if (NOT Termo_FOUND)
message (FATAL_ERROR "System termo library not found") message (FATAL_ERROR "System termo library not found")
endif (NOT Termo_FOUND) endif (NOT Termo_FOUND)
else (USE_SYSTEM_TERMO) else ()
add_subdirectory (termo EXCLUDE_FROM_ALL) add_subdirectory (termo EXCLUDE_FROM_ALL)
# We don't have many good choices when we don't want to install it and want # 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 # to support older versions of CMake; this is a relatively clean approach
@@ -45,17 +36,23 @@ else (USE_SYSTEM_TERMO)
get_directory_property (Termo_INCLUDE_DIRS get_directory_property (Termo_INCLUDE_DIRS
DIRECTORY termo INCLUDE_DIRECTORIES) DIRECTORY termo INCLUDE_DIRECTORIES)
set (Termo_LIBRARIES termo-static) set (Termo_LIBRARIES termo-static)
endif (USE_SYSTEM_TERMO) endif ()
include_directories (${UNISTRING_INCLUDE_DIRS} include_directories (${Unistring_INCLUDE_DIRS}
${NCURSESW_INCLUDE_DIRS} ${Termo_INCLUDE_DIRS} ${curl_INCLUDE_DIRS}) ${Ncursesw_INCLUDE_DIRS} ${Termo_INCLUDE_DIRS} ${curl_INCLUDE_DIRS})
link_directories (${curl_LIBRARY_DIRS}) link_directories (${curl_LIBRARY_DIRS})
# Configuration # Configuration
include (CheckFunctionExists) include (CheckFunctionExists)
set (CMAKE_REQUIRED_LIBRARIES ${NCURSESW_LIBRARIES}) set (CMAKE_REQUIRED_LIBRARIES ${Ncursesw_LIBRARIES})
CHECK_FUNCTION_EXISTS ("resizeterm" HAVE_RESIZETERM) 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 # Generate a configuration file
configure_file (${PROJECT_SOURCE_DIR}/config.h.in configure_file (${PROJECT_SOURCE_DIR}/config.h.in
${PROJECT_BINARY_DIR}/config.h) ${PROJECT_BINARY_DIR}/config.h)
@@ -63,30 +60,33 @@ include_directories (${PROJECT_SOURCE_DIR} ${PROJECT_BINARY_DIR})
# Build the main executable and link it # Build the main executable and link it
add_executable (${PROJECT_NAME} ${PROJECT_NAME}.c) add_executable (${PROJECT_NAME} ${PROJECT_NAME}.c)
target_link_libraries (${PROJECT_NAME} ${UNISTRING_LIBRARIES} target_link_libraries (${PROJECT_NAME} ${Unistring_LIBRARIES}
${NCURSESW_LIBRARIES} termo-static ${curl_LIBRARIES}) ${Ncursesw_LIBRARIES} termo-static ${curl_LIBRARIES})
add_threads (${PROJECT_NAME}) add_threads (${PROJECT_NAME})
# Installation # Installation
include (GNUInstallDirs) include (GNUInstallDirs)
install (TARGETS ${PROJECT_NAME} DESTINATION ${CMAKE_INSTALL_BINDIR}) install (TARGETS ${PROJECT_NAME} DESTINATION ${CMAKE_INSTALL_BINDIR})
install (FILES LICENSE DESTINATION ${CMAKE_INSTALL_DOCDIR}) install (FILES LICENSE DESTINATION ${CMAKE_INSTALL_DOCDIR})
install (DIRECTORY contrib DESTINATION ${CMAKE_INSTALL_DATADIR}/${PROJECT_NAME})
# Generate documentation from program help # Generate documentation from text markup
find_program (HELP2MAN_EXECUTABLE help2man) find_program (ASCIIDOCTOR_EXECUTABLE asciidoctor)
if (NOT HELP2MAN_EXECUTABLE) if (NOT ASCIIDOCTOR_EXECUTABLE)
message (FATAL_ERROR "help2man not found") message (FATAL_ERROR "asciidoctor not found")
endif (NOT HELP2MAN_EXECUTABLE) endif ()
foreach (page ${PROJECT_NAME}) foreach (page ${PROJECT_NAME})
set (page_output "${PROJECT_BINARY_DIR}/${page}.1") set (page_output "${PROJECT_BINARY_DIR}/${page}.1")
list (APPEND project_MAN_PAGES "${page_output}") list (APPEND project_MAN_PAGES "${page_output}")
add_custom_command (OUTPUT ${page_output} add_custom_command (OUTPUT ${page_output}
COMMAND ${HELP2MAN_EXECUTABLE} -N COMMAND ${ASCIIDOCTOR_EXECUTABLE} -b manpage
"${PROJECT_BINARY_DIR}/${page}" -o ${page_output} -a release-version=${PROJECT_VERSION}
DEPENDS ${page} "${PROJECT_SOURCE_DIR}/${page}.adoc"
-o "${page_output}"
DEPENDS ${page}.adoc
COMMENT "Generating man page for ${page}" VERBATIM) COMMENT "Generating man page for ${page}" VERBATIM)
endforeach (page) endforeach ()
add_custom_target (docs ALL DEPENDS ${project_MAN_PAGES}) 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}") string (REGEX MATCH "\\.([0-9])$" manpage_suffix "${page}")
install (FILES "${page}" install (FILES "${page}"
DESTINATION "${CMAKE_INSTALL_MANDIR}/man${CMAKE_MATCH_1}") DESTINATION "${CMAKE_INSTALL_MANDIR}/man${CMAKE_MATCH_1}")
endforeach (page) endforeach ()
# CPack # CPack
set (CPACK_PACKAGE_DESCRIPTION_SUMMARY "MPD client") set (CPACK_PACKAGE_DESCRIPTION_SUMMARY "MPD client")
set (CPACK_PACKAGE_VENDOR "Premysl Janouch") set (CPACK_PACKAGE_VENDOR "Premysl Eric Janouch")
set (CPACK_PACKAGE_CONTACT "Přemysl Janouch <p@janouch.name>") set (CPACK_PACKAGE_CONTACT "Přemysl Eric Janouch <p@janouch.name>")
set (CPACK_RESOURCE_FILE_LICENSE "${PROJECT_SOURCE_DIR}/LICENSE") 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_GENERATOR "TGZ;ZIP")
set (CPACK_PACKAGE_FILE_NAME set (CPACK_PACKAGE_FILE_NAME
"${PROJECT_NAME}-${project_VERSION}-${CMAKE_SYSTEM_NAME}-${CMAKE_SYSTEM_PROCESSOR}") "${PROJECT_NAME}-${PROJECT_VERSION}-${CMAKE_SYSTEM_NAME}-${CMAKE_SYSTEM_PROCESSOR}")
set (CPACK_PACKAGE_INSTALL_DIRECTORY "${PROJECT_NAME}-${project_VERSION}") set (CPACK_PACKAGE_INSTALL_DIRECTORY "${PROJECT_NAME}-${PROJECT_VERSION}")
set (CPACK_SOURCE_GENERATOR "TGZ;ZIP") set (CPACK_SOURCE_GENERATOR "TGZ;ZIP")
set (CPACK_SOURCE_IGNORE_FILES "/\\\\.git;/build;/CMakeLists.txt.user") 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) set (CPACK_SET_DESTDIR TRUE)
include (CPack) include (CPack)

View File

@@ -1,4 +1,4 @@
Copyright (c) 2016 - 2018, Přemysl Janouch <p@janouch.name> Copyright (c) 2016 - 2020, Přemysl Eric Janouch <p@janouch.name>
Permission to use, copy, modify, and/or distribute this software for any Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted. purpose with or without fee is hereby granted.

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) 0.9.0 (2018-11-02)
* Initial release * Initial release

View File

@@ -22,9 +22,14 @@ Packages
Regular releases are sporadic. git master should be stable enough. You can get Regular releases are sporadic. git master should be stable enough. You can get
a package with the latest development version from Archlinux's AUR. a package with the latest development version from Archlinux's AUR.
Building and Running Documentation
-------------------- -------------
Build dependencies: CMake, pkg-config, help2man, liberty (included), 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) + termo (included) +
Runtime dependencies: ncursesw, libunistring, cURL 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 $ cpack -G DEB
# dpkg -i nncmpp-*.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 Terminal caveats
---------------- ----------------
This application aspires to be as close to a GUI as possible. It expects you 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 CONFIG_H
#define PROGRAM_NAME "${CMAKE_PROJECT_NAME}" #define PROGRAM_NAME "${CMAKE_PROJECT_NAME}"
#define PROGRAM_VERSION "${project_VERSION}" #define PROGRAM_VERSION "${PROJECT_VERSION}"
#cmakedefine HAVE_RESIZETERM #cmakedefine HAVE_RESIZETERM

Submodule liberty updated: bb30c7d86e...d71c47f8ce

View File

@@ -1,7 +1,7 @@
/* /*
* line-editor.c: a line editor component for the TUI part of liberty * line-editor.c: a line editor component for the TUI part of liberty
* *
* Copyright (c) 2017 - 2018, Přemysl Janouch <p@janouch.name> * Copyright (c) 2017 - 2018, Přemysl Eric Janouch <p@janouch.name>
* *
* Permission to use, copy, modify, and/or distribute this software for any * Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted. * purpose with or without fee is hereby granted.

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)

500
nncmpp.c
View File

@@ -1,7 +1,7 @@
/* /*
* nncmpp -- the MPD client you never knew you needed * nncmpp -- the MPD client you never knew you needed
* *
* Copyright (c) 2016 - 2018, Přemysl Janouch <p@janouch.name> * Copyright (c) 2016 - 2020, Přemysl Eric Janouch <p@janouch.name>
* *
* Permission to use, copy, modify, and/or distribute this software for any * Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted. * purpose with or without fee is hereby granted.
@@ -21,30 +21,30 @@
// We "need" to have an enum for attributes before including liberty. // We "need" to have an enum for attributes before including liberty.
// Avoiding colours in the defaults here in order to support dumb terminals. // Avoiding colours in the defaults here in order to support dumb terminals.
#define ATTRIBUTE_TABLE(XX) \ #define ATTRIBUTE_TABLE(XX) \
XX( NORMAL, "normal", -1, -1, 0 ) \ XX( NORMAL, normal, -1, -1, 0 ) \
XX( HIGHLIGHT, "highlight", -1, -1, A_BOLD ) \ XX( HIGHLIGHT, highlight, -1, -1, A_BOLD ) \
/* Gauge */ \ /* Gauge */ \
XX( ELAPSED, "elapsed", -1, -1, A_REVERSE ) \ XX( ELAPSED, elapsed, -1, -1, A_REVERSE ) \
XX( REMAINS, "remains", -1, -1, A_UNDERLINE ) \ XX( REMAINS, remains, -1, -1, A_UNDERLINE ) \
/* Tab bar */ \ /* Tab bar */ \
XX( TAB_BAR, "tab_bar", -1, -1, A_REVERSE ) \ XX( TAB_BAR, tab_bar, -1, -1, A_REVERSE ) \
XX( TAB_ACTIVE, "tab_active", -1, -1, A_UNDERLINE ) \ XX( TAB_ACTIVE, tab_active, -1, -1, A_UNDERLINE ) \
/* Listview */ \ /* Listview */ \
XX( HEADER, "header", -1, -1, A_UNDERLINE ) \ XX( HEADER, header, -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( DIRECTORY, "directory", -1, -1, 0 ) \ XX( DIRECTORY, directory, -1, -1, 0 ) \
XX( SELECTION, "selection", -1, -1, A_REVERSE ) \ XX( SELECTION, selection, -1, -1, A_REVERSE ) \
/* Cyan is good with both black and white. /* Cyan is good with both black and white.
* Can't use A_REVERSE because bold'd be bright. * Can't use A_REVERSE because bold'd be bright.
* Unfortunately ran out of B&W attributes. */ \ * Unfortunately ran out of B&W attributes. */ \
XX( MULTISELECT, "multiselect",-1, 6, 0 ) \ XX( MULTISELECT, multiselect, -1, 6, 0 ) \
XX( SCROLLBAR, "scrollbar", -1, -1, 0 ) \ XX( SCROLLBAR, scrollbar, -1, -1, 0 ) \
/* These are for debugging only */ \ /* These are for debugging only */ \
XX( WARNING, "warning", 3, -1, 0 ) \ XX( WARNING, warning, 3, -1, 0 ) \
XX( ERROR, "error", 1, -1, 0 ) \ XX( ERROR, error, 1, -1, 0 ) \
XX( INCOMING, "incoming", 2, -1, 0 ) \ XX( INCOMING, incoming, 2, -1, 0 ) \
XX( OUTGOING, "outgoing", 4, -1, 0 ) XX( OUTGOING, outgoing, 4, -1, 0 )
enum enum
{ {
@@ -82,6 +82,9 @@ enum
// ncurses is notoriously retarded for input handling, we need something // ncurses is notoriously retarded for input handling, we need something
// different if only to receive mouse events reliably. // 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" #include "termo.h"
@@ -174,7 +177,7 @@ print_curl_debug (CURL *easy, curl_infotype type, char *data, size_t len,
for (size_t i = 0; i < len; i++) for (size_t i = 0; i < len; i++)
{ {
uint8_t c = data[i]; uint8_t c = data[i];
copy[i] = c >= 32 || c == '\n' ? c : '.'; copy[i] = !iscntrl_ascii (c) || c == '\n' ? c : '.';
} }
copy[len] = '\0'; copy[len] = '\0';
@@ -206,6 +209,35 @@ mpd_parse_kv (char *line, char **value)
return key; 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 ------------------------------------------------------ // --- cURL async wrapper ------------------------------------------------------
// You are meant to subclass this structure, no user_data pointers needed // You are meant to subclass this structure, no user_data pointers needed
@@ -286,6 +318,9 @@ poller_curl_on_socket_action (CURL *easy, curl_socket_t s, int what,
} }
if (what == CURL_POLL_REMOVE) if (what == CURL_POLL_REMOVE)
{ {
// Some annoying cURL bug. Never trust libraries.
fd->fd.closed = fcntl(fd->fd.fd, F_GETFL) < 0 && errno == EBADF;
poller_fd_reset (&fd->fd); poller_fd_reset (&fd->fd);
LIST_UNLINK (self->fds, fd); LIST_UNLINK (self->fds, fd);
free (fd); free (fd);
@@ -606,7 +641,8 @@ static struct app_context
struct str_map playback_info; ///< Current song info struct str_map playback_info; ///< Current song info
struct poller_timer elapsed_event; ///< Seconds elapsed event struct poller_timer elapsed_event; ///< Seconds elapsed event
int64_t elapsed_since; ///< Time of the next 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 // TODO: initialize these to -1
int song; ///< Current song index int song; ///< Current song index
@@ -689,6 +725,13 @@ tab_selection_range (struct tab *self)
// --- Configuration ----------------------------------------------------------- // --- Configuration -----------------------------------------------------------
static void
on_poll_elapsed_time_changed (struct config_item *item)
{
// This is only set once, on application startup
g.elapsed_poll = item->value.boolean;
}
static struct config_schema g_config_settings[] = static struct config_schema g_config_settings[] =
{ {
{ .name = "address", { .name = "address",
@@ -698,16 +741,29 @@ static struct config_schema g_config_settings[] =
{ .name = "password", { .name = "password",
.comment = "Password to use for MPD authentication", .comment = "Password to use for MPD authentication",
.type = CONFIG_ITEM_STRING }, .type = CONFIG_ITEM_STRING },
// NOTE: this is unused--in theory we could allow manual metadata adjustment
{ .name = "root", { .name = "root",
.comment = "Where all the files MPD is playing are located", .comment = "Where all the files MPD is playing are located",
.type = CONFIG_ITEM_STRING }, .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_ = "on" },
{} {}
}; };
static struct config_schema g_config_colors[] = static struct config_schema g_config_colors[] =
{ {
#define XX(name_, config, fg_, bg_, attrs_) \ #define XX(name_, config, fg_, bg_, attrs_) \
{ .name = config, .type = CONFIG_ITEM_STRING }, { .name = #config, .type = CONFIG_ITEM_STRING },
ATTRIBUTE_TABLE (XX) ATTRIBUTE_TABLE (XX)
#undef XX #undef XX
{} {}
@@ -742,7 +798,7 @@ load_config_colors (struct config_item *subtree, void *user_data)
// For simplicity, we should reload the entire table on each change anyway. // For simplicity, we should reload the entire table on each change anyway.
const char *value; const char *value;
#define XX(name, config, fg_, bg_, attrs_) \ #define XX(name, config, fg_, bg_, attrs_) \
if ((value = get_config_string (subtree, config))) \ if ((value = get_config_string (subtree, #config))) \
g.attrs[ATTRIBUTE_ ## name] = attrs_decode (value); g.attrs[ATTRIBUTE_ ## name] = attrs_decode (value);
ATTRIBUTE_TABLE (XX) ATTRIBUTE_TABLE (XX)
#undef XX #undef XX
@@ -1528,67 +1584,69 @@ app_goto_tab (int tab_index)
// --- Actions ----------------------------------------------------------------- // --- Actions -----------------------------------------------------------------
#define ACTIONS(XX) \ #define ACTIONS(XX) \
XX( NONE, "Do nothing" ) \ XX( NONE, Do nothing ) \
\ \
XX( QUIT, "Quit" ) \ XX( QUIT, Quit ) \
XX( REDRAW, "Redraw screen" ) \ XX( REDRAW, Redraw screen ) \
XX( HELP_TAB, "Switch to help tab" ) \ XX( TAB_HELP, Switch to help tab ) \
XX( LAST_TAB, "Switch to previous tab" ) \ XX( TAB_LAST, Switch to last tab ) \
XX( TAB_PREVIOUS, Switch to previous tab ) \
XX( TAB_NEXT, Switch to next tab ) \
\ \
XX( MPD_TOGGLE, "Toggle play/pause" ) \ XX( MPD_TOGGLE, Toggle play/pause ) \
XX( MPD_STOP, "Stop playback" ) \ XX( MPD_STOP, Stop playback ) \
XX( MPD_PREVIOUS, "Previous song" ) \ XX( MPD_PREVIOUS, Previous song ) \
XX( MPD_NEXT, "Next song" ) \ XX( MPD_NEXT, Next song ) \
XX( MPD_BACKWARD, "Seek backwards" ) \ XX( MPD_BACKWARD, Seek backwards ) \
XX( MPD_FORWARD, "Seek forwards" ) \ XX( MPD_FORWARD, Seek forwards ) \
XX( MPD_VOLUME_UP, "Increase volume" ) \ XX( MPD_VOLUME_UP, Increase volume ) \
XX( MPD_VOLUME_DOWN, "Decrease volume" ) \ XX( MPD_VOLUME_DOWN, Decrease volume ) \
\ \
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_REPEAT, Toggle repeat ) \
XX( MPD_RANDOM, "Toggle random playback" ) \ XX( MPD_RANDOM, Toggle random playback ) \
XX( MPD_SINGLE, "Toggle single song playback" ) \ XX( MPD_SINGLE, Toggle single song playback ) \
XX( MPD_CONSUME, "Toggle consume" ) \ 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 ) \
\ \
XX( CHOOSE, "Choose item" ) \ XX( CHOOSE, Choose item ) \
XX( DELETE, "Delete item" ) \ XX( DELETE, Delete item ) \
XX( UP, "Go up a level" ) \ XX( UP, Go up a level ) \
XX( MULTISELECT, "Toggle multiselect" ) \ XX( MULTISELECT, Toggle multiselect ) \
\ \
XX( SCROLL_UP, "Scroll up" ) \ XX( SCROLL_UP, Scroll up ) \
XX( SCROLL_DOWN, "Scroll down" ) \ XX( SCROLL_DOWN, Scroll down ) \
XX( MOVE_UP, "Move selection up" ) \ XX( MOVE_UP, Move selection up ) \
XX( MOVE_DOWN, "Move selection down" ) \ XX( MOVE_DOWN, Move selection down ) \
\ \
XX( GOTO_TOP, "Go to top" ) \ XX( GOTO_TOP, Go to top ) \
XX( GOTO_BOTTOM, "Go to bottom" ) \ XX( GOTO_BOTTOM, Go to bottom ) \
XX( GOTO_ITEM_PREVIOUS, "Go to previous item" ) \ XX( GOTO_ITEM_PREVIOUS, Go to previous item ) \
XX( GOTO_ITEM_NEXT, "Go to next item" ) \ XX( GOTO_ITEM_NEXT, Go to next item ) \
XX( GOTO_PAGE_PREVIOUS, "Go to previous page" ) \ XX( GOTO_PAGE_PREVIOUS, Go to previous page ) \
XX( GOTO_PAGE_NEXT, "Go to next page" ) \ XX( GOTO_PAGE_NEXT, Go to next page ) \
\ \
XX( GOTO_VIEW_TOP, "Select top item" ) \ XX( GOTO_VIEW_TOP, Select top item ) \
XX( GOTO_VIEW_CENTER, "Select center item" ) \ XX( GOTO_VIEW_CENTER, Select center item ) \
XX( GOTO_VIEW_BOTTOM, "Select bottom item" ) \ XX( GOTO_VIEW_BOTTOM, Select bottom item ) \
\ \
XX( EDITOR_CONFIRM, "Confirm input" ) \ XX( EDITOR_CONFIRM, Confirm input ) \
\ \
XX( EDITOR_B_CHAR, "Go back a character" ) \ XX( EDITOR_B_CHAR, Go back a character ) \
XX( EDITOR_F_CHAR, "Go forward a character" ) \ XX( EDITOR_F_CHAR, Go forward a character ) \
XX( EDITOR_B_WORD, "Go back a word" ) \ XX( EDITOR_B_WORD, Go back a word ) \
XX( EDITOR_F_WORD, "Go forward a word" ) \ XX( EDITOR_F_WORD, Go forward a word ) \
XX( EDITOR_HOME, "Go to start of line" ) \ XX( EDITOR_HOME, Go to start of line ) \
XX( EDITOR_END, "Go to end of line" ) \ XX( EDITOR_END, Go to end of line ) \
\ \
XX( EDITOR_B_DELETE, "Delete last character" ) \ XX( EDITOR_B_DELETE, Delete last character ) \
XX( EDITOR_F_DELETE, "Delete next character" ) \ XX( EDITOR_F_DELETE, Delete next character ) \
XX( EDITOR_B_KILL_WORD, "Delete last word" ) \ XX( EDITOR_B_KILL_WORD, Delete last word ) \
XX( EDITOR_B_KILL_LINE, "Delete everything up to BOL" ) \ XX( EDITOR_B_KILL_LINE, Delete everything up to BOL ) \
XX( EDITOR_F_KILL_LINE, "Delete everything up to EOL" ) XX( EDITOR_F_KILL_LINE, Delete everything up to EOL )
enum action enum action
{ {
@@ -1605,7 +1663,7 @@ static struct action_info
} }
g_actions[] = g_actions[] =
{ {
#define XX(name, description) { #name, description }, #define XX(name, description) { #name, #description },
ACTIONS (XX) ACTIONS (XX)
#undef XX #undef XX
}; };
@@ -1759,14 +1817,30 @@ app_process_action (enum action action)
tab->item_mark = tab->item_selected; tab->item_mark = tab->item_selected;
return true; return true;
case ACTION_LAST_TAB: case ACTION_TAB_LAST:
if (!g.last_tab) if (!g.last_tab)
return false; return false;
app_switch_tab (g.last_tab); app_switch_tab (g.last_tab);
return true; return true;
case ACTION_HELP_TAB: case ACTION_TAB_HELP:
app_switch_tab (g.help_tab); app_switch_tab (g.help_tab);
return true; return true;
case ACTION_TAB_PREVIOUS:
if (g.active_tab == g.help_tab)
return false;
if (!g.active_tab->prev)
app_switch_tab (g.help_tab);
else
app_switch_tab (g.active_tab->prev);
return true;
case ACTION_TAB_NEXT:
if (g.active_tab == g.help_tab)
app_switch_tab (g.tabs);
else if (g.active_tab->next)
app_switch_tab (g.active_tab->next);
else
return false;
return true;
case ACTION_MPD_TOGGLE: case ACTION_MPD_TOGGLE:
if (g.state == PLAYER_PLAYING) return MPD_SIMPLE ("pause", "1"); if (g.state == PLAYER_PLAYING) return MPD_SIMPLE ("pause", "1");
@@ -1987,8 +2061,12 @@ g_normal_defaults[] =
{ "Escape", ACTION_QUIT }, { "Escape", ACTION_QUIT },
{ "q", ACTION_QUIT }, { "q", ACTION_QUIT },
{ "C-l", ACTION_REDRAW }, { "C-l", ACTION_REDRAW },
{ "M-Tab", ACTION_LAST_TAB }, { "M-Tab", ACTION_TAB_LAST },
{ "F1", ACTION_HELP_TAB }, { "F1", ACTION_TAB_HELP },
{ "C-Left", ACTION_TAB_PREVIOUS },
{ "C-Right", ACTION_TAB_NEXT },
{ "C-PageUp", ACTION_TAB_PREVIOUS },
{ "C-PageDown", ACTION_TAB_NEXT },
{ "Home", ACTION_GOTO_TOP }, { "Home", ACTION_GOTO_TOP },
{ "End", ACTION_GOTO_BOTTOM }, { "End", ACTION_GOTO_BOTTOM },
@@ -2008,6 +2086,8 @@ g_normal_defaults[] =
{ "C-n", ACTION_GOTO_ITEM_NEXT }, { "C-n", ACTION_GOTO_ITEM_NEXT },
{ "C-b", ACTION_GOTO_PAGE_PREVIOUS }, { "C-b", ACTION_GOTO_PAGE_PREVIOUS },
{ "C-f", ACTION_GOTO_PAGE_NEXT }, { "C-f", ACTION_GOTO_PAGE_NEXT },
{ "C-y", ACTION_SCROLL_UP },
{ "C-e", ACTION_SCROLL_DOWN },
{ "H", ACTION_GOTO_VIEW_TOP }, { "H", ACTION_GOTO_VIEW_TOP },
{ "M", ACTION_GOTO_VIEW_CENTER }, { "M", ACTION_GOTO_VIEW_CENTER },
@@ -2017,6 +2097,7 @@ g_normal_defaults[] =
{ "Enter", ACTION_CHOOSE }, { "Enter", ACTION_CHOOSE },
{ "Delete", ACTION_DELETE }, { "Delete", ACTION_DELETE },
{ "d", ACTION_DELETE }, { "d", ACTION_DELETE },
{ "M-Up", ACTION_UP },
{ "Backspace", ACTION_UP }, { "Backspace", ACTION_UP },
{ "v", ACTION_MULTISELECT }, { "v", ACTION_MULTISELECT },
{ "/", ACTION_MPD_SEARCH }, { "/", ACTION_MPD_SEARCH },
@@ -2175,13 +2256,13 @@ app_process_termo_event (termo_key_t *event)
static struct tab g_current_tab; static struct tab g_current_tab;
#define DURATION_MAX_LEN (1 /*separator */ + 2 /* h */ + 3 /* m */+ 3 /* s */)
static void static void
current_tab_on_item_draw (size_t item_index, struct row_buffer *buffer, current_tab_on_item_draw (size_t item_index, struct row_buffer *buffer,
int width) int width)
{ {
// TODO: configurable output, maybe dynamically sized columns // 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); compact_map_t map = item_list_get (&g.playlist, item_index);
const char *artist = compact_map_find (map, "artist"); const char *artist = compact_map_find (map, "artist");
const char *title = compact_map_find (map, "title"); const char *title = compact_map_find (map, "title");
@@ -2193,15 +2274,14 @@ current_tab_on_item_draw (size_t item_index, struct row_buffer *buffer,
else else
row_buffer_append (buffer, compact_map_find (map, "file"), attrs); 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; int duration = -1;
unsigned long n; mpd_read_time (compact_map_find (map, "duration"), &duration, NULL);
const char *time = compact_map_find (map, "time"); mpd_read_time (compact_map_find (map, "time"), &duration, NULL);
if (!time || !xstrtoul (&n, time, 10) || !(s = app_time_string (n)))
s = xstrdup ("?");
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); row_buffer_append (buffer, right_aligned, attrs);
free (right_aligned); free (right_aligned);
free (s); free (s);
@@ -2334,17 +2414,6 @@ struct library_level
char path[]; ///< Path of the 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 enum
{ {
// This list is also ordered by ASCII and important for sorting // This list is also ordered by ASCII and important for sorting
@@ -2352,31 +2421,46 @@ enum
LIBRARY_ROOT = '/', ///< Root entry LIBRARY_ROOT = '/', ///< Root entry
LIBRARY_UP = '^', ///< Upper directory LIBRARY_UP = '^', ///< Upper directory
LIBRARY_DIR = 'd', ///< Directory LIBRARY_DIR = 'd', ///< Directory
LIBRARY_FILE = 'f' ///< File LIBRARY_FILE = 'f', ///< File
LIBRARY_PLAYLIST = 'p', ///< Playlist (unsupported)
}; };
struct library_tab_item struct library_tab_item
{ {
int type; ///< Type of the item int type; ///< Type of the item
const char *name; ///< Visible name int duration; ///< Duration or -1 if N/A or unknown
const char *path; ///< MPD path char *name; ///< Visible name
const char *path; ///< MPD path (follows the name)
}; };
static void static struct
library_tab_add (int type, const char *name, const char *path)
{ {
strv_append_owned (&g_library_tab.items, struct tab super; ///< Parent class
xstrdup_printf ("%c%s%c%s", type, name, 0, path)); struct str path; ///< Current path
} struct library_level *above; ///< Upper levels
static struct library_tab_item /// Current items
library_tab_resolve (const char *raw) 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; // Slightly reduce memory overhead while retaining friendly access
item.type = *raw++; size_t name_len = strlen (name), path_len = strlen (path);
item.name = raw; char *combined = xmalloc (++name_len + ++path_len);
item.path = strchr (raw, '\0') + 1;
return item; 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),
};
} }
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@@ -2386,21 +2470,27 @@ library_tab_on_item_draw (size_t item_index, struct row_buffer *buffer,
int width) int width)
{ {
(void) 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 = struct library_tab_item *x = &g_library_tab.items[item_index];
library_tab_resolve (g_library_tab.items.vector[item_index]);
const char *prefix, *name; const char *prefix, *name;
switch (x.type) switch (x->type)
{ {
case LIBRARY_ROOT: prefix = "/"; name = ""; break; case LIBRARY_ROOT: prefix = "/"; name = ""; break;
case LIBRARY_UP: prefix = "/"; name = ".."; break; case LIBRARY_UP: prefix = "/"; name = ".."; break;
case LIBRARY_DIR: prefix = "/"; name = x.name; break; case LIBRARY_DIR: prefix = "/"; name = x->name; break;
case LIBRARY_FILE: prefix = " "; name = x.name; break; case LIBRARY_FILE: prefix = " "; name = x->name; break;
default: hard_assert (!"invalid item type"); 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); 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 static char
@@ -2408,31 +2498,38 @@ library_tab_header_type (const char *key)
{ {
if (!strcasecmp_ascii (key, "file")) return LIBRARY_FILE; if (!strcasecmp_ascii (key, "file")) return LIBRARY_FILE;
if (!strcasecmp_ascii (key, "directory")) return LIBRARY_DIR; if (!strcasecmp_ascii (key, "directory")) return LIBRARY_DIR;
if (!strcasecmp_ascii (key, "playlist")) return LIBRARY_PLAYLIST;
return 0; return 0;
} }
static void static void
library_tab_chunk (char type, const char *path, struct str_map *map) 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 *artist = str_map_find (map, "artist");
const char *title = str_map_find (map, "title"); const char *title = str_map_find (map, "title");
char *name = (artist && title) char *name = (artist && title)
? xstrdup_printf ("%s - %s", artist, title) ? xstrdup_printf ("%s - %s", artist, title)
: xstrdup (xbasename (path)); : 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); free (name);
} }
static int 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); if (a->type != b->type)
struct library_tab_item xb = library_tab_resolve (*b); return a->type - b->type;
if (xa.type != xb.type) return app_casecmp ((uint8_t *) a->path, (uint8_t *) b->path);
return xa.type - xb.type;
return app_casecmp ((uint8_t *) xa.path, (uint8_t *) xb.path);
} }
static char * static char *
@@ -2493,24 +2590,32 @@ library_tab_change_level (const char *new_path)
str_reset (path); str_reset (path);
str_append (path, new_path); str_append (path, new_path);
free (g_library_tab.super.header); cstr_set (&g_library_tab.super.header, NULL);
g_library_tab.super.header = NULL;
g_library_tab.super.item_mark = -1; 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);
} }
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 static void
library_tab_load_data (const struct strv *data) library_tab_load_data (const struct strv *data)
{ {
strv_reset (&g_library_tab.items); library_tab_reset ();
char *parent = library_tab_parent (); char *parent = library_tab_parent ();
if (parent) if (parent)
{ {
library_tab_add (LIBRARY_ROOT, "", ""); library_tab_add (LIBRARY_ROOT, -1, "", "");
library_tab_add (LIBRARY_UP, "", parent); library_tab_add (LIBRARY_UP, -1, "", parent);
free (parent); free (parent);
} }
@@ -2530,16 +2635,16 @@ library_tab_load_data (const struct strv *data)
} }
str_map_free (&map); str_map_free (&map);
struct strv *items = &g_library_tab.items; struct library_tab_item *items = g_library_tab.items;
qsort (items->vector, items->len, sizeof *items->vector, 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); (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 // XXX: this unmarks even if just the database updates
g_library_tab.super.item_mark = -1; g_library_tab.super.item_mark = -1;
// Don't force the selection visible when there's no need to touch it // 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_move_selection (0);
app_invalidate (); app_invalidate ();
@@ -2627,9 +2732,8 @@ library_tab_is_range_playable (struct tab_range range)
{ {
for (int i = range.from; i <= range.upto; i++) for (int i = range.from; i <= range.upto; i++)
{ {
struct library_tab_item x = struct library_tab_item *x = &g_library_tab.items[i];
library_tab_resolve (g_library_tab.items.vector[i]); if (x->type == LIBRARY_DIR || x->type == LIBRARY_FILE)
if (x.type == LIBRARY_DIR || x.type == LIBRARY_FILE)
return true; return true;
} }
return false; return false;
@@ -2647,9 +2751,7 @@ library_tab_on_action (enum action action)
if (range.from < 0) if (range.from < 0)
return false; return false;
struct library_tab_item x = struct library_tab_item *x = &g_library_tab.items[range.from];
library_tab_resolve (g_library_tab.items.vector[range.from]);
switch (action) switch (action)
{ {
case ACTION_CHOOSE: case ACTION_CHOOSE:
@@ -2657,12 +2759,12 @@ library_tab_on_action (enum action action)
if (range.from != range.upto) if (range.from != range.upto)
break; break;
switch (x.type) switch (x->type)
{ {
case LIBRARY_ROOT: case LIBRARY_ROOT:
case LIBRARY_UP: case LIBRARY_UP:
case LIBRARY_DIR: library_tab_reload (x.path); break; case LIBRARY_DIR: library_tab_reload (x->path); break;
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; tab->item_mark = -1;
@@ -2692,8 +2794,7 @@ library_tab_on_action (enum action action)
free (fake_subdir); free (fake_subdir);
} }
free (tab->header); cstr_set (&tab->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,
@@ -2712,10 +2813,9 @@ library_tab_on_action (enum action action)
for (int i = range.from; i <= range.upto; i++) for (int i = range.from; i <= range.upto; i++)
{ {
struct library_tab_item x = struct library_tab_item *x = &g_library_tab.items[i];
library_tab_resolve (g_library_tab.items.vector[i]); 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; tab->item_mark = -1;
return true; return true;
@@ -2731,10 +2831,9 @@ library_tab_on_action (enum action action)
mpd_client_send_command (c, "clear", NULL); mpd_client_send_command (c, "clear", NULL);
for (int i = range.from; i <= range.upto; i++) for (int i = range.from; i <= range.upto; i++)
{ {
struct library_tab_item x = struct library_tab_item *x = &g_library_tab.items[i];
library_tab_resolve (g_library_tab.items.vector[i]); if (x->type == LIBRARY_DIR || x->type == LIBRARY_FILE)
if (x.type == LIBRARY_DIR || x.type == LIBRARY_FILE) mpd_client_send_command (c, "add", x->path, NULL);
mpd_client_send_command (c, "add", x.path, NULL);
} }
if (g.state == PLAYER_PLAYING) if (g.state == PLAYER_PLAYING)
mpd_client_send_command (c, "play", NULL); mpd_client_send_command (c, "play", NULL);
@@ -2754,7 +2853,7 @@ static struct tab *
library_tab_init (void) library_tab_init (void)
{ {
g_library_tab.path = str_make (); 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; struct tab *super = &g_library_tab.super;
tab_init (super, "Library"); tab_init (super, "Library");
@@ -2849,7 +2948,7 @@ streams_tab_extract_links (struct str *data, const char *content_type,
for (size_t i = 0; i < data->len; i++) for (size_t i = 0; i < data->len; i++)
{ {
uint8_t c = data->str[i]; uint8_t c = data->str[i];
if ((c < 32) & (c != '\t') & (c != '\r') & (c != '\n')) if (iscntrl_ascii (c) & (c != '\t') & (c != '\r') & (c != '\n'))
return false; return false;
} }
@@ -2943,6 +3042,8 @@ streams_tab_process (const char *uri, bool replace, struct error **e)
task.replace = replace; task.replace = replace;
bool result = false; bool result = false;
struct curl_slist *ok_headers = curl_slist_append (NULL, "ICY 200 OK");
CURLcode res; CURLcode res;
if ((res = curl_easy_setopt (easy, CURLOPT_FOLLOWLOCATION, 1L)) if ((res = curl_easy_setopt (easy, CURLOPT_FOLLOWLOCATION, 1L))
|| (res = curl_easy_setopt (easy, CURLOPT_NOPROGRESS, 1L)) || (res = curl_easy_setopt (easy, CURLOPT_NOPROGRESS, 1L))
@@ -2952,6 +3053,7 @@ streams_tab_process (const char *uri, bool replace, struct error **e)
|| (res = curl_easy_setopt (easy, CURLOPT_SSL_VERIFYPEER, 0L)) || (res = curl_easy_setopt (easy, CURLOPT_SSL_VERIFYPEER, 0L))
|| (res = curl_easy_setopt (easy, CURLOPT_SSL_VERIFYHOST, 0L)) || (res = curl_easy_setopt (easy, CURLOPT_SSL_VERIFYHOST, 0L))
|| (res = curl_easy_setopt (easy, CURLOPT_URL, uri)) || (res = curl_easy_setopt (easy, CURLOPT_URL, uri))
|| (res = curl_easy_setopt (easy, CURLOPT_HTTP200ALIASES, ok_headers))
|| (res = curl_easy_setopt (easy, CURLOPT_VERBOSE, (long) g_debug_mode)) || (res = curl_easy_setopt (easy, CURLOPT_VERBOSE, (long) g_debug_mode))
|| (res = curl_easy_setopt (easy, CURLOPT_DEBUGFUNCTION, print_curl_debug)) || (res = curl_easy_setopt (easy, CURLOPT_DEBUGFUNCTION, print_curl_debug))
@@ -2975,6 +3077,7 @@ streams_tab_process (const char *uri, bool replace, struct error **e)
error: error:
curl_easy_cleanup (task.curl.easy); curl_easy_cleanup (task.curl.easy);
curl_slist_free_all (ok_headers);
str_free (&task.data); str_free (&task.data);
poller_curl_free (&pc); poller_curl_free (&pc);
@@ -3306,25 +3409,6 @@ debug_tab_init (void)
// --- MPD interface ----------------------------------------------------------- // --- 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 static void
mpd_update_playlist_time (void) mpd_update_playlist_time (void)
{ {
@@ -3341,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 static void
mpd_update_playback_state (void) mpd_update_playback_state (void)
{ {
@@ -3377,13 +3488,11 @@ mpd_update_playback_state (void)
mpd_read_time (duration, &g.song_duration, NULL); mpd_read_time (duration, &g.song_duration, NULL);
strv_free (&fields); strv_free (&fields);
// We could also just poll the server each half a second but let's not
poller_timer_reset (&g.elapsed_event); poller_timer_reset (&g.elapsed_event);
if (g.state == PLAYER_PLAYING) if (g.state == PLAYER_PLAYING)
{ mpd_set_elapsed_timer (msec_past_second);
poller_timer_set (&g.elapsed_event, 1000 - msec_past_second); else
g.elapsed_since = clock_msec (CLOCK_BEST) - msec_past_second; g.elapsed_since = -1;
}
// The server sends -1 when nothing is being played right now // The server sends -1 when nothing is being played right now
unsigned long n; unsigned long n;
@@ -3508,15 +3617,19 @@ mpd_on_info_response (const struct mpd_response *response,
} }
static void static void
mpd_on_tick (void *user_data) mpd_on_elapsed_time_tick (void *user_data)
{ {
(void) user_data; (void) user_data;
// Compute how much time has elapsed since the last round second
int64_t diff_msec = clock_msec (CLOCK_BEST) - g.elapsed_since; int64_t diff_msec = clock_msec (CLOCK_BEST) - g.elapsed_since;
int elapsed_sec = diff_msec / 1000; int elapsed_sec = diff_msec / 1000;
int elapsed_msec = diff_msec % 1000; int elapsed_msec = diff_msec % 1000;
g.song_elapsed += elapsed_sec; g.song_elapsed += elapsed_sec;
g.elapsed_since += elapsed_sec * 1000; g.elapsed_since += elapsed_sec * 1000;
// Try to get called on the next round second of playback
poller_timer_set (&g.elapsed_event, 1000 - elapsed_msec); poller_timer_set (&g.elapsed_event, 1000 - elapsed_msec);
app_invalidate (); app_invalidate ();
@@ -3537,6 +3650,15 @@ mpd_request_info (void)
mpd_client_idle (c, 0); mpd_client_idle (c, 0);
} }
static void
mpd_on_elapsed_time_tick_poll (void *user_data)
{
(void) user_data;
// As soon as the reply arrives, we (may) set the timer again
mpd_request_info ();
}
static void static void
mpd_on_events (unsigned subsystems, void *user_data) mpd_on_events (unsigned subsystems, void *user_data)
{ {
@@ -3829,8 +3951,7 @@ app_on_message_timer (void *user_data)
{ {
(void) user_data; (void) user_data;
free (g.message); cstr_set (&g.message, NULL);
g.message = NULL;
app_invalidate (); app_invalidate ();
} }
@@ -3858,8 +3979,7 @@ app_log_handler (void *user_data, const char *quote, const char *fmt,
user_data == NULL ? 0 : g.attrs[(intptr_t) user_data].attrs); user_data == NULL ? 0 : g.attrs[(intptr_t) user_data].attrs);
else else
{ {
free (g.message); cstr_set (&g.message, xstrdup (message.str));
g.message = xstrdup (message.str);
app_invalidate (); app_invalidate ();
poller_timer_set (&g.message_timer, 5000); poller_timer_set (&g.message_timer, 5000);
} }
@@ -3890,7 +4010,9 @@ app_init_poller_events (void)
poller_timer_set (&g.connect_event, 0); poller_timer_set (&g.connect_event, 0);
g.elapsed_event = poller_timer_make (&g.poller); g.elapsed_event = poller_timer_make (&g.poller);
g.elapsed_event.dispatcher = mpd_on_tick; g.elapsed_event.dispatcher = g.elapsed_poll
? mpd_on_elapsed_time_tick_poll
: mpd_on_elapsed_time_tick;
g.refresh_event = poller_idle_make (&g.poller); g.refresh_event = poller_idle_make (&g.poller);
g.refresh_event.dispatcher = app_on_refresh; g.refresh_event.dispatcher = app_on_refresh;
@@ -3908,7 +4030,7 @@ main (int argc, char *argv[])
}; };
struct opt_handler oh = 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; int c;
while ((c = opt_handler_get (&oh)) != -1) while ((c = opt_handler_get (&oh)) != -1)

2
termo

Submodule termo updated: 30e0eee1a8...f7912a8ce7