21 Commits

Author SHA1 Message Date
5aa07fd8af Clean up mpd_process_info() better 2021-12-07 20:38:02 +01:00
2060da4a8e Do not jump to beginning after unqueueing
Instead, assume that the whole previously selected range
has been removed, and try to go after or before it accordingly.
2021-12-07 20:34:32 +01:00
f5b5cec340 Clean up unreadable code 2021-12-07 20:10:35 +01:00
1a671dfad5 Document PulseAudio integration 2021-11-16 05:17:15 +01:00
587a02fa15 Indent man page snippets with spaces 2021-11-16 05:16:51 +01:00
227b8e0fa2 Do not show both volumes if unnecessary
Also, make it apparent which value comes from where.
2021-11-16 04:48:52 +01:00
e66e9f249a Rename an action to be shorter
Also, fix make dependencies to include the source file for actions.
2021-11-16 04:48:52 +01:00
32203f8117 Fix the comment for settings.pulseaudio 2021-11-08 07:23:08 +01:00
6b871898d8 Fix build on macOS and other non-GNU systems 2021-11-08 06:36:01 +01:00
4598c45d2f Generate actions from a text file
Mostly because I wanted to nest preprocessing.

This makes the build more complex and slightly less portable,
but the code itself is much cleaner.
2021-11-08 06:07:04 +01:00
66c77c3f8d Update README 2021-11-07 23:21:32 +01:00
7165a8eb02 Add ability to control PulseAudio volume
I know, son, it might be hard to accept,
but you're imported.  Your true parents are wmstatus
and paswitch, from the desktop-tools family.

Also, fix unnecessary linking of optional dependencies.
2021-11-07 23:07:55 +01:00
87b57bb24c Add a comment about the music directory 2021-11-07 13:29:13 +01:00
ba86961ba5 Bump version, update NEWS 2021-11-04 14:19:50 +01:00
0cdb4989e5 Bump termo 2021-11-04 14:16:25 +01:00
6de940fe96 Do not beep on focus changes 2021-11-04 13:24:15 +01:00
6bd8c1db2f CMakeLists.txt: fix macOS build 2021-11-02 17:17:32 +01:00
56efe9c6a9 Update .gitignore 2021-10-30 03:35:49 +02:00
8a17e674f8 CMakeLists.txt: clean up 2021-10-30 03:02:00 +02:00
bd0ee66c19 Add clang-format configuration 2021-10-30 03:02:00 +02:00
6f6efe077b CMakeLists.txt: synchronize with sdtui 2021-10-27 19:48:47 +02:00
11 changed files with 665 additions and 186 deletions

32
.clang-format Normal file
View File

@@ -0,0 +1,32 @@
# clang-format is fairly limited, and these rules are approximate:
# - array initializers can get terribly mangled with clang-format 12.0,
# - sometimes it still aligns with space characters,
# - struct name NL { NL ... NL } NL name; is unachievable.
BasedOnStyle: GNU
ColumnLimit: 80
IndentWidth: 4
TabWidth: 4
UseTab: ForContinuationAndIndentation
BreakBeforeBraces: Allman
SpaceAfterCStyleCast: true
AlignAfterOpenBracket: DontAlign
AlignOperands: DontAlign
AlignConsecutiveMacros: Consecutive
AllowAllArgumentsOnNextLine: false
AllowAllParametersOfDeclarationOnNextLine: false
IndentGotoLabels: false
# IncludeCategories has some potential, but it may also break the build.
# Note that the documentation says the value should be "Never".
SortIncludes: false
# This is a compromise, it generally works out aesthetically better.
BinPackArguments: false
# Unfortunately, this can't be told to align to column 40 or so.
SpacesBeforeTrailingComments: 2
# liberty-specific macro body wrappers.
MacroBlockBegin: "BLOCK_START"
MacroBlockEnd: "BLOCK_END"
ForEachMacros: ["LIST_FOR_EACH"]

