Compare commits
	
		
			3 Commits
		
	
	
		
			b6dd940720
			...
			8aac4ae0a8
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 8aac4ae0a8 | |||
| e72ed71f53 | |||
| 28ed7a85a8 | 
| @ -104,6 +104,7 @@ foreach (extra m) | |||||||
| endforeach () | endforeach () | ||||||
| 
 | 
 | ||||||
| # Generate a configuration file | # Generate a configuration file | ||||||
|  | include (GNUInstallDirs) | ||||||
| configure_file (${PROJECT_SOURCE_DIR}/config.h.in | configure_file (${PROJECT_SOURCE_DIR}/config.h.in | ||||||
| 	${PROJECT_BINARY_DIR}/config.h) | 	${PROJECT_BINARY_DIR}/config.h) | ||||||
| include_directories (${PROJECT_SOURCE_DIR} ${PROJECT_BINARY_DIR}) | include_directories (${PROJECT_SOURCE_DIR} ${PROJECT_BINARY_DIR}) | ||||||
| @ -123,10 +124,10 @@ target_link_libraries (${PROJECT_NAME} ${Unistring_LIBRARIES} | |||||||
| add_threads (${PROJECT_NAME}) | add_threads (${PROJECT_NAME}) | ||||||
| 
 | 
 | ||||||
| # Installation | # Installation | ||||||
| include (GNUInstallDirs) |  | ||||||
| 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}) | ||||||
| 
 | 
 | ||||||
| # Generate documentation from text markup | # Generate documentation from text markup | ||||||
| find_program (ASCIIDOCTOR_EXECUTABLE asciidoctor) | find_program (ASCIIDOCTOR_EXECUTABLE asciidoctor) | ||||||
|  | |||||||
							
								
								
									
										10
									
								
								NEWS
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								NEWS
									
									
									
									
									
								
							| @ -1,3 +1,13 @@ | |||||||
|  | Unreleased | ||||||
|  | 
 | ||||||
|  |  * Added ability to look up song lyrics, | ||||||
|  |    using a new scriptable extension interface for the Info tab | ||||||
|  | 
 | ||||||
|  |  * Made the X11 interface support italic fonts | ||||||
|  | 
 | ||||||
|  |  * Added Readline-like M-u, M-l, M-c editor bindings | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| 2.0.0 (2022-09-03) | 2.0.0 (2022-09-03) | ||||||
| 
 | 
 | ||||||
|  * Added an optional X11 user interface |  * Added an optional X11 user interface | ||||||
|  | |||||||
| @ -18,6 +18,7 @@ Features | |||||||
| Most stuff is there.  I've been using the program exclusively for many years. | Most stuff is there.  I've been using the program exclusively for many years. | ||||||
| Among other things, it can display and change PulseAudio volume directly | 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, | to cover the use case of remote control, it has a fast spectrum visualiser, | ||||||
|  | it can be extended with plugins to fetch lyrics or other song-related info, | ||||||
| and both its appearance and key bindings can be customized. | and both its appearance and key bindings can be customized. | ||||||
| 
 | 
 | ||||||
| Note that currently only the filesystem browsing mode is implemented, | Note that currently only the filesystem browsing mode is implemented, | ||||||
| @ -40,7 +41,7 @@ Building | |||||||
| Build dependencies: CMake, pkg-config, asciidoctor or asciidoc, | Build dependencies: CMake, pkg-config, asciidoctor or asciidoc, | ||||||
|                     liberty (included), termo (included) + |                     liberty (included), termo (included) + | ||||||
| Runtime dependencies: ncursesw, libunistring, cURL + | Runtime dependencies: ncursesw, libunistring, cURL + | ||||||
| Optional runtime dependencies: fftw3, libpulse, x11, xft | Optional runtime dependencies: fftw3, libpulse, x11, xft, Perl + cURL (lyrics) | ||||||
| 
 | 
 | ||||||
|  $ git clone --recursive https://git.janouch.name/p/nncmpp.git |  $ git clone --recursive https://git.janouch.name/p/nncmpp.git | ||||||
|  $ mkdir nncmpp/build |  $ mkdir nncmpp/build | ||||||
|  | |||||||
| @ -4,6 +4,9 @@ | |||||||
| #define PROGRAM_NAME "${PROJECT_NAME}" | #define PROGRAM_NAME "${PROJECT_NAME}" | ||||||
| #define PROGRAM_VERSION "${PROJECT_VERSION}" | #define PROGRAM_VERSION "${PROJECT_VERSION}" | ||||||
| 
 | 
 | ||||||
|  | // We use the XDG Base Directory Specification, but may be installed anywhere.
 | ||||||
|  | #define PROJECT_DATADIR "${CMAKE_INSTALL_FULL_DATADIR}" | ||||||
|  | 
 | ||||||
| #cmakedefine HAVE_RESIZETERM | #cmakedefine HAVE_RESIZETERM | ||||||
| #cmakedefine WITH_FFTW | #cmakedefine WITH_FFTW | ||||||
| #cmakedefine WITH_PULSE | #cmakedefine WITH_PULSE | ||||||
|  | |||||||
							
								
								
									
										43
									
								
								info/10-azlyrics.pl
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										43
									
								
								info/10-azlyrics.pl
									
									
									
									
									
										Executable file
									
								
							| @ -0,0 +1,43 @@ | |||||||
