Compare commits
17 Commits
fa4443a3ce
...
v1.1.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
ba86961ba5
|
|||
|
0cdb4989e5
|
|||
|
6de940fe96
|
|||
|
6bd8c1db2f
|
|||
|
56efe9c6a9
|
|||
|
8a17e674f8
|
|||
|
bd0ee66c19
|
|||
|
6f6efe077b
|
|||
|
ee5c41b2bf
|
|||
|
9a67e076a9
|
|||
|
53fbb3dec1
|
|||
|
267598643a
|
|||
|
fba1210e9f
|
|||
|
30777e8fd3
|
|||
|
353174ee3c
|
|||
|
2d641d087f
|
|||
|
20c8385f2e
|
32
.clang-format
Normal file
32
.clang-format
Normal 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
2
.gitignore
vendored
@@ -7,3 +7,5 @@
|
||||
/nncmpp.files
|
||||
/nncmpp.creator*
|
||||
/nncmpp.includes
|
||||
/nncmpp.cflags
|
||||
/nncmpp.cxxflags
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
cmake_minimum_required (VERSION 3.0)
|
||||
project (nncmpp VERSION 1.0.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,7 +45,7 @@ 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 ()
|
||||
endif ()
|
||||
|
||||
include_directories (${Unistring_INCLUDE_DIRS}
|
||||
@@ -51,16 +54,18 @@ include_directories (${Unistring_INCLUDE_DIRS}
|
||||
link_directories (${curl_LIBRARY_DIRS} ${fftw_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})
|
||||
|
||||
11
NEWS
11
NEWS
@@ -1,10 +1,19 @@
|
||||
1.1.0 (20xx-xx-xx)
|
||||
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,
|
||||
using a new "defocused" attribute for selected rows
|
||||
|
||||
* Made it possible to show a spectrum visualiser when built against FFTW
|
||||
|
||||
* Any program arguments are now added to MPD's current playlist
|
||||
|
||||
|
||||
1.0.0 (2020-11-05)
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ Building
|
||||
--------
|
||||
Build dependencies: CMake, pkg-config, asciidoctor, liberty (included),
|
||||
termo (included) +
|
||||
Runtime dependencies: ncursesw, libunistring, cURL
|
||||
Runtime dependencies: ncursesw, libunistring, cURL, fftw3 (optional)
|
||||
|
||||
$ git clone --recursive https://git.janouch.name/p/nncmpp.git
|
||||
$ mkdir nncmpp/build
|
||||
|
||||
@@ -10,7 +10,7 @@ nncmpp - terminal-based MPD client
|
||||
|
||||
Synopsis
|
||||
--------
|
||||
*nncmpp* [_OPTION_]...
|
||||
*nncmpp* [_OPTION_]... [_URL_ | _PATH_]...
|
||||
|
||||
Description
|
||||
-----------
|
||||
@@ -19,6 +19,10 @@ 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.
|
||||
|
||||
As a convenience utility, any program arguments are added to the MPD queue.
|
||||
Note that to add files outside the database, you need to connect to MPD using
|
||||
a socket file.
|
||||
|
||||
Options
|
||||
-------
|
||||
*-d*, *--debug*::
|
||||
|
||||
163
nncmpp.c
163
nncmpp.c
@@ -78,9 +78,7 @@ 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.
|
||||
@@ -110,7 +108,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))
|
||||
{
|
||||
@@ -569,6 +567,9 @@ item_list_resize (struct item_list *self, size_t len)
|
||||
|
||||
// --- Spectrum analyzer -------------------------------------------------------
|
||||
|
||||
// See http://www.zytrax.com/tech/audio/equalization.html
|
||||
// for a good write-up about this problem domain
|
||||
|
||||
#ifdef WITH_FFTW
|
||||
|
||||
struct spectrum
|
||||
@@ -638,13 +639,13 @@ spectrum_decode_8 (struct spectrum *s, int sample)
|
||||
{
|
||||
size_t n = s->useful_bins;
|
||||
float *data = s->data + n;
|
||||
int8_t *p = (int8_t *) s->buffer + sample * n * s->channels;
|
||||
while (n--)
|
||||
for (int8_t *p = (int8_t *) s->buffer + sample * n * s->channels;
|
||||
n--; p += s->channels)
|
||||
{
|
||||
int32_t acc = 0;
|
||||
for (int ch = 0; ch < s->channels; ch++)
|
||||
acc += *p++;
|
||||
*data++ = (float) acc / -INT8_MIN / s->channels;
|
||||
acc += p[ch];
|
||||
*data++ = (float) acc / s->channels / -INT8_MIN;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -653,16 +654,25 @@ spectrum_decode_16 (struct spectrum *s, int sample)
|
||||
{
|
||||
size_t n = s->useful_bins;
|
||||
float *data = s->data + n;
|
||||
int16_t *p = (int16_t *) s->buffer + sample * n * s->channels;
|
||||
while (n--)
|
||||
for (int16_t *p = (int16_t *) s->buffer + sample * n * s->channels;
|
||||
n--; p += s->channels)
|
||||
{
|
||||
int32_t acc = 0;
|
||||
for (int ch = 0; ch < s->channels; ch++)
|
||||
acc += *p++;
|
||||
*data++ = (float) acc / -INT16_MIN / s->channels;
|
||||
acc += p[ch];
|
||||
*data++ = (float) acc / s->channels / -INT16_MIN;
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
spectrum_decode_16_2 (struct spectrum *s, int sample)
|
||||
{
|
||||
size_t n = s->useful_bins;
|
||||
float *data = s->data + n;
|
||||
for (int16_t *p = (int16_t *) s->buffer + sample * n * 2; n--; p += 2)
|
||||
*data++ = ((int32_t) p[0] + p[1]) / 2. / -INT16_MIN;
|
||||
}
|
||||
|
||||
// - - Spectrum analysis - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
static const char *spectrum_bars[] =
|
||||
@@ -781,7 +791,8 @@ spectrum_init (struct spectrum *s, char *format, int bars, struct error **e)
|
||||
// having the amount of bins be strictly a factor of Nyquist / 20 (stemming
|
||||
// from the equation 20 = Nyquist / bins). Since log2(44100 / 2 / 20) > 10,
|
||||
// it would be fairly expensive, and somewhat slowly updating. Always.
|
||||
// (Note that you can increase window overlap to get smoother framerates.)
|
||||
// (Note that you can increase window overlap to get smoother framerates,
|
||||
// but it would remain laggy.)
|
||||
double audible_ratio = s->sampling_rate / 2. / 20000;
|
||||
s->bins = ceil (necessary_bins * MAX (audible_ratio, 1));
|
||||
s->useful_bins = s->bins / 2;
|
||||
@@ -800,9 +811,14 @@ spectrum_init (struct spectrum *s, char *format, int bars, struct error **e)
|
||||
if (s->samples < 1)
|
||||
s->samples = 1;
|
||||
|
||||
// XXX: we average the channels but might want to average the DFT results
|
||||
if (s->bits == 8) s->decode = spectrum_decode_8;
|
||||
if (s->bits == 16) s->decode = spectrum_decode_16;
|
||||
|
||||
// Micro-optimize to achieve some piece of mind; it's weak but measurable
|
||||
if (s->bits == 16 && s->channels == 2)
|
||||
s->decode = spectrum_decode_16_2;
|
||||
|
||||
s->buffer_size = s->samples * s->useful_bins * s->bits / 8 * s->channels;
|
||||
s->buffer = xcalloc (1, s->buffer_size);
|
||||
|
||||
@@ -944,6 +960,7 @@ static struct app_context
|
||||
|
||||
struct config config; ///< Program configuration
|
||||
struct strv streams; ///< List of "name NUL URI NUL"
|
||||
struct strv enqueue; ///< Items to enqueue once connected
|
||||
|
||||
struct tab *help_tab; ///< Special help tab
|
||||
struct tab *tabs; ///< All other tabs
|
||||
@@ -1206,6 +1223,7 @@ app_init_context (void)
|
||||
g.client = mpd_client_make (&g.poller);
|
||||
g.config = config_make ();
|
||||
g.streams = strv_make ();
|
||||
g.enqueue = strv_make ();
|
||||
g.playlist = item_list_make ();
|
||||
|
||||
g.playback_info = str_map_make (free);
|
||||
@@ -1236,9 +1254,9 @@ app_init_terminal (void)
|
||||
{
|
||||
TERMO_CHECK_VERSION;
|
||||
if (!(g.tk = termo_new (STDIN_FILENO, NULL, 0)))
|
||||
abort ();
|
||||
exit_fatal ("failed to set up the terminal");
|
||||
if (!initscr () || nonl () == ERR)
|
||||
abort ();
|
||||
exit_fatal ("failed to set up the terminal");
|
||||
|
||||
// By default we don't use any colors so they're not required...
|
||||
if (start_color () == ERR
|
||||
@@ -1267,6 +1285,7 @@ app_free_context (void)
|
||||
mpd_client_free (&g.client);
|
||||
str_map_free (&g.playback_info);
|
||||
strv_free (&g.streams);
|
||||
strv_free (&g.enqueue);
|
||||
item_list_free (&g.playlist);
|
||||
|
||||
#ifdef WITH_FFTW
|
||||
@@ -2567,10 +2586,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;
|
||||
@@ -2580,7 +2601,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 ();
|
||||
@@ -2599,7 +2620,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 -------------------------------------------------------------
|
||||
@@ -3768,9 +3789,15 @@ spectrum_redraw (void)
|
||||
// let's hack around it in this case
|
||||
if (g.spectrum_row != -1)
|
||||
{
|
||||
// Don't mess up the line editor caret, when it's shown
|
||||
int last_x, last_y;
|
||||
getyx (stdscr, last_y, last_x);
|
||||
|
||||
attrset (APP_ATTR (TAB_BAR));
|
||||
mvaddstr (g.spectrum_row, g.spectrum_column, g.spectrum.spectrum);
|
||||
attrset (0);
|
||||
|
||||
move (last_y, last_x);
|
||||
refresh ();
|
||||
}
|
||||
else
|
||||
@@ -4168,6 +4195,57 @@ mpd_queue_reconnect (void)
|
||||
poller_timer_set (&g.connect_event, 5 * 1000);
|
||||
}
|
||||
|
||||
// On an error, MPD discards the rest of our enqueuing commands--work it around
|
||||
static void mpd_enqueue_step (size_t start_offset);
|
||||
|
||||
static void
|
||||
mpd_on_enqueue_response (const struct mpd_response *response,
|
||||
const struct strv *data, void *user_data)
|
||||
{
|
||||
(void) data;
|
||||
intptr_t start_offset = (intptr_t) user_data;
|
||||
|
||||
if (response->success)
|
||||
strv_reset (&g.enqueue);
|
||||
else
|
||||
{
|
||||
// Their addition may also overflow, but YOLO
|
||||
hard_assert (start_offset >= 0 && response->list_offset >= 0);
|
||||
|
||||
print_error ("%s: %s", response->message_text,
|
||||
g.enqueue.vector[start_offset + response->list_offset]);
|
||||
mpd_enqueue_step (start_offset + response->list_offset + 1);
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
mpd_enqueue_step (size_t start_offset)
|
||||
{
|
||||
struct mpd_client *c = &g.client;
|
||||
if (start_offset >= g.enqueue.len)
|
||||
{
|
||||
strv_reset (&g.enqueue);
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: might want to consider using addid and autoplaying
|
||||
mpd_client_list_begin (c);
|
||||
for (size_t i = start_offset; i < g.enqueue.len; i++)
|
||||
mpd_client_send_command (c, "add", g.enqueue.vector[i], NULL);
|
||||
mpd_client_list_end (c);
|
||||
mpd_client_add_task (c, mpd_on_enqueue_response, (void *) start_offset);
|
||||
mpd_client_idle (c, 0);
|
||||
}
|
||||
|
||||
static void
|
||||
mpd_on_ready (void)
|
||||
{
|
||||
mpd_request_info ();
|
||||
library_tab_reload (NULL);
|
||||
spectrum_setup_fifo ();
|
||||
mpd_enqueue_step (0);
|
||||
}
|
||||
|
||||
static void
|
||||
mpd_on_password_response (const struct mpd_response *response,
|
||||
const struct strv *data, void *user_data)
|
||||
@@ -4177,10 +4255,7 @@ mpd_on_password_response (const struct mpd_response *response,
|
||||
struct mpd_client *c = &g.client;
|
||||
|
||||
if (response->success)
|
||||
{
|
||||
mpd_request_info ();
|
||||
library_tab_reload (NULL);
|
||||
}
|
||||
mpd_on_ready ();
|
||||
else
|
||||
{
|
||||
print_error ("%s: %s",
|
||||
@@ -4203,12 +4278,7 @@ mpd_on_connected (void *user_data)
|
||||
mpd_client_add_task (c, mpd_on_password_response, NULL);
|
||||
}
|
||||
else
|
||||
{
|
||||
mpd_request_info ();
|
||||
library_tab_reload (NULL);
|
||||
}
|
||||
|
||||
spectrum_setup_fifo ();
|
||||
mpd_on_ready ();
|
||||
}
|
||||
|
||||
static void
|
||||
@@ -4507,6 +4577,28 @@ app_init_poller_events (void)
|
||||
g.refresh_event.dispatcher = app_on_refresh;
|
||||
}
|
||||
|
||||
static void
|
||||
app_init_enqueue (char *argv[], int argc)
|
||||
{
|
||||
// TODO: MPD is unwilling to play directories, so perhaps recurse ourselves
|
||||
char cwd[4096] = "";
|
||||
for (int i = 0; i < argc; i++)
|
||||
{
|
||||
// This is a super-trivial method of URL detection, however anything
|
||||
// contaning the scheme and authority delimiters in a sequence is most
|
||||
// certainly not a filesystem path, and thus it will work as expected.
|
||||
// Error handling may be done by MPD.
|
||||
const char *path_or_URL = argv[i];
|
||||
if (*path_or_URL == '/' || strstr (path_or_URL, "://"))
|
||||
strv_append (&g.enqueue, path_or_URL);
|
||||
else if (!*cwd && !getcwd (cwd, sizeof cwd))
|
||||
exit_fatal ("getcwd: %s", strerror (errno));
|
||||
else
|
||||
strv_append_owned (&g.enqueue,
|
||||
xstrdup_printf ("%s/%s", cwd, path_or_URL));
|
||||
}
|
||||
}
|
||||
|
||||
int
|
||||
main (int argc, char *argv[])
|
||||
{
|
||||
@@ -4519,7 +4611,8 @@ main (int argc, char *argv[])
|
||||
};
|
||||
|
||||
struct opt_handler oh =
|
||||
opt_handler_make (argc, argv, opts, NULL, "Terminal-based MPD client.");
|
||||
opt_handler_make (argc, argv, opts,
|
||||
"[URL | PATH]...", "Terminal-based MPD client.");
|
||||
|
||||
int c;
|
||||
while ((c = opt_handler_get (&oh)) != -1)
|
||||
@@ -4542,12 +4635,6 @@ main (int argc, char *argv[])
|
||||
|
||||
argc -= optind;
|
||||
argv += optind;
|
||||
|
||||
if (argc)
|
||||
{
|
||||
opt_handler_usage (&oh, stderr);
|
||||
exit (EXIT_FAILURE);
|
||||
}
|
||||
opt_handler_free (&oh);
|
||||
|
||||
// We only need to convert to and from the terminal encoding
|
||||
@@ -4555,10 +4642,11 @@ main (int argc, char *argv[])
|
||||
print_warning ("failed to set the locale");
|
||||
|
||||
app_init_context ();
|
||||
app_init_enqueue (argv, argc);
|
||||
app_load_configuration ();
|
||||
app_init_terminal ();
|
||||
signals_setup_handlers ();
|
||||
app_init_poller_events ();
|
||||
app_init_terminal ();
|
||||
|
||||
g_normal_keys = app_init_bindings ("normal",
|
||||
g_normal_defaults, N_ELEMENTS (g_normal_defaults), &g_normal_keys_len);
|
||||
@@ -4578,6 +4666,11 @@ main (int argc, char *argv[])
|
||||
app_prepend_tab (current_tab_init ());
|
||||
app_switch_tab ((g.help_tab = help_tab_init ()));
|
||||
|
||||
// TODO: the help tab should be the default for new users only,
|
||||
// so provide a configuration option to flip this
|
||||
if (argc)
|
||||
app_switch_tab (&g_current_tab);
|
||||
|
||||
g.polling = true;
|
||||
while (g.polling)
|
||||
poller_run (&g.poller);
|
||||
|
||||
2
termo
2
termo
Submodule termo updated: 94a77a10d8...8265f075b1
Reference in New Issue
Block a user