Compare commits
12 Commits
v2.0.0
...
61fac878ad
| Author | SHA1 | Date | |
|---|---|---|---|
|
61fac878ad
|
|||
|
da83dbee1f
|
|||
|
41fda4e317
|
|||
|
d4d2259825
|
|||
|
568abc896c
|
|||
|
8aac4ae0a8
|
|||
|
e72ed71f53
|
|||
|
28ed7a85a8
|
|||
|
b6dd940720
|
|||
|
d8e0d1b2fe
|
|||
|
5cda848f94
|
|||
|
a167ae40b3
|
@@ -10,6 +10,14 @@ endif ()
|
|||||||
# For custom modules
|
# For custom modules
|
||||||
set (CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/liberty/cmake)
|
set (CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/liberty/cmake)
|
||||||
|
|
||||||
|
# Collect important build toggles for our simple preprocessor
|
||||||
|
# (cpp(1) isn't part of POSIX, otherwise we could reuse config.h)
|
||||||
|
set (options)
|
||||||
|
macro (add_option variable help value)
|
||||||
|
option (${ARGV})
|
||||||
|
list (APPEND options "${variable}=$<BOOL:${${variable}}>")
|
||||||
|
endmacro ()
|
||||||
|
|
||||||
# Dependencies
|
# Dependencies
|
||||||
find_package (Ncursesw REQUIRED)
|
find_package (Ncursesw REQUIRED)
|
||||||
find_package (PkgConfig REQUIRED)
|
find_package (PkgConfig REQUIRED)
|
||||||
@@ -19,7 +27,7 @@ pkg_check_modules (curl REQUIRED libcurl)
|
|||||||
include (AddThreads)
|
include (AddThreads)
|
||||||
|
|
||||||
find_package (Termo QUIET NO_MODULE)
|
find_package (Termo QUIET NO_MODULE)
|
||||||
option (USE_SYSTEM_TERMO
|
add_option (USE_SYSTEM_TERMO
|
||||||
"Don't compile our own termo library, use the system one" ${Termo_FOUND})
|
"Don't compile our own termo library, use the system one" ${Termo_FOUND})
|
||||||
if (USE_SYSTEM_TERMO)
|
if (USE_SYSTEM_TERMO)
|
||||||
if (NOT Termo_FOUND)
|
if (NOT Termo_FOUND)
|
||||||
@@ -41,7 +49,7 @@ else ()
|
|||||||
endif ()
|
endif ()
|
||||||
|
|
||||||
pkg_check_modules (fftw fftw3 fftw3f)
|
pkg_check_modules (fftw fftw3 fftw3f)
|
||||||
option (WITH_FFTW "Use FFTW to enable spectrum visualisation" ${fftw_FOUND})
|
add_option (WITH_FFTW "Use FFTW to enable spectrum visualisation" ${fftw_FOUND})
|
||||||
if (WITH_FFTW)
|
if (WITH_FFTW)
|
||||||
if (NOT fftw_FOUND)
|
if (NOT fftw_FOUND)
|
||||||
message (FATAL_ERROR "FFTW not found")
|
message (FATAL_ERROR "FFTW not found")
|
||||||
@@ -50,7 +58,8 @@ if (WITH_FFTW)
|
|||||||
endif ()
|
endif ()
|
||||||
|
|
||||||
pkg_check_modules (libpulse libpulse)
|
pkg_check_modules (libpulse libpulse)
|
||||||
option (WITH_PULSE "Enable control of PulseAudio sink volume" ${libpulse_FOUND})
|
add_option (WITH_PULSE
|
||||||
|
"Enable PulseAudio sink volume control" ${libpulse_FOUND})
|
||||||
if (WITH_PULSE)
|
if (WITH_PULSE)
|
||||||
if (NOT libpulse_FOUND)
|
if (NOT libpulse_FOUND)
|
||||||
message (FATAL_ERROR "libpulse not found")
|
message (FATAL_ERROR "libpulse not found")
|
||||||
@@ -59,7 +68,7 @@ if (WITH_PULSE)
|
|||||||
endif ()
|
endif ()
|
||||||
|
|
||||||
pkg_check_modules (x11 x11 xrender xft fontconfig)
|
pkg_check_modules (x11 x11 xrender xft fontconfig)
|
||||||
option (WITH_X11 "Use FFTW to enable spectrum visualisation" ${x11_FOUND})
|
add_option (WITH_X11 "Use FFTW to enable spectrum visualisation" ${x11_FOUND})
|
||||||
if (WITH_X11)
|
if (WITH_X11)
|
||||||
if (NOT x11_FOUND)
|
if (NOT x11_FOUND)
|
||||||
message (FATAL_ERROR "Some X11 libraries were not found")
|
message (FATAL_ERROR "Some X11 libraries were not found")
|
||||||
@@ -95,24 +104,18 @@ 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})
|
||||||
|
|
||||||
# Assuming a Unix-compatible system with a standalone preprocessor
|
|
||||||
set (actions_list ${PROJECT_SOURCE_DIR}/nncmpp.actions)
|
set (actions_list ${PROJECT_SOURCE_DIR}/nncmpp.actions)
|
||||||
|
set (actions_awk ${PROJECT_SOURCE_DIR}/nncmpp.actions.awk)
|
||||||
set (actions ${PROJECT_BINARY_DIR}/nncmpp-actions.h)
|
set (actions ${PROJECT_BINARY_DIR}/nncmpp-actions.h)
|
||||||
add_custom_command (OUTPUT ${actions}
|
add_custom_command (OUTPUT ${actions}
|
||||||
COMMAND cpp -I${PROJECT_BINARY_DIR} -P ${actions_list}
|
COMMAND env LC_ALL=C ${options}
|
||||||
| grep . | tr [[\n]] ^ | sed -ne [[h; s/,[^^]*/,/g]] -e [[s/$/COUNT/]]
|
awk -f ${actions_awk} ${actions_list} > ${actions}
|
||||||
-e [[s/[^^]*/\tACTION_&/g]] -e [[s/.*/enum action {\n&\n};\n/p]]
|
DEPENDS ${actions_awk} ${actions_list} VERBATIM)
|
||||||
-e [[g; s/,[^^]*//g; y/_/-/]] -e [[s/[^^]\{1,\}/\t"&",/g]]
|
|
||||||
-e [[s/.*/static const char *g_action_names[] = {\n&};\n/p]]
|
|
||||||
-e [[g; s/[^^]*, *//g;]] -e [[s/[^^]\{1,\}/\t"&",/g]]
|
|
||||||
-e [[s/.*/static const char *g_action_descriptions[] = {\n&};/p]]
|
|
||||||
| tr ^ [[\n]] > ${actions}
|
|
||||||
COMMAND test -s ${actions}
|
|
||||||
DEPENDS ${actions_list} ${PROJECT_BINARY_DIR}/config.h VERBATIM)
|
|
||||||
|
|
||||||
# Build the main executable and link it
|
# Build the main executable and link it
|
||||||
add_executable (${PROJECT_NAME} ${PROJECT_NAME}.c ${actions})
|
add_executable (${PROJECT_NAME} ${PROJECT_NAME}.c ${actions})
|
||||||
@@ -121,16 +124,17 @@ 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)
|
||||||
find_program (A2X_EXECUTABLE a2x)
|
find_program (A2X_EXECUTABLE a2x)
|
||||||
if (NOT ASCIIDOCTOR_EXECUTABLE AND NOT A2X_EXECUTABLE)
|
if (NOT ASCIIDOCTOR_EXECUTABLE AND NOT A2X_EXECUTABLE)
|
||||||
message (FATAL_ERROR "Neither asciidoctor nor a2x were found")
|
message (WARNING "Neither asciidoctor nor a2x were found, "
|
||||||
|
"falling back to a substandard manual page generator")
|
||||||
endif ()
|
endif ()
|
||||||
|
|
||||||
foreach (page ${PROJECT_NAME})
|
foreach (page ${PROJECT_NAME})
|
||||||
@@ -152,6 +156,14 @@ foreach (page ${PROJECT_NAME})
|
|||||||
"${PROJECT_SOURCE_DIR}/${page}.adoc"
|
"${PROJECT_SOURCE_DIR}/${page}.adoc"
|
||||||
DEPENDS ${page}.adoc
|
DEPENDS ${page}.adoc
|
||||||
COMMENT "Generating man page for ${page}" VERBATIM)
|
COMMENT "Generating man page for ${page}" VERBATIM)
|
||||||
|
else ()
|
||||||
|
set (ASCIIMAN ${PROJECT_SOURCE_DIR}/liberty/tools/asciiman.awk)
|
||||||
|
add_custom_command (OUTPUT ${page_output}
|
||||||
|
COMMAND env LC_ALL=C asciidoc-release-version=${PROJECT_VERSION}
|
||||||
|
awk -f ${ASCIIMAN} "${PROJECT_SOURCE_DIR}/${page}.adoc"
|
||||||
|
> ${page_output}
|
||||||
|
DEPENDS ${page}.adoc ${ASCIIMAN}
|
||||||
|
COMMENT "Generating man page for ${page}" VERBATIM)
|
||||||
endif ()
|
endif ()
|
||||||
endforeach ()
|
endforeach ()
|
||||||
|
|
||||||
|
|||||||
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,
|
||||||
@@ -37,10 +38,10 @@ The rest of this README will concern itself with externalities.
|
|||||||
|
|
||||||
Building
|
Building
|
||||||
--------
|
--------
|
||||||
Build dependencies: CMake, pkg-config, asciidoctor or asciidoc,
|
Build dependencies: CMake, pkg-config, awk, liberty (included),
|
||||||
liberty (included), termo (included) +
|
termo (included), asciidoctor or asciidoc (recommended but optional) +
|
||||||
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-z0-9]//gr;
|
||||||
|
$title = lc(NFD($title)) =~ s/\(.*?\)//gr =~ s/[^a-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;
|
||||||
2
liberty
2
liberty
Submodule liberty updated: 63aed8f0fd...0e86ffe7c3
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* line-editor.c: a line editor component for the TUI part of liberty
|
* line-editor.c: a line editor component for the TUI part of liberty
|
||||||
*
|
*
|
||||||
* Copyright (c) 2017 - 2018, Přemysl Eric Janouch <p@janouch.name>
|
* Copyright (c) 2017 - 2022, 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.
|
||||||
@@ -48,6 +48,10 @@ enum line_editor_action
|
|||||||
LINE_EDITOR_HOME, ///< Go to start of line
|
LINE_EDITOR_HOME, ///< Go to start of line
|
||||||
LINE_EDITOR_END, ///< Go to end of line
|
LINE_EDITOR_END, ///< Go to end of line
|
||||||
|
|
||||||
|
LINE_EDITOR_UPCASE_WORD, ///< Convert word to uppercase
|
||||||
|
LINE_EDITOR_DOWNCASE_WORD, ///< Convert word to lowercase
|
||||||
|
LINE_EDITOR_CAPITALIZE_WORD, ///< Capitalize word
|
||||||
|
|
||||||
LINE_EDITOR_B_DELETE, ///< Delete last character
|
LINE_EDITOR_B_DELETE, ///< Delete last character
|
||||||
LINE_EDITOR_F_DELETE, ///< Delete next character
|
LINE_EDITOR_F_DELETE, ///< Delete next character
|
||||||
LINE_EDITOR_B_KILL_WORD, ///< Delete last word
|
LINE_EDITOR_B_KILL_WORD, ///< Delete last word
|
||||||
@@ -185,8 +189,8 @@ line_editor_action (struct line_editor *self, enum line_editor_action action)
|
|||||||
if (self->point + 1 > (int) self->len)
|
if (self->point + 1 > (int) self->len)
|
||||||
return false;
|
return false;
|
||||||
int i = self->point;
|
int i = self->point;
|
||||||
while (i < (int) self->len && self->line[i] != ' ') i++;
|
|
||||||
while (i < (int) self->len && self->line[i] == ' ') i++;
|
while (i < (int) self->len && self->line[i] == ' ') i++;
|
||||||
|
while (i < (int) self->len && self->line[i] != ' ') i++;
|
||||||
self->point = i;
|
self->point = i;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -197,6 +201,41 @@ line_editor_action (struct line_editor *self, enum line_editor_action action)
|
|||||||
self->point = self->len;
|
self->point = self->len;
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
case LINE_EDITOR_UPCASE_WORD:
|
||||||
|
{
|
||||||
|
int i = self->point;
|
||||||
|
for (; i < (int) self->len && self->line[i] == ' '; i++);
|
||||||
|
for (; i < (int) self->len && self->line[i] != ' '; i++)
|
||||||
|
self->line[i] = uc_toupper (self->line[i]);
|
||||||
|
self->point = i;
|
||||||
|
line_editor_changed (self);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
case LINE_EDITOR_DOWNCASE_WORD:
|
||||||
|
{
|
||||||
|
int i = self->point;
|
||||||
|
for (; i < (int) self->len && self->line[i] == ' '; i++);
|
||||||
|
for (; i < (int) self->len && self->line[i] != ' '; i++)
|
||||||
|
self->line[i] = uc_tolower (self->line[i]);
|
||||||
|
self->point = i;
|
||||||
|
line_editor_changed (self);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
case LINE_EDITOR_CAPITALIZE_WORD:
|
||||||
|
{
|
||||||
|
int i = self->point;
|
||||||
|
ucs4_t (*converter) (ucs4_t) = uc_totitle;
|
||||||
|
for (; i < (int) self->len && self->line[i] == ' '; i++);
|
||||||
|
for (; i < (int) self->len && self->line[i] != ' '; i++)
|
||||||
|
{
|
||||||
|
self->line[i] = converter (self->line[i]);
|
||||||
|
converter = uc_tolower;
|
||||||
|
}
|
||||||
|
self->point = i;
|
||||||
|
line_editor_changed (self);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
case LINE_EDITOR_B_DELETE:
|
case LINE_EDITOR_B_DELETE:
|
||||||
{
|
{
|
||||||
if (self->point < 1)
|
if (self->point < 1)
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#include "config.h"
|
|
||||||
|
|
||||||
NONE, Do nothing
|
NONE, Do nothing
|
||||||
|
|
||||||
QUIT, Quit
|
QUIT, Quit
|
||||||
@@ -28,11 +26,11 @@ MPD_CONSUME, Toggle consume
|
|||||||
MPD_UPDATE_DB, Update MPD database
|
MPD_UPDATE_DB, Update MPD database
|
||||||
MPD_COMMAND, Send raw command to MPD
|
MPD_COMMAND, Send raw command to MPD
|
||||||
|
|
||||||
#ifdef WITH_PULSE
|
.ifdef WITH_PULSE
|
||||||
PULSE_VOLUME_UP, Increase PulseAudio volume
|
PULSE_VOLUME_UP, Increase PulseAudio volume
|
||||||
PULSE_VOLUME_DOWN, Decrease PulseAudio volume
|
PULSE_VOLUME_DOWN, Decrease PulseAudio volume
|
||||||
PULSE_MUTE, Toggle PulseAudio sink mute
|
PULSE_MUTE, Toggle PulseAudio sink mute
|
||||||
#endif
|
.endif
|
||||||
|
|
||||||
CHOOSE, Choose item
|
CHOOSE, Choose item
|
||||||
DELETE, Delete item
|
DELETE, Delete item
|
||||||
@@ -67,6 +65,10 @@ EDITOR_F_WORD, Go forward a word
|
|||||||
EDITOR_HOME, Go to start of line
|
EDITOR_HOME, Go to start of line
|
||||||
EDITOR_END, Go to end of line
|
EDITOR_END, Go to end of line
|
||||||
|
|
||||||
|
EDITOR_UPCASE_WORD, Convert word to uppercase
|
||||||
|
EDITOR_DOWNCASE_WORD, Convert word to lowercase
|
||||||
|
EDITOR_CAPITALIZE_WORD, Capitalize word
|
||||||
|
|
||||||
EDITOR_B_DELETE, Delete last character
|
EDITOR_B_DELETE, Delete last character
|
||||||
EDITOR_F_DELETE, Delete next character
|
EDITOR_F_DELETE, Delete next character
|
||||||
EDITOR_B_KILL_WORD, Delete last word
|
EDITOR_B_KILL_WORD, Delete last word
|
||||||
|
|||||||
106
nncmpp.actions.awk
Normal file
106
nncmpp.actions.awk
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
# nncmpp.actions.awk: produce C code for a list of user actions
|
||||||
|
#
|
||||||
|
# Copyright (c) 2022, 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 \
|
||||||
|
# nncmpp.actions > nncmpp-actions.h
|
||||||
|
|
||||||
|
# --- Preprocessor -------------------------------------------------------------
|
||||||
|
|
||||||
|
function fatal(message) {
|
||||||
|
print "// " FILENAME ":" FNR ": fatal error: " message
|
||||||
|
print FILENAME ":" FNR ": fatal error: " message > "/dev/stderr"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function condition(pass, passing, a, i) {
|
||||||
|
split(substr($0, RSTART + RLENGTH), a, /[[:space:]]+/)
|
||||||
|
if (!(1 in a))
|
||||||
|
fatal("missing condition")
|
||||||
|
|
||||||
|
passing = 0
|
||||||
|
for (i in a)
|
||||||
|
if (a[i] && !pass == !ENVIRON[a[i]])
|
||||||
|
passing = 1
|
||||||
|
|
||||||
|
while (getline > 0) {
|
||||||
|
if (match($0, /^[[:space:]]*[.]endif[[:space:]]*$/))
|
||||||
|
return 1
|
||||||
|
|
||||||
|
if (match($0, /^[[:space:]]*[.]else[[:space:]]*$/))
|
||||||
|
passing = !passing
|
||||||
|
else if (!directive() && passing)
|
||||||
|
process()
|
||||||
|
}
|
||||||
|
|
||||||
|
fatal("unterminated condition body")
|
||||||
|
}
|
||||||
|
|
||||||
|
# Multiple arguments mean logical OR, multiple directives mean logical AND.
|
||||||
|
# Similar syntax is also used by Exim, BSD make, or various assemblers.
|
||||||
|
#
|
||||||
|
# Looking at what others have picked for their preprocessor syntax:
|
||||||
|
# {OpenGL, FreeBASIC} reuse #ifdef, which would be confusing with C code around,
|
||||||
|
# {Mental Ray, RapidQ and UniVerse BASIC} use $ifdef, NSIS has !ifdef,
|
||||||
|
# and Verilog went for `ifdef. Not much more can be easily found.
|
||||||
|
function directive() {
|
||||||
|
sub(/#.*/, "")
|
||||||
|
if (match($0, /^[[:space:]]*[.]ifdef[[:space:]]+/))
|
||||||
|
return condition(1)
|
||||||
|
if (match($0, /^[[:space:]]*[.]ifndef[[:space:]]+/))
|
||||||
|
return condition(0)
|
||||||
|
if (/^[[:space:]]*[.]/)
|
||||||
|
fatal("unexpected or unsupported directive")
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
!directive() {
|
||||||
|
process()
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Postprocessor ------------------------------------------------------------
|
||||||
|
|
||||||
|
function strip(string) {
|
||||||
|
gsub(/^[[:space:]]*|[[:space:]]*$/, "", string)
|
||||||
|
return string
|
||||||
|
}
|
||||||
|
|
||||||
|
function process( constant, name, description) {
|
||||||
|
if (match($0, /,/)) {
|
||||||
|
constant = name = strip(substr($0, 1, RSTART - 1))
|
||||||
|
description = strip(substr($0, RSTART + RLENGTH))
|
||||||
|
gsub(/_/, "-", name)
|
||||||
|
|
||||||
|
N++
|
||||||
|
Constants[N] = constant
|
||||||
|
Names[N] = tolower(name)
|
||||||
|
Descriptions[N] = description
|
||||||
|
} else if (/[^[:space:]]/) {
|
||||||
|
fatal("invalid action definition syntax")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function tocstring(string) {
|
||||||
|
gsub(/\\/, "\\\\", string)
|
||||||
|
gsub(/"/, "\\\"", string)
|
||||||
|
return "\"" string "\""
|
||||||
|
}
|
||||||
|
|
||||||
|
END {
|
||||||
|
print "enum action {"
|
||||||
|
for (i in Constants)
|
||||||
|
print "\t" "ACTION_" Constants[i] ","
|
||||||
|
print "\t" "ACTION_COUNT"
|
||||||
|
print "};"
|
||||||
|
print ""
|
||||||
|
print "static const char *g_action_names[] = {"
|
||||||
|
for (i in Names)
|
||||||
|
print "\t" tocstring(Names[i]) ","
|
||||||
|
print "};"
|
||||||
|
print ""
|
||||||
|
print "static const char *g_action_descriptions[] = {"
|
||||||
|
for (i in Descriptions)
|
||||||
|
print "\t" tocstring(Descriptions[i]) ","
|
||||||
|
print "};"
|
||||||
|
}
|
||||||
25
nncmpp.adoc
25
nncmpp.adoc
@@ -81,6 +81,10 @@ The distribution contains example colour schemes in the _contrib_ directory.
|
|||||||
// TODO: it seems like liberty should contain an includable snippet about
|
// TODO: it seems like liberty should contain an includable snippet about
|
||||||
// the format, which could form a part of nncmpp.conf(5).
|
// the format, which could form a part of nncmpp.conf(5).
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
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"
|
||||||
@@ -124,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.
|
||||||
@@ -131,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,
|
||||||
|
|||||||
526
nncmpp.c
526
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
|
||||||
@@ -2774,6 +2804,13 @@ app_editor_process_action (enum action action)
|
|||||||
case ACTION_EDITOR_END:
|
case ACTION_EDITOR_END:
|
||||||
return line_editor_action (&g.editor, LINE_EDITOR_END);
|
return line_editor_action (&g.editor, LINE_EDITOR_END);
|
||||||
|
|
||||||
|
case ACTION_EDITOR_UPCASE_WORD:
|
||||||
|
return line_editor_action (&g.editor, LINE_EDITOR_UPCASE_WORD);
|
||||||
|
case ACTION_EDITOR_DOWNCASE_WORD:
|
||||||
|
return line_editor_action (&g.editor, LINE_EDITOR_DOWNCASE_WORD);
|
||||||
|
case ACTION_EDITOR_CAPITALIZE_WORD:
|
||||||
|
return line_editor_action (&g.editor, LINE_EDITOR_CAPITALIZE_WORD);
|
||||||
|
|
||||||
case ACTION_EDITOR_B_DELETE:
|
case ACTION_EDITOR_B_DELETE:
|
||||||
return line_editor_action (&g.editor, LINE_EDITOR_B_DELETE);
|
return line_editor_action (&g.editor, LINE_EDITOR_B_DELETE);
|
||||||
case ACTION_EDITOR_F_DELETE:
|
case ACTION_EDITOR_F_DELETE:
|
||||||
@@ -3020,6 +3057,10 @@ g_editor_defaults[] =
|
|||||||
{ "C-a", ACTION_EDITOR_HOME },
|
{ "C-a", ACTION_EDITOR_HOME },
|
||||||
{ "C-e", ACTION_EDITOR_END },
|
{ "C-e", ACTION_EDITOR_END },
|
||||||
|
|
||||||
|
{ "M-u", ACTION_EDITOR_UPCASE_WORD },
|
||||||
|
{ "M-l", ACTION_EDITOR_DOWNCASE_WORD },
|
||||||
|
{ "M-c", ACTION_EDITOR_CAPITALIZE_WORD },
|
||||||
|
|
||||||
{ "C-h", ACTION_EDITOR_B_DELETE },
|
{ "C-h", ACTION_EDITOR_B_DELETE },
|
||||||
{ "DEL", ACTION_EDITOR_B_DELETE },
|
{ "DEL", ACTION_EDITOR_B_DELETE },
|
||||||
{ "Backspace", ACTION_EDITOR_B_DELETE },
|
{ "Backspace", ACTION_EDITOR_B_DELETE },
|
||||||
@@ -4078,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:", item->prefix);
|
||||||
|
app_push (&l, g.ui->label (A_BOLD, prefix))
|
||||||
|
->width = 8 * g.ui_hunit;
|
||||||
|
app_push (&l, g.ui->padding (0, 0.5, 1));
|
||||||
|
}
|
||||||
|
|
||||||
char *prefix = xstrdup_printf ("%s:", key);
|
if (item->plugin)
|
||||||
app_push (&l, g.ui->label (A_BOLD, prefix))
|
app_push (&l, g.ui->label (A_BOLD, item->plugin->description));
|
||||||
->width = 8 * g.ui_hunit;
|
else if (!item->text || !*item->text)
|
||||||
app_push (&l, g.ui->padding (0, 0.5, 1));
|
app_push (&l, g.ui->padding (0, 1, 1));
|
||||||
app_push_fill (&l, g.ui->label (0, value));
|
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;
|
compact_map_t map = item_list_get (&g.playlist, g.song);
|
||||||
if ((map = item_list_get (&g.playlist, g.song)))
|
if (!map)
|
||||||
|
return;
|
||||||
|
|
||||||
|
info_tab_add (map, "Title");
|
||||||
|
info_tab_add (map, "Artist");
|
||||||
|
info_tab_add (map, "Album");
|
||||||
|
info_tab_add (map, "Track");
|
||||||
|
info_tab_add (map, "Genre");
|
||||||
|
// We actually receive it as "file", but the key is also used for display
|
||||||
|
info_tab_add (map, "File");
|
||||||
|
|
||||||
|
if (g_info_tab.plugins)
|
||||||
{
|
{
|
||||||
info_tab_add (map, "Title");
|
(void) info_tab_prepare ();
|
||||||
info_tab_add (map, "Artist");
|
LIST_FOR_EACH (struct info_tab_plugin, plugin, g_info_tab.plugins)
|
||||||
info_tab_add (map, "Album");
|
info_tab_prepare ()->plugin = plugin;
|
||||||
info_tab_add (map, "Track");
|
}
|
||||||
info_tab_add (map, "Genre");
|
|
||||||
// Yes, it is "file", but this is also for display
|
if (g_info_tab.plugin_pid != -1)
|
||||||
info_tab_add (map, "File");
|
{
|
||||||
|
(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;
|
||||||
}
|
}
|
||||||
@@ -4168,9 +4597,8 @@ help_tab_on_action (enum action action)
|
|||||||
|
|
||||||
if (action == ACTION_DESCRIBE)
|
if (action == ACTION_DESCRIBE)
|
||||||
{
|
{
|
||||||
char *name = xstrdup (g_action_names[a]);
|
app_show_message (xstrdup ("Configuration name: "),
|
||||||
cstr_transform (name, tolower_ascii);
|
xstrdup (g_action_names[a]));
|
||||||
app_show_message (xstrdup ("Configuration name: "), name);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (action != ACTION_CHOOSE || a == ACTION_CHOOSE /* avoid recursion */)
|
if (action != ACTION_CHOOSE || a == ACTION_CHOOSE /* avoid recursion */)
|
||||||
@@ -5367,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)
|
||||||
@@ -5438,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 *
|
||||||
@@ -5816,7 +6248,8 @@ static void
|
|||||||
x11_render_list (struct widget *self)
|
x11_render_list (struct widget *self)
|
||||||
{
|
{
|
||||||
// We could do that for all widgets, but it would be kind-of pointless.
|
// We could do that for all widgets, but it would be kind-of pointless.
|
||||||
XRenderSetPictureClipRectangles (g.dpy, g.x11_pixmap_picture, 0, 0,
|
// We need to go through Xft, or XftTextRenderUtf8() might skip glyphs.
|
||||||
|
XftDrawSetClipRectangles (g.xft_draw, 0, 0,
|
||||||
&(XRectangle) { self->x, self->y, self->width, self->height }, 1);
|
&(XRectangle) { self->x, self->y, self->width, self->height }, 1);
|
||||||
|
|
||||||
x11_render_padding (self);
|
x11_render_padding (self);
|
||||||
@@ -5826,8 +6259,7 @@ x11_render_list (struct widget *self)
|
|||||||
free (w);
|
free (w);
|
||||||
}
|
}
|
||||||
|
|
||||||
XRenderChangePicture (g.dpy, g.x11_pixmap_picture, CPClipMask,
|
XftDrawSetClip (g.xft_draw, None);
|
||||||
&(XRenderPictureAttributes) { .clip_mask = None });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static struct widget *
|
static struct widget *
|
||||||
@@ -5919,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);
|
||||||
@@ -6420,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);
|
||||||
@@ -6439,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
|
||||||
@@ -6665,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));
|
||||||
|
|||||||
Reference in New Issue
Block a user