2
.gitignore vendored
View File

@@ -7,3 +7,5 @@
/nncmpp.files
/nncmpp.creator*
/nncmpp.includes
/nncmpp.cflags
/nncmpp.cxxflags

View File

@@ -1,5 +1,5 @@
cmake_minimum_required (VERSION 3.0)
project (nncmpp VERSION 1.1.0 LANGUAGES C)
project (nncmpp VERSION 1.1.1 LANGUAGES C)
# Moar warnings
if ("${CMAKE_C_COMPILER_ID}" MATCHES "GNU" OR CMAKE_COMPILER_IS_GNUCC)
@@ -24,14 +24,17 @@ option (USE_SYSTEM_TERMO
if (USE_SYSTEM_TERMO)
if (NOT Termo_FOUND)
message (FATAL_ERROR "System termo library not found")
endif (NOT Termo_FOUND)
endif ()
else ()
# We don't want the library to install, but EXCLUDE_FROM_ALL ignores tests
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
# (other possibilities: setting a variable in the parent scope, using a
# cache variable, writing a special config file with build paths in it and
# including it here, or setting a custom property on the targets).
file (WRITE ${PROJECT_BINARY_DIR}/CTestCustom.cmake
"execute_process (COMMAND ${CMAKE_COMMAND} --build termo)")
# We don't have many good choices; this is a relatively clean approach
# (other possibilities: setting a variable in the parent scope, using
# a cache variable, writing a special config file with build paths in it
# and including it here, or setting a custom property on the targets)
get_directory_property (Termo_INCLUDE_DIRS
DIRECTORY termo INCLUDE_DIRECTORIES)
set (Termo_LIBRARIES termo-static)
@@ -42,25 +45,38 @@ option (WITH_FFTW "Use FFTW to enable spectrum visualisation" ${fftw_FOUND})
if (WITH_FFTW)
if (NOT fftw_FOUND)
message (FATAL_ERROR "FFTW not found")
endif()
endif ()
list (APPEND extra_libraries ${fftw_LIBRARIES})
endif ()
pkg_check_modules (libpulse libpulse)
option (WITH_PULSE "Enable control of PulseAudio sink volume" ${libpulse_FOUND})
if (WITH_PULSE)
if (NOT libpulse_FOUND)
message (FATAL_ERROR "libpulse not found")
endif ()
list (APPEND extra_libraries ${libpulse_LIBRARIES})
endif ()
include_directories (${Unistring_INCLUDE_DIRS}
${Ncursesw_INCLUDE_DIRS} ${Termo_INCLUDE_DIRS} ${curl_INCLUDE_DIRS}
${fftw_INCLUDE_DIRS})
link_directories (${curl_LIBRARY_DIRS} ${fftw_LIBRARY_DIRS})
${fftw_INCLUDE_DIRS} ${libpulse_INCLUDE_DIRS})
link_directories (${curl_LIBRARY_DIRS}
${fftw_LIBRARY_DIRS} ${libpulse_LIBRARY_DIRS})
# Configuration
include (CheckFunctionExists)
set (CMAKE_REQUIRED_LIBRARIES ${Ncursesw_LIBRARIES})
CHECK_FUNCTION_EXISTS ("resizeterm" HAVE_RESIZETERM)
if ("${CMAKE_SYSTEM_NAME}" MATCHES "BSD")
# Need this for SIGWINCH in FreeBSD and OpenBSD respectively;
# our POSIX version macros make it undefined
add_definitions (-D__BSD_VISIBLE=1 -D_BSD_SOURCE=1)
elseif (APPLE)
add_definitions (-D_DARWIN_C_SOURCE)
endif ()
include (CheckFunctionExists)
set (CMAKE_REQUIRED_LIBRARIES ${Ncursesw_LIBRARIES})
CHECK_FUNCTION_EXISTS ("resizeterm" HAVE_RESIZETERM)
# -lm may or may not be a part of libc
foreach (extra m)
find_library (extra_lib_${extra} ${extra})
@@ -74,11 +90,25 @@ 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}
${fftw_LIBRARIES} ${extra_libraries})
${Ncursesw_LIBRARIES} termo-static ${curl_LIBRARIES} ${extra_libraries})
add_threads (${PROJECT_NAME})
# Installation

