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 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,17 +36,23 @@ 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})
include_directories (${Unistring_INCLUDE_DIRS}
${Ncursesw_INCLUDE_DIRS} ${Termo_INCLUDE_DIRS} ${curl_INCLUDE_DIRS})
link_directories (${curl_LIBRARY_DIRS})
# Configuration
include (CheckFunctionExists)
set (CMAKE_REQUIRED_LIBRARIES ${NCURSESW_LIBRARIES})
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)
@@ -63,30 +60,33 @@ include_directories (${PROJECT_SOURCE_DIR} ${PROJECT_BINARY_DIR})
# Build the main executable and link it
add_executable (${PROJECT_NAME} ${PROJECT_NAME}.c)
target_link_libraries (${PROJECT_NAME} ${UNISTRING_LIBRARIES}
${NCURSESW_LIBRARIES} termo-static ${curl_LIBRARIES})
target_link_libraries (${PROJECT_NAME} ${Unistring_LIBRARIES}
${Ncursesw_LIBRARIES} termo-static ${curl_LIBRARIES})
add_threads (${PROJECT_NAME})
# Installation
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 Janouch")
set (CPACK_PACKAGE_CONTACT "Přemysl Janouch <p@janouch.name>")
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)

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
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)
* 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: bb30c7d86e...d71c47f8ce

View File

