Compare commits
	
		
			17 Commits
		
	
	
		
			ef19337bad
			...
			master
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						
						
							
						
						708b78f145
	
				 | 
					
					
						|||
| 
						
						
							
						
						3361df0ccb
	
				 | 
					
					
						|||
| 
						
						
							
						
						13195e797a
	
				 | 
					
					
						|||
| 
						
						
							
						
						7fee01a5c6
	
				 | 
					
					
						|||
| 
						
						
							
						
						8b4300c796
	
				 | 
					
					
						|||
| 
						
						
							
						
						cd3d4eb1ad
	
				 | 
					
					
						|||
| 
						
						
							
						
						641803df35
	
				 | 
					
					
						|||
| 
						
						
							
						
						eec0706333
	
				 | 
					
					
						|||
| 
						
						
							
						
						9d0acddf9a
	
				 | 
					
					
						|||
| 
						
						
							
						
						ec12e2ac25
	
				 | 
					
					
						|||
| 
						
						
							
						
						24449fe721
	
				 | 
					
					
						|||
| 
						
						
							
						
						60171cc5a3
	
				 | 
					
					
						|||
| 
						
						
							
						
						20502200d9
	
				 | 
					
					
						|||
| 
						
						
							
						
						e6de4f11e7
	
				 | 
					
					
						|||
| 
						
						
							
						
						c4bce75866
	
				 | 
					
					
						|||
| 
						
						
							
						
						08b87bccbd
	
				 | 
					
					
						|||
| 
						
						
							
						
						fa460b97cf
	
				 | 
					
					
						
@@ -1,5 +1,5 @@
 | 
			
		||||
cmake_minimum_required (VERSION 3.0...3.27)
 | 
			
		||||
project (nncmpp VERSION 2.1.0 LANGUAGES C)
 | 
			
		||||
project (nncmpp VERSION 2.1.1 LANGUAGES C)
 | 
			
		||||
 | 
			
		||||
# Moar warnings
 | 
			
		||||
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
 | 
			
		||||
add_executable (${PROJECT_NAME} ${PROJECT_NAME}.c ${actions})
 | 
			
		||||
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})
 | 
			
		||||
 | 
			
		||||
# Installation
 | 
			
		||||
install (TARGETS ${PROJECT_NAME} DESTINATION ${CMAKE_INSTALL_BINDIR})
 | 
			
		||||
install (FILES LICENSE DESTINATION ${CMAKE_INSTALL_DOCDIR})
 | 
			
		||||
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)
 | 
			
		||||
	include (IconUtils)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								LICENSE
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								LICENSE
									
									
									
									
									
								
							@@ -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
 | 
			
		||||
purpose with or without fee is hereby granted.
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										25
									
								
								NEWS
									
									
									
									
									
								
							
							
						
						
									
										25
									
								
								NEWS
									
									
									
									
									
								
							@@ -1,3 +1,28 @@
 | 
			
		||||
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,
 | 
			
		||||
 
 | 
			
		||||
@@ -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],
 | 
			
		||||
or as a https://git.janouch.name/p/nixexprs[Nix derivation].
 | 
			
		||||
 | 
			
		||||
Stable versions are present in: OpenBSD ports.
 | 
			
		||||
 | 
			
		||||
Documentation
 | 
			
		||||
-------------
 | 
			
		||||
See the link:nncmpp.adoc[man page] for information about usage.
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								liberty
									
									
									
									
									
								
							
							
								
								
								
								
								
							
						
						
									
										2
									
								
								liberty
									
									
									
									
									
								
							 Submodule liberty updated: 2a1f17a8f7...7425355d01
									
								
							@@ -1,6 +1,6 @@
 | 
			
		||||
# 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
 | 
			
		||||
#
 | 
			
		||||
# Usage: env LC_ALL=C A=0 B=1 awk -f nncmpp.actions.awk \
 | 
			
		||||
@@ -91,7 +91,7 @@ END {
 | 
			
		||||
	print "enum action {"
 | 
			
		||||
	for (i in Constants)
 | 
			
		||||
		print "\t" "ACTION_" Constants[i] ","
 | 
			
		||||
	print "\t" "ACTION_COUNT"
 | 
			
		||||
	print "\t" "ACTION_USER_0"
 | 
			
		||||
	print "};"
 | 
			
		||||
	print ""
 | 
			
		||||
	print "static const char *g_action_names[] = {"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										14
									
								
								nncmpp.adoc
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								nncmpp.adoc
									
									
									
									
									
								
							@@ -69,7 +69,7 @@ colors = {
 | 
			
		||||
  scrollbar   = ""
 | 
			
		||||
}
 | 
			
		||||
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"
 | 
			
		||||
}
 | 
			
		||||
....
 | 
			
		||||
