19 Commits

Author SHA1 Message Date
708b78f145 Bump liberty
All checks were successful
Alpine 3.21 Success
Arch Linux AUR Success
OpenBSD 7.6 Success
2025-08-02 18:24:16 +02:00
3361df0ccb Bump liberty
All checks were successful
Alpine 3.21 Success
Arch Linux AUR Success
OpenBSD 7.6 Success
2025-06-04 22:00:25 +02:00
13195e797a Bump liberty and termo
All checks were successful
Alpine 3.20 Success
Arch Linux AUR Success
OpenBSD 7.6 Success
OpenBSD 7.5 Abandoned
2025-04-23 22:02:50 +02:00
7fee01a5c6 Update NEWS 2025-01-08 09:42:31 +01:00
8b4300c796 Bump liberty, check the connection while searching
All checks were successful
Alpine 3.20 Success
Arch Linux AUR Success
OpenBSD 7.5 Success
This just prevents immediate assertion failures.
2025-01-08 08:10:19 +01:00
cd3d4eb1ad README.adoc: update package information 2024-11-13 08:36:52 +01:00
641803df35 Enable user-defined actions
All checks were successful
Alpine 3.20 Success
Arch Linux AUR Success
OpenBSD 7.5 Success
Also fix pclose() handling within Info plugins,
and prevent them from screwing up the terminal with error output
on initialization.

This is still rather crude, but at least it's possible.
2024-10-31 21:19:28 +01:00
eec0706333 Support libunistring versions before 1.2
All checks were successful
Alpine 3.20 Success
Arch Linux AUR Success
OpenBSD 7.5 Success
2024-09-02 23:25:08 +02:00
9d0acddf9a Implement font fallbacks in the editor
Some checks failed
Alpine 3.20 Success
Arch Linux AUR Success
OpenBSD 7.5 Scripts failed
Opting for not making any changes in the XUI.
2024-09-02 21:47:50 +02:00
ec12e2ac25 Bump liberty, fix calloc argument order
All checks were successful
Alpine 3.20 Success
Arch Linux AUR Success
OpenBSD 7.5 Success
2024-08-08 09:19:29 +02:00
24449fe721 Make global search match on filenames
All checks were successful
Alpine 3.20 Success
OpenBSD 7.5 Success
Arch Linux AUR Success
2024-08-07 22:12:43 +02:00
60171cc5a3 Bump liberty 2024-08-07 22:05:29 +02:00
20502200d9 Make global search indicate the search terms
All checks were successful
Alpine 3.20 Success
Arch Linux AUR Success
OpenBSD 7.5 Success
2024-07-21 21:04:17 +02:00
e6de4f11e7 Be actually able to use a system Termo library
All checks were successful
Alpine 3.19 Success
Arch Linux AUR Success
OpenBSD 7.3 Success
2024-04-10 17:31:40 +02:00
c4bce75866 Bump version, update NEWS
All checks were successful
Arch Linux AUR Success
Alpine 3.19 Success
2024-02-27 00:39:23 +01:00
08b87bccbd Bump liberty, fix a dead link 2024-02-27 00:30:39 +01:00
fa460b97cf Fix info plugins installation
They could never become executable.
2024-02-25 05:15:00 +01:00
ef19337bad Bump version, update NEWS 2024-02-10 18:39:57 +01:00
9699b80e9d Very mildly improve stream handling 2024-02-10 18:35:10 +01:00
9 changed files with 319 additions and 79 deletions

View File