|  | #!/usr/bin/env perl | ||||||
|  | # 10-azlyrics.pl: nncmpp info plugin to fetch song lyrics on AZLyrics | ||||||
|  | # | ||||||
|  | # Copyright (c) 2022, Přemysl Eric Janouch <p@janouch.name> | ||||||
|  | # SPDX-License-Identifier: 0BSD | ||||||
|  | # | ||||||
|  | # Inspired by a similar ncmpc plugin. | ||||||
|  | 
 | ||||||
|  | use warnings; | ||||||
|  | use strict; | ||||||
|  | use utf8; | ||||||
|  | use open ':std', ':utf8'; | ||||||
|  | unless (@ARGV) { | ||||||
|  | 	print "Look up on AZLyrics\n"; | ||||||
|  | 	exit; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | use Encode; | ||||||
|  | my ($title, $artist, $album) = map {decode_utf8($_)} @ARGV; | ||||||
|  | 
 | ||||||
|  | # TODO: An upgrade would be transliteration with, e.g., Text::Unidecode. | ||||||
|  | use Unicode::Normalize; | ||||||
|  | $artist = lc(NFD($artist) =~ s/^the\s+//ir =~ s/[^A-Za-z0-9]//gr); | ||||||
|  | $title  = lc(NFD($title)  =~ s/\(.*?\)//gr =~ s/[^A-Za-z0-9]//gr); | ||||||
|  | 
 | ||||||
|  | # TODO: Consider caching the results in a location like | ||||||
|  | # $XDG_CACHE_HOME/nncmpp/info/azlyrics/$artist-$title | ||||||
|  | my $found = 0; | ||||||
|  | if ($title ne '') { | ||||||
|  | 	open(my $curl, '-|', 'curl', '-sA', 'nncmpp/2.0', | ||||||
|  | 		"https://www.azlyrics.com/lyrics/$artist/$title.html") or die $!; | ||||||
|  | 	while (<$curl>) { | ||||||
|  | 		next unless /^<div>/ .. /^<\/div>/; s/<!--.*?-->//g; s/\s+$//gs; | ||||||
|  | 
 | ||||||
|  | 		$found = 1; | ||||||
|  | 		s/<\/?b>/\x01/g; s/<\/?i>/\x02/g; s/<br>/\n/; s/<.+?>//g; | ||||||
|  | 		s/</</g; s/>/>/g; s/"/"/g; s/'/'/g; s/&/&/g; | ||||||
|  | 		print; | ||||||
|  | 	} | ||||||
|  | 	close($curl) or die $?; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | print "No lyrics have been found.\n" unless $found; | ||||||
							
								
								
									
										21
									
								
								nncmpp.adoc
									
									
									
									
									
								
							
							
						
						
									
										21
									
								
								nncmpp.adoc
									
									
									
									
									
								
							| @ -128,6 +128,19 @@ 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 | your setup is unusual, consult the list of environment variables in | ||||||
| *pulseaudio*(1).  MPD-compatibles are currently unsupported. | *pulseaudio*(1).  MPD-compatibles are currently unsupported. | ||||||
| 
 | 
 | ||||||
|  | Info plugins | ||||||
|  | ------------ | ||||||
|  | You can invoke various plugins from the Info tab, for example to look up | ||||||
|  | song lyrics. | ||||||
|  | 
 | ||||||
|  | Plugins can be arbitrary scripts or binaries.  When run without command line | ||||||
|  | arguments, a plugin outputs a user interface description of what it does. | ||||||
|  | When invoked by a user, it receives the following self-explanatory arguments: | ||||||
|  | _TITLE_ _ARTIST_ [_ALBUM_], and anything it writes to its standard output | ||||||
|  | or standard error stream is presented back to the user.  Here, bold and italic | ||||||
|  | formatting can be toggled with ASCII control characters 1 (SOH) and 2 (STX), | ||||||
|  | respectively.  Otherwise, all input and output makes use of the UTF-8 encoding. | ||||||
|  | 
 | ||||||
| Files | Files | ||||||
| ----- | ----- | ||||||
| *nncmpp* follows the XDG Base Directory Specification. | *nncmpp* follows the XDG Base Directory Specification. | ||||||
| @ -135,6 +148,14 @@ Files | |||||||
| _~/.config/nncmpp/nncmpp.conf_:: | _~/.config/nncmpp/nncmpp.conf_:: | ||||||
| 	The configuration file. | 	The configuration file. | ||||||
| 
 | 
 | ||||||
|  | _~/.local/share/nncmpp/info/_:: | ||||||
|  | _/usr/local/share/nncmpp/info/_:: | ||||||
|  | _/usr/share/nncmpp/info/_:: | ||||||
|  | 	Info plugins are loaded from these directories, in order, | ||||||
|  | 	then listed lexicographically. | ||||||
|  | 	Only the first occurence of a particular filename is used, | ||||||
|  | 	and empty files act as silent disablers. | ||||||
|  | 
 | ||||||
| Reporting bugs | Reporting bugs | ||||||
| -------------- | -------------- | ||||||
| Use https://git.janouch.name/p/nncmpp to report bugs, request features, | Use https://git.janouch.name/p/nncmpp to report bugs, request features, | ||||||
|  | |||||||
							
								
								
									
										490
									
								
								nncmpp.c
									
									
									
									
									
								
							
							
						
						
									
										490
									
								
								nncmpp.c
									
									
									
									
									
								
							| @ -75,10 +75,11 @@ enum | |||||||
| #define HAVE_LIBERTY | #define HAVE_LIBERTY | ||||||
| #include "line-editor.c" | #include "line-editor.c" | ||||||
| 
 | 
 | ||||||
| #include <math.h> | #include <dirent.h> | ||||||
| #include <locale.h> | #include <locale.h> | ||||||
| #include <termios.h> | #include <math.h> | ||||||
| #include <sys/ioctl.h> | #include <sys/ioctl.h> | ||||||
|  | #include <termios.h> | ||||||
| 
 | 
 | ||||||
| // ncurses is notoriously retarded for input handling, we need something
 | // ncurses is notoriously retarded for input handling, we need something
 | ||||||
| // different if only to receive mouse events reliably.
 | // different if only to receive mouse events reliably.
 | ||||||
| @ -130,6 +131,20 @@ clock_msec (clockid_t clock) | |||||||
| 	return (int64_t) tp.tv_sec * 1000 + (int64_t) tp.tv_nsec / 1000000; | 	return (int64_t) tp.tv_sec * 1000 + (int64_t) tp.tv_nsec / 1000000; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | static void | ||||||
|  | shell_quote (const char *str, struct str *output) | ||||||
|  | { | ||||||
|  | 	// See SUSv3 Shell and Utilities, 2.2.3 Double-Quotes
 | ||||||
|  | 	str_append_c (output, '"'); | ||||||
|  | 	for (const char *p = str; *p; p++) | ||||||
|  | 	{ | ||||||
|  | 		if (strchr ("`$\"\\", *p)) | ||||||
|  | 			str_append_c (output, '\\'); | ||||||
|  | 		str_append_c (output, *p); | ||||||
|  | 	} | ||||||
|  | 	str_append_c (output, '"'); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| static bool | static bool | ||||||
| xstrtoul_map (const struct str_map *map, const char *key, unsigned long *out) | xstrtoul_map (const struct str_map *map, const char *key, unsigned long *out) | ||||||
| { | { | ||||||
| @ -164,6 +179,18 @@ latin1_to_utf8 (const char *latin1) | |||||||
| 	return str_steal (&converted); | 	return str_steal (&converted); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | static void | ||||||
|  | str_enforce_utf8 (struct str *self) | ||||||
|  | { | ||||||
|  | 	if (!utf8_validate (self->str, self->len)) | ||||||
|  | 	{ | ||||||
|  | 		char *sanitized = latin1_to_utf8 (self->str); | ||||||
|  | 		str_reset (self); | ||||||
|  | 		str_append (self, sanitized); | ||||||
|  | 		free (sanitized); | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
| static void | static void | ||||||
| cstr_uncapitalize (char *s) | cstr_uncapitalize (char *s) | ||||||
| { | { | ||||||
| @ -318,6 +345,8 @@ poller_curl_on_socket_action (CURL *easy, curl_socket_t s, int what, | |||||||
| 	struct poller_curl_fd *fd; | 	struct poller_curl_fd *fd; | ||||||
| 	if (!(fd = socket_data)) | 	if (!(fd = socket_data)) | ||||||
| 	{ | 	{ | ||||||
|  | 		set_cloexec (s); | ||||||
|  | 
 | ||||||
| 		fd = xmalloc (sizeof *fd); | 		fd = xmalloc (sizeof *fd); | ||||||
| 		LIST_PREPEND (self->fds, fd); | 		LIST_PREPEND (self->fds, fd); | ||||||
| 
 | 
 | ||||||
| @ -1368,6 +1397,7 @@ static struct app_context | |||||||
| 	XftDraw *xft_draw;                  ///< Xft rendering context
 | 	XftDraw *xft_draw;                  ///< Xft rendering context
 | ||||||
| 	XftFont *xft_regular;               ///< Regular font
 | 	XftFont *xft_regular;               ///< Regular font
 | ||||||
| 	XftFont *xft_bold;                  ///< Bold font
 | 	XftFont *xft_bold;                  ///< Bold font
 | ||||||
|  | 	XftFont *xft_italic;                ///< Italic font
 | ||||||
| 	char *x11_selection;                ///< CLIPBOARD selection
 | 	char *x11_selection;                ///< CLIPBOARD selection
 | ||||||
| 
 | 
 | ||||||
| 	XRenderColor x_fg[ATTRIBUTE_COUNT]; ///< Foreground per attribute
 | 	XRenderColor x_fg[ATTRIBUTE_COUNT]; ///< Foreground per attribute
 | ||||||
| @ -4089,68 +4119,456 @@ streams_tab_init (void) | |||||||
| 
 | 
 | ||||||
| // --- Info tab ----------------------------------------------------------------
 | // --- Info tab ----------------------------------------------------------------
 | ||||||
| 
 | 
 | ||||||
|  | struct info_tab_plugin | ||||||
|  | { | ||||||
|  | 	LIST_HEADER (struct info_tab_plugin) | ||||||
|  | 
 | ||||||
|  | 	char *path;                         ///< Filesystem path to plugin
 | ||||||
|  | 	char *description;                  ///< What the plugin does
 | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | static struct info_tab_plugin * | ||||||
|  | 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)) | ||||||
|  | 	{ | ||||||
|  | 		str_free (&description); | ||||||
|  | 		print_error ("%s: %s", path, strerror (errno)); | ||||||
|  | 		return NULL; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	char *newline = strpbrk (description.str, "\r\n"); | ||||||
|  | 	if (newline) | ||||||
|  | 	{ | ||||||
|  | 		description.len = newline - description.str; | ||||||
|  | 		*newline = '\0'; | ||||||
|  | 	} | ||||||
|  | 	str_enforce_utf8 (&description); | ||||||
|  | 	if (!description.len) | ||||||
|  | 	{ | ||||||
|  | 		str_free (&description); | ||||||
|  | 		print_error ("%s: %s", path, "missing description"); | ||||||
|  | 		return NULL; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	struct info_tab_plugin *plugin = xcalloc (1, sizeof *plugin); | ||||||
|  | 	plugin->path = xstrdup (path); | ||||||
|  | 	plugin->description = str_steal (&description); | ||||||
|  | 	return plugin; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static void | ||||||
|  | info_tab_plugin_load_dir (struct str_map *basename_to_path, const char *dirname) | ||||||
|  | { | ||||||
|  | 	DIR *dir = opendir (dirname); | ||||||
|  | 	if (!dir) | ||||||
|  | 	{ | ||||||
|  | 		print_debug ("opendir: %s: %s", dirname, strerror (errno)); | ||||||
|  | 		return; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	struct dirent *entry = NULL; | ||||||
|  | 	while ((entry = readdir (dir))) | ||||||
|  | 	{ | ||||||
|  | 		struct stat st = {}; | ||||||
|  | 		char *path = xstrdup_printf ("%s/%s", dirname, entry->d_name); | ||||||
|  | 		if (stat (path, &st) || !S_ISREG (st.st_mode)) | ||||||
|  | 		{ | ||||||
|  | 			free (path); | ||||||
|  | 			continue; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// Empty files silently erase formerly found basenames.
 | ||||||
|  | 		if (!st.st_size) | ||||||
|  | 			cstr_set (&path, NULL); | ||||||
|  | 
 | ||||||
|  | 		str_map_set (basename_to_path, entry->d_name, path); | ||||||
|  | 	} | ||||||
|  | 	closedir (dir); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static int | ||||||
|  | strv_sort_cb (const void *a, const void *b) | ||||||
|  | { | ||||||
|  | 	return strcmp (*(const char **) a, *(const char **) b); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static struct info_tab_plugin * | ||||||
|  | info_tab_plugin_load_all (void) | ||||||
|  | { | ||||||
|  | 	struct str_map basename_to_path = str_map_make (free); | ||||||
|  | 	struct strv paths = strv_make (); | ||||||
|  | 	get_xdg_data_dirs (&paths); | ||||||
|  | 	strv_append (&paths, PROJECT_DATADIR); | ||||||
|  | 	for (size_t i = paths.len; i--; ) | ||||||
|  | 	{ | ||||||
|  | 		char *dirname = | ||||||
|  | 			xstrdup_printf ("%s/" PROGRAM_NAME "/info", paths.vector[i]); | ||||||
|  | 		info_tab_plugin_load_dir (&basename_to_path, dirname); | ||||||
|  | 		free (dirname); | ||||||
|  | 	} | ||||||
|  | 	strv_free (&paths); | ||||||
|  | 
 | ||||||
|  | 	struct strv sorted = strv_make (); | ||||||
|  | 	struct str_map_iter iter = str_map_iter_make (&basename_to_path); | ||||||
|  | 	while (str_map_iter_next (&iter)) | ||||||
|  | 		strv_append (&sorted, iter.link->key); | ||||||
|  | 	qsort (sorted.vector, sorted.len, sizeof *sorted.vector, strv_sort_cb); | ||||||
|  | 
 | ||||||
|  | 	struct info_tab_plugin *result = NULL; | ||||||
|  | 	for (size_t i = sorted.len; i--; ) | ||||||
|  | 	{ | ||||||
|  | 		const char *path = str_map_find (&basename_to_path, sorted.vector[i]); | ||||||
|  | 		struct info_tab_plugin *plugin = info_tab_plugin_load (path); | ||||||
|  | 		if (plugin) | ||||||
|  | 			LIST_PREPEND (result, plugin); | ||||||
|  | 	} | ||||||
|  | 	str_map_free (&basename_to_path); | ||||||
|  | 	strv_free (&sorted); | ||||||
|  | 	return result; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | ||||||
|  | 
 | ||||||
|  | struct info_tab_item | ||||||
|  | { | ||||||
|  | 	char *prefix;                       ///< Fixed-width prefix column or NULL
 | ||||||
|  | 	char *text;                         ///< Text or NULL
 | ||||||
|  | 	bool formatted;                     ///< Interpret inline formatting marks?
 | ||||||
|  | 	struct info_tab_plugin *plugin;     ///< Activatable plugin
 | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | static void | ||||||
|  | info_tab_item_free (struct info_tab_item *self) | ||||||
|  | { | ||||||
|  | 	cstr_set (&self->prefix, NULL); | ||||||
|  | 	cstr_set (&self->text, NULL); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| static struct | static struct | ||||||
| { | { | ||||||
| 	struct tab super;                   ///< Parent class
 | 	struct tab super;                   ///< Parent class
 | ||||||
| 	struct strv keys;                   ///< Data keys
 | 	struct info_tab_item *items;        ///< Items array
 | ||||||
| 	struct strv values;                 ///< Data values
 | 	size_t items_alloc;                 ///< How many items are allocated
 | ||||||
|  | 
 | ||||||
|  | 	struct info_tab_plugin *plugins;    ///< Plugins
 | ||||||
|  | 
 | ||||||
|  | 	int plugin_songid;                  ///< Song ID or -1
 | ||||||
|  | 	pid_t plugin_pid;                   ///< Running plugin's process ID or -1
 | ||||||
|  | 	int plugin_stdout;                  ///< pid != -1: read end of stdout
 | ||||||
|  | 	struct poller_fd plugin_event;      ///< pid != -1: stdout is readable
 | ||||||
|  | 	struct str plugin_output;           ///< pid != -1: buffer, otherwise result
 | ||||||
| } | } | ||||||
| g_info_tab; | g_info_tab; | ||||||
| 
 | 
 | ||||||
|  | static chtype | ||||||
|  | info_tab_format_decode_toggle (char c) | ||||||
|  | { | ||||||
|  | 	switch (c) | ||||||
|  | 	{ | ||||||
|  | 	case '\x01': | ||||||
|  | 		return A_BOLD; | ||||||
|  | 	case '\x02': | ||||||
|  | 		return A_ITALIC; | ||||||
|  | 	default: | ||||||
|  | 		return 0; | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static void | ||||||
|  | info_tab_format (struct layout *l, const char *text) | ||||||
|  | { | ||||||
|  | 	chtype attrs = 0; | ||||||
|  | 	for (const char *p = text; *p; p++) | ||||||
|  | 	{ | ||||||
|  | 		chtype toggled = info_tab_format_decode_toggle (*p); | ||||||
|  | 		if (!toggled) | ||||||
|  | 			continue; | ||||||
|  | 
 | ||||||
|  | 		if (p != text) | ||||||
|  | 		{ | ||||||
|  | 			char *slice = xstrndup (text, p - text); | ||||||
|  | 			app_push (l, g.ui->label (attrs, slice)); | ||||||
|  | 			free (slice); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		attrs ^= toggled; | ||||||
|  | 		text = p + 1; | ||||||
|  | 	} | ||||||
|  | 	if (*text) | ||||||
|  | 		app_push (l, g.ui->label (attrs, text)); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| static struct layout | static struct layout | ||||||
| info_tab_on_item_layout (size_t item_index) | info_tab_on_item_layout (size_t item_index) | ||||||
| { | { | ||||||
| 	const char *key = g_info_tab.keys.vector[item_index]; | 	struct info_tab_item *item = &g_info_tab.items[item_index]; | ||||||
| 	const char *value = g_info_tab.values.vector[item_index]; |  | ||||||
| 	struct layout l = {}; | 	struct layout l = {}; | ||||||
| 
 | 	if (item->prefix) | ||||||
| 	char *prefix = xstrdup_printf ("%s:", key); | 	{ | ||||||
|  | 		char *prefix = xstrdup_printf ("%s:", item->prefix); | ||||||
| 		app_push (&l, g.ui->label (A_BOLD, prefix)) | 		app_push (&l, g.ui->label (A_BOLD, prefix)) | ||||||
| 			->width = 8 * g.ui_hunit; | 			->width = 8 * g.ui_hunit; | ||||||
| 		app_push (&l, g.ui->padding (0, 0.5, 1)); | 		app_push (&l, g.ui->padding (0, 0.5, 1)); | ||||||
| 	app_push_fill (&l, g.ui->label (0, value)); | 	} | ||||||
|  | 
 | ||||||
|  | 	if (item->plugin) | ||||||
|  | 		app_push (&l, g.ui->label (A_BOLD, item->plugin->description)); | ||||||
|  | 	else if (!item->text || !*item->text) | ||||||
|  | 		app_push (&l, g.ui->padding (0, 1, 1)); | ||||||
|  | 	else if (item->formatted) | ||||||
|  | 		info_tab_format (&l, item->text); | ||||||
|  | 	else | ||||||
|  | 		app_push (&l, g.ui->label (0, item->text)); | ||||||
|  | 
 | ||||||
|  | 	if (l.tail) | ||||||
|  | 		l.tail->width = -1; | ||||||
| 	return l; | 	return l; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | static struct info_tab_item * | ||||||
|  | info_tab_prepare (void) | ||||||
|  | { | ||||||
|  | 	if (g_info_tab.super.item_count == g_info_tab.items_alloc) | ||||||
|  | 		g_info_tab.items = xreallocarray (g_info_tab.items, | ||||||
|  | 			sizeof *g_info_tab.items, (g_info_tab.items_alloc <<= 1)); | ||||||
|  | 
 | ||||||
|  | 	struct info_tab_item *item = | ||||||
|  | 		&g_info_tab.items[g_info_tab.super.item_count++]; | ||||||
|  | 	memset (item, 0, sizeof *item); | ||||||
|  | 	return item; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| static void | static void | ||||||
| info_tab_add (compact_map_t data, const char *field) | info_tab_add (compact_map_t data, const char *field) | ||||||
| { | { | ||||||
| 	const char *value = compact_map_find (data, field); | 	struct info_tab_item *item = info_tab_prepare (); | ||||||
| 	if (!value) value = ""; | 	item->prefix = xstrdup (field); | ||||||
| 
 | 	item->text = xstrdup0 (compact_map_find (data, field)); | ||||||
| 	strv_append (&g_info_tab.keys, field); |  | ||||||
| 	strv_append (&g_info_tab.values, value); |  | ||||||
| 	g_info_tab.super.item_count++; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static void | static void | ||||||
| info_tab_update (void) | info_tab_update (void) | ||||||
| { | { | ||||||
| 	strv_reset (&g_info_tab.keys); | 	while (g_info_tab.super.item_count) | ||||||
| 	strv_reset (&g_info_tab.values); | 		info_tab_item_free (&g_info_tab.items[--g_info_tab.super.item_count]); | ||||||
| 	g_info_tab.super.item_count = 0; | 
 | ||||||
|  | 	compact_map_t map = item_list_get (&g.playlist, g.song); | ||||||
|  | 	if (!map) | ||||||
|  | 		return; | ||||||
| 
 | 
 | ||||||
| 	compact_map_t map; |  | ||||||
| 	if ((map = item_list_get (&g.playlist, g.song))) |  | ||||||
| 	{ |  | ||||||
| 	info_tab_add (map, "Title"); | 	info_tab_add (map, "Title"); | ||||||
| 	info_tab_add (map, "Artist"); | 	info_tab_add (map, "Artist"); | ||||||
| 	info_tab_add (map, "Album"); | 	info_tab_add (map, "Album"); | ||||||
| 	info_tab_add (map, "Track"); | 	info_tab_add (map, "Track"); | ||||||
| 	info_tab_add (map, "Genre"); | 	info_tab_add (map, "Genre"); | ||||||
| 		// Yes, it is "file", but this is also for display
 | 	// We actually receive it as "file", but the key is also used for display
 | ||||||
| 	info_tab_add (map, "File"); | 	info_tab_add (map, "File"); | ||||||
|  | 
 | ||||||
|  | 	if (g_info_tab.plugins) | ||||||
|  | 	{ | ||||||
|  | 		(void) info_tab_prepare (); | ||||||
|  | 		LIST_FOR_EACH (struct info_tab_plugin, plugin, g_info_tab.plugins) | ||||||
|  | 			info_tab_prepare ()->plugin = plugin; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if (g_info_tab.plugin_pid != -1) | ||||||
|  | 	{ | ||||||
|  | 		(void) info_tab_prepare (); | ||||||
|  | 		info_tab_prepare ()->text = xstrdup ("Processing..."); | ||||||
|  | 		return; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	const char *songid = compact_map_find (map, "Id"); | ||||||
|  | 	if (songid && atoi (songid) == g_info_tab.plugin_songid | ||||||
|  | 	 && g_info_tab.plugin_output.len) | ||||||
|  | 	{ | ||||||
|  | 		struct strv lines = strv_make (); | ||||||
|  | 		cstr_split (g_info_tab.plugin_output.str, "\r\n", false, &lines); | ||||||
|  | 
 | ||||||
|  | 		(void) info_tab_prepare (); | ||||||
|  | 		for (size_t i = 0; i < lines.len; i++) | ||||||
|  | 		{ | ||||||
|  | 			struct info_tab_item *item = info_tab_prepare (); | ||||||
|  | 			item->formatted = true; | ||||||
|  | 			item->text = lines.vector[i]; | ||||||
|  | 		} | ||||||
|  | 		free (lines.vector); | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | ||||||
|  | 
 | ||||||
|  | static void | ||||||
|  | info_tab_plugin_abort (void) | ||||||
|  | { | ||||||
|  | 	if (g_info_tab.plugin_pid == -1) | ||||||
|  | 		return; | ||||||
|  | 
 | ||||||
|  | 	// XXX: our methods of killing are very crude, we hope to improve;
 | ||||||
|  | 	//   at least install a SIGCHLD handler to collect zombies
 | ||||||
|  | 	(void) kill (-g_info_tab.plugin_pid, SIGTERM); | ||||||
|  | 
 | ||||||
|  | 	int status = 0; | ||||||
|  | 	while (waitpid (g_info_tab.plugin_pid, &status, WNOHANG) == -1 | ||||||
|  | 		&& errno == EINTR) | ||||||
|  | 		; | ||||||
|  | 	if (WIFEXITED (status) && WEXITSTATUS (status) != EXIT_SUCCESS) | ||||||
|  | 		print_error ("plugin reported failure"); | ||||||
|  | 
 | ||||||
|  | 	g_info_tab.plugin_pid = -1; | ||||||
|  | 	poller_fd_reset (&g_info_tab.plugin_event); | ||||||
|  | 	xclose (g_info_tab.plugin_stdout); | ||||||
|  | 	g_info_tab.plugin_stdout = -1; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static void | ||||||
|  | info_tab_on_plugin_stdout (const struct pollfd *fd, void *user_data) | ||||||
|  | { | ||||||
|  | 	(void) user_data; | ||||||
|  | 
 | ||||||
|  | 	struct str *buf = &g_info_tab.plugin_output; | ||||||
|  | 	switch (socket_io_try_read (fd->fd, buf)) | ||||||
|  | 	{ | ||||||
|  | 	case SOCKET_IO_OK: | ||||||
|  | 		str_enforce_utf8 (buf); | ||||||
|  | 		return; | ||||||
|  | 	case SOCKET_IO_ERROR: | ||||||
|  | 		print_error ("error reading from plugin: %s", strerror (errno)); | ||||||
|  | 		// Fall-through
 | ||||||
|  | 	case SOCKET_IO_EOF: | ||||||
|  | 		info_tab_plugin_abort (); | ||||||
|  | 		info_tab_update (); | ||||||
|  | 		app_invalidate (); | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static void | ||||||
|  | info_tab_plugin_run (struct info_tab_plugin *plugin, compact_map_t map) | ||||||
|  | { | ||||||
|  | 	info_tab_plugin_abort (); | ||||||
|  | 	if (!map) | ||||||
|  | 		return; | ||||||
|  | 
 | ||||||
|  | 	const char *songid = compact_map_find (map, "Id"); | ||||||
|  | 	const char *title  = compact_map_find (map, "Title"); | ||||||
|  | 	const char *artist = compact_map_find (map, "Artist"); | ||||||
|  | 	const char *album  = compact_map_find (map, "Album"); | ||||||
|  | 	if (!songid || !title || !artist) | ||||||
|  | 	{ | ||||||
|  | 		print_error ("unknown song title or artist"); | ||||||
|  | 		return; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	int stdout_pipe[2]; | ||||||
|  | 	if (pipe (stdout_pipe)) | ||||||
|  | 	{ | ||||||
|  | 		print_error ("%s: %s", "pipe", strerror (errno)); | ||||||
|  | 		return; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	enum { READ, WRITE }; | ||||||
|  | 	set_cloexec (stdout_pipe[READ]); | ||||||
|  | 	set_cloexec (stdout_pipe[WRITE]); | ||||||
|  | 
 | ||||||
|  | 	const char *argv[] = | ||||||
|  | 		{ xbasename (plugin->path), title, artist, album, NULL }; | ||||||
|  | 
 | ||||||
|  | 	pid_t child = fork (); | ||||||
|  | 	switch (child) | ||||||
|  | 	{ | ||||||
|  | 	case -1: | ||||||
|  | 		print_error ("%s: %s", "fork", strerror (errno)); | ||||||
|  | 		xclose (stdout_pipe[READ]); | ||||||
|  | 		xclose (stdout_pipe[WRITE]); | ||||||
|  | 		return; | ||||||
|  | 	case 0: | ||||||
|  | 		if (setpgid (0, 0) == -1 || !freopen ("/dev/null", "r", stdin) | ||||||
|  | 		 || dup2 (stdout_pipe[WRITE], STDOUT_FILENO) == -1 | ||||||
|  | 		 || dup2 (stdout_pipe[WRITE], STDERR_FILENO) == -1) | ||||||
|  | 			_exit (EXIT_FAILURE); | ||||||
|  | 
 | ||||||
|  | 		signal (SIGPIPE, SIG_DFL); | ||||||
|  | 
 | ||||||
|  | 		(void) execv (plugin->path, (char **) argv); | ||||||
|  | 		fprintf (stderr, "%s\n", strerror (errno)); | ||||||
|  | 		_exit (EXIT_FAILURE); | ||||||
|  | 	default: | ||||||
|  | 		// Resolve the race, even though it isn't critical for us
 | ||||||
|  | 		(void) setpgid (child, child); | ||||||
|  | 
 | ||||||
|  | 		g_info_tab.plugin_songid = atoi (songid); | ||||||
|  | 		g_info_tab.plugin_pid = child; | ||||||
|  | 		set_blocking ((g_info_tab.plugin_stdout = stdout_pipe[READ]), false); | ||||||
|  | 		xclose (stdout_pipe[WRITE]); | ||||||
|  | 
 | ||||||
|  | 		struct poller_fd *event = &g_info_tab.plugin_event; | ||||||
|  | 		*event = poller_fd_make (&g.poller, g_info_tab.plugin_stdout); | ||||||
|  | 		event->dispatcher = info_tab_on_plugin_stdout; | ||||||
|  | 		str_reset (&g_info_tab.plugin_output); | ||||||
|  | 		poller_fd_set (&g_info_tab.plugin_event, POLLIN); | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static bool | ||||||
|  | info_tab_on_action (enum action action) | ||||||
|  | { | ||||||
|  | 	struct tab *tab = g.active_tab; | ||||||
|  | 	if (tab->item_selected < 0 | ||||||
|  | 	 || tab->item_selected >= (int) tab->item_count) | ||||||
|  | 		return false; | ||||||
|  | 
 | ||||||
|  | 	struct info_tab_item *item = &g_info_tab.items[tab->item_selected]; | ||||||
|  | 	if (!item->plugin) | ||||||
|  | 		return false; | ||||||
|  | 
 | ||||||
|  | 	switch (action) | ||||||
|  | 	{ | ||||||
|  | 	case ACTION_DESCRIBE: | ||||||
|  | 		app_show_message (xstrdup ("Path: "), xstrdup (item->plugin->path)); | ||||||
|  | 		return true; | ||||||
|  | 	case ACTION_CHOOSE: | ||||||
|  | 		info_tab_plugin_run (item->plugin, item_list_get (&g.playlist, g.song)); | ||||||
|  | 		info_tab_update (); | ||||||
|  | 		app_invalidate (); | ||||||
|  | 		return true; | ||||||
|  | 	default: | ||||||
|  | 		return false; | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static struct tab * | static struct tab * | ||||||
| info_tab_init (void) | info_tab_init (void) | ||||||
| { | { | ||||||
| 	g_info_tab.keys = strv_make (); | 	g_info_tab.items = | ||||||
| 	g_info_tab.values = strv_make (); | 		xcalloc ((g_info_tab.items_alloc = 16), sizeof *g_info_tab.items); | ||||||
|  | 
 | ||||||
|  | 	g_info_tab.plugins = info_tab_plugin_load_all (); | ||||||
|  | 	g_info_tab.plugin_songid = -1; | ||||||
|  | 	g_info_tab.plugin_pid = -1; | ||||||
|  | 	g_info_tab.plugin_stdout = -1; | ||||||
|  | 	g_info_tab.plugin_output = str_make (); | ||||||
| 
 | 
 | ||||||
| 	struct tab *super = &g_info_tab.super; | 	struct tab *super = &g_info_tab.super; | ||||||
| 	tab_init (super, "Info"); | 	tab_init (super, "Info"); | ||||||
|  | 	super->on_action = info_tab_on_action; | ||||||
| 	super->on_item_layout = info_tab_on_item_layout; | 	super->on_item_layout = info_tab_on_item_layout; | ||||||
| 	return super; | 	return super; | ||||||
| } | } | ||||||
| @ -5377,7 +5795,7 @@ tui_on_tty_readable (const struct pollfd *fd, void *user_data) | |||||||
| 	poller_timer_reset (&g.tk_timer); | 	poller_timer_reset (&g.tk_timer); | ||||||
| 	termo_advisereadable (g.tk); | 	termo_advisereadable (g.tk); | ||||||
| 
 | 
 | ||||||
| 	termo_key_t event; | 	termo_key_t event = {}; | ||||||
| 	int64_t event_ts = clock_msec (CLOCK_BEST); | 	int64_t event_ts = clock_msec (CLOCK_BEST); | ||||||
| 	termo_result_t res; | 	termo_result_t res; | ||||||
| 	while ((res = termo_getkey (g.tk, &event)) == TERMO_RES_KEY) | 	while ((res = termo_getkey (g.tk, &event)) == TERMO_RES_KEY) | ||||||
| @ -5448,7 +5866,11 @@ static XErrorHandler x11_default_error_handler; | |||||||
| static XftFont * | static XftFont * | ||||||
| x11_font (struct widget *self) | x11_font (struct widget *self) | ||||||
| { | { | ||||||
| 	return (self->attrs & A_BOLD) ? g.xft_bold : g.xft_regular; | 	if (self->attrs & A_BOLD) | ||||||
|  | 		return g.xft_bold; | ||||||
|  | 	if (self->attrs & A_ITALIC) | ||||||
|  | 		return g.xft_italic; | ||||||
|  | 	return g.xft_regular; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static XRenderColor * | static XRenderColor * | ||||||
| @ -5929,6 +6351,7 @@ x11_destroy (void) | |||||||
| 	XftDrawDestroy (g.xft_draw); | 	XftDrawDestroy (g.xft_draw); | ||||||
| 	XftFontClose (g.dpy, g.xft_regular); | 	XftFontClose (g.dpy, g.xft_regular); | ||||||
| 	XftFontClose (g.dpy, g.xft_bold); | 	XftFontClose (g.dpy, g.xft_bold); | ||||||
|  | 	XftFontClose (g.dpy, g.xft_italic); | ||||||
| 	cstr_set (&g.x11_selection, NULL); | 	cstr_set (&g.x11_selection, NULL); | ||||||
| 
 | 
 | ||||||
| 	poller_fd_reset (&g.x11_event); | 	poller_fd_reset (&g.x11_event); | ||||||
| @ -6430,8 +6853,11 @@ x11_init_fonts (void) | |||||||
| 
 | 
 | ||||||
| 	FcPattern *query_regular = FcNameParse ((const FcChar8 *) name); | 	FcPattern *query_regular = FcNameParse ((const FcChar8 *) name); | ||||||
| 	FcPattern *query_bold = FcPatternDuplicate (query_regular); | 	FcPattern *query_bold = FcPatternDuplicate (query_regular); | ||||||
| 	FcPatternAdd (query_bold, FC_STYLE, | 	FcPatternAdd (query_bold, FC_STYLE, (FcValue) { | ||||||
| 		(FcValue) { .type = FcTypeString, .u.s = (FcChar8 *) "Bold" }, FcFalse); | 		.type = FcTypeString, .u.s = (FcChar8 *) "Bold" }, FcFalse); | ||||||
|  | 	FcPattern *query_italic = FcPatternDuplicate (query_regular); | ||||||
|  | 	FcPatternAdd (query_italic, FC_STYLE, (FcValue) { | ||||||
|  | 		.type = FcTypeString, .u.s = (FcChar8 *) "Italic" }, FcFalse); | ||||||
| 
 | 
 | ||||||
| 	FcPattern *regular = XftFontMatch (g.dpy, screen, query_regular, &result); | 	FcPattern *regular = XftFontMatch (g.dpy, screen, query_regular, &result); | ||||||
| 	FcPatternDestroy (query_regular); | 	FcPatternDestroy (query_regular); | ||||||
| @ -6449,6 +6875,13 @@ x11_init_fonts (void) | |||||||
| 		FcPatternDestroy (bold); | 		FcPatternDestroy (bold); | ||||||
| 	if (!g.xft_bold) | 	if (!g.xft_bold) | ||||||
| 		g.xft_bold = XftFontCopy (g.dpy, g.xft_regular); | 		g.xft_bold = XftFontCopy (g.dpy, g.xft_regular); | ||||||
|  | 
 | ||||||
|  | 	FcPattern *italic = XftFontMatch (g.dpy, screen, query_italic, &result); | ||||||
|  | 	FcPatternDestroy (query_italic); | ||||||
|  | 	if (italic && !(g.xft_italic = XftFontOpenPattern (g.dpy, italic))) | ||||||
|  | 		FcPatternDestroy (italic); | ||||||
|  | 	if (!g.xft_italic) | ||||||
|  | 		g.xft_italic = XftFontCopy (g.dpy, g.xft_regular); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static void | static void | ||||||
| @ -6675,6 +7108,7 @@ app_log_handler (void *user_data, const char *quote, const char *fmt, | |||||||
| 	str_append_vprintf (&message, fmt, ap); | 	str_append_vprintf (&message, fmt, ap); | ||||||
| 
 | 
 | ||||||
| 	// Show it prettified to the user, then maybe log it elsewhere as well.
 | 	// Show it prettified to the user, then maybe log it elsewhere as well.
 | ||||||
|  | 	// TODO: Review locale encoding vs UTF-8 in the entire program.
 | ||||||
| 	message.str[0] = toupper_ascii (message.str[0]); | 	message.str[0] = toupper_ascii (message.str[0]); | ||||||
| 	app_show_message (xstrndup (message.str, quote_len), | 	app_show_message (xstrndup (message.str, quote_len), | ||||||
| 		xstrdup (message.str + quote_len)); | 		xstrdup (message.str + quote_len)); | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user