@@ -1,7 +1,7 @@
/*
* 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
* 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
*
* 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
* purpose with or without fee is hereby granted.
@@ -21,30 +21,30 @@
// We "need" to have an enum for attributes before including liberty.
// Avoiding colours in the defaults here in order to support dumb terminals.
#define ATTRIBUTE_TABLE(XX) \
XX( NORMAL, "normal", -1, -1, 0 ) \
XX( HIGHLIGHT, "highlight", -1, -1, A_BOLD ) \
XX( NORMAL, normal, -1, -1, 0 ) \
XX( HIGHLIGHT, highlight, -1, -1, A_BOLD ) \
/* Gauge */ \
XX( ELAPSED, "elapsed", -1, -1, A_REVERSE ) \
XX( REMAINS, "remains", -1, -1, A_UNDERLINE ) \
XX( ELAPSED, elapsed, -1, -1, A_REVERSE ) \
XX( REMAINS, remains, -1, -1, A_UNDERLINE ) \
/* Tab bar */ \
XX( TAB_BAR, "tab_bar", -1, -1, A_REVERSE ) \
XX( TAB_ACTIVE, "tab_active", -1, -1, A_UNDERLINE ) \
XX( TAB_BAR, tab_bar, -1, -1, A_REVERSE ) \
XX( TAB_ACTIVE, tab_active, -1, -1, A_UNDERLINE ) \
/* Listview */ \
XX( HEADER, "header", -1, -1, A_UNDERLINE ) \
XX( EVEN, "even", -1, -1, 0 ) \
XX( ODD, "odd", -1, -1, 0 ) \
XX( DIRECTORY, "directory", -1, -1, 0 ) \
XX( SELECTION, "selection", -1, -1, A_REVERSE ) \
XX( HEADER, header, -1, -1, A_UNDERLINE ) \
XX( EVEN, even, -1, -1, 0 ) \
XX( ODD, odd, -1, -1, 0 ) \
XX( DIRECTORY, directory, -1, -1, 0 ) \
XX( SELECTION, selection, -1, -1, A_REVERSE ) \
/* Cyan is good with both black and white.
* Can't use A_REVERSE because bold'd be bright.
* Unfortunately ran out of B&W attributes. */ \
XX( MULTISELECT, "multiselect",-1, 6, 0 ) \
XX( SCROLLBAR, "scrollbar", -1, -1, 0 ) \
XX( MULTISELECT, multiselect, -1, 6, 0 ) \
XX( SCROLLBAR, scrollbar, -1, -1, 0 ) \
/* These are for debugging only */ \
XX( WARNING, "warning", 3, -1, 0 ) \
XX( ERROR, "error", 1, -1, 0 ) \
XX( INCOMING, "incoming", 2, -1, 0 ) \
XX( OUTGOING, "outgoing", 4, -1, 0 )
XX( WARNING, warning, 3, -1, 0 ) \
XX( ERROR, error, 1, -1, 0 ) \
XX( INCOMING, incoming, 2, -1, 0 ) \
XX( OUTGOING, outgoing, 4, -1, 0 )
enum
{
@@ -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"
@@ -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++)
{
uint8_t c = data[i];
copy[i] = c >= 32 || c == '\n' ? c : '.';
copy[i] = !iscntrl_ascii (c) || c == '\n' ? c : '.';
}
copy[len] = '\0';
@@ -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
@@ -286,6 +318,9 @@ poller_curl_on_socket_action (CURL *easy, curl_socket_t s, int what,
}
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);
LIST_UNLINK (self->fds, fd);
free (fd);
@@ -606,7 +641,8 @@ 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 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
int song; ///< Current song index
@@ -689,6 +725,13 @@ tab_selection_range (struct tab *self)
// --- 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[] =
{
{ .name = "address",
@@ -698,16 +741,29 @@ 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_ = "on" },
{}
};
static struct config_schema g_config_colors[] =
{
#define XX(name_, config, fg_, bg_, attrs_) \
{ .name = config, .type = CONFIG_ITEM_STRING },
{ .name = #config, .type = CONFIG_ITEM_STRING },
ATTRIBUTE_TABLE (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.
const char *value;
#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);
ATTRIBUTE_TABLE (XX)
#undef XX
@@ -1528,67 +1584,69 @@ app_goto_tab (int tab_index)
// --- Actions -----------------------------------------------------------------
#define ACTIONS(XX) \
XX( NONE, "Do nothing" ) \
XX( NONE, Do nothing ) \
\
XX( QUIT, "Quit" ) \
XX( REDRAW, "Redraw screen" ) \
XX( HELP_TAB, "Switch to help tab" ) \
XX( LAST_TAB, "Switch to previous tab" ) \
XX( QUIT, Quit ) \
XX( REDRAW, Redraw screen ) \
XX( TAB_HELP, Switch to help 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_STOP, "Stop playback" ) \
XX( MPD_PREVIOUS, "Previous song" ) \
XX( MPD_NEXT, "Next song" ) \
XX( MPD_BACKWARD, "Seek backwards" ) \
XX( MPD_FORWARD, "Seek forwards" ) \
XX( MPD_VOLUME_UP, "Increase volume" ) \
XX( MPD_VOLUME_DOWN, "Decrease volume" ) \
XX( MPD_TOGGLE, Toggle play/pause ) \
XX( MPD_STOP, Stop playback ) \
XX( MPD_PREVIOUS, Previous song ) \
XX( MPD_NEXT, Next song ) \
XX( MPD_BACKWARD, Seek backwards ) \
XX( MPD_FORWARD, Seek forwards ) \
XX( MPD_VOLUME_UP, Increase volume ) \
XX( MPD_VOLUME_DOWN, Decrease volume ) \
\
XX( MPD_SEARCH, "Global search" ) \
XX( MPD_ADD, "Add selection to playlist" ) \
XX( MPD_REPLACE, "Replace playlist" ) \
XX( MPD_REPEAT, "Toggle repeat" ) \
XX( MPD_RANDOM, "Toggle random playback" ) \
XX( MPD_SINGLE, "Toggle single song playback" ) \
XX( MPD_CONSUME, "Toggle consume" ) \
XX( MPD_UPDATE_DB, "Update MPD database" ) \
XX( MPD_COMMAND, "Send raw command to MPD" ) \
XX( MPD_SEARCH, Global search ) \
XX( MPD_ADD, Add selection to playlist ) \
XX( MPD_REPLACE, Replace playlist ) \
XX( MPD_REPEAT, Toggle repeat ) \
XX( MPD_RANDOM, Toggle random playback ) \
XX( MPD_SINGLE, Toggle single song playback ) \
XX( MPD_CONSUME, Toggle consume ) \
XX( MPD_UPDATE_DB, Update MPD database ) \
XX( MPD_COMMAND, Send raw command to MPD ) \
\
XX( CHOOSE, "Choose item" ) \
XX( DELETE, "Delete item" ) \
XX( UP, "Go up a level" ) \
XX( MULTISELECT, "Toggle multiselect" ) \
XX( CHOOSE, Choose item ) \
XX( DELETE, Delete item ) \
XX( UP, Go up a level ) \
XX( MULTISELECT, Toggle multiselect ) \
\
XX( SCROLL_UP, "Scroll up" ) \
XX( SCROLL_DOWN, "Scroll down" ) \
XX( MOVE_UP, "Move selection up" ) \
XX( MOVE_DOWN, "Move selection down" ) \
XX( SCROLL_UP, Scroll up ) \
XX( SCROLL_DOWN, Scroll down ) \
XX( MOVE_UP, Move selection up ) \
XX( MOVE_DOWN, Move selection down ) \
\
XX( GOTO_TOP, "Go to top" ) \
XX( GOTO_BOTTOM, "Go to bottom" ) \
XX( GOTO_ITEM_PREVIOUS, "Go to previous item" ) \
XX( GOTO_ITEM_NEXT, "Go to next item" ) \
XX( GOTO_PAGE_PREVIOUS, "Go to previous page" ) \
XX( GOTO_PAGE_NEXT, "Go to next page" ) \
XX( GOTO_TOP, Go to top ) \
XX( GOTO_BOTTOM, Go to bottom ) \
XX( GOTO_ITEM_PREVIOUS, Go to previous item ) \
XX( GOTO_ITEM_NEXT, Go to next item ) \
XX( GOTO_PAGE_PREVIOUS, Go to previous page ) \
XX( GOTO_PAGE_NEXT, Go to next page ) \
\
XX( GOTO_VIEW_TOP, "Select top item" ) \
XX( GOTO_VIEW_CENTER, "Select center item" ) \
XX( GOTO_VIEW_BOTTOM, "Select bottom item" ) \
XX( GOTO_VIEW_TOP, Select top item ) \
XX( GOTO_VIEW_CENTER, Select center 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_F_CHAR, "Go forward a character" ) \
XX( EDITOR_B_WORD, "Go back a word" ) \
XX( EDITOR_F_WORD, "Go forward a word" ) \
XX( EDITOR_HOME, "Go to start of line" ) \
XX( EDITOR_END, "Go to end of line" ) \
XX( EDITOR_B_CHAR, Go back a character ) \
XX( EDITOR_F_CHAR, Go forward a character ) \
XX( EDITOR_B_WORD, Go back a word ) \
XX( EDITOR_F_WORD, Go forward a word ) \
XX( EDITOR_HOME, Go to start of line ) \
XX( EDITOR_END, Go to end of line ) \
\
XX( EDITOR_B_DELETE, "Delete last character" ) \
XX( EDITOR_F_DELETE, "Delete next character" ) \
XX( EDITOR_B_KILL_WORD, "Delete last word" ) \
XX( EDITOR_B_KILL_LINE, "Delete everything up to BOL" ) \
XX( EDITOR_F_KILL_LINE, "Delete everything up to EOL" )
XX( EDITOR_B_DELETE, Delete last character ) \
XX( EDITOR_F_DELETE, Delete next character ) \
XX( EDITOR_B_KILL_WORD, Delete last word ) \
XX( EDITOR_B_KILL_LINE, Delete everything up to BOL ) \
XX( EDITOR_F_KILL_LINE, Delete everything up to EOL )
enum action
{
@@ -1605,7 +1663,7 @@ static struct action_info
}
g_actions[] =
{
#define XX(name, description) { #name, description },
#define XX(name, description) { #name, #description },
ACTIONS (XX)
#undef XX
};
@@ -1759,14 +1817,30 @@ app_process_action (enum action action)
tab->item_mark = tab->item_selected;
return true;
case ACTION_LAST_TAB:
case ACTION_TAB_LAST:
if (!g.last_tab)
return false;
app_switch_tab (g.last_tab);
return true;
case ACTION_HELP_TAB:
case ACTION_TAB_HELP:
app_switch_tab (g.help_tab);
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:
if (g.state == PLAYER_PLAYING) return MPD_SIMPLE ("pause", "1");
@@ -1987,8 +2061,12 @@ g_normal_defaults[] =
{ "Escape", ACTION_QUIT },
{ "q", ACTION_QUIT },
{ "C-l", ACTION_REDRAW },
{ "M-Tab", ACTION_LAST_TAB },
{ "F1", ACTION_HELP_TAB },
{ "M-Tab", ACTION_TAB_LAST },
{ "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 },
{ "End", ACTION_GOTO_BOTTOM },
@@ -2008,6 +2086,8 @@ g_normal_defaults[] =
{ "C-n", ACTION_GOTO_ITEM_NEXT },
{ "C-b", ACTION_GOTO_PAGE_PREVIOUS },
{ "C-f", ACTION_GOTO_PAGE_NEXT },
{ "C-y", ACTION_SCROLL_UP },
{ "C-e", ACTION_SCROLL_DOWN },
{ "H", ACTION_GOTO_VIEW_TOP },
{ "M", ACTION_GOTO_VIEW_CENTER },
@@ -2017,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 },
@@ -2175,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");
@@ -2193,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);
@@ -2334,17 +2414,6 @@ 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
@@ -2352,31 +2421,46 @@ enum
LIBRARY_ROOT = '/', ///< Root entry
LIBRARY_UP = '^', ///< Upper directory
LIBRARY_DIR = 'd', ///< Directory
LIBRARY_FILE = 'f' ///< File
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),
};
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@@ -2386,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_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
@@ -2408,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 *
@@ -2493,24 +2590,32 @@ library_tab_change_level (const char *new_path)
str_reset (path);
str_append (path, new_path);
free (g_library_tab.super.header);
g_library_tab.super.header = NULL;
cstr_set (&g_library_tab.super.header, NULL);
g_library_tab.super.item_mark = -1;
if (path->len)
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);
}
@@ -2530,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 ();
@@ -2627,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;
@@ -2647,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:
@@ -2657,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;
@@ -2692,8 +2794,7 @@ library_tab_on_action (enum action action)
free (fake_subdir);
}
free (tab->header);
tab->header = xstrdup_printf ("Global search");
cstr_set (&tab->header, xstrdup_printf ("Global search"));
g_library_tab.searching = true;
// 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++)
{
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;
@@ -2731,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);
@@ -2754,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");
@@ -2849,7 +2948,7 @@ streams_tab_extract_links (struct str *data, const char *content_type,
for (size_t i = 0; i < data->len; 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;
}
@@ -2943,6 +3042,8 @@ streams_tab_process (const char *uri, bool replace, struct error **e)
task.replace = replace;
bool result = false;
struct curl_slist *ok_headers = curl_slist_append (NULL, "ICY 200 OK");
CURLcode res;
if ((res = curl_easy_setopt (easy, CURLOPT_FOLLOWLOCATION, 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_VERIFYHOST, 0L))
|| (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_DEBUGFUNCTION, print_curl_debug))
@@ -2975,6 +3077,7 @@ streams_tab_process (const char *uri, bool replace, struct error **e)
error:
curl_easy_cleanup (task.curl.easy);
curl_slist_free_all (ok_headers);
str_free (&task.data);
poller_curl_free (&pc);
@@ -3306,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)
{
@@ -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
mpd_update_playback_state (void)
{
@@ -3377,13 +3488,11 @@ mpd_update_playback_state (void)
mpd_read_time (duration, &g.song_duration, NULL);
strv_free (&fields);
// We could also just poll the server each half a second but let's not
poller_timer_reset (&g.elapsed_event);
if (g.state == PLAYER_PLAYING)
{
poller_timer_set (&g.elapsed_event, 1000 - msec_past_second);
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;
@@ -3508,15 +3617,19 @@ mpd_on_info_response (const struct mpd_response *response,
}
static void
mpd_on_tick (void *user_data)
mpd_on_elapsed_time_tick (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;
int elapsed_sec = diff_msec / 1000;
int elapsed_msec = diff_msec % 1000;
g.song_elapsed += elapsed_sec;
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);
app_invalidate ();
@@ -3537,6 +3650,15 @@ mpd_request_info (void)
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
mpd_on_events (unsigned subsystems, void *user_data)
{
@@ -3829,8 +3951,7 @@ app_on_message_timer (void *user_data)
{
(void) user_data;
free (g.message);
g.message = NULL;
cstr_set (&g.message, NULL);
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);
else
{
free (g.message);
g.message = xstrdup (message.str);
cstr_set (&g.message, xstrdup (message.str));
app_invalidate ();
poller_timer_set (&g.message_timer, 5000);
}
@@ -3890,7 +4010,9 @@ app_init_poller_events (void)
poller_timer_set (&g.connect_event, 0);
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.dispatcher = app_on_refresh;
@@ -3908,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: 30e0eee1a8...f7912a8ce7