7
NEWS
View File

@@ -1,3 +1,10 @@
1.1.1 (2021-11-04)
* Terminal focus in/out events no longer ring the terminall bell
* Made mouse work in non-rxvt terminals with recent xterm terminfo
1.1.0 (2021-10-21)
* Now requesting and processing terminal de/focus events,

View File

@@ -11,9 +11,14 @@ names, and should be pronounced as "nincompoop".
Features
--------
Most things are there. Enough for me to use it exclusively. Note that since I
only use the filesystem browsing mode, that's also the only thing I care to
implement for the time being.
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. Similarly, the search feature is
known to be clumsy.
image::nncmpp.png[align="center"]
@@ -29,9 +34,10 @@ 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, fftw3 (optional)
Build dependencies: CMake, pkg-config, asciidoctor,
liberty (included), termo (included) +
Runtime dependencies: ncursesw, libunistring, cURL,
fftw3 (optional), libpulse (optional)
$ git clone --recursive https://git.janouch.name/p/nncmpp.git
$ mkdir nncmpp/build

View File

@@ -1,11 +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 */

Submodule liberty updated: d71c47f8ce...782a9a5977

71
nncmpp.actions Normal file
View File

@@ -0,0 +1,71 @@
#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
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

View File

@@ -37,16 +37,17 @@ Options
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:
Unless you run MPD on a remote machine, on an unusual port, protected by
a password, or only accessible through a Unix socket, 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"
address = "~/.mpd/mpd.socket"
password = "<your password>"
root = "~/Music"
pulseaudio = on
}
colors = {
normal = ""
@@ -96,6 +97,28 @@ settings = {
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.
@@ -110,4 +133,4 @@ or submit pull requests.
See also
--------
*mpd*(1)
*mpd*(1), *pulseaudio*(1)

566
nncmpp.c
View File

@@ -78,30 +78,35 @@ enum
#include <math.h>
#include <locale.h>
#include <termios.h>
#ifndef TIOCGWINSZ
#include <sys/ioctl.h>
#endif // ! TIOCGWINSZ
// ncurses is notoriously retarded for input handling, we need something
// different if only to receive mouse events reliably.
//
// 2021 update: ncurses is mostly reliable now, though rxvt-unicode only
// supports the 1006 mode that ncurses also supports mode starting with 9.25.
#include "termo.h"
// We need cURL to extract links from Internet stream playlists. It'd be way
// too much code to do this all by ourselves, and there's nothing better around.
#include <curl/curl.h>
// The spectrum analyser requires a DFT transform. The FFTW library is fairly
// efficient, and doesn't have a requirement on the number of bins.
#ifdef WITH_FFTW
#include <fftw3.h>
#endif // WITH_FFTW
// Remote MPD control needs appropriate volume controls.
#ifdef WITH_PULSE
#include "liberty/liberty-pulse.c"
#include <pulse/context.h>
#include <pulse/error.h>
#include <pulse/introspect.h>
#include <pulse/subscribe.h>
#include <pulse/sample.h>
#endif // WITH_PULSE
#define APP_TITLE PROGRAM_NAME ///< Left top corner
// --- Utilities ---------------------------------------------------------------
@@ -110,7 +115,7 @@ enum
static void
update_curses_terminal_size (void)
{
#if defined (HAVE_RESIZETERM) && defined (TIOCGWINSZ)
#if defined HAVE_RESIZETERM && defined TIOCGWINSZ
struct winsize size;
if (!ioctl (STDOUT_FILENO, TIOCGWINSZ, (char *) &size))
{
@@ -149,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)
{
@@ -862,6 +869,256 @@ spectrum_free (struct spectrum *s)
#endif // WITH_FFTW
// --- PulseAudio --------------------------------------------------------------
#ifdef WITH_PULSE
struct pulse
{
struct poller_timer make_context; ///< Event to establish connection
pa_mainloop_api *api; ///< PulseAudio event loop proxy
pa_context *context; ///< PulseAudio connection context
uint32_t sink_candidate; ///< Used while searching for MPD
uint32_t sink; ///< The relevant sink or -1
pa_cvolume sink_volume; ///< Current volume
bool sink_muted; ///< Currently muted?
void (*on_update) (void); ///< Update callback
};
static void
pulse_on_sink_info (pa_context *context, const pa_sink_info *info, int eol,
void *userdata)
{
(void) context;
(void) eol;
struct pulse *self = userdata;
if (info)
{
self->sink_volume = info->volume;
self->sink_muted = !!info->mute;
self->on_update ();
}
}
static void
pulse_update_from_sink (struct pulse *self)
{
if (self->sink == PA_INVALID_INDEX)
return;
pa_operation_unref (pa_context_get_sink_info_by_index
(self->context, self->sink, pulse_on_sink_info, self));
}
static void
pulse_on_sink_input_info (pa_context *context,
const struct pa_sink_input_info *info, int eol, void *userdata)
{
(void) context;
(void) eol;
struct pulse *self = userdata;
if (!info)
{
if ((self->sink = self->sink_candidate) != PA_INVALID_INDEX)
pulse_update_from_sink (self);
else
self->on_update ();
return;
}
// TODO: also save info->mute as a different mute level,
// and perhaps info->index (they can appear and disappear)
const char *name =
pa_proplist_gets (info->proplist, PA_PROP_APPLICATION_NAME);
if (name && !strcmp (name, "Music Player Daemon"))
self->sink_candidate = info->sink;
}
static void
pulse_read_sink_inputs (struct pulse *self)
{
self->sink_candidate = PA_INVALID_INDEX;
pa_operation_unref (pa_context_get_sink_input_info_list
(self->context, pulse_on_sink_input_info, self));
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
pulse_on_event (pa_context *context, pa_subscription_event_type_t event,
uint32_t index, void *userdata)
{
(void) context;
struct pulse *self = userdata;
switch (event & PA_SUBSCRIPTION_EVENT_FACILITY_MASK)
{
case PA_SUBSCRIPTION_EVENT_SINK_INPUT:
pulse_read_sink_inputs (self);
break;
case PA_SUBSCRIPTION_EVENT_SINK:
if (index == self->sink)
pulse_update_from_sink (self);
}
}
static void
pulse_on_subscribe_finish (pa_context *context, int success, void *userdata)
{
(void) context;
struct pulse *self = userdata;
if (success)
pulse_read_sink_inputs (self);
else
{
print_debug ("PulseAudio failed to subscribe for events");
self->on_update ();
pa_context_disconnect (context);
}
}
static void
pulse_on_context_state_change (pa_context *context, void *userdata)
{
struct pulse *self = userdata;
switch (pa_context_get_state (context))
{
case PA_CONTEXT_FAILED:
case PA_CONTEXT_TERMINATED:
print_debug ("PulseAudio context failed or has been terminated");
pa_context_unref (context);
self->context = NULL;
self->sink = PA_INVALID_INDEX;
self->on_update ();
// Retry after an arbitrary delay of 5 seconds
poller_timer_set (&self->make_context, 5000);
break;
case PA_CONTEXT_READY:
pa_context_set_subscribe_callback (context, pulse_on_event, userdata);
pa_operation_unref (pa_context_subscribe (context,
PA_SUBSCRIPTION_MASK_SINK | PA_SUBSCRIPTION_MASK_SINK_INPUT,
pulse_on_subscribe_finish, userdata));
default:
break;
}
}
static void
pulse_make_context (void *user_data)
{
struct pulse *self = user_data;
self->context = pa_context_new (self->api, PROGRAM_NAME);
pa_context_set_state_callback (self->context,
pulse_on_context_state_change, self);
pa_context_connect (self->context, NULL, PA_CONTEXT_NOAUTOSPAWN, NULL);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
pulse_on_finish (pa_context *context, int success, void *userdata)
{
(void) context;
(void) success;
(void) userdata;
// Just like... whatever, man
}
static bool
pulse_volume_mute (struct pulse *self)
{
if (!self->context || self->sink == PA_INVALID_INDEX)
return false;
pa_operation_unref (pa_context_set_sink_mute_by_index (self->context,
self->sink, !self->sink_muted, pulse_on_finish, self));
return true;
}
static bool
pulse_volume_set (struct pulse *self, int arg)
{
if (!self->context || self->sink == PA_INVALID_INDEX)
return false;
pa_cvolume volume = self->sink_volume;
if (arg > 0)
pa_cvolume_inc (&volume, (pa_volume_t) arg * PA_VOLUME_NORM / 100);
else
pa_cvolume_dec (&volume, (pa_volume_t) -arg * PA_VOLUME_NORM / 100);
pa_operation_unref (pa_context_set_sink_volume_by_index (self->context,
self->sink, &volume, pulse_on_finish, self));
return true;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
pulse_init (struct pulse *self, struct poller *poller)
{
memset (self, 0, sizeof *self);
self->sink = PA_INVALID_INDEX;
if (!poller)
return;
self->api = poller_pa_new (poller);
self->make_context = poller_timer_make (poller);
self->make_context.dispatcher = pulse_make_context;
self->make_context.user_data = self;
poller_timer_set (&self->make_context, 0);
}
static void
pulse_free (struct pulse *self)
{
if (self->context)
pa_context_unref (self->context);
if (self->api)
{
poller_pa_destroy (self->api);
poller_timer_reset (&self->make_context);
}
pulse_init (self, NULL);
}
#define VOLUME_PERCENT(x) (((x) * 100 + PA_VOLUME_NORM / 2) / PA_VOLUME_NORM)
static bool
pulse_volume_status (struct pulse *self, struct str *s)
{
if (!self->context || self->sink == PA_INVALID_INDEX
|| !self->sink_volume.channels)
return false;
if (self->sink_muted)
{
str_append (s, "Muted");
return true;
}
str_append_printf (s,
"%u%%", VOLUME_PERCENT (self->sink_volume.values[0]));
if (!pa_cvolume_channels_equal_to (&self->sink_volume,
self->sink_volume.values[0]))
{
for (size_t i = 1; i < self->sink_volume.channels; i++)
str_append_printf (s, " / %u%%",
VOLUME_PERCENT (self->sink_volume.values[i]));
}
return true;
}
#endif // WITH_PULSE
// --- Application -------------------------------------------------------------
// Function names are prefixed mostly because of curses which clutters the
@@ -985,6 +1242,11 @@ static struct app_context
struct poller_fd spectrum_event; ///< FIFO watcher
#endif // WITH_FFTW
#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
@@ -1045,6 +1307,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",
@@ -1056,6 +1325,7 @@ static struct config_schema g_config_settings[] =
.type = CONFIG_ITEM_STRING },
// NOTE: this is unused--in theory we could allow manual metadata adjustment
// NOTE: the "config" command may return "music_directory" for local clients
{ .name = "root",
.comment = "Where all the files MPD is playing are located",
.type = CONFIG_ITEM_STRING },
@@ -1076,6 +1346,14 @@ static struct config_schema g_config_settings[] =
.default_ = "8" },
#endif // WITH_FFTW
#ifdef WITH_PULSE
{ .name = "pulseaudio",
.comment = "Look up MPD in PulseAudio for improved volume controls",
.type = CONFIG_ITEM_BOOLEAN,
.on_change = on_pulseaudio_changed,
.default_ = "off" },
#endif // WITH_PULSE
// 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
@@ -1236,6 +1514,10 @@ app_init_context (void)
g.spectrum_row = g.spectrum_column = -1;
#endif // WITH_FFTW
#ifdef WITH_PULSE
pulse_init (&g.pulse, NULL);
#endif // WITH_PULSE
// This is also approximately what libunistring does internally,
// since the locale name is canonicalized by locale_charset().
// Note that non-Unicode locales are handled pretty inefficiently.
@@ -1299,6 +1581,10 @@ app_free_context (void)
}
#endif // WITH_FFTW
#ifdef WITH_PULSE
pulse_free (&g.pulse);
#endif // WITH_PULSE
line_editor_free (&g.editor);
config_free (&g.config);
@@ -1498,14 +1784,32 @@ app_draw_status (void)
}
// It gets a bit complicated due to the only right-aligned item on the row
char *volume = NULL;
int remaining = COLS - buf.total_width;
if (g.volume >= 0)
struct str volume = str_make ();
#ifdef WITH_PULSE
if (g.pulse_control_requested)
{
volume = xstrdup_printf (" %3d%%", g.volume);
remaining -= strlen (volume);
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)
{
@@ -1517,11 +1821,10 @@ app_draw_status (void)
else
row_buffer_space (&buf, remaining, attr_normal);
if (volume)
{
row_buffer_append (&buf, volume, attr_normal);
free (volume);
}
if (volume.len)
row_buffer_append (&buf, volume.str, attr_normal);
str_free (&volume);
g.controls_offset = g.header_height;
app_flush_header (&buf, attr_normal);
}
@@ -1948,105 +2251,14 @@ app_goto_tab (int tab_index)
// --- Actions -----------------------------------------------------------------
#define ACTIONS(XX) \
XX( NONE, Do nothing ) \
\
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_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( 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_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_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 )
enum action
{
#define XX(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
}
g_actions[] =
{
#define XX(name, description) { #name, #description },
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])
if (!strcasecmp_ascii (g_action_names[i], name))
return i;
}
return -1;
}
@@ -2225,6 +2437,12 @@ app_process_action (enum action action)
case ACTION_MPD_VOLUME_UP: return app_setvol (g.volume + 10);
case ACTION_MPD_VOLUME_DOWN: return app_setvol (g.volume - 10);
#ifdef WITH_PULSE
case ACTION_PULSE_VOLUME_UP: return pulse_volume_set (&g.pulse, +10);
case ACTION_PULSE_VOLUME_DOWN: return pulse_volume_set (&g.pulse, -10);
case ACTION_PULSE_MUTE: return pulse_volume_mute (&g.pulse);
#endif // WITH_PULSE
// XXX: these should rather be parametrized
case ACTION_SCROLL_UP: return app_scroll (-3);
case ACTION_SCROLL_DOWN: return app_scroll (3);
@@ -2588,10 +2806,12 @@ app_init_bindings (const char *keymap,
static bool
app_process_termo_event (termo_key_t *event)
{
if (event->type == TERMO_TYPE_FOCUS)
bool handled = false;
if ((handled = event->type == TERMO_TYPE_FOCUS))
{
g.focused = !!event->code.focused;
app_invalidate ();
// Senseless fall-through
}
struct binding dummy = { *event, 0, 0 }, *binding;
@@ -2601,7 +2821,7 @@ app_process_termo_event (termo_key_t *event)
sizeof *binding, app_binding_cmp)))
return app_editor_process_action (binding->action);
if (event->type != TERMO_TYPE_KEY || event->modifiers != 0)
return false;
return handled;
line_editor_insert (&g.editor, event->code.codepoint);
app_invalidate ();
@@ -2620,7 +2840,7 @@ app_process_termo_event (termo_key_t *event)
if (app_goto_tab ((n == 0 ? 10 : n) - 1))
return true;
}
return false;
return handled;
}
// --- Current tab -------------------------------------------------------------
@@ -3644,7 +3864,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;
@@ -3661,7 +3881,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);
}
}
@@ -3910,11 +4130,85 @@ spectrum_setup_fifo (void)
}
#else // ! WITH_FFTW
#define spectrum_setup_fifo()
#define spectrum_clear()
#define spectrum_discard_fifo()
#define spectrum_setup_fifo() BLOCK_START BLOCK_END
#define spectrum_clear() BLOCK_START BLOCK_END
#define spectrum_discard_fifo() BLOCK_START BLOCK_END
#endif // ! WITH_FFTW
// --- PulseAudio --------------------------------------------------------------
#ifdef WITH_PULSE
static bool
mpd_find_output (const struct strv *data, const char *wanted)
{
// The plugin field is new in MPD 0.21, by default take any output
unsigned long n, accept = 1;
for (size_t i = data->len; i--; )
{
char *key, *value;
if (!(key = mpd_parse_kv (data->vector[i], &value)))
continue;
if (!strcasecmp_ascii (key, "outputid"))
{
if (accept)
return true;
accept = 1;
}
else if (!strcasecmp_ascii (key, "plugin"))
accept &= !strcmp (value, wanted);
else if (!strcasecmp_ascii (key, "outputenabled")
&& xstrtoul (&n, value, 10))
accept &= n == 1;
}
return false;
}
static void
mpd_on_outputs_response (const struct mpd_response *response,
const struct strv *data, void *user_data)
{
(void) user_data;
// TODO: check whether an action is actually necessary
pulse_free (&g.pulse);
if (response->success && !mpd_find_output (data, "pulse"))
print_debug ("MPD has no PulseAudio output to control");
else
{
pulse_init (&g.pulse, &g.poller);
g.pulse.on_update = app_invalidate;
}
app_invalidate ();
}
static void
pulse_update (void)
{
struct mpd_client *c = &g.client;
if (!g.pulse_control_requested)
return;
// The read permission is sufficient for this command
mpd_client_send_command (c, "outputs", NULL);
mpd_client_add_task (c, mpd_on_outputs_response, NULL);
}
static void
pulse_disable (void)
{
pulse_free (&g.pulse);
app_invalidate ();
}
#else // ! WITH_PULSE
#define pulse_update() BLOCK_START BLOCK_END
#define pulse_disable() BLOCK_START BLOCK_END
#endif // ! WITH_PULSE
// --- MPD interface -----------------------------------------------------------
static void
@@ -4072,7 +4366,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);
@@ -4082,29 +4376,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
@@ -4179,6 +4483,8 @@ mpd_on_events (unsigned subsystems, void *user_data)
if (subsystems & MPD_SUBSYSTEM_DATABASE)
library_tab_reload (NULL);
if (subsystems & MPD_SUBSYSTEM_OUTPUT)
pulse_update ();
if (subsystems & (MPD_SUBSYSTEM_PLAYER | MPD_SUBSYSTEM_OPTIONS
| MPD_SUBSYSTEM_PLAYLIST | MPD_SUBSYSTEM_MIXER | MPD_SUBSYSTEM_UPDATE))
@@ -4243,6 +4549,7 @@ mpd_on_ready (void)
mpd_request_info ();
library_tab_reload (NULL);
spectrum_setup_fifo ();
pulse_update ();
mpd_enqueue_step (0);
}
@@ -4297,6 +4604,7 @@ mpd_on_failure (void *user_data)
info_tab_update ();
spectrum_discard_fifo ();
pulse_disable ();
}
static void

2
termo

Submodule termo updated: 94a77a10d8...8265f075b1