@@ -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.
 | 
			
		||||
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
 | 
			
		||||
-------------------
 | 
			
		||||
When built against the FFTW library, *nncmpp* can make use of MPD's "fifo"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										326
									
								
								nncmpp.c
									
									
									
									
									
								
							
							
						
						
									
										326
									
								
								nncmpp.c
									
									
									
									
									
								
							@@ -1,7 +1,7 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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
 | 
			
		||||
 * purpose with or without fee is hereby granted.
 | 
			
		||||
@@ -522,7 +522,7 @@ static struct item_list
 | 
			
		||||
item_list_make (void)
 | 
			
		||||
{
 | 
			
		||||
	struct item_list self = {};
 | 
			
		||||
	self.items = xcalloc (sizeof *self.items, (self.alloc = 16));
 | 
			
		||||
	self.items = xcalloc ((self.alloc = 16), sizeof *self.items);
 | 
			
		||||
	return self;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -814,9 +814,9 @@ spectrum_init (struct spectrum *s, char *format, int bars, int fps,
 | 
			
		||||
	s->useful_bins = s->bins / 2;
 | 
			
		||||
 | 
			
		||||
	int used_bins = necessary_bins / 2;
 | 
			
		||||
	s->rendered = xcalloc (sizeof *s->rendered, s->bars * 3 + 1);
 | 
			
		||||
	s->spectrum = xcalloc (sizeof *s->spectrum, s->bars);
 | 
			
		||||
	s->top_bins = xcalloc (sizeof *s->top_bins, s->bars);
 | 
			
		||||
	s->rendered = xcalloc (s->bars * 3 + 1, sizeof *s->rendered);
 | 
			
		||||
	s->spectrum = xcalloc (s->bars, sizeof *s->spectrum);
 | 
			
		||||
	s->top_bins = xcalloc (s->bars, sizeof *s->top_bins);
 | 
			
		||||
	for (int bar = 0; bar < s->bars; bar++)
 | 
			
		||||
	{
 | 
			
		||||
		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);
 | 
			
		||||
 | 
			
		||||
	// 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);
 | 
			
		||||
 | 
			
		||||
	// 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);
 | 
			
		||||
	s->accumulator_scale = 2 * 2 / coherent_gain / coherent_gain / s->samples;
 | 
			
		||||
 | 
			
		||||
	s->data = xcalloc (sizeof *s->data, s->bins);
 | 
			
		||||
	s->windowed = fftw_malloc (sizeof *s->windowed * s->bins);
 | 
			
		||||
	s->out = fftw_malloc (sizeof *s->out * (s->useful_bins + 1));
 | 
			
		||||
	s->data = xcalloc (s->bins, sizeof *s->data);
 | 
			
		||||
	s->windowed = fftw_malloc (s->bins * sizeof *s->windowed);
 | 
			
		||||
	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->accumulator = xcalloc (sizeof *s->accumulator, s->useful_bins);
 | 
			
		||||
	s->accumulator = xcalloc (s->useful_bins, sizeof *s->accumulator);
 | 
			
		||||
	return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -1256,6 +1256,9 @@ 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 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 *tabs;                   ///< All other tabs
 | 
			
		||||
@@ -1337,7 +1340,7 @@ on_pulseaudio_changed (struct config_item *item)
 | 
			
		||||
	g.pulse_control_requested = item->value.boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static struct config_schema g_config_settings[] =
 | 
			
		||||
static const struct config_schema g_config_settings[] =
 | 
			
		||||
{
 | 
			
		||||
	{ .name      = "address",
 | 
			
		||||
	  .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_) \
 | 
			
		||||
	{ .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 *
 | 
			
		||||
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);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
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, "colors",   load_config_colors,   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
 | 
			
		||||
	config_load (config, config_item_object ());
 | 
			
		||||
@@ -1548,6 +1593,9 @@ app_init_context (void)
 | 
			
		||||
	g.config = config_make ();
 | 
			
		||||
	g.streams = 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.key_xfrm = tolower_ascii_strxfrm;
 | 
			
		||||
@@ -1570,6 +1618,9 @@ app_free_context (void)
 | 
			
		||||
	str_map_free (&g.playback_info);
 | 
			
		||||
	strv_free (&g.streams);
 | 
			
		||||
	strv_free (&g.enqueue);
 | 
			
		||||
	strv_free (&g.action_names);
 | 
			
		||||
	strv_free (&g.action_descriptions);
 | 
			
		||||
	strv_free (&g.action_commands);
 | 
			
		||||
	item_list_free (&g.playlist);
 | 
			
		||||
 | 
			
		||||
#ifdef WITH_FFTW
 | 
			
		||||
@@ -2379,12 +2430,52 @@ app_goto_tab (int tab_index)
 | 
			
		||||
static int
 | 
			
		||||
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))
 | 
			
		||||
			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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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 -----------------------------------------------------
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
app_mpd_toggle (const char *name)
 | 
			
		||||
{
 | 
			
		||||
@@ -2562,10 +2711,6 @@ app_process_action (enum action action)
 | 
			
		||||
		xui_invalidate ();
 | 
			
		||||
		app_hide_message ();
 | 
			
		||||
		return true;
 | 
			
		||||
	default:
 | 
			
		||||
		print_error ("\"%s\" is not allowed here",
 | 
			
		||||
			g_action_descriptions[action]);
 | 
			
		||||
		return false;
 | 
			
		||||
 | 
			
		||||
	case ACTION_MULTISELECT:
 | 
			
		||||
		if (!tab->can_multiselect
 | 
			
		||||
@@ -2677,8 +2822,14 @@ app_process_action (enum action action)
 | 
			
		||||
	case ACTION_GOTO_VIEW_BOTTOM:
 | 
			
		||||
		g.active_tab->item_selected = g.active_tab->item_top;
 | 
			
		||||
		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
 | 
			
		||||
@@ -2696,8 +2847,7 @@ app_editor_process_action (enum action action)
 | 
			
		||||
		g.editor.on_end = NULL;
 | 
			
		||||
		return true;
 | 
			
		||||
	default:
 | 
			
		||||
		print_error ("\"%s\" is not allowed here",
 | 
			
		||||
			g_action_descriptions[action]);
 | 
			
		||||
		print_error ("\"%s\" is not allowed here", action_description (action));
 | 
			
		||||
		return false;
 | 
			
		||||
 | 
			
		||||
	case ACTION_EDITOR_B_CHAR:
 | 
			
		||||
@@ -3599,30 +3749,67 @@ static void
 | 
			
		||||
library_tab_on_search_data (const struct mpd_response *response,
 | 
			
		||||
	const struct strv *data, void *user_data)
 | 
			
		||||
{
 | 
			
		||||
	(void) user_data;
 | 
			
		||||
	char *filter = user_data;
 | 
			
		||||
	if (!g_library_tab.searching)
 | 
			
		||||
		return;
 | 
			
		||||
		goto out;
 | 
			
		||||
 | 
			
		||||
	if (!response->success)
 | 
			
		||||
	{
 | 
			
		||||
		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 ("ed, '\'');
 | 
			
		||||
	for (const char *p = value; *p; p++)
 | 
			
		||||
	{
 | 
			
		||||
		if (mpd_client_must_escape_in_quote (*p))
 | 
			
		||||
			str_append_c ("ed, '\\');
 | 
			
		||||
		str_append_c ("ed, *p);
 | 
			
		||||
	}
 | 
			
		||||
	str_append_c ("ed, '\'');
 | 
			
		||||
	return str_steal ("ed);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void
 | 
			
		||||
search_on_changed (void)
 | 
			
		||||
{
 | 
			
		||||
	struct mpd_client *c = &g.client;
 | 
			
		||||
	if (c->state != MPD_CONNECTED)
 | 
			
		||||
		return;
 | 
			
		||||
 | 
			
		||||
	size_t 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);
 | 
			
		||||
	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);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -4075,24 +4262,16 @@ info_tab_plugin_load (const char *path)
 | 
			
		||||
	// Shell quoting is less annoying than process management.
 | 
			
		||||
	struct str escaped = str_make ();
 | 
			
		||||
	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 ();
 | 
			
		||||
	char buf[BUFSIZ];
 | 
			
		||||
	size_t len;
 | 
			
		||||
	while ((len = fread (buf, 1, sizeof buf, fp)) == sizeof buf)
 | 
			
		||||
		str_append_data (&description, buf, len);
 | 
			
		||||
	str_append_data (&description, buf, len);
 | 
			
		||||
	if (pclose (fp))
 | 
			
		||||
	struct error *error = NULL;
 | 
			
		||||
	(void) run_command (escaped.str, &description, &error);
 | 
			
		||||
	str_free (&escaped);
 | 
			
		||||
	if (error)
 | 
			
		||||
	{
 | 
			
		||||
		print_error ("%s: %s", path, error->message);
 | 
			
		||||
		error_free (error);
 | 
			
		||||
		str_free (&description);
 | 
			
		||||
		print_error ("%s: %s", path, strerror (errno));
 | 
			
		||||
		return NULL;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -4105,8 +4284,8 @@ info_tab_plugin_load (const char *path)
 | 
			
		||||
	str_enforce_utf8 (&description);
 | 
			
		||||
	if (!description.len)
 | 
			
		||||
	{
 | 
			
		||||
		str_free (&description);
 | 
			
		||||
		print_error ("%s: %s", path, "missing description");
 | 
			
		||||
		str_free (&description);
 | 
			
		||||
		return NULL;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -4544,7 +4723,7 @@ help_tab_on_action (enum action action)
 | 
			
		||||
	if (action == ACTION_DESCRIBE)
 | 
			
		||||
	{
 | 
			
		||||
		app_show_message (xstrdup ("Configuration name: "),
 | 
			
		||||
			xstrdup (g_action_names[a]));
 | 
			
		||||
			xstrdup (action_name (a)));
 | 
			
		||||
		return true;
 | 
			
		||||
	}
 | 
			
		||||
	if (action != ACTION_CHOOSE || a == ACTION_CHOOSE /* avoid recursion */)
 | 
			
		||||
@@ -4569,9 +4748,9 @@ help_tab_assign_action (enum action action)
 | 
			
		||||
 | 
			
		||||
static void
 | 
			
		||||
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 ();
 | 
			
		||||
		for (size_t k = 0; k < len; k++)
 | 
			
		||||
@@ -4581,7 +4760,7 @@ help_tab_group (struct binding *keys, size_t len, struct strv *out,
 | 
			
		||||
		{
 | 
			
		||||
			char *joined = strv_join (&ass, ", ");
 | 
			
		||||
			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);
 | 
			
		||||
 | 
			
		||||
			bound[i] = true;
 | 
			
		||||
@@ -4592,13 +4771,13 @@ help_tab_group (struct binding *keys, size_t len, struct strv *out,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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])
 | 
			
		||||
		{
 | 
			
		||||
			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);
 | 
			
		||||
		}
 | 
			
		||||
}
 | 
			
		||||
@@ -4628,27 +4807,30 @@ help_tab_init (void)
 | 
			
		||||
	struct strv *lines = &g_help_tab.lines;
 | 
			
		||||
	*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");
 | 
			
		||||
	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, "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, "");
 | 
			
		||||
 | 
			
		||||
	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])
 | 
			
		||||
			have_unbound = true;
 | 
			
		||||
 | 
			
		||||
	if (have_unbound)
 | 
			
		||||
	{
 | 
			
		||||
		strv_append (lines, "Unbound actions");
 | 
			
		||||
		help_tab_unbound (lines, bound);
 | 
			
		||||
		help_tab_unbound (lines, bound, bound_len);
 | 
			
		||||
		strv_append (lines, "");
 | 
			
		||||
	}
 | 
			
		||||
	free (bound);
 | 
			
		||||
 | 
			
		||||
	struct tab *super = &g_help_tab.super;
 | 
			
		||||
	tab_init (super, "Help");
 | 
			
		||||
@@ -5860,28 +6042,36 @@ x11_render_editor (struct widget *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) };
 | 
			
		||||
 | 
			
		||||
	// A simplistic adaptation of line_editor_write() follows.
 | 
			
		||||
	int x = self->x, y = self->y + font->ascent;
 | 
			
		||||
	XGlyphInfo extents = {};
 | 
			
		||||
	if (g.editor.prompt)
 | 
			
		||||
	// A simplistic adaptation of tui_render_editor() follows.
 | 
			
		||||
	const struct line_editor *e = &g.editor;
 | 
			
		||||
	int x = self->x;
 | 
			
		||||
	if (e->prompt)
 | 
			
		||||
	{
 | 
			
		||||
		FT_UInt i = XftCharIndex (g_xui.dpy, font, g.editor.prompt);
 | 
			
		||||
		XftDrawGlyphs (g_xui.xft_draw, &color, font, x, y, &i, 1);
 | 
			
		||||
		XftGlyphExtents (g_xui.dpy, font, &i, 1, &extents);
 | 
			
		||||
		x += extents.xOff + g_xui.vunit / 4;
 | 
			
		||||
		hard_assert (e->prompt < 127);
 | 
			
		||||
		x += x11_font_draw (font, &color, x, self->y,
 | 
			
		||||
			(char[2]) { e->prompt, 0 }) + g_xui.vunit / 4;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// TODO: Adapt x11_font_{hadvance,draw}().
 | 
			
		||||
	// TODO: Make this scroll around the caret, and fade like labels.
 | 
			
		||||
	XftDrawString32 (g_xui.xft_draw, &color, font, x, y,
 | 
			
		||||
		g.editor.line, g.editor.len);
 | 
			
		||||
	size_t 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,
 | 
			
		||||
		&color.color, x + extents.xOff, self->y, 2, self->height);
 | 
			
		||||
		&color.color, caret, self->y, 2, self->height);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static struct widget *
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								termo
									
									
									
									
									
								
							
							
								
								
								
								
								
							
						
						
									
										2
									
								
								termo
									
									
									
									
									
								
							 Submodule termo updated: 2518b53e5a...f9a102456f
									
								
							
		Reference in New Issue
	
	Block a user