Compare commits
17 Commits
7165a8eb02
...
v1.2.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
61a141203b
|
|||
|
48482ef2e5
|
|||
|
840c69767c
|
|||
|
a14a907b18
|
|||
|
333049de01
|
|||
|
4e3596db35
|
|||
|
5aa07fd8af
|
|||
|
2060da4a8e
|
|||
|
f5b5cec340
|
|||
|
1a671dfad5
|
|||
|
587a02fa15
|
|||
|
227b8e0fa2
|
|||
|
e66e9f249a
|
|||
|
32203f8117
|
|||
|
6b871898d8
|
|||
|
4598c45d2f
|
|||
|
66c77c3f8d
|
@@ -1,5 +1,5 @@
|
||||
cmake_minimum_required (VERSION 3.0)
|
||||
project (nncmpp VERSION 1.1.1 LANGUAGES C)
|
||||
project (nncmpp VERSION 1.2.0 LANGUAGES C)
|
||||
|
||||
# Moar warnings
|
||||
if ("${CMAKE_C_COMPILER_ID}" MATCHES "GNU" OR CMAKE_COMPILER_IS_GNUCC)
|
||||
@@ -90,8 +90,23 @@ configure_file (${PROJECT_SOURCE_DIR}/config.h.in
|
||||
${PROJECT_BINARY_DIR}/config.h)
|
||||
include_directories (${PROJECT_SOURCE_DIR} ${PROJECT_BINARY_DIR})
|
||||
|
||||
# Assuming a Unix-compatible system with a standalone preprocessor
|
||||
set (actions_list ${PROJECT_SOURCE_DIR}/nncmpp.actions)
|
||||
set (actions ${PROJECT_BINARY_DIR}/nncmpp-actions.h)
|
||||
add_custom_command (OUTPUT ${actions}
|
||||
COMMAND cpp -I${PROJECT_BINARY_DIR} -P ${actions_list}
|
||||
| grep . | tr [[\n]] ^ | sed -ne [[h; s/,[^^]*/,/g]] -e [[s/$/COUNT/]]
|
||||
-e [[s/[^^]*/\tACTION_&/g]] -e [[s/.*/enum action {\n&\n};\n/p]]
|
||||
-e [[g; s/,[^^]*//g; y/_/-/]] -e [[s/[^^]\{1,\}/\t"&",/g]]
|
||||
-e [[s/.*/static const char *g_action_names[] = {\n&};\n/p]]
|
||||
-e [[g; s/[^^]*, *//g;]] -e [[s/[^^]\{1,\}/\t"&",/g]]
|
||||
-e [[s/.*/static const char *g_action_descriptions[] = {\n&};/p]]
|
||||
| tr ^ [[\n]] > ${actions}
|
||||
COMMAND test -s ${actions}
|
||||
DEPENDS ${actions_list} ${PROJECT_BINARY_DIR}/config.h VERBATIM)
|
||||
|
||||
# Build the main executable and link it
|
||||
add_executable (${PROJECT_NAME} ${PROJECT_NAME}.c)
|
||||
add_executable (${PROJECT_NAME} ${PROJECT_NAME}.c ${actions})
|
||||
target_link_libraries (${PROJECT_NAME} ${Unistring_LIBRARIES}
|
||||
${Ncursesw_LIBRARIES} termo-static ${curl_LIBRARIES} ${extra_libraries})
|
||||
add_threads (${PROJECT_NAME})
|
||||
|
||||
11
NEWS
11
NEWS
@@ -1,3 +1,14 @@
|
||||
1.2.0 (2021-12-21)
|
||||
|
||||
* Added ability to control the volume of MPD's current PulseAudio sink
|
||||
|
||||
* Now fetching Internet stream information asynchronously
|
||||
|
||||
* Added basic incremental search, normally bound to C-s, in all tabs
|
||||
|
||||
* Fixed jumping to the beginning of the queue after deleting items
|
||||
|
||||
|
||||
1.1.1 (2021-11-04)
|
||||
|
||||
* Terminal focus in/out events no longer ring the terminall bell
|
||||
|
||||
10
README.adoc
10
README.adoc
@@ -11,12 +11,14 @@ names, and should be pronounced as "nincompoop".
|
||||
|
||||
Features
|
||||
--------
|
||||
Most things are there. Enough for me to use it exclusively. Notably, it can
|
||||
control PulseAudio volume directly to cover the use case of remote control,
|
||||
and it has a fast spectrum visualiser.
|
||||
Most stuff is there. Enough for me to use the program exclusively. Among other
|
||||
things, it can display and change PulseAudio volume directly to cover the use
|
||||
case of remote control, it has a fast spectrum visualiser, and both
|
||||
the appearance and key bindings can be customized.
|
||||
|
||||
Note that since I only use the filesystem browsing mode, that's also the only
|
||||
thing I care to implement for the time being. The search feature is awkward.
|
||||
thing I care to implement for the time being. Similarly, the search feature is
|
||||
known to be clumsy.
|
||||
|
||||
image::nncmpp.png[align="center"]
|
||||
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
#ifndef CONFIG_H
|
||||
#define CONFIG_H
|
||||
|
||||
#define PROGRAM_NAME "${CMAKE_PROJECT_NAME}"
|
||||
#define PROGRAM_NAME "${PROJECT_NAME}"
|
||||
#define PROGRAM_VERSION "${PROJECT_VERSION}"
|
||||
|
||||
#cmakedefine HAVE_RESIZETERM
|
||||
#cmakedefine WITH_FFTW
|
||||
#cmakedefine WITH_PULSE
|
||||
|
||||
#endif // ! CONFIG_H
|
||||
|
||||
#endif /* ! CONFIG_H */
|
||||
|
||||
2
liberty
2
liberty
Submodule liberty updated: 782a9a5977...7e8e085c97
72
nncmpp.actions
Normal file
72
nncmpp.actions
Normal file
@@ -0,0 +1,72 @@
|
||||
#include "config.h"
|
||||
|
||||
NONE, Do nothing
|
||||
|
||||
QUIT, Quit
|
||||
REDRAW, Redraw screen
|
||||
TAB_HELP, Switch to help tab
|
||||
TAB_LAST, Switch to last tab
|
||||
TAB_PREVIOUS, Switch to previous tab
|
||||
TAB_NEXT, Switch to next tab
|
||||
|
||||
MPD_TOGGLE, Toggle play/pause
|
||||
MPD_STOP, Stop playback
|
||||
MPD_PREVIOUS, Previous song
|
||||
MPD_NEXT, Next song
|
||||
MPD_BACKWARD, Seek backwards
|
||||
MPD_FORWARD, Seek forwards
|
||||
MPD_VOLUME_UP, Increase MPD volume
|
||||
MPD_VOLUME_DOWN, Decrease MPD volume
|
||||
|
||||
MPD_SEARCH, Global search
|
||||
MPD_ADD, Add selection to playlist
|
||||
MPD_REPLACE, Replace playlist
|
||||
MPD_REPEAT, Toggle repeat
|
||||
MPD_RANDOM, Toggle random playback
|
||||
MPD_SINGLE, Toggle single song playback
|
||||
MPD_CONSUME, Toggle consume
|
||||
MPD_UPDATE_DB, Update MPD database
|
||||
MPD_COMMAND, Send raw command to MPD
|
||||
|
||||
#ifdef WITH_PULSE
|
||||
PULSE_VOLUME_UP, Increase PulseAudio volume
|
||||
PULSE_VOLUME_DOWN, Decrease PulseAudio volume
|
||||
PULSE_MUTE, Toggle PulseAudio sink mute
|
||||
#endif
|
||||
|
||||
CHOOSE, Choose item
|
||||
DELETE, Delete item
|
||||
UP, Go up a level
|
||||
MULTISELECT, Toggle multiselect
|
||||
INCREMENTAL_SEARCH, Incremental search
|
||||
|
||||
SCROLL_UP, Scroll up
|
||||
SCROLL_DOWN, Scroll down
|
||||
MOVE_UP, Move selection up
|
||||
MOVE_DOWN, Move selection down
|
||||
|
||||
GOTO_TOP, Go to top
|
||||
GOTO_BOTTOM, Go to bottom
|
||||
GOTO_ITEM_PREVIOUS, Go to previous item
|
||||
GOTO_ITEM_NEXT, Go to next item
|
||||
GOTO_PAGE_PREVIOUS, Go to previous page
|
||||
GOTO_PAGE_NEXT, Go to next page
|
||||
|
||||
GOTO_VIEW_TOP, Select top item
|
||||
GOTO_VIEW_CENTER, Select center item
|
||||
GOTO_VIEW_BOTTOM, Select bottom item
|
||||
|
||||
EDITOR_CONFIRM, Confirm input
|
||||
|
||||
EDITOR_B_CHAR, Go back a character
|
||||
EDITOR_F_CHAR, Go forward a character
|
||||
EDITOR_B_WORD, Go back a word
|
||||
EDITOR_F_WORD, Go forward a word
|
||||
EDITOR_HOME, Go to start of line
|
||||
EDITOR_END, Go to end of line
|
||||
|
||||
EDITOR_B_DELETE, Delete last character
|
||||
EDITOR_F_DELETE, Delete next character
|
||||
EDITOR_B_KILL_WORD, Delete last word
|
||||
EDITOR_B_KILL_LINE, Delete everything up to BOL
|
||||
EDITOR_F_KILL_LINE, Delete everything up to EOL
|
||||
69
nncmpp.adoc
69
nncmpp.adoc
@@ -45,28 +45,27 @@ snippet:
|
||||
|
||||
....
|
||||
settings = {
|
||||
address = "~/.mpd/mpd.socket"
|
||||
password = "<your password>"
|
||||
root = "~/Music"
|
||||
pulseaudio = on
|
||||
address = "~/.mpd/mpd.socket"
|
||||
password = "<your password>"
|
||||
pulseaudio = on
|
||||
}
|
||||
colors = {
|
||||
normal = ""
|
||||
highlight = "bold"
|
||||
elapsed = "reverse"
|
||||
remains = "ul"
|
||||
tab_bar = "reverse"
|
||||
tab_active = "ul"
|
||||
even = ""
|
||||
odd = ""
|
||||
selection = "reverse"
|
||||
multiselect = "-1 6"
|
||||
defocused = "ul"
|
||||
scrollbar = ""
|
||||
normal = ""
|
||||
highlight = "bold"
|
||||
elapsed = "reverse"
|
||||
remains = "ul"
|
||||
tab_bar = "reverse"
|
||||
tab_active = "ul"
|
||||
even = ""
|
||||
odd = ""
|
||||
selection = "reverse"
|
||||
multiselect = "-1 6"
|
||||
defocused = "ul"
|
||||
scrollbar = ""
|
||||
}
|
||||
streams = {
|
||||
"dnbradio.com" = "http://www.dnbradio.com/hi.m3u"
|
||||
"BassDrive.com" = "http://bassdrive.com/v2/streams/BassDrive.pls"
|
||||
"dnbradio.com" = "http://www.dnbradio.com/hi.m3u"
|
||||
"BassDrive.com" = "http://bassdrive.com/v2/streams/BassDrive.pls"
|
||||
}
|
||||
....
|
||||
|
||||
@@ -87,17 +86,39 @@ need to set it up manually to match your MPD configuration, e.g.:
|
||||
|
||||
....
|
||||
settings = {
|
||||
...
|
||||
spectrum_path = "~/.mpd/mpd.fifo" # "path"
|
||||
spectrum_format = "44100:16:2" # "format" (samplerate:bits:channels)
|
||||
spectrum_bars = 8 # beware of exponential complexity
|
||||
...
|
||||
...
|
||||
spectrum_path = "~/.mpd/mpd.fifo" # "path"
|
||||
spectrum_format = "44100:16:2" # "format" (samplerate:bits:channels)
|
||||
spectrum_bars = 8 # beware of exponential complexity
|
||||
...
|
||||
}
|
||||
....
|
||||
|
||||
The sample rate should be greater than 40 kHz, the number of bits 8 or 16,
|
||||
and the number of channels doesn't matter, as they're simply averaged together.
|
||||
|
||||
PulseAudio
|
||||
----------
|
||||
If you find standard MPD volume control useless, you may instead configure
|
||||
*nncmpp* to show and control the volume of any PulseAudio sink MPD is currently
|
||||
connected to.
|
||||
|
||||
This feature may be enabled with the *settings.pulseaudio* configuration option,
|
||||
as in the snippet above. To replace the default volume control bindings, use:
|
||||
|
||||
....
|
||||
normal = {
|
||||
"M-PageUp" = "pulse-volume-up"
|
||||
"M-PageDown" = "pulse-volume-down"
|
||||
}
|
||||
....
|
||||
|
||||
The respective actions may also be invoked from the help tab directly.
|
||||
|
||||
For this to work, *nncmpp* needs to access the right PulseAudio daemon--in case
|
||||
your setup is unusual, consult the list of environment variables in
|
||||
*pulseaudio*(1). MPD-compatibles are currently unsupported.
|
||||
|
||||
Files
|
||||
-----
|
||||
*nncmpp* follows the XDG Base Directory Specification.
|
||||
@@ -112,4 +133,4 @@ or submit pull requests.
|
||||
|
||||
See also
|
||||
--------
|
||||
*mpd*(1)
|
||||
*mpd*(1), *pulseaudio*(1)
|
||||
|
||||
363
nncmpp.c
363
nncmpp.c
@@ -154,6 +154,8 @@ xbasename (const char *path)
|
||||
return last_slash ? last_slash + 1 : path;
|
||||
}
|
||||
|
||||
static char *xstrdup0 (const char *s) { return s ? xstrdup (s) : NULL; }
|
||||
|
||||
static char *
|
||||
latin1_to_utf8 (const char *latin1)
|
||||
{
|
||||
@@ -280,6 +282,10 @@ struct poller_curl
|
||||
struct poller_timer timer; ///< cURL timer
|
||||
CURLM *multi; ///< cURL multi interface handle
|
||||
struct poller_curl_fd *fds; ///< List of all FDs
|
||||
|
||||
// TODO: also make sure to dispose of them at the end of the program
|
||||
|
||||
int registered; ///< Number of attached easy handles
|
||||
};
|
||||
|
||||
static void
|
||||
@@ -388,6 +394,7 @@ poller_curl_init (struct poller_curl *self, struct poller *poller,
|
||||
|| (mres = curl_multi_setopt (self->multi, CURLMOPT_TIMERDATA, self)))
|
||||
{
|
||||
curl_multi_cleanup (self->multi);
|
||||
self->multi = NULL;
|
||||
return error_set (e, "%s: %s",
|
||||
"cURL setup failed", curl_multi_strerror (mres));
|
||||
}
|
||||
@@ -448,6 +455,7 @@ poller_curl_add (struct poller_curl *self, CURL *easy, struct error **e)
|
||||
// "CURLMOPT_TIMERFUNCTION [...] will be called from within this function"
|
||||
if ((mres = curl_multi_add_handle (self->multi, easy)))
|
||||
return error_set (e, "%s", curl_multi_strerror (mres));
|
||||
self->registered++;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -457,6 +465,7 @@ poller_curl_remove (struct poller_curl *self, CURL *easy, struct error **e)
|
||||
CURLMcode mres;
|
||||
if ((mres = curl_multi_remove_handle (self->multi, easy)))
|
||||
return error_set (e, "%s", curl_multi_strerror (mres));
|
||||
self->registered--;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1182,6 +1191,7 @@ static struct app_context
|
||||
// Event loop:
|
||||
|
||||
struct poller poller; ///< Poller
|
||||
struct poller_curl poller_curl; ///< cURL abstractor
|
||||
bool quitting; ///< Quit signal for the event loop
|
||||
bool polling; ///< The event loop is running
|
||||
|
||||
@@ -1243,6 +1253,7 @@ static struct app_context
|
||||
#ifdef WITH_PULSE
|
||||
struct pulse pulse; ///< PulseAudio control
|
||||
#endif // WITH_PULSE
|
||||
bool pulse_control_requested; ///< PulseAudio control desired by user
|
||||
|
||||
struct line_editor editor; ///< Line editor
|
||||
struct poller_idle refresh_event; ///< Refresh the screen
|
||||
@@ -1304,6 +1315,13 @@ on_poll_elapsed_time_changed (struct config_item *item)
|
||||
g.elapsed_poll = item->value.boolean;
|
||||
}
|
||||
|
||||
static void
|
||||
on_pulseaudio_changed (struct config_item *item)
|
||||
{
|
||||
// This is only set once, on application startup
|
||||
g.pulse_control_requested = item->value.boolean;
|
||||
}
|
||||
|
||||
static struct config_schema g_config_settings[] =
|
||||
{
|
||||
{ .name = "address",
|
||||
@@ -1338,8 +1356,9 @@ static struct config_schema g_config_settings[] =
|
||||
|
||||
#ifdef WITH_PULSE
|
||||
{ .name = "pulseaudio",
|
||||
.comment = "Visualizer feed data format",
|
||||
.comment = "Look up MPD in PulseAudio for improved volume controls",
|
||||
.type = CONFIG_ITEM_BOOLEAN,
|
||||
.on_change = on_pulseaudio_changed,
|
||||
.default_ = "off" },
|
||||
#endif // WITH_PULSE
|
||||
|
||||
@@ -1376,14 +1395,6 @@ get_config_string (struct config_item *root, const char *key)
|
||||
return item->value.string.str;
|
||||
}
|
||||
|
||||
static bool
|
||||
get_config_boolean (struct config_item *root, const char *key)
|
||||
{
|
||||
struct config_item *item = config_item_get (root, key, NULL);
|
||||
hard_assert (item && item->type == CONFIG_ITEM_BOOLEAN);
|
||||
return item->value.boolean;
|
||||
}
|
||||
|
||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
static void
|
||||
@@ -1497,6 +1508,7 @@ static void
|
||||
app_init_context (void)
|
||||
{
|
||||
poller_init (&g.poller);
|
||||
hard_assert (poller_curl_init (&g.poller_curl, &g.poller, NULL));
|
||||
g.client = mpd_client_make (&g.poller);
|
||||
g.config = config_make ();
|
||||
g.streams = strv_make ();
|
||||
@@ -1585,6 +1597,7 @@ app_free_context (void)
|
||||
line_editor_free (&g.editor);
|
||||
|
||||
config_free (&g.config);
|
||||
poller_curl_free (&g.poller_curl);
|
||||
poller_free (&g.poller);
|
||||
free (g.message);
|
||||
|
||||
@@ -1782,18 +1795,31 @@ app_draw_status (void)
|
||||
|
||||
// It gets a bit complicated due to the only right-aligned item on the row
|
||||
struct str volume = str_make ();
|
||||
int remaining = COLS - buf.total_width;
|
||||
if (g.volume >= 0)
|
||||
{
|
||||
str_append (&volume, " ");
|
||||
#ifdef WITH_PULSE
|
||||
if (pulse_volume_status (&g.pulse, &volume))
|
||||
str_append (&volume, " @ ");
|
||||
#endif // WITH_PULSE
|
||||
str_append_printf (&volume, "%3d%%", g.volume);
|
||||
remaining -= volume.len;
|
||||
}
|
||||
if (g.pulse_control_requested)
|
||||
{
|
||||
struct str buf = str_make ();
|
||||
if (pulse_volume_status (&g.pulse, &buf))
|
||||
{
|
||||
if (g.volume >= 0 && g.volume != 100)
|
||||
str_append_printf (&buf, " (%d%%)", g.volume);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (g.volume >= 0)
|
||||
str_append_printf (&buf, "(%d%%)", g.volume);
|
||||
}
|
||||
if (buf.len)
|
||||
str_append_printf (&volume, " %s", buf.str);
|
||||
|
||||
str_free (&buf);
|
||||
}
|
||||
else
|
||||
#endif // WITH_PULSE
|
||||
if (g.volume >= 0)
|
||||
str_append_printf (&volume, " %3d%%", g.volume);
|
||||
|
||||
int remaining = COLS - buf.total_width - volume.len;
|
||||
if (!stopped && g.song_elapsed >= 0 && g.song_duration >= 1
|
||||
&& remaining > 0)
|
||||
{
|
||||
@@ -2063,6 +2089,8 @@ app_write_mpd_status (struct row_buffer *buf)
|
||||
row_buffer_append (buf, msg, APP_ATTR (HIGHLIGHT));
|
||||
free (msg);
|
||||
}
|
||||
else if (g.poller_curl.registered)
|
||||
row_buffer_append (buf, "Downloading...", APP_ATTR (NORMAL));
|
||||
else if (str_map_find (map, "updating_db"))
|
||||
row_buffer_append (buf, "Updating database...", APP_ATTR (NORMAL));
|
||||
else
|
||||
@@ -2235,117 +2263,14 @@ app_goto_tab (int tab_index)
|
||||
|
||||
// --- Actions -----------------------------------------------------------------
|
||||
|
||||
#ifdef WITH_PULSE
|
||||
#define WITH_PULSE_01 1
|
||||
#else
|
||||
#define WITH_PULSE_01 0
|
||||
#endif
|
||||
|
||||
// TODO: use the C preprocessor and a tool to generate this from nncmpp.actions
|
||||
#define ACTIONS(XX) \
|
||||
XX( 1, NONE, Do nothing ) \
|
||||
\
|
||||
XX( 1, QUIT, Quit ) \
|
||||
XX( 1, REDRAW, Redraw screen ) \
|
||||
XX( 1, TAB_HELP, Switch to help tab ) \
|
||||
XX( 1, TAB_LAST, Switch to last tab ) \
|
||||
XX( 1, TAB_PREVIOUS, Switch to previous tab ) \
|
||||
XX( 1, TAB_NEXT, Switch to next tab ) \
|
||||
\
|
||||
XX( 1, MPD_TOGGLE, Toggle play/pause ) \
|
||||
XX( 1, MPD_STOP, Stop playback ) \
|
||||
XX( 1, MPD_PREVIOUS, Previous song ) \
|
||||
XX( 1, MPD_NEXT, Next song ) \
|
||||
XX( 1, MPD_BACKWARD, Seek backwards ) \
|
||||
XX( 1, MPD_FORWARD, Seek forwards ) \
|
||||
XX( 1, MPD_VOLUME_UP, Increase volume ) \
|
||||
XX( 1, MPD_VOLUME_DOWN, Decrease volume ) \
|
||||
\
|
||||
XX( 1, MPD_SEARCH, Global search ) \
|
||||
XX( 1, MPD_ADD, Add selection to playlist ) \
|
||||
XX( 1, MPD_REPLACE, Replace playlist ) \
|
||||
XX( 1, MPD_REPEAT, Toggle repeat ) \
|
||||
XX( 1, MPD_RANDOM, Toggle random playback ) \
|
||||
XX( 1, MPD_SINGLE, Toggle single song playback ) \
|
||||
XX( 1, MPD_CONSUME, Toggle consume ) \
|
||||
XX( 1, MPD_UPDATE_DB, Update MPD database ) \
|
||||
XX( 1, MPD_COMMAND, Send raw command to MPD ) \
|
||||
\
|
||||
XX( WITH_PULSE_01, PULSE_VOLUME_UP, Increase PulseAudio volume ) \
|
||||
XX( WITH_PULSE_01, PULSE_VOLUME_DOWN, Decrease PulseAudio volume ) \
|
||||
XX( WITH_PULSE_01, PULSE_MUTE, Toggle mute of MPD PulseAudio sink ) \
|
||||
\
|
||||
XX( 1, CHOOSE, Choose item ) \
|
||||
XX( 1, DELETE, Delete item ) \
|
||||
XX( 1, UP, Go up a level ) \
|
||||
XX( 1, MULTISELECT, Toggle multiselect ) \
|
||||
\
|
||||
XX( 1, SCROLL_UP, Scroll up ) \
|
||||
XX( 1, SCROLL_DOWN, Scroll down ) \
|
||||
XX( 1, MOVE_UP, Move selection up ) \
|
||||
XX( 1, MOVE_DOWN, Move selection down ) \
|
||||
\
|
||||
XX( 1, GOTO_TOP, Go to top ) \
|
||||
XX( 1, GOTO_BOTTOM, Go to bottom ) \
|
||||
XX( 1, GOTO_ITEM_PREVIOUS, Go to previous item ) \
|
||||
XX( 1, GOTO_ITEM_NEXT, Go to next item ) \
|
||||
XX( 1, GOTO_PAGE_PREVIOUS, Go to previous page ) \
|
||||
XX( 1, GOTO_PAGE_NEXT, Go to next page ) \
|
||||
\
|
||||
XX( 1, GOTO_VIEW_TOP, Select top item ) \
|
||||
XX( 1, GOTO_VIEW_CENTER, Select center item ) \
|
||||
XX( 1, GOTO_VIEW_BOTTOM, Select bottom item ) \
|
||||
\
|
||||
XX( 1, EDITOR_CONFIRM, Confirm input ) \
|
||||
\
|
||||
XX( 1, EDITOR_B_CHAR, Go back a character ) \
|
||||
XX( 1, EDITOR_F_CHAR, Go forward a character ) \
|
||||
XX( 1, EDITOR_B_WORD, Go back a word ) \
|
||||
XX( 1, EDITOR_F_WORD, Go forward a word ) \
|
||||
XX( 1, EDITOR_HOME, Go to start of line ) \
|
||||
XX( 1, EDITOR_END, Go to end of line ) \
|
||||
\
|
||||
XX( 1, EDITOR_B_DELETE, Delete last character ) \
|
||||
XX( 1, EDITOR_F_DELETE, Delete next character ) \
|
||||
XX( 1, EDITOR_B_KILL_WORD, Delete last word ) \
|
||||
XX( 1, EDITOR_B_KILL_LINE, Delete everything up to BOL ) \
|
||||
XX( 1, EDITOR_F_KILL_LINE, Delete everything up to EOL )
|
||||
|
||||
enum action
|
||||
{
|
||||
#define XX(usable, name, description) ACTION_ ## name,
|
||||
ACTIONS (XX)
|
||||
#undef XX
|
||||
ACTION_COUNT
|
||||
};
|
||||
|
||||
static struct action_info
|
||||
{
|
||||
const char *name; ///< Name for user bindings
|
||||
const char *description; ///< Human-readable description
|
||||
bool usable; ///< Usable?
|
||||
}
|
||||
g_actions[] =
|
||||
{
|
||||
#define XX(usable, name, description) { #name, #description, usable },
|
||||
ACTIONS (XX)
|
||||
#undef XX
|
||||
};
|
||||
|
||||
/// Accept a more human format of action-name instead of ACTION_NAME
|
||||
static int action_toupper (int c) { return c == '-' ? '_' : toupper_ascii (c); }
|
||||
#include "nncmpp-actions.h"
|
||||
|
||||
static int
|
||||
action_resolve (const char *name)
|
||||
{
|
||||
const unsigned char *s = (const unsigned char *) name;
|
||||
for (int i = 0; i < ACTION_COUNT; i++)
|
||||
{
|
||||
const char *target = g_actions[i].name;
|
||||
for (size_t k = 0; action_toupper (s[k]) == target[k]; k++)
|
||||
if (!s[k] && !target[k])
|
||||
return i;
|
||||
}
|
||||
if (!strcasecmp_ascii (g_action_names[i], name))
|
||||
return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -2406,7 +2331,7 @@ app_setvol (int value)
|
||||
}
|
||||
|
||||
static void
|
||||
app_on_editor_end (bool confirmed)
|
||||
app_on_mpd_command_editor_end (bool confirmed)
|
||||
{
|
||||
struct mpd_client *c = &g.client;
|
||||
if (!confirmed)
|
||||
@@ -2421,6 +2346,54 @@ app_on_editor_end (bool confirmed)
|
||||
mpd_client_idle (c, 0);
|
||||
}
|
||||
|
||||
static size_t
|
||||
incremental_search_match (const ucs4_t *needle, size_t len,
|
||||
const struct row_buffer *row)
|
||||
{
|
||||
// XXX: this is slow and simplistic, but unistring is awkward to use
|
||||
size_t best = 0;
|
||||
for (size_t start = 0; start < row->chars_len; start++)
|
||||
{
|
||||
size_t i = 0;
|
||||
for (; i < len && start + i < row->chars_len; i++)
|
||||
if (uc_tolower(needle[i]) != uc_tolower(row->chars[start + i].c))
|
||||
break;
|
||||
best = MAX (best, i);
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
static void
|
||||
incremental_search_on_changed (void)
|
||||
{
|
||||
struct tab *tab = g.active_tab;
|
||||
if (!tab->item_count)
|
||||
return;
|
||||
|
||||
size_t best = 0, current = 0, index = MAX (tab->item_selected, 0), i = 0;
|
||||
while (i++ < tab->item_count)
|
||||
{
|
||||
struct row_buffer buf = row_buffer_make ();
|
||||
tab->on_item_draw (index, &buf, COLS);
|
||||
current = incremental_search_match (g.editor.line, g.editor.len, &buf);
|
||||
row_buffer_free (&buf);
|
||||
if (best < current)
|
||||
{
|
||||
best = current;
|
||||
tab->item_selected = index;
|
||||
app_move_selection (0);
|
||||
}
|
||||
index = (index + 1) % tab->item_count;
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
incremental_search_on_end (bool confirmed)
|
||||
{
|
||||
(void) confirmed;
|
||||
// Required callback, nothing to do here.
|
||||
}
|
||||
|
||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
static bool
|
||||
@@ -2463,7 +2436,7 @@ app_process_action (enum action action)
|
||||
return true;
|
||||
case ACTION_MPD_COMMAND:
|
||||
line_editor_start (&g.editor, ':');
|
||||
g.editor.on_end = app_on_editor_end;
|
||||
g.editor.on_end = app_on_mpd_command_editor_end;
|
||||
app_invalidate ();
|
||||
return true;
|
||||
default:
|
||||
@@ -2480,6 +2453,12 @@ app_process_action (enum action action)
|
||||
else
|
||||
tab->item_mark = tab->item_selected;
|
||||
return true;
|
||||
case ACTION_INCREMENTAL_SEARCH:
|
||||
line_editor_start (&g.editor, '/');
|
||||
g.editor.on_changed = incremental_search_on_changed;
|
||||
g.editor.on_end = incremental_search_on_end;
|
||||
app_invalidate ();
|
||||
return true;
|
||||
|
||||
case ACTION_TAB_LAST:
|
||||
if (!g.last_tab)
|
||||
@@ -2770,6 +2749,7 @@ g_normal_defaults[] =
|
||||
{ "M-Up", ACTION_UP },
|
||||
{ "Backspace", ACTION_UP },
|
||||
{ "v", ACTION_MULTISELECT },
|
||||
{ "C-s", ACTION_INCREMENTAL_SEARCH },
|
||||
{ "/", ACTION_MPD_SEARCH },
|
||||
{ "a", ACTION_MPD_ADD },
|
||||
{ "r", ACTION_MPD_REPLACE },
|
||||
@@ -3549,8 +3529,8 @@ struct stream_tab_task
|
||||
{
|
||||
struct poller_curl_task curl; ///< Superclass
|
||||
struct str data; ///< Downloaded data
|
||||
bool polling; ///< Still downloading
|
||||
bool replace; ///< Should playlist be replaced?
|
||||
struct curl_slist *alias_ok;
|
||||
};
|
||||
|
||||
static bool
|
||||
@@ -3634,24 +3614,39 @@ streams_tab_extract_links (struct str *data, const char *content_type,
|
||||
return true;
|
||||
}
|
||||
|
||||
static void
|
||||
streams_tab_task_finalize (struct stream_tab_task *self)
|
||||
{
|
||||
curl_easy_cleanup (self->curl.easy);
|
||||
curl_slist_free_all (self->alias_ok);
|
||||
str_free (&self->data);
|
||||
free (self);
|
||||
}
|
||||
|
||||
static void
|
||||
streams_tab_task_dispose (struct stream_tab_task *self)
|
||||
{
|
||||
hard_assert (poller_curl_remove (&g.poller_curl, self->curl.easy, NULL));
|
||||
streams_tab_task_finalize (self);
|
||||
}
|
||||
|
||||
static void
|
||||
streams_tab_on_downloaded (CURLMsg *msg, struct poller_curl_task *task)
|
||||
{
|
||||
struct stream_tab_task *self =
|
||||
CONTAINER_OF (task, struct stream_tab_task, curl);
|
||||
self->polling = false;
|
||||
|
||||
if (msg->data.result
|
||||
&& msg->data.result != CURLE_WRITE_ERROR)
|
||||
{
|
||||
cstr_uncapitalize (self->curl.curl_error);
|
||||
print_error ("%s", self->curl.curl_error);
|
||||
return;
|
||||
goto dispose;
|
||||
}
|
||||
|
||||
struct mpd_client *c = &g.client;
|
||||
if (c->state != MPD_CONNECTED)
|
||||
return;
|
||||
goto dispose;
|
||||
|
||||
CURL *easy = msg->easy_handle;
|
||||
CURLcode res;
|
||||
@@ -3664,13 +3659,13 @@ streams_tab_on_downloaded (CURLMsg *msg, struct poller_curl_task *task)
|
||||
{
|
||||
print_error ("%s: %s",
|
||||
"cURL info retrieval failed", curl_easy_strerror (res));
|
||||
return;
|
||||
goto dispose;
|
||||
}
|
||||
// cURL is not willing to parse the ICY header, the code is zero then
|
||||
if (code && code != 200)
|
||||
{
|
||||
print_error ("%s: %ld", "unexpected HTTP response", code);
|
||||
return;
|
||||
goto dispose;
|
||||
}
|
||||
|
||||
mpd_client_list_begin (c);
|
||||
@@ -3689,6 +3684,9 @@ streams_tab_on_downloaded (CURLMsg *msg, struct poller_curl_task *task)
|
||||
mpd_client_list_end (c);
|
||||
mpd_client_add_task (c, mpd_on_simple_response, NULL);
|
||||
mpd_client_idle (c, 0);
|
||||
|
||||
dispose:
|
||||
streams_tab_task_dispose (self);
|
||||
}
|
||||
|
||||
static size_t
|
||||
@@ -3707,60 +3705,44 @@ write_callback (char *ptr, size_t size, size_t nmemb, void *user_data)
|
||||
static bool
|
||||
streams_tab_process (const char *uri, bool replace, struct error **e)
|
||||
{
|
||||
struct poller poller;
|
||||
poller_init (&poller);
|
||||
// TODO: streams_tab_task_dispose() on that running task
|
||||
if (g.poller_curl.registered)
|
||||
{
|
||||
print_error ("waiting for the last stream to time out");
|
||||
return false;
|
||||
}
|
||||
|
||||
struct poller_curl pc;
|
||||
hard_assert (poller_curl_init (&pc, &poller, NULL));
|
||||
struct stream_tab_task task;
|
||||
hard_assert (poller_curl_spawn (&task.curl, NULL));
|
||||
struct stream_tab_task *task = xcalloc (1, sizeof *task);
|
||||
hard_assert (poller_curl_spawn (&task->curl, NULL));
|
||||
|
||||
CURL *easy = task.curl.easy;
|
||||
task.data = str_make ();
|
||||
task.replace = replace;
|
||||
bool result = false;
|
||||
|
||||
struct curl_slist *ok_headers = curl_slist_append (NULL, "ICY 200 OK");
|
||||
CURL *easy = task->curl.easy;
|
||||
task->data = str_make ();
|
||||
task->replace = replace;
|
||||
task->alias_ok = 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))
|
||||
// TODO: make the timeout a bit larger once we're asynchronous
|
||||
|| (res = curl_easy_setopt (easy, CURLOPT_TIMEOUT, 5L))
|
||||
|| (res = curl_easy_setopt (easy, CURLOPT_TIMEOUT, 10L))
|
||||
// Not checking anything, we just want some data, any data
|
||||
|| (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_HTTP200ALIASES, task->alias_ok))
|
||||
|
||||
|| (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_WRITEDATA, &task.data))
|
||||
|| (res = curl_easy_setopt (easy, CURLOPT_WRITEDATA, &task->data))
|
||||
|| (res = curl_easy_setopt (easy, CURLOPT_WRITEFUNCTION, write_callback)))
|
||||
{
|
||||
error_set (e, "%s: %s", "cURL setup failed", curl_easy_strerror (res));
|
||||
goto error;
|
||||
streams_tab_task_finalize (task);
|
||||
return false;
|
||||
}
|
||||
|
||||
task.curl.on_done = streams_tab_on_downloaded;
|
||||
hard_assert (poller_curl_add (&pc, task.curl.easy, NULL));
|
||||
|
||||
// TODO: don't run a subloop, run the task fully asynchronously
|
||||
task.polling = true;
|
||||
while (task.polling)
|
||||
poller_run (&poller);
|
||||
|
||||
hard_assert (poller_curl_remove (&pc, task.curl.easy, NULL));
|
||||
result = true;
|
||||
|
||||
error:
|
||||
curl_easy_cleanup (task.curl.easy);
|
||||
curl_slist_free_all (ok_headers);
|
||||
str_free (&task.data);
|
||||
poller_curl_free (&pc);
|
||||
|
||||
poller_free (&poller);
|
||||
return result;
|
||||
task->curl.on_done = streams_tab_on_downloaded;
|
||||
hard_assert (poller_curl_add (&g.poller_curl, task->curl.easy, NULL));
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
@@ -3943,9 +3925,6 @@ help_tab_group (struct binding *keys, size_t len, struct strv *out,
|
||||
{
|
||||
for (enum action i = 0; i < ACTION_COUNT; i++)
|
||||
{
|
||||
if (!g_actions[i].usable)
|
||||
continue;
|
||||
|
||||
struct strv ass = strv_make ();
|
||||
for (size_t k = 0; k < len; k++)
|
||||
if (keys[k].action == i)
|
||||
@@ -3954,7 +3933,7 @@ help_tab_group (struct binding *keys, size_t len, struct strv *out,
|
||||
{
|
||||
char *joined = strv_join (&ass, ", ");
|
||||
strv_append_owned (out, xstrdup_printf
|
||||
(" %-30s %s", g_actions[i].description, joined));
|
||||
(" %-30s %s", g_action_descriptions[i], joined));
|
||||
free (joined);
|
||||
|
||||
bound[i] = true;
|
||||
@@ -3971,7 +3950,7 @@ help_tab_unbound (struct strv *out, bool bound[ACTION_COUNT])
|
||||
if (!bound[i])
|
||||
{
|
||||
strv_append_owned (out,
|
||||
xstrdup_printf (" %-30s", g_actions[i].description));
|
||||
xstrdup_printf (" %-30s", g_action_descriptions[i]));
|
||||
help_tab_assign_action (i);
|
||||
}
|
||||
}
|
||||
@@ -4279,7 +4258,7 @@ static void
|
||||
pulse_update (void)
|
||||
{
|
||||
struct mpd_client *c = &g.client;
|
||||
if (!get_config_boolean (g.config.root, "settings.pulseaudio"))
|
||||
if (!g.pulse_control_requested)
|
||||
return;
|
||||
|
||||
// The read permission is sufficient for this command
|
||||
@@ -4456,7 +4435,7 @@ mpd_find_pos_of_id (const char *desired_id)
|
||||
return -1;
|
||||
}
|
||||
|
||||
static char *
|
||||
static const char *
|
||||
mpd_id_of_pos (int pos)
|
||||
{
|
||||
compact_map_t map = item_list_get (&g.playlist, pos);
|
||||
@@ -4466,29 +4445,39 @@ mpd_id_of_pos (int pos)
|
||||
static void
|
||||
mpd_process_info (const struct strv *data)
|
||||
{
|
||||
int *selected = &g_current_tab.item_selected;
|
||||
int *marked = &g_current_tab.item_mark;
|
||||
char *prev_sel_id = mpd_id_of_pos (*selected);
|
||||
char *prev_mark_id = mpd_id_of_pos (*marked);
|
||||
if (prev_sel_id) prev_sel_id = xstrdup (prev_sel_id);
|
||||
if (prev_mark_id) prev_mark_id = xstrdup (prev_mark_id);
|
||||
struct tab *tab = &g_current_tab;
|
||||
char *prev_sel_id = xstrdup0 (mpd_id_of_pos (tab->item_selected));
|
||||
char *prev_mark_id = xstrdup0 (mpd_id_of_pos (tab->item_mark));
|
||||
char *fallback_id = NULL;
|
||||
|
||||
struct tab_range r = tab_selection_range (g.active_tab);
|
||||
if (r.upto >= 0)
|
||||
{
|
||||
if (!(fallback_id = xstrdup0 (mpd_id_of_pos (r.upto + 1))))
|
||||
fallback_id = xstrdup0 (mpd_id_of_pos (r.from - 1));
|
||||
}
|
||||
|
||||
mpd_process_info_data (data);
|
||||
|
||||
const char *sel_id = mpd_id_of_pos (*selected);
|
||||
const char *mark_id = mpd_id_of_pos (*marked);
|
||||
const char *sel_id = mpd_id_of_pos (tab->item_selected);
|
||||
const char *mark_id = mpd_id_of_pos (tab->item_mark);
|
||||
|
||||
if (prev_mark_id && (!mark_id || strcmp (prev_mark_id, mark_id)))
|
||||
*marked = mpd_find_pos_of_id (prev_mark_id);
|
||||
tab->item_mark = mpd_find_pos_of_id (prev_mark_id);
|
||||
if (prev_sel_id && (!sel_id || strcmp (prev_sel_id, sel_id)))
|
||||
{
|
||||
if ((*selected = mpd_find_pos_of_id (prev_sel_id)) < 0)
|
||||
*marked = -1;
|
||||
if ((tab->item_selected = mpd_find_pos_of_id (prev_sel_id)) < 0)
|
||||
{
|
||||
tab->item_mark = -1;
|
||||
if (fallback_id)
|
||||
tab->item_selected = mpd_find_pos_of_id (fallback_id);
|
||||
}
|
||||
app_move_selection (0);
|
||||
}
|
||||
|
||||
free (prev_sel_id);
|
||||
free (prev_mark_id);
|
||||
free (fallback_id);
|
||||
}
|
||||
|
||||
static void
|
||||
|
||||
Reference in New Issue
Block a user