@@ -1,5 +1,5 @@
cmake_minimum_required (VERSION 3.0...3.27) cmake_minimum_required (VERSION 3.0...3.27)
project (nncmpp VERSION 2.0.0 LANGUAGES C) project (nncmpp VERSION 2.1.1 LANGUAGES C)
# Moar warnings # Moar warnings
if ("${CMAKE_C_COMPILER_ID}" MATCHES "GNU" OR CMAKE_COMPILER_IS_GNUCC) if ("${CMAKE_C_COMPILER_ID}" MATCHES "GNU" OR CMAKE_COMPILER_IS_GNUCC)
@@ -121,14 +121,16 @@ add_custom_command (OUTPUT ${actions}
# Build the main executable and link it # Build the main executable and link it
add_executable (${PROJECT_NAME} ${PROJECT_NAME}.c ${actions}) add_executable (${PROJECT_NAME} ${PROJECT_NAME}.c ${actions})
target_link_libraries (${PROJECT_NAME} ${Unistring_LIBRARIES} target_link_libraries (${PROJECT_NAME} ${Unistring_LIBRARIES}
${Ncursesw_LIBRARIES} termo-static ${curl_LIBRARIES} ${extra_libraries}) ${Ncursesw_LIBRARIES} ${Termo_LIBRARIES} ${curl_LIBRARIES}
${extra_libraries})
add_threads (${PROJECT_NAME}) add_threads (${PROJECT_NAME})
# Installation # Installation
install (TARGETS ${PROJECT_NAME} DESTINATION ${CMAKE_INSTALL_BINDIR}) install (TARGETS ${PROJECT_NAME} DESTINATION ${CMAKE_INSTALL_BINDIR})
install (FILES LICENSE DESTINATION ${CMAKE_INSTALL_DOCDIR}) install (FILES LICENSE DESTINATION ${CMAKE_INSTALL_DOCDIR})
install (DIRECTORY contrib DESTINATION ${CMAKE_INSTALL_DATADIR}/${PROJECT_NAME}) install (DIRECTORY contrib DESTINATION ${CMAKE_INSTALL_DATADIR}/${PROJECT_NAME})
install (DIRECTORY info DESTINATION ${CMAKE_INSTALL_DATADIR}/${PROJECT_NAME}) install (DIRECTORY info DESTINATION ${CMAKE_INSTALL_DATADIR}/${PROJECT_NAME}
USE_SOURCE_PERMISSIONS)
if (WITH_X11) if (WITH_X11)
include (IconUtils) include (IconUtils)

View File

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

29
NEWS
View File

@@ -1,5 +1,30 @@
Unreleased Unreleased
* Made global search indicate the search terms, and match on filenames
* Added ability to configure bindable user-defined actions;
these can launch arbitrary shell commands
* Prevented crashes when the daemon disconnects during search
* X11: added support for font fallbacks to the editor as well
* X11: fixed that XSettings had to be present
* X11: fixed a new Fontconfig warning
2.1.1 (2024-02-27)
* Fixed installation of Info tab plugins
* Fixed display of playback mode toggles in the terminal user interface
* Fixed a dead link in the manual page
2.1.0 (2024-02-11)
* Added ability to look up song lyrics, * Added ability to look up song lyrics,
using a new scriptable extension interface for the Info tab using a new scriptable extension interface for the Info tab
@@ -13,10 +38,14 @@ Unreleased
* X11: fixed rendering of overflowing, partially visible list items * X11: fixed rendering of overflowing, partially visible list items
* X11: fixed a crash when resizing the window to zero dimensions
* Added a "o" binding to select the currently playing song * Added a "o" binding to select the currently playing song
* Added Readline-like M-u, M-l, M-c editor bindings * Added Readline-like M-u, M-l, M-c editor bindings
* Made the scroll wheel work on the elapsed time gauge and the volume display
* Changed volume adjustment bindings to use +/- keys * Changed volume adjustment bindings to use +/- keys
* Changed volume adjustment to go in steps of 5 rather than 10 % * Changed volume adjustment to go in steps of 5 rather than 10 %

View File

@@ -33,6 +33,8 @@ You can get a package with the latest development version using Arch Linux's
https://aur.archlinux.org/packages/nncmpp-git[AUR], https://aur.archlinux.org/packages/nncmpp-git[AUR],
or as a https://git.janouch.name/p/nixexprs[Nix derivation]. or as a https://git.janouch.name/p/nixexprs[Nix derivation].
Stable versions are present in: OpenBSD ports.
Documentation Documentation
------------- -------------
See the link:nncmpp.adoc[man page] for information about usage. See the link:nncmpp.adoc[man page] for information about usage.

Submodule liberty updated: 2a1f17a8f7...7425355d01

View File

@@ -1,6 +1,6 @@
# nncmpp.actions.awk: produce C code for a list of user actions # nncmpp.actions.awk: produce C code for a list of user actions
# #
# Copyright (c) 2022, Přemysl Eric Janouch <p@janouch.name> # Copyright (c) 2022 - 2024, Přemysl Eric Janouch <p@janouch.name>
# SPDX-License-Identifier: 0BSD # SPDX-License-Identifier: 0BSD
# #
# Usage: env LC_ALL=C A=0 B=1 awk -f nncmpp.actions.awk \ # Usage: env LC_ALL=C A=0 B=1 awk -f nncmpp.actions.awk \
@@ -91,7 +91,7 @@ END {
print "enum action {" print "enum action {"
for (i in Constants) for (i in Constants)
print "\t" "ACTION_" Constants[i] "," print "\t" "ACTION_" Constants[i] ","
print "\t" "ACTION_COUNT" print "\t" "ACTION_USER_0"
print "};" print "};"
print "" print ""
print "static const char *g_action_names[] = {" print "static const char *g_action_names[] = {"

View File

@@ -69,7 +69,7 @@ colors = {
scrollbar = "" scrollbar = ""
} }
streams = { streams = {
"dnbradio.com" = "http://www.dnbradio.com/hi.m3u" "dnbradio.com" = "https://dnbradio.com/hi.pls"
"BassDrive.com" = "http://bassdrive.com/v2/streams/BassDrive.pls" "BassDrive.com" = "http://bassdrive.com/v2/streams/BassDrive.pls"
} }
.... ....
@@ -85,6 +85,18 @@ To adjust key bindings, put them within a *normal* or *editor* object.
Run *nncmpp* with the *--debug* option to find out key combinations names. Run *nncmpp* with the *--debug* option to find out key combinations names.
Press *?* in the help tab to learn the action identifiers to use. Press *?* in the help tab to learn the action identifiers to use.
You may also define and bind your own actions, launching arbitrary
shell commands. Note that you cannot override internal actions in this manner.
....
actions = {
"pioneer-on-off" = {
description = "Pioneer amplifier: turn on/off"
command = "elksmart-comm --nec A538"
}
}
....
Spectrum visualiser Spectrum visualiser
------------------- -------------------
When built against the FFTW library, *nncmpp* can make use of MPD's "fifo" When built against the FFTW library, *nncmpp* can make use of MPD's "fifo"

335
nncmpp.c
View File

@@ -1,7 +1,7 @@
/* /*
* nncmpp -- the MPD client you never knew you needed * nncmpp -- the MPD client you never knew you needed
* *
* Copyright (c) 2016 - 2023, Přemysl Eric Janouch <p@janouch.name> * Copyright (c) 2016 - 2024, Přemysl Eric Janouch <p@janouch.name>
* *
* Permission to use, copy, modify, and/or distribute this software for any * Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted. * purpose with or without fee is hereby granted.
@@ -522,7 +522,7 @@ static struct item_list
item_list_make (void) item_list_make (void)
{ {
struct item_list self = {}; struct item_list self = {};
self.items = xcalloc (sizeof *self.items, (self.alloc = 16)); self.items = xcalloc ((self.alloc = 16), sizeof *self.items);
return self; return self;
} }
@@ -814,9 +814,9 @@ spectrum_init (struct spectrum *s, char *format, int bars, int fps,
s->useful_bins = s->bins / 2; s->useful_bins = s->bins / 2;
int used_bins = necessary_bins / 2; int used_bins = necessary_bins / 2;
s->rendered = xcalloc (sizeof *s->rendered, s->bars * 3 + 1); s->rendered = xcalloc (s->bars * 3 + 1, sizeof *s->rendered);
s->spectrum = xcalloc (sizeof *s->spectrum, s->bars); s->spectrum = xcalloc (s->bars, sizeof *s->spectrum);
s->top_bins = xcalloc (sizeof *s->top_bins, s->bars); s->top_bins = xcalloc (s->bars, sizeof *s->top_bins);
for (int bar = 0; bar < s->bars; bar++) for (int bar = 0; bar < s->bars; bar++)
{ {
int top_bin = floor (pow (used_bins + 1, (bar + 1.) / s->bars)) - 1; int top_bin = floor (pow (used_bins + 1, (bar + 1.) / s->bars)) - 1;
@@ -839,7 +839,7 @@ spectrum_init (struct spectrum *s, char *format, int bars, int fps,
s->buffer = xcalloc (1, s->buffer_size); s->buffer = xcalloc (1, s->buffer_size);
// Prepare the window // Prepare the window
s->window = xcalloc (sizeof *s->window, s->bins); s->window = xcalloc (s->bins, sizeof *s->window);
window_hann (s->window, s->bins); window_hann (s->window, s->bins);
// Multiply by 2 for only using half of the DFT's result, then adjust to // Multiply by 2 for only using half of the DFT's result, then adjust to
@@ -849,11 +849,11 @@ spectrum_init (struct spectrum *s, char *format, int bars, int fps,
float coherent_gain = window_coherent_gain (s->window, s->bins); float coherent_gain = window_coherent_gain (s->window, s->bins);
s->accumulator_scale = 2 * 2 / coherent_gain / coherent_gain / s->samples; s->accumulator_scale = 2 * 2 / coherent_gain / coherent_gain / s->samples;
s->data = xcalloc (sizeof *s->data, s->bins); s->data = xcalloc (s->bins, sizeof *s->data);
s->windowed = fftw_malloc (sizeof *s->windowed * s->bins); s->windowed = fftw_malloc (s->bins * sizeof *s->windowed);
s->out = fftw_malloc (sizeof *s->out * (s->useful_bins + 1)); s->out = fftw_malloc ((s->useful_bins + 1) * sizeof *s->out);
s->p = fftwf_plan_dft_r2c_1d (s->bins, s->windowed, s->out, FFTW_MEASURE); s->p = fftwf_plan_dft_r2c_1d (s->bins, s->windowed, s->out, FFTW_MEASURE);
s->accumulator = xcalloc (sizeof *s->accumulator, s->useful_bins); s->accumulator = xcalloc (s->useful_bins, sizeof *s->accumulator);
return true; return true;
} }
@@ -1256,6 +1256,9 @@ static struct app_context
struct config config; ///< Program configuration struct config config; ///< Program configuration
struct strv streams; ///< List of "name NUL URI NUL" struct strv streams; ///< List of "name NUL URI NUL"
struct strv enqueue; ///< Items to enqueue once connected struct strv enqueue; ///< Items to enqueue once connected
struct strv action_names; ///< User-defined action names
struct strv action_descriptions; ///< User-defined action descriptions
struct strv action_commands; ///< User-defined action commands
struct tab *help_tab; ///< Special help tab struct tab *help_tab; ///< Special help tab
struct tab *tabs; ///< All other tabs struct tab *tabs; ///< All other tabs
@@ -1337,7 +1340,7 @@ on_pulseaudio_changed (struct config_item *item)
g.pulse_control_requested = item->value.boolean; g.pulse_control_requested = item->value.boolean;
} }
static struct config_schema g_config_settings[] = static const struct config_schema g_config_settings[] =
{ {
{ .name = "address", { .name = "address",
.comment = "Address to connect to the MPD server", .comment = "Address to connect to the MPD server",
@@ -1401,7 +1404,7 @@ static struct config_schema g_config_settings[] =
{} {}
}; };
static struct config_schema g_config_colors[] = static const struct config_schema g_config_colors[] =
{ {
#define XX(name_, config, fg_, bg_, attrs_) \ #define XX(name_, config, fg_, bg_, attrs_) \
{ .name = #config, .type = CONFIG_ITEM_STRING }, { .name = #config, .type = CONFIG_ITEM_STRING },
@@ -1410,6 +1413,17 @@ static struct config_schema g_config_colors[] =
{} {}
}; };
static const struct config_schema g_config_actions[] =
{
{ .name = "description",
.comment = "Human-readable description of the action",
.type = CONFIG_ITEM_STRING },
{ .name = "command",
.comment = "Shell command to run",
.type = CONFIG_ITEM_STRING },
{}
};
static const char * static const char *
get_config_string (struct config_item *root, const char *key) get_config_string (struct config_item *root, const char *key)
{ {
@@ -1484,6 +1498,35 @@ load_config_streams (struct config_item *subtree, void *user_data)
sizeof *g.streams.vector, strv_sort_utf8_cb); sizeof *g.streams.vector, strv_sort_utf8_cb);
} }
static void
load_config_actions (struct config_item *subtree, void *user_data)
{
(void) user_data;
struct str_map_iter iter = str_map_iter_make (&subtree->value.object);
while (str_map_iter_next (&iter))
strv_append (&g.action_names, iter.link->key);
qsort (g.action_names.vector, g.action_names.len,
sizeof *g.action_names.vector, strv_sort_utf8_cb);
for (size_t i = 0; i < g.action_names.len; i++)
{
const char *name = g.action_names.vector[i];
struct config_item *item = config_item_get (subtree, name, NULL);
hard_assert (item != NULL);
if (item->type != CONFIG_ITEM_OBJECT)
exit_fatal ("`%s': invalid user action, expected an object", name);
config_schema_apply_to_object (g_config_actions, item, NULL);
config_schema_call_changed (item);
const char *description = get_config_string (item, "description");
const char *command = get_config_string (item, "command");
strv_append (&g.action_descriptions, description ? description : name);
strv_append (&g.action_commands, command ? command : "");
}
}
static void static void
app_load_configuration (void) app_load_configuration (void)
{ {
@@ -1491,6 +1534,8 @@ app_load_configuration (void)
config_register_module (config, "settings", load_config_settings, NULL); config_register_module (config, "settings", load_config_settings, NULL);
config_register_module (config, "colors", load_config_colors, NULL); config_register_module (config, "colors", load_config_colors, NULL);
config_register_module (config, "streams", load_config_streams, NULL); config_register_module (config, "streams", load_config_streams, NULL);
// This must run before bindings are parsed in app_init_ui().
config_register_module (config, "actions", load_config_actions, NULL);
// Bootstrap configuration, so that we can access schema items at all // Bootstrap configuration, so that we can access schema items at all
config_load (config, config_item_object ()); config_load (config, config_item_object ());
@@ -1548,6 +1593,9 @@ app_init_context (void)
g.config = config_make (); g.config = config_make ();
g.streams = strv_make (); g.streams = strv_make ();
g.enqueue = strv_make (); g.enqueue = strv_make ();
g.action_names = strv_make ();
g.action_descriptions = strv_make ();
g.action_commands = strv_make ();
g.playback_info = str_map_make (free); g.playback_info = str_map_make (free);
g.playback_info.key_xfrm = tolower_ascii_strxfrm; g.playback_info.key_xfrm = tolower_ascii_strxfrm;
@@ -1570,6 +1618,9 @@ app_free_context (void)
str_map_free (&g.playback_info); str_map_free (&g.playback_info);
strv_free (&g.streams); strv_free (&g.streams);
strv_free (&g.enqueue); strv_free (&g.enqueue);
strv_free (&g.action_names);
strv_free (&g.action_descriptions);
strv_free (&g.action_commands);
item_list_free (&g.playlist); item_list_free (&g.playlist);
#ifdef WITH_FFTW #ifdef WITH_FFTW
@@ -2379,12 +2430,52 @@ app_goto_tab (int tab_index)
static int static int
action_resolve (const char *name) action_resolve (const char *name)
{ {
for (int i = 0; i < ACTION_COUNT; i++) for (int i = 0; i < ACTION_USER_0; i++)
if (!strcasecmp_ascii (g_action_names[i], name)) if (!strcasecmp_ascii (g_action_names[i], name))
return i; return i;
// We could put this lookup first, and accordingly adjust
// app_init_bindings() to do action_resolve(action_name(action)),
// however the ability to override internal actions seems pointless.
for (size_t i = 0; i < g.action_names.len; i++)
if (!strcasecmp_ascii (g.action_names.vector[i], name))
return ACTION_USER_0 + i;
return -1; return -1;
} }
static const char *
action_name (enum action action)
{
if (action < ACTION_USER_0)
return g_action_names[action];
size_t user_action = action - ACTION_USER_0;
hard_assert (user_action < g.action_names.len);
return g.action_names.vector[user_action];
}
static const char *
action_description (enum action action)
{
if (action < ACTION_USER_0)
return g_action_descriptions[action];
size_t user_action = action - ACTION_USER_0;
hard_assert (user_action < g.action_descriptions.len);
return g.action_descriptions.vector[user_action];
}
static const char *
action_command (enum action action)
{
if (action < ACTION_USER_0)
return NULL;
size_t user_action = action - ACTION_USER_0;
hard_assert (user_action < g.action_commands.len);
return g.action_commands.vector[user_action];
}
// --- User input handling ----------------------------------------------------- // --- User input handling -----------------------------------------------------
static void static void
@@ -2516,6 +2607,64 @@ incremental_search_on_end (bool confirmed)
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static bool
run_command (const char *command, struct str *output, struct error **e)
{
char *adjusted = xstrdup_printf ("2>&1 %s", command);
print_debug ("running command: %s", adjusted);
FILE *fp = popen (adjusted, "r");
free (adjusted);
if (!fp)
return error_set (e, "%s", strerror (errno));
char buf[BUFSIZ];
size_t len;
while ((len = fread (buf, 1, sizeof buf, fp)) == sizeof buf)
str_append_data (output, buf, len);
str_append_data (output, buf, len);
int status = pclose (fp);
if (status < 0)
return error_set (e, "%s", strerror (errno));
if (WIFEXITED (status) && WEXITSTATUS (status))
return error_set (e, "exit status %d", WEXITSTATUS (status));
if (WIFSIGNALED (status))
return error_set (e, "terminated on signal %d", WTERMSIG (status));
if (WIFSTOPPED (status))
return error_set (e, "stopped on signal %d", WSTOPSIG (status));
return true;
}
static bool
app_process_action_command (enum action action)
{
const char *command = action_command (action);
if (!command)
return false;
struct str output = str_make ();
struct error *error = NULL;
(void) run_command (command, &output, &error);
str_enforce_utf8 (&output);
struct strv lines = strv_make ();
cstr_split (output.str, "\r\n", false, &lines);
str_free (&output);
while (lines.len && !*lines.vector[lines.len - 1])
free (strv_steal (&lines, lines.len - 1));
for (size_t i = 0; i < lines.len; i++)
print_debug ("output: %s", lines.vector[i]);
strv_free (&lines);
if (error)
{
print_error ("\"%s\": %s", action_description (action), error->message);
error_free (error);
}
return true;
}
static bool static bool
app_mpd_toggle (const char *name) app_mpd_toggle (const char *name)
{ {
@@ -2562,10 +2711,6 @@ app_process_action (enum action action)
xui_invalidate (); xui_invalidate ();
app_hide_message (); app_hide_message ();
return true; return true;
default:
print_error ("\"%s\" is not allowed here",
g_action_descriptions[action]);
return false;
case ACTION_MULTISELECT: case ACTION_MULTISELECT:
if (!tab->can_multiselect if (!tab->can_multiselect
@@ -2677,9 +2822,15 @@ app_process_action (enum action action)
case ACTION_GOTO_VIEW_BOTTOM: case ACTION_GOTO_VIEW_BOTTOM:
g.active_tab->item_selected = g.active_tab->item_top; g.active_tab->item_selected = g.active_tab->item_top;
return app_move_selection (MAX (0, app_visible_items () - 1)); return app_move_selection (MAX (0, app_visible_items () - 1));
}
default:
if (app_process_action_command (action))
return true;
print_error ("\"%s\" is not allowed here", action_description (action));
return false; return false;
} }
}
static bool static bool
app_editor_process_action (enum action action) app_editor_process_action (enum action action)
@@ -2696,8 +2847,7 @@ app_editor_process_action (enum action action)
g.editor.on_end = NULL; g.editor.on_end = NULL;
return true; return true;
default: default:
print_error ("\"%s\" is not allowed here", print_error ("\"%s\" is not allowed here", action_description (action));
g_action_descriptions[action]);
return false; return false;
case ACTION_EDITOR_B_CHAR: case ACTION_EDITOR_B_CHAR:
@@ -3599,30 +3749,67 @@ static void
library_tab_on_search_data (const struct mpd_response *response, library_tab_on_search_data (const struct mpd_response *response,
const struct strv *data, void *user_data) const struct strv *data, void *user_data)
{ {
(void) user_data; char *filter = user_data;
if (!g_library_tab.searching) if (!g_library_tab.searching)
return; goto out;
if (!response->success) if (!response->success)
{
print_error ("cannot search: %s", response->message_text); print_error ("cannot search: %s", response->message_text);
return; else
{
cstr_set (&g_library_tab.super.header,
xstrdup_printf ("%s: %s", "Global search", filter));
library_tab_load_data (data);
} }
library_tab_load_data (data); out:
free (filter);
}
static char *
mpd_quoted_filter_string (const char *value)
{
struct str quoted = str_make ();
str_append_c (&quoted, '\'');
for (const char *p = value; *p; p++)
{
if (mpd_client_must_escape_in_quote (*p))
str_append_c (&quoted, '\\');
str_append_c (&quoted, *p);
}
str_append_c (&quoted, '\'');
return str_steal (&quoted);
} }
static void static void
search_on_changed (void) search_on_changed (void)
{ {
struct mpd_client *c = &g.client; struct mpd_client *c = &g.client;
if (c->state != MPD_CONNECTED)
return;
size_t len; size_t len;
char *u8 = (char *) u32_to_u8 (g.editor.line, g.editor.len + 1, NULL, &len); char *u8 = (char *) u32_to_u8 (g.editor.line, g.editor.len + 1, NULL, &len);
mpd_client_list_begin (c);
mpd_client_send_command (c, "search", "any", u8, NULL); mpd_client_send_command (c, "search", "any", u8, NULL);
free (u8);
mpd_client_add_task (c, library_tab_on_search_data, NULL); // Just tag search doesn't consider filenames.
// Older MPD can do `search any X file X` but without the negation,
// which is necessary to avoid duplicates. Neither syntax supports OR.
// XXX: We should parse this, but it's probably not going to reach 100 soon,
// and it is not really documented what this should even look like.
if (strcmp (c->got_hello, "0.21.") > 1)
{
char *quoted = mpd_quoted_filter_string (u8);
char *expression = xstrdup_printf ("((!(any contains %s)) AND "
"(file contains %s))", quoted, quoted);
mpd_client_send_command (c, "search", expression, NULL);
free (expression);
free (quoted);
}
mpd_client_list_end (c);
mpd_client_add_task (c, library_tab_on_search_data, u8);
mpd_client_idle (c, 0); mpd_client_idle (c, 0);
} }
@@ -3843,8 +4030,13 @@ streams_tab_parse_playlist (const char *playlist, const char *content_type,
|| (content_type && is_content_type (content_type, "audio", "x-scpls"))) || (content_type && is_content_type (content_type, "audio", "x-scpls")))
extract_re = "^File[^=]*=(.+)"; extract_re = "^File[^=]*=(.+)";
else if ((lines.len && !strcasecmp_ascii (lines.vector[0], "#EXTM3U")) else if ((lines.len && !strcasecmp_ascii (lines.vector[0], "#EXTM3U"))
|| (content_type && is_content_type (content_type, "audio", "mpegurl"))
|| (content_type && is_content_type (content_type, "audio", "x-mpegurl"))) || (content_type && is_content_type (content_type, "audio", "x-mpegurl")))
extract_re = "^([^#].*)"; // This could be "^([^#].*)", however 1. we would need to resolve
// relative URIs, and 2. relative URIs probably mean a Media Playlist,
// which must be passed to MPD. The better thing to do here would be to
// reject anything with EXT-X-TARGETDURATION, and to resolve the URIs.
extract_re = "^(https?://.+)";
regex_t *re = regex_compile (extract_re, REG_EXTENDED, NULL); regex_t *re = regex_compile (extract_re, REG_EXTENDED, NULL);
hard_assert (re != NULL); hard_assert (re != NULL);
@@ -3867,7 +4059,7 @@ streams_tab_extract_links (struct str *data, const char *content_type,
} }
streams_tab_parse_playlist (data->str, content_type, out); streams_tab_parse_playlist (data->str, content_type, out);
return true; return out->len != 0;
} }
static void static void
@@ -4070,24 +4262,16 @@ info_tab_plugin_load (const char *path)
// Shell quoting is less annoying than process management. // Shell quoting is less annoying than process management.
struct str escaped = str_make (); struct str escaped = str_make ();
shell_quote (path, &escaped); shell_quote (path, &escaped);
FILE *fp = popen (escaped.str, "r");
str_free (&escaped);
if (!fp)
{
print_error ("%s: %s", path, strerror (errno));
return NULL;
}
struct str description = str_make (); struct str description = str_make ();
char buf[BUFSIZ]; struct error *error = NULL;
size_t len; (void) run_command (escaped.str, &description, &error);
while ((len = fread (buf, 1, sizeof buf, fp)) == sizeof buf) str_free (&escaped);
str_append_data (&description, buf, len); if (error)
str_append_data (&description, buf, len);
if (pclose (fp))
{ {
print_error ("%s: %s", path, error->message);
error_free (error);
str_free (&description); str_free (&description);
print_error ("%s: %s", path, strerror (errno));
return NULL; return NULL;
} }
@@ -4100,8 +4284,8 @@ info_tab_plugin_load (const char *path)
str_enforce_utf8 (&description); str_enforce_utf8 (&description);
if (!description.len) if (!description.len)
{ {
str_free (&description);
print_error ("%s: %s", path, "missing description"); print_error ("%s: %s", path, "missing description");
str_free (&description);
return NULL; return NULL;
} }
@@ -4539,7 +4723,7 @@ help_tab_on_action (enum action action)
if (action == ACTION_DESCRIBE) if (action == ACTION_DESCRIBE)
{ {
app_show_message (xstrdup ("Configuration name: "), app_show_message (xstrdup ("Configuration name: "),
xstrdup (g_action_names[a])); xstrdup (action_name (a)));
return true; return true;
} }
if (action != ACTION_CHOOSE || a == ACTION_CHOOSE /* avoid recursion */) if (action != ACTION_CHOOSE || a == ACTION_CHOOSE /* avoid recursion */)
@@ -4564,9 +4748,9 @@ help_tab_assign_action (enum action action)
static void static void
help_tab_group (struct binding *keys, size_t len, struct strv *out, help_tab_group (struct binding *keys, size_t len, struct strv *out,
bool bound[ACTION_COUNT]) bool bound[], size_t action_count)
{ {
for (enum action i = 0; i < ACTION_COUNT; i++) for (enum action i = 0; i < action_count; i++)
{ {
struct strv ass = strv_make (); struct strv ass = strv_make ();
for (size_t k = 0; k < len; k++) for (size_t k = 0; k < len; k++)
@@ -4576,7 +4760,7 @@ help_tab_group (struct binding *keys, size_t len, struct strv *out,
{ {
char *joined = strv_join (&ass, ", "); char *joined = strv_join (&ass, ", ");
strv_append_owned (out, xstrdup_printf strv_append_owned (out, xstrdup_printf
(" %s%c%s", g_action_descriptions[i], 0, joined)); (" %s%c%s", action_description (i), 0, joined));
free (joined); free (joined);
bound[i] = true; bound[i] = true;
@@ -4587,13 +4771,13 @@ help_tab_group (struct binding *keys, size_t len, struct strv *out,
} }
static void static void
help_tab_unbound (struct strv *out, bool bound[ACTION_COUNT]) help_tab_unbound (struct strv *out, bool bound[], size_t action_count)
{ {
for (enum action i = 0; i < ACTION_COUNT; i++) for (enum action i = 0; i < action_count; i++)
if (!bound[i]) if (!bound[i])
{ {
strv_append_owned (out, strv_append_owned (out,
xstrdup_printf (" %s%c", g_action_descriptions[i], 0)); xstrdup_printf (" %s%c", action_description (i), 0));
help_tab_assign_action (i); help_tab_assign_action (i);
} }
} }
@@ -4623,27 +4807,30 @@ help_tab_init (void)
struct strv *lines = &g_help_tab.lines; struct strv *lines = &g_help_tab.lines;
*lines = strv_make (); *lines = strv_make ();
bool bound[ACTION_COUNT] = { [ACTION_NONE] = true }; size_t bound_len = ACTION_USER_0 + g.action_names.len;
bool *bound = xcalloc (bound_len, sizeof *bound);
bound[ACTION_NONE] = true;
strv_append (lines, "Normal mode actions"); strv_append (lines, "Normal mode actions");
help_tab_group (g_normal_keys, g_normal_keys_len, lines, bound); help_tab_group (g_normal_keys, g_normal_keys_len, lines, bound, bound_len);
strv_append (lines, ""); strv_append (lines, "");
strv_append (lines, "Editor mode actions"); strv_append (lines, "Editor mode actions");
help_tab_group (g_editor_keys, g_editor_keys_len, lines, bound); help_tab_group (g_editor_keys, g_editor_keys_len, lines, bound, bound_len);
strv_append (lines, ""); strv_append (lines, "");
bool have_unbound = false; bool have_unbound = false;
for (enum action i = 0; i < ACTION_COUNT; i++) for (size_t i = 0; i < bound_len; i++)
if (!bound[i]) if (!bound[i])
have_unbound = true; have_unbound = true;
if (have_unbound) if (have_unbound)
{ {
strv_append (lines, "Unbound actions"); strv_append (lines, "Unbound actions");
help_tab_unbound (lines, bound); help_tab_unbound (lines, bound, bound_len);
strv_append (lines, ""); strv_append (lines, "");
} }
free (bound);
struct tab *super = &g_help_tab.super; struct tab *super = &g_help_tab.super;
tab_init (super, "Help"); tab_init (super, "Help");
@@ -5855,28 +6042,36 @@ x11_render_editor (struct widget *self)
{ {
x11_render_padding (self); x11_render_padding (self);
XftFont *font = x11_widget_font (self)->list->font; struct x11_font *font = x11_widget_font (self);
XftColor color = { .color = *x11_fg (self) }; XftColor color = { .color = *x11_fg (self) };
// A simplistic adaptation of line_editor_write() follows. // A simplistic adaptation of tui_render_editor() follows.
int x = self->x, y = self->y + font->ascent; const struct line_editor *e = &g.editor;
XGlyphInfo extents = {}; int x = self->x;
if (g.editor.prompt) if (e->prompt)
{ {
FT_UInt i = XftCharIndex (g_xui.dpy, font, g.editor.prompt); hard_assert (e->prompt < 127);
XftDrawGlyphs (g_xui.xft_draw, &color, font, x, y, &i, 1); x += x11_font_draw (font, &color, x, self->y,
XftGlyphExtents (g_xui.dpy, font, &i, 1, &extents); (char[2]) { e->prompt, 0 }) + g_xui.vunit / 4;
x += extents.xOff + g_xui.vunit / 4;
} }
// TODO: Adapt x11_font_{hadvance,draw}().
// TODO: Make this scroll around the caret, and fade like labels. // TODO: Make this scroll around the caret, and fade like labels.
XftDrawString32 (g_xui.xft_draw, &color, font, x, y, size_t len;
g.editor.line, g.editor.len); ucs4_t *buf = xcalloc (e->len + 1, sizeof *buf);
u32_cpy (buf, e->line, e->point);
char *a = (char *) u32_to_u8 (buf, u32_strlen (buf) + 1, NULL, &len);
u32_cpy (buf, e->line + e->point, e->len - e->point + 1);
char *b = (char *) u32_to_u8 (buf, u32_strlen (buf) + 1, NULL, &len);
free (buf);
x += x11_font_draw (font, &color, x, self->y, a);
int caret = x;
x += x11_font_draw (font, &color, x, self->y, b);
free (a);
free (b);
XftTextExtents32 (g_xui.dpy, font, g.editor.line, g.editor.point, &extents);
XRenderFillRectangle (g_xui.dpy, PictOpSrc, g_xui.x11_pixmap_picture, XRenderFillRectangle (g_xui.dpy, PictOpSrc, g_xui.x11_pixmap_picture,
&color.color, x + extents.xOff, self->y, 2, self->height); &color.color, caret, self->y, 2, self->height);
} }
static struct widget * static struct widget *

2
termo

Submodule termo updated: 2518b53e5a...f9a102456f