diff options
-rw-r--r-- | CMakeLists.txt | 68 | ||||
-rw-r--r-- | LICENSE | 2 | ||||
-rw-r--r-- | NEWS | 43 | ||||
-rw-r--r-- | README.adoc | 16 | ||||
m--------- | liberty | 0 | ||||
-rw-r--r-- | line-editor.c | 327 | ||||
-rw-r--r-- | nncmpp.actions | 3 | ||||
-rw-r--r-- | nncmpp.actions.awk | 4 | ||||
-rw-r--r-- | nncmpp.adoc | 18 | ||||
-rw-r--r-- | nncmpp.c | 2264 | ||||
-rw-r--r-- | nncmpp.desktop | 9 | ||||
-rw-r--r-- | nncmpp.svg | 9 | ||||
m--------- | termo | 0 |
13 files changed, 840 insertions, 1923 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index 909e54d..93df5e8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ -cmake_minimum_required (VERSION 3.0) -project (nncmpp VERSION 2.0.0 LANGUAGES C) +cmake_minimum_required (VERSION 3.0...3.27) +project (nncmpp VERSION 2.1.1 LANGUAGES C) # Moar warnings if ("${CMAKE_C_COMPILER_ID}" MATCHES "GNU" OR CMAKE_COMPILER_IS_GNUCC) @@ -28,7 +28,7 @@ include (AddThreads) find_package (Termo QUIET NO_MODULE) 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 (NOT Termo_FOUND) message (FATAL_ERROR "System termo library not found") @@ -49,7 +49,8 @@ else () endif () pkg_check_modules (fftw fftw3 fftw3f) -add_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 (NOT fftw_FOUND) message (FATAL_ERROR "FFTW not found") @@ -59,7 +60,7 @@ endif () pkg_check_modules (libpulse libpulse) add_option (WITH_PULSE - "Enable PulseAudio sink volume control" ${libpulse_FOUND}) + "Enable PulseAudio sink volume control" "${libpulse_FOUND}") if (WITH_PULSE) if (NOT libpulse_FOUND) message (FATAL_ERROR "libpulse not found") @@ -67,8 +68,8 @@ if (WITH_PULSE) list (APPEND extra_libraries ${libpulse_LIBRARIES}) endif () -pkg_check_modules (x11 x11 xrender xft fontconfig) -add_option (WITH_X11 "Use FFTW to enable spectrum visualisation" ${x11_FOUND}) +pkg_check_modules (x11 x11 xrender xft fontconfig libpng) +add_option (WITH_X11 "Build with X11 support" "${x11_FOUND}") if (WITH_X11) if (NOT x11_FOUND) message (FATAL_ERROR "Some X11 libraries were not found") @@ -120,14 +121,36 @@ 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) + + set (icon_base ${PROJECT_BINARY_DIR}/icons) + set (icon_png_list) + foreach (icon_size 16 32 48) + icon_to_png (${PROJECT_NAME} ${PROJECT_SOURCE_DIR}/${PROJECT_NAME}.svg + ${icon_size} ${icon_base} icon_png) + list (APPEND icon_png_list ${icon_png}) + endforeach () + + add_custom_target (icons ALL DEPENDS ${icon_png_list}) + + install (FILES ${PROJECT_NAME}.svg + DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/scalable/apps) + install (DIRECTORY ${icon_base} + DESTINATION ${CMAKE_INSTALL_DATADIR}) + install (FILES ${PROJECT_NAME}.desktop + DESTINATION ${CMAKE_INSTALL_DATADIR}/applications) +endif () # Generate documentation from text markup find_program (ASCIIDOCTOR_EXECUTABLE asciidoctor) @@ -175,6 +198,33 @@ foreach (page ${project_MAN_PAGES}) DESTINATION "${CMAKE_INSTALL_MANDIR}/man${CMAKE_MATCH_1}") endforeach () +# Testing +option (BUILD_TESTING "Build tests" OFF) +if (BUILD_TESTING) + enable_testing () + + find_program (xmlwf_EXECUTABLE xmlwf) + find_program (xmllint_EXECUTABLE xmllint) + foreach (xml ${PROJECT_NAME}.svg) + if (xmlwf_EXECUTABLE) + add_test (test-xmlwf-${xml} ${xmlwf_EXECUTABLE} + ${PROJECT_SOURCE_DIR}/${xml}) + endif () + if (xmllint_EXECUTABLE) + add_test (test-xmllint-${xml} ${xmllint_EXECUTABLE} --noout + ${PROJECT_SOURCE_DIR}/${xml}) + endif () + endforeach () + + find_program (dfv_EXECUTABLE desktop-file-validate) + if (dfv_EXECUTABLE) + foreach (df ${PROJECT_NAME}.desktop) + add_test (test-dfv-${df} ${dfv_EXECUTABLE} + ${PROJECT_SOURCE_DIR}/${df}) + endforeach () + endif () +endif () + # CPack set (CPACK_PACKAGE_DESCRIPTION_SUMMARY "Terminal/X11 MPD client") set (CPACK_PACKAGE_VENDOR "Premysl Eric Janouch") @@ -1,4 +1,4 @@ -Copyright (c) 2016 - 2022, 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. @@ -1,12 +1,53 @@ 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 + + +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, using a new scriptable extension interface for the Info tab - * Made the X11 interface support italic fonts + * Improved song information shown in the window header + + * Escape no longer quits the program + + * X11: added an icon and a desktop entry file + + * X11: added support for font fallbacks and italic fonts + + * X11: fixed rendering of overflowing, partially visible list items + + * X11: fixed a crash when resizing the window to zero dimensions + + * Added a "o" binding to select the currently playing song * Added Readline-like M-u, M-l, M-c editor bindings + * Made the scroll wheel work on the elapsed time gauge and the volume display + + * Changed volume adjustment bindings to use +/- keys + + * Changed volume adjustment to go in steps of 5 rather than 10 % + 2.0.0 (2022-09-03) diff --git a/README.adoc b/README.adoc index 5ed2c51..775c5a3 100644 --- a/README.adoc +++ b/README.adoc @@ -28,8 +28,12 @@ image::nncmpp.png[align="center"] Packages -------- -Regular releases are sporadic. git master should be stable enough. You can get -a package with the latest development version from Archlinux's AUR. +Regular releases are sporadic. git master should be stable enough. +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 ------------- @@ -38,10 +42,12 @@ The rest of this README will concern itself with externalities. Building -------- -Build dependencies: CMake, pkg-config, awk, liberty (included), - termo (included), asciidoctor or asciidoc (recommended but optional) + +Build-only dependencies: CMake, pkg-config, awk, liberty (included), + termo (included), asciidoctor or asciidoc (recommended but optional), + rsvg-convert (X11) + Runtime dependencies: ncursesw, libunistring, cURL + -Optional runtime dependencies: fftw3, libpulse, x11, xft, Perl + cURL (lyrics) +Optional runtime dependencies: fftw3, libpulse, x11 + xft + libpng (X11), + Perl + cURL (lyrics) $ git clone --recursive https://git.janouch.name/p/nncmpp.git $ mkdir nncmpp/build diff --git a/liberty b/liberty -Subproject 0e86ffe7c30a4d52eea35856b792567ca1040f5 +Subproject d8f785eae54d2b9898cc4a6b8d3c96957161538 diff --git a/line-editor.c b/line-editor.c deleted file mode 100644 index 8e42b5a..0000000 --- a/line-editor.c +++ /dev/null @@ -1,327 +0,0 @@ -/* - * line-editor.c: a line editor component for the TUI part of liberty - * - * Copyright (c) 2017 - 2022, 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. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY - * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION - * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN - * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - * - */ - -// This is here just for IDE code model reasons -#ifndef HAVE_LIBERTY -#include "liberty/liberty.c" -#include "liberty/liberty-tui.c" -#endif - -static void -row_buffer_append_c (struct row_buffer *self, ucs4_t c, chtype attrs) -{ - struct row_char current = { .attrs = attrs, .c = c }; - struct row_char invalid = { .attrs = attrs, .c = '?', .width = 1 }; - - current.width = uc_width (current.c, locale_charset ()); - if (current.width < 0 || !app_is_character_in_locale (current.c)) - current = invalid; - - ARRAY_RESERVE (self->chars, 1); - self->chars[self->chars_len++] = current; - self->total_width += current.width; -} - -// --- Line editor ------------------------------------------------------------- - -enum line_editor_action -{ - LINE_EDITOR_B_CHAR, ///< Go back a character - LINE_EDITOR_F_CHAR, ///< Go forward a character - LINE_EDITOR_B_WORD, ///< Go back a word - LINE_EDITOR_F_WORD, ///< Go forward a word - LINE_EDITOR_HOME, ///< Go to start 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_F_DELETE, ///< Delete next character - LINE_EDITOR_B_KILL_WORD, ///< Delete last word - LINE_EDITOR_B_KILL_LINE, ///< Delete everything up to BOL - LINE_EDITOR_F_KILL_LINE, ///< Delete everything up to EOL -}; - -struct line_editor -{ - int point; ///< Caret index into line data - ucs4_t *line; ///< Line data, 0-terminated - int *w; ///< Codepoint widths, 0-terminated - size_t len; ///< Editor length - size_t alloc; ///< Editor allocated - char prompt; ///< Prompt character - - void (*on_changed) (void); ///< Callback on text change - void (*on_end) (bool); ///< Callback on abort -}; - -static void -line_editor_free (struct line_editor *self) -{ - free (self->line); - free (self->w); -} - -/// Notify whomever invoked the editor that it's been either confirmed or -/// cancelled and clean up editor state -static void -line_editor_abort (struct line_editor *self, bool status) -{ - self->on_end (status); - self->on_changed = NULL; - - free (self->line); - self->line = NULL; - free (self->w); - self->w = NULL; - self->alloc = 0; - self->len = 0; - self->point = 0; - self->prompt = 0; -} - -/// Start the line editor; remember to fill in "change" and "end" callbacks -static void -line_editor_start (struct line_editor *self, char prompt) -{ - self->alloc = 16; - self->line = xcalloc (sizeof *self->line, self->alloc); - self->w = xcalloc (sizeof *self->w, self->alloc); - self->len = 0; - self->point = 0; - self->prompt = prompt; -} - -static void -line_editor_changed (struct line_editor *self) -{ - self->line[self->len] = 0; - self->w[self->len] = 0; - - if (self->on_changed) - self->on_changed (); -} - -static void -line_editor_move (struct line_editor *self, int to, int from, int len) -{ - memmove (self->line + to, self->line + from, - sizeof *self->line * len); - memmove (self->w + to, self->w + from, - sizeof *self->w * len); -} - -static void -line_editor_insert (struct line_editor *self, ucs4_t codepoint) -{ - while (self->alloc - self->len < 2 /* inserted + sentinel */) - { - self->alloc <<= 1; - self->line = xreallocarray - (self->line, sizeof *self->line, self->alloc); - self->w = xreallocarray - (self->w, sizeof *self->w, self->alloc); - } - - line_editor_move (self, self->point + 1, self->point, - self->len - self->point); - self->line[self->point] = codepoint; - self->w[self->point] = app_is_character_in_locale (codepoint) - ? uc_width (codepoint, locale_charset ()) - : 1 /* the replacement question mark */; - - self->point++; - self->len++; - line_editor_changed (self); -} - -static bool -line_editor_action (struct line_editor *self, enum line_editor_action action) -{ - switch (action) - { - default: - return soft_assert (!"unknown line editor action"); - - case LINE_EDITOR_B_CHAR: - if (self->point < 1) - return false; - do self->point--; - while (self->point > 0 - && !self->w[self->point]); - return true; - case LINE_EDITOR_F_CHAR: - if (self->point + 1 > (int) self->len) - return false; - do self->point++; - while (self->point < (int) self->len - && !self->w[self->point]); - return true; - case LINE_EDITOR_B_WORD: - { - if (self->point < 1) - return false; - int i = self->point; - while (i && self->line[--i] == ' '); - while (i-- && self->line[i] != ' '); - self->point = ++i; - return true; - } - case LINE_EDITOR_F_WORD: - { - if (self->point + 1 > (int) self->len) - return false; - int i = self->point; - while (i < (int) self->len && self->line[i] == ' ') i++; - while (i < (int) self->len && self->line[i] != ' ') i++; - self->point = i; - return true; - } - case LINE_EDITOR_HOME: - self->point = 0; - return true; - case LINE_EDITOR_END: - self->point = self->len; - 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: - { - if (self->point < 1) - return false; - int len = 1; - while (self->point - len > 0 - && !self->w[self->point - len]) - len++; - line_editor_move (self, self->point - len, self->point, - self->len - self->point); - self->len -= len; - self->point -= len; - line_editor_changed (self); - return true; - } - case LINE_EDITOR_F_DELETE: - { - if (self->point + 1 > (int) self->len) - return false; - int len = 1; - while (self->point + len < (int) self->len - && !self->w[self->point + len]) - len++; - self->len -= len; - line_editor_move (self, self->point, self->point + len, - self->len - self->point); - line_editor_changed (self); - return true; - } - case LINE_EDITOR_B_KILL_WORD: - { - if (self->point < 1) - return false; - - int i = self->point; - while (i && self->line[--i] == ' '); - while (i-- && self->line[i] != ' '); - i++; - - line_editor_move (self, i, self->point, (self->len - self->point)); - self->len -= self->point - i; - self->point = i; - line_editor_changed (self); - return true; - } - case LINE_EDITOR_B_KILL_LINE: - self->len -= self->point; - line_editor_move (self, 0, self->point, self->len); - self->point = 0; - line_editor_changed (self); - return true; - case LINE_EDITOR_F_KILL_LINE: - self->len = self->point; - line_editor_changed (self); - return true; - } -} - -static int -line_editor_write (const struct line_editor *self, struct row_buffer *row, - int width, chtype attrs) -{ - if (self->prompt) - { - hard_assert (self->prompt < 127); - row_buffer_append_c (row, self->prompt, attrs); - width--; - } - - int following = 0; - for (size_t i = self->point; i < self->len; i++) - following += self->w[i]; - - int preceding = 0; - size_t start = self->point; - while (start && preceding < width / 2) - preceding += self->w[--start]; - - // There can be one extra space at the end of the line but this way we - // don't need to care about non-spacing marks following full-width chars - while (start && width - preceding - following > 2 /* widest char */) - preceding += self->w[--start]; - - // XXX: we should also show < > indicators for overflow but it'd probably - // considerably complicate this algorithm - for (; start < self->len; start++) - row_buffer_append_c (row, self->line[start], attrs); - return !!self->prompt + preceding; -} diff --git a/nncmpp.actions b/nncmpp.actions index 403f51f..1eed3b7 100644 --- a/nncmpp.actions +++ b/nncmpp.actions @@ -2,6 +2,7 @@ NONE, Do nothing QUIT, Quit REDRAW, Redraw screen +ABORT, Abort TAB_HELP, Switch to help tab TAB_LAST, Switch to last tab TAB_PREVIOUS, Switch to previous tab @@ -45,6 +46,8 @@ CENTER_CURSOR, Center the cursor MOVE_UP, Move selection up MOVE_DOWN, Move selection down +GOTO_PLAYING, Go to playing song + GOTO_TOP, Go to top GOTO_BOTTOM, Go to bottom GOTO_ITEM_PREVIOUS, Go to previous item diff --git a/nncmpp.actions.awk b/nncmpp.actions.awk index b4d7eaf..8cd18e7 100644 --- a/nncmpp.actions.awk +++ b/nncmpp.actions.awk @@ -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[] = {" diff --git a/nncmpp.adoc b/nncmpp.adoc index 5b1cfdb..eaebfc3 100644 --- a/nncmpp.adoc +++ b/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" @@ -117,8 +129,8 @@ as in the snippet above. To replace the default volume control bindings, use: .... normal = { - "M-PageUp" = "pulse-volume-up" - "M-PageDown" = "pulse-volume-down" + "+" = "pulse-volume-up" + "-" = "pulse-volume-down" } .... @@ -1,7 +1,7 @@ /* * nncmpp -- the MPD client you never knew you needed * - * Copyright (c) 2016 - 2022, 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. @@ -28,7 +28,7 @@ XX( REMAINS, remains, -1, -1, A_UNDERLINE ) \ /* Tab bar */ \ XX( TAB_BAR, tab_bar, -1, -1, A_REVERSE ) \ - XX( TAB_ACTIVE, tab_active, -1, -1, A_UNDERLINE ) \ + XX( TAB_ACTIVE, tab_active, -1, -1, A_BOLD ) \ /* Listview */ \ XX( HEADER, header, -1, -1, A_UNDERLINE ) \ XX( EVEN, even, -1, -1, 0 ) \ @@ -70,23 +70,15 @@ enum #define LIBERTY_WANT_PROTO_HTTP #define LIBERTY_WANT_PROTO_MPD #include "liberty/liberty.c" -#include "liberty/liberty-tui.c" -#define HAVE_LIBERTY -#include "line-editor.c" +#ifdef WITH_X11 +#define LIBERTY_XUI_WANT_X11 +#endif // WITH_X11 +#include "liberty/liberty-xui.c" #include <dirent.h> #include <locale.h> #include <math.h> -#include <sys/ioctl.h> -#include <termios.h> - -// ncurses is notoriously retarded for input handling, we need something -// different if only to receive mouse events reliably. -// -// 2021 update: ncurses is mostly reliable now, though rxvt-unicode only -// supports the 1006 mode that ncurses also supports mode starting with 9.25. -#include "termo.h" // We need cURL to extract links from Internet stream playlists. It'd be way // too much code to do this all by ourselves, and there's nothing better around. @@ -108,29 +100,12 @@ enum #include <pulse/sample.h> #endif // WITH_PULSE -// Elementary port of the TUI to X11. -#ifdef WITH_X11 -#include <X11/Xatom.h> -#include <X11/Xlib.h> -#include <X11/keysym.h> -#include <X11/XKBlib.h> -#include <X11/Xft/Xft.h> -#endif // WITH_X11 - #define APP_TITLE PROGRAM_NAME ///< Left top corner #include "nncmpp-actions.h" // --- Utilities --------------------------------------------------------------- -static int64_t -clock_msec (clockid_t clock) -{ - struct timespec tp; - hard_assert (clock_gettime (clock, &tp) != -1); - return (int64_t) tp.tv_sec * 1000 + (int64_t) tp.tv_nsec / 1000000; -} - static void shell_quote (const char *str, struct str *output) { @@ -547,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; } @@ -839,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; @@ -864,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 @@ -874,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; } @@ -1173,37 +1148,8 @@ pulse_volume_status (struct pulse *self, struct str *s) // Widget identification, mostly for mouse events. enum { - WIDGET_NONE = 0, WIDGET_BUTTON, WIDGET_GAUGE, WIDGET_TAB, WIDGET_SPECTRUM, - WIDGET_LIST, WIDGET_SCROLLBAR, WIDGET_MESSAGE, -}; - -struct widget; - -/// Draw a widget on the window -typedef void (*widget_render_fn) (struct widget *self); - -/// Extract the contents of container widgets -typedef struct widget *(*widget_sublayout_fn) (struct widget *self); - -/// A minimal abstraction appropriate for both TUI and GUI widgets. -/// Units for the widget's region are frontend-specific. -/// Having this as a linked list simplifies layouting and memory management. -struct widget -{ - LIST_HEADER (struct widget) - - int x; ///< X coordinate - int y; ///< Y coordinate - int width; ///< Width, initialized by UI methods - int height; ///< Height, initialized by UI methods - - widget_render_fn on_render; ///< Render callback - widget_sublayout_fn on_sublayout; ///< Optional sublayout callback - chtype attrs; ///< Rendition, in Curses terms - - short id; ///< Post-layouting identification - short subid; ///< Action ID/Tab index/... - char text[]; ///< Any text label + WIDGET_NONE = 0, WIDGET_BUTTON, WIDGET_GAUGE, WIDGET_VOLUME, + WIDGET_TAB, WIDGET_SPECTRUM, WIDGET_LIST, WIDGET_SCROLLBAR, WIDGET_MESSAGE, }; struct layout @@ -1212,7 +1158,7 @@ struct layout struct widget *tail; }; -struct ui +struct app_ui { struct widget *(*padding) (chtype attrs, float width, float height); struct widget *(*label) (chtype attrs, const char *label); @@ -1223,54 +1169,9 @@ struct ui struct widget *(*list) (void); struct widget *(*editor) (chtype attrs); - void (*render) (void); - void (*flip) (void); - void (*winch) (void); - void (*destroy) (void); - bool have_icons; }; -/// Replaces negative widths amongst widgets in the sublist by redistributing -/// any width remaining after all positive claims are satisfied from "width". -/// Also unifies heights to the maximum value of the run, and returns it. -/// Then the widths are taken as final, and used to initialize X coordinates. -static int -widget_redistribute (struct widget *head, int width) -{ - int parts = 0, max_height = 0; - LIST_FOR_EACH (struct widget, w, head) - { - max_height = MAX (max_height, w->height); - if (w->width < 0) - parts -= w->width; - else - width -= w->width; - } - - int remaining = MAX (width, 0), part_width = parts ? remaining / parts : 0; - struct widget *last = NULL; - LIST_FOR_EACH (struct widget, w, head) - { - w->height = max_height; - if (w->width < 0) - { - remaining -= (w->width *= -part_width); - last = w; - } - } - if (last) - last->width += remaining; - - int x = 0; - LIST_FOR_EACH (struct widget, w, head) - { - w->x = x; - x += w->width; - } - return max_height; -} - // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - struct tab; @@ -1355,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 @@ -1363,14 +1267,8 @@ static struct app_context // User interface: - struct ui *ui; ///< User interface interface - struct layout widgets; ///< Layouted widgets - int ui_width; ///< Window width - int ui_height; ///< Window height - int ui_hunit; ///< Horizontal unit - int ui_vunit; ///< Vertical unit - bool ui_focused; ///< Whether the window has focus - short ui_dragging; ///< ID of any dragged widget + struct app_ui *ui; ///< User interface interface + int ui_dragging; ///< ID of any dragged widget #ifdef WITH_FFTW struct spectrum spectrum; ///< Spectrum analyser @@ -1383,36 +1281,10 @@ static struct app_context #endif // WITH_PULSE bool pulse_control_requested; ///< PulseAudio control desired by user -#ifdef WITH_X11 - XIM x11_im; ///< Input method - XIC x11_ic; ///< Input method context - Display *dpy; ///< X display handle - struct poller_fd x11_event; ///< X11 events on wire - struct poller_idle xpending_event; ///< X11 events possibly in I/O queues - int xkb_base_event_code; ///< Xkb base event code - Window x11_window; ///< Application window - Pixmap x11_pixmap; ///< Off-screen bitmap - Region x11_clip; ///< Invalidated region - Picture x11_pixmap_picture; ///< XRender wrap for x11_pixmap - XftDraw *xft_draw; ///< Xft rendering context - XftFont *xft_regular; ///< Regular font - XftFont *xft_bold; ///< Bold font - XftFont *xft_italic; ///< Italic font - char *x11_selection; ///< CLIPBOARD selection - - XRenderColor x_fg[ATTRIBUTE_COUNT]; ///< Foreground per attribute - XRenderColor x_bg[ATTRIBUTE_COUNT]; ///< Background per attribute -#endif // WITH_X11 - struct line_editor editor; ///< Line editor - struct poller_idle refresh_event; ///< Refresh the window's contents - struct poller_idle flip_event; ///< Draw rendered widgets on screen // Terminal: - termo_t *tk; ///< termo handle (TUI/X11) - struct poller_timer tk_timer; ///< termo timeout timer - bool locale_is_utf8; ///< The locale is Unicode bool use_partial_boxes; ///< Use Unicode box drawing chars struct attrs attrs[ATTRIBUTE_COUNT]; @@ -1468,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", @@ -1532,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 }, @@ -1541,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) { @@ -1616,12 +1499,43 @@ load_config_streams (struct config_item *subtree, void *user_data) } 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) { struct config *config = &g.config; 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 ()); @@ -1661,6 +1575,13 @@ app_init_attributes (void) #undef XX } +static bool +app_on_insufficient_color (void) +{ + app_init_attributes (); + return true; +} + static void app_init_context (void) { @@ -1672,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; @@ -1684,22 +1608,6 @@ app_init_context (void) pulse_init (&g.pulse, NULL); #endif // WITH_PULSE - TERMO_CHECK_VERSION; - if (!(g.tk = termo_new (STDIN_FILENO, NULL, TERMO_FLAG_NOSTART))) - exit_fatal ("failed to initialize termo"); - - // This is also approximately what libunistring does internally, - // since the locale name is canonicalized by locale_charset(). - // Note that non-Unicode locales are handled pretty inefficiently. - g.locale_is_utf8 = !strcasecmp_ascii (locale_charset (), "UTF-8"); - - // It doesn't work 100% (e.g. incompatible with undelining in urxvt) - // TODO: make this configurable - g.use_partial_boxes = g.locale_is_utf8; - - // Presumably, although not necessarily; unsure if queryable at all - g.ui_focused = true; - app_init_attributes (); } @@ -1710,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 @@ -1732,9 +1643,6 @@ app_free_context (void) poller_free (&g.poller); free (g.message); free (g.message_detail); - - if (g.tk) - termo_destroy (g.tk); } static void @@ -1747,57 +1655,75 @@ app_quit (void) g.polling = false; } -static bool -app_is_character_in_locale (ucs4_t ch) -{ - // Avoid the overhead joined with calling iconv() for all characters. - if (g.locale_is_utf8) - return true; - - // The library really creates a new conversion object every single time - // and doesn't provide any smarter APIs. Luckily, most users use UTF-8. - size_t len; - char *tmp = u32_conv_to_encoding (locale_charset (), iconveh_error, - &ch, 1, NULL, NULL, &len); - if (!tmp) - return false; - free (tmp); - return true; -} - // --- Layouting --------------------------------------------------------------- static void -app_invalidate (void) -{ - poller_idle_set (&g.refresh_event); -} - -static void -app_flush_layout_to (struct layout *l, int width, struct layout *dest) +app_append_layout (struct layout *l, struct layout *dest) { - hard_assert (l != NULL && l->head != NULL); - widget_redistribute (l->head, width); - struct widget *last = dest->tail; if (!last) *dest = *l; - else + else if (l->head) { // Assuming there is no unclaimed vertical space. LIST_FOR_EACH (struct widget, w, l->head) - w->y = last->y + last->height; + widget_move (w, 0, last->y + last->height); last->next = l->head; l->head->prev = last; dest->tail = l->tail; } + + *l = (struct layout) {}; } +/// Replaces negative widths amongst widgets in the layout by redistributing +/// any width remaining after all positive claims are satisfied from "width". +/// Also unifies heights to the maximum value of the run. +/// Then the widths are taken as final, and used to initialize X coordinates. static void -app_flush_layout (struct layout *l) +app_flush_layout_full (struct layout *l, int width, struct layout *dest) { - app_flush_layout_to (l, g.ui_width, &g.widgets); + hard_assert (l != NULL && l->head != NULL); + + int parts = 0, max_height = 0; + LIST_FOR_EACH (struct widget, w, l->head) + { + max_height = MAX (max_height, w->height); + if (w->width < 0) + parts -= w->width; + else + width -= w->width; + } + + int remaining = MAX (width, 0), part_width = parts ? remaining / parts : 0; + struct widget *last = NULL; + LIST_FOR_EACH (struct widget, w, l->head) + { + w->height = max_height; + if (w->width < 0) + { + remaining -= (w->width *= -part_width); + last = w; + } + } + if (last) + last->width += remaining; + + int x = 0; + LIST_FOR_EACH (struct widget, w, l->head) + { + widget_move (w, x - w->x, 0); + x += w->width; + } + + app_append_layout (l, dest); +} + +static void +app_flush_layout (struct layout *l, struct layout *out) +{ + app_flush_layout_full (l, g_xui.width, out); } static struct widget * @@ -1818,19 +1744,19 @@ app_push_fill (struct layout *l, struct widget *w) /// Write the given UTF-8 string padded with spaces. /// @param[in] attrs Text attributes for the text, including padding. static void -app_layout_text (const char *str, chtype attrs) +app_layout_text (const char *str, chtype attrs, struct layout *out) { struct layout l = {}; app_push (&l, g.ui->padding (attrs, 0.25, 1)); app_push_fill (&l, g.ui->label (attrs, str)); app_push (&l, g.ui->padding (attrs, 0.25, 1)); - app_flush_layout (&l); + app_flush_layout (&l, out); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static void -app_layout_song_info (void) +app_layout_song_info (struct layout *out) { compact_map_t map; if (!(map = item_list_get (&g.playlist, g.song))) @@ -1838,41 +1764,70 @@ app_layout_song_info (void) chtype attrs[2] = { APP_ATTR (NORMAL), APP_ATTR (HIGHLIGHT) }; - char *title; + // Split the path for files lying within MPD's "music_directory". + const char *file = compact_map_find (map, "file"); + const char *subroot_basename = NULL; + if (file && *file != '/' && !strstr (file, "://")) + { + const char *last_slash = strrchr (file, '/'); + if (last_slash) + subroot_basename = last_slash + 1; + else + subroot_basename = file; + } + + const char *title = NULL; + const char *name = compact_map_find (map, "name"); if ((title = compact_map_find (map, "title")) - || (title = compact_map_find (map, "name")) - || (title = compact_map_find (map, "file"))) + || (title = name) + || (title = subroot_basename) + || (title = file)) { struct layout l = {}; app_push (&l, g.ui->padding (attrs[0], 0.25, 1)); app_push (&l, g.ui->label (attrs[1], title)); app_push_fill (&l, g.ui->padding (attrs[0], 0, 1)); app_push (&l, g.ui->padding (attrs[0], 0.25, 1)); - app_flush_layout (&l); + app_flush_layout (&l, out); } - char *artist = compact_map_find (map, "artist"); - char *album = compact_map_find (map, "album"); - if (!artist && !album) - return; - + // Showing a blank line is better than having the controls jump around + // while switching between files that we do and don't have enough data for. struct layout l = {}; app_push (&l, g.ui->padding (attrs[0], 0.25, 1)); - if (artist) + char *artist = compact_map_find (map, "artist"); + char *album = compact_map_find (map, "album"); + if (artist || album) + { + if (artist) + { + app_push (&l, g.ui->label (attrs[0], "by ")); + app_push (&l, g.ui->label (attrs[1], artist)); + } + if (album) + { + app_push (&l, g.ui->label (attrs[0], &" from "[!artist])); + app_push (&l, g.ui->label (attrs[1], album)); + } + } + else if (subroot_basename && subroot_basename != file) { - app_push (&l, g.ui->label (attrs[0], "by ")); - app_push (&l, g.ui->label (attrs[1], artist)); + char *parent = xstrndup (file, subroot_basename - file - 1); + app_push (&l, g.ui->label (attrs[0], "in ")); + app_push (&l, g.ui->label (attrs[1], parent)); + free (parent); } - if (album) + else if (file && *file != '/' && strstr (file, "://") + && name && name != title) { - app_push (&l, g.ui->label (attrs[0], &" from "[!artist])); - app_push (&l, g.ui->label (attrs[1], album)); + // This is likely to contain the name of an Internet radio. + app_push (&l, g.ui->label (attrs[1], name)); } app_push_fill (&l, g.ui->padding (attrs[0], 0, 1)); app_push (&l, g.ui->padding (attrs[0], 0.25, 1)); - app_flush_layout (&l); + app_flush_layout (&l, out); } static char * @@ -1892,11 +1847,11 @@ app_time_string (int seconds) } static void -app_layout_status (void) +app_layout_status (struct layout *out) { bool stopped = g.state == PLAYER_STOPPED; if (!stopped) - app_layout_song_info (); + app_layout_song_info (out); chtype attrs[2] = { APP_ATTR (NORMAL), APP_ATTR (HIGHLIGHT) }; struct layout l = {}; @@ -1962,16 +1917,17 @@ app_layout_status (void) if (volume.len) { app_push (&l, g.ui->padding (attrs[0], 1, 1)); - app_push (&l, g.ui->label (attrs[0], volume.str)); + app_push (&l, g.ui->label (attrs[0], volume.str)) + ->id = WIDGET_VOLUME; } str_free (&volume); app_push (&l, g.ui->padding (attrs[0], 0.25, 1)); - app_flush_layout (&l); + app_flush_layout (&l, out); } static void -app_layout_tabs (void) +app_layout_tabs (struct layout *out) { chtype attrs[2] = { APP_ATTR (TAB_BAR), APP_ATTR (TAB_ACTIVE) }; struct layout l = {}; @@ -1992,7 +1948,7 @@ app_layout_tabs (void) struct widget *w = app_push (&l, g.ui->label (attrs[iter == g.active_tab], iter->name)); w->id = WIDGET_TAB; - w->subid = ++i; + w->userdata = ++i; } app_push_fill (&l, g.ui->padding (attrs[0], 1, 1)); @@ -2006,50 +1962,32 @@ app_layout_tabs (void) } #endif // WITH_FFTW - app_flush_layout (&l); + app_flush_layout (&l, out); +} + +static void +app_layout_padding (chtype attrs, struct layout *out) +{ + struct layout l = {}; + app_push_fill (&l, g.ui->padding (attrs, 0, 0.125)); + app_flush_layout (&l, out); } static void -app_layout_header (void) +app_layout_header (struct layout *out) { if (g.client.state == MPD_CONNECTED) { - struct layout lt = {}; - app_push_fill (<, g.ui->padding (APP_ATTR (NORMAL), 0, 0.125)); - app_flush_layout (<); - - app_layout_status (); - - struct layout lb = {}; - app_push_fill (&lb, g.ui->padding (APP_ATTR (NORMAL), 0, 0.125)); - app_flush_layout (&lb); + app_layout_padding (APP_ATTR (NORMAL), out); + app_layout_status (out); + app_layout_padding (APP_ATTR (NORMAL), out); } - app_layout_tabs (); + app_layout_tabs (out); const char *header = g.active_tab->header; if (header) - app_layout_text (header, APP_ATTR (HEADER)); -} - -static int -app_visible_items_height (void) -{ - struct widget *list = NULL; - LIST_FOR_EACH (struct widget, w, g.widgets.head) - if (w->id == WIDGET_LIST) - list = w; - - hard_assert (list != NULL); - - // The raw number of items that would have fit on the terminal - return MAX (0, list->height); -} - -static int -app_visible_items (void) -{ - return app_visible_items_height () / g.ui_vunit; + app_layout_text (header, APP_ATTR (HEADER), out); } /// Figure out scrollbar appearance. @a s is the minimal slider length as well @@ -2087,12 +2025,12 @@ app_layout_row (struct tab *tab, int item_index) bool override_colors = true; if (item_index == tab->item_selected) - row_attrs = g.ui_focused + row_attrs = g_xui.focused ? APP_ATTR (SELECTION) : APP_ATTR (DEFOCUSED); else if (tab->item_mark > -1 && ((item_index >= tab->item_mark && item_index <= tab->item_selected) || (item_index >= tab->item_selected && item_index <= tab->item_mark))) - row_attrs = g.ui_focused + row_attrs = g_xui.focused ? APP_ATTR (MULTISELECT) : APP_ATTR (DEFOCUSED); else override_colors = false; @@ -2117,50 +2055,37 @@ app_layout_row (struct tab *tab, int item_index) return l; } -// XXX: This isn't a very clean design, in that part of layouting -// is done during the rendering stage. -static struct widget * -app_sublayout_list (struct widget *list) +static void +app_layout_view (struct layout *out, int height) { + struct layout l = {}; + struct widget *list = app_push_fill (&l, g.ui->list ()); + list->id = WIDGET_LIST; + list->height = height; + list->width = g_xui.width; + struct tab *tab = g.active_tab; + if ((int) tab->item_count * g_xui.vunit > list->height) + { + struct widget *scrollbar = g.ui->scrollbar (APP_ATTR (SCROLLBAR)); + list->width -= scrollbar->width; + app_push (&l, scrollbar)->id = WIDGET_SCROLLBAR; + } + int to_show = MIN ((int) tab->item_count - tab->item_top, - ceil ((double) list->height / g.ui_vunit)); + ceil ((double) list->height / g_xui.vunit)); - struct layout l = {}; + struct layout children = {}; for (int row = 0; row < to_show; row++) { int item_index = tab->item_top + row; struct layout subl = app_layout_row (tab, item_index); - app_flush_layout_to (&subl, list->width, &l); - } - LIST_FOR_EACH (struct widget, w, l.head) - { - w->x += list->x; - w->y += list->y; + // TODO: Change layouting so that we don't need to know list->width. + app_flush_layout_full (&subl, list->width, &children); } - return l.head; -} - -static void -app_layout_view (void) -{ - // XXX: Expecting the status bar to always be there, one row tall. - struct widget *last = g.widgets.tail; - int unavailable_height = last->y + last->height + g.ui_vunit; + list->children = children.head; - struct layout l = {}; - struct widget *w = app_push_fill (&l, g.ui->list ()); - w->id = WIDGET_LIST; - w->height = g.ui_height - unavailable_height; - - struct tab *tab = g.active_tab; - if ((int) tab->item_count * g.ui_vunit > w->height) - { - app_push (&l, g.ui->scrollbar (APP_ATTR (SCROLLBAR))) - ->id = WIDGET_SCROLLBAR; - } - - app_flush_layout (&l); + app_flush_layout (&l, out); } static void @@ -2206,7 +2131,7 @@ app_layout_mpd_status_playlist (struct layout *l, chtype attrs) } static void -app_layout_mpd_status (void) +app_layout_mpd_status (struct layout *out) { struct layout l = {}; chtype attrs[2] = { APP_ATTR (NORMAL), APP_ATTR (HIGHLIGHT) }; @@ -2259,14 +2184,16 @@ app_layout_mpd_status (void) } app_push (&l, g.ui->padding (attrs[0], 0.25, 1)); - app_flush_layout (&l); + app_flush_layout (&l, out); } static void -app_layout_statusbar (void) +app_layout_statusbar (struct layout *out) { - struct layout l = {}; chtype attrs[2] = { APP_ATTR (NORMAL), APP_ATTR (HIGHLIGHT) }; + app_layout_padding (attrs[0], out); + + struct layout l = {}; if (g.message) { app_push (&l, g.ui->padding (attrs[0], 0.25, 1)); @@ -2279,7 +2206,7 @@ app_layout_statusbar (void) } app_push (&l, g.ui->padding (attrs[0], 0.25, 1)); - app_flush_layout (&l); + app_flush_layout (&l, out); LIST_FOR_EACH (struct widget, w, l.head) w->id = WIDGET_MESSAGE; } @@ -2288,18 +2215,45 @@ app_layout_statusbar (void) app_push (&l, g.ui->padding (attrs[0], 0.25, 1)); app_push (&l, g.ui->editor (attrs[1])); app_push (&l, g.ui->padding (attrs[0], 0.25, 1)); - app_flush_layout (&l); + app_flush_layout (&l, out); } else if (g.client.state == MPD_CONNECTED) - app_layout_mpd_status (); + app_layout_mpd_status (out); else if (g.client.state == MPD_CONNECTING) - app_layout_text ("Connecting to MPD...", attrs[0]); + app_layout_text ("Connecting to MPD...", attrs[0], out); else if (g.client.state == MPD_DISCONNECTED) - app_layout_text ("Disconnected", attrs[0]); + app_layout_text ("Disconnected", attrs[0], out); + + app_layout_padding (attrs[0], out); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +static struct widget * +app_widget_by_id (int id) +{ + LIST_FOR_EACH (struct widget, w, g_xui.widgets) + if (w->id == id) + return w; + return NULL; +} + +static int +app_visible_items_height (void) +{ + struct widget *list = app_widget_by_id (WIDGET_LIST); + hard_assert (list != NULL); + + // The raw number of items that would have fit on the terminal + return MAX (0, list->height); +} + +static int +app_visible_items (void) +{ + return app_visible_items_height () / g_xui.vunit; +} + /// Checks what items are visible and returns if the range was alright static bool app_fix_view_range (void) @@ -2325,35 +2279,27 @@ app_fix_view_range (void) } static void -app_on_flip (void *user_data) -{ - (void) user_data; - poller_idle_reset (&g.flip_event); - - // Waste of time, and may cause X11 to render uninitialised pixmaps. - if (g.polling && !g.refresh_event.active) - g.ui->flip (); -} - -static void -app_on_refresh (void *user_data) +app_layout (void) { - (void) user_data; - poller_idle_reset (&g.refresh_event); - - LIST_FOR_EACH (struct widget, w, g.widgets.head) - free (w); + struct layout top = {}, bottom = {}; + app_layout_header (&top); + app_layout_statusbar (&bottom); - g.widgets = (struct layout) {}; + int available_height = g_xui.height; + if (top.tail) + available_height -= top.tail->y + top.tail->height; + if (bottom.tail) + available_height -= bottom.tail->y + bottom.tail->height; - app_layout_header (); - app_layout_view (); - app_layout_statusbar (); + struct layout widgets = {}; + app_append_layout (&top, &widgets); + app_layout_view (&widgets, available_height); + app_append_layout (&bottom, &widgets); + g_xui.widgets = widgets.head; app_fix_view_range(); - g.ui->render (); - poller_idle_set (&g.flip_event); + curs_set (0); } // --- Actions ----------------------------------------------------------------- @@ -2363,7 +2309,7 @@ static bool app_scroll (int n) { g.active_tab->item_top += n; - app_invalidate (); + xui_invalidate (); return app_fix_view_range (); } @@ -2407,7 +2353,7 @@ app_move_selection (int diff) bool result = !diff || tab->item_selected != fixed; tab->item_selected = fixed; - app_invalidate (); + xui_invalidate (); app_ensure_selection_visible (); return result; @@ -2419,7 +2365,7 @@ app_show_message (char *message, char *detail) cstr_set (&g.message, message); cstr_set (&g.message_detail, detail); poller_timer_set (&g.message_timer, 5000); - app_invalidate (); + xui_invalidate (); } static void @@ -2431,7 +2377,19 @@ app_hide_message (void) cstr_set (&g.message, NULL); cstr_set (&g.message_detail, NULL); poller_timer_reset (&g.message_timer); - app_invalidate (); + xui_invalidate (); +} + +static void +app_on_clipboard_copy (const char *text) +{ + app_show_message (xstrdup ("Text copied to clipboard: "), xstrdup (text)); +} + +static struct widget * +app_make_label (chtype attrs, const char *label) +{ + return g_xui.ui->label (attrs, 0, label); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -2440,7 +2398,7 @@ static void app_prepend_tab (struct tab *tab) { LIST_PREPEND (g.tabs, tab); - app_invalidate (); + xui_invalidate (); } static void @@ -2451,7 +2409,7 @@ app_switch_tab (struct tab *tab) g.last_tab = g.active_tab; g.active_tab = tab; - app_invalidate (); + xui_invalidate (); } static bool @@ -2472,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 @@ -2581,7 +2579,7 @@ incremental_search_on_changed (void) LIST_FOR_EACH (struct widget, w, tab->on_item_layout (index).head) { str_append (&s, w->text); - free (w); + widget_destroy (w); } size_t len; @@ -2610,6 +2608,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) { const char *s = str_map_find (&g.playback_info, name); @@ -2624,7 +2680,7 @@ app_process_action (enum action action) struct tab *tab = g.active_tab; if (tab->on_action && tab->on_action (action)) { - app_invalidate (); + xui_invalidate (); return true; } @@ -2633,37 +2689,35 @@ app_process_action (enum action action) case ACTION_NONE: return true; case ACTION_QUIT: + app_quit (); + return true; + case ACTION_REDRAW: + clear (); + xui_invalidate (); + return true; + + case ACTION_ABORT: // It is a pseudomode, avoid surprising the user if (tab->item_mark > -1) { tab->item_mark = -1; - app_invalidate (); + xui_invalidate (); return true; } - - app_quit (); - return true; - case ACTION_REDRAW: - clear (); - app_invalidate (); - return true; + return false; case ACTION_MPD_COMMAND: line_editor_start (&g.editor, ':'); g.editor.on_end = app_on_mpd_command_editor_end; - app_invalidate (); + 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 || !tab->item_count || tab->item_selected < 0) return false; - app_invalidate (); + xui_invalidate (); if (tab->item_mark > -1) tab->item_mark = -1; else @@ -2673,7 +2727,7 @@ app_process_action (enum action action) line_editor_start (&g.editor, '/'); g.editor.on_changed = incremental_search_on_changed; g.editor.on_end = incremental_search_on_end; - app_invalidate (); + xui_invalidate (); app_hide_message (); return true; @@ -2717,12 +2771,12 @@ app_process_action (enum action action) case ACTION_MPD_CONSUME: return app_mpd_toggle ("consume"); case ACTION_MPD_UPDATE_DB: return MPD_SIMPLE ("update"); - case ACTION_MPD_VOLUME_UP: return app_setvol (g.volume + 10); - case ACTION_MPD_VOLUME_DOWN: return app_setvol (g.volume - 10); + case ACTION_MPD_VOLUME_UP: return app_setvol (g.volume + 5); + case ACTION_MPD_VOLUME_DOWN: return app_setvol (g.volume - 5); #ifdef WITH_PULSE - case ACTION_PULSE_VOLUME_UP: return pulse_volume_set (&g.pulse, +10); - case ACTION_PULSE_VOLUME_DOWN: return pulse_volume_set (&g.pulse, -10); + case ACTION_PULSE_VOLUME_UP: return pulse_volume_set (&g.pulse, +5); + case ACTION_PULSE_VOLUME_DOWN: return pulse_volume_set (&g.pulse, -5); case ACTION_PULSE_MUTE: return pulse_volume_mute (&g.pulse); #endif // WITH_PULSE @@ -2736,7 +2790,7 @@ app_process_action (enum action action) { g.active_tab->item_selected = 0; app_ensure_selection_visible (); - app_invalidate (); + xui_invalidate (); } return true; case ACTION_GOTO_BOTTOM: @@ -2745,7 +2799,7 @@ app_process_action (enum action action) g.active_tab->item_selected = MAX (0, (int) g.active_tab->item_count - 1); app_ensure_selection_visible (); - app_invalidate (); + xui_invalidate (); } return true; @@ -2768,17 +2822,23 @@ 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 app_editor_process_action (enum action action) { - app_invalidate (); + xui_invalidate (); switch (action) { - case ACTION_QUIT: + case ACTION_ABORT: line_editor_abort (&g.editor, false); g.editor.on_end = NULL; return true; @@ -2787,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: @@ -2835,7 +2894,7 @@ app_process_left_mouse_click (struct widget *w, int x, int y, int modifiers) switch (w->id) { case WIDGET_BUTTON: - app_process_action (w->subid); + app_process_action (w->userdata); break; case WIDGET_GAUGE: { @@ -2854,7 +2913,7 @@ app_process_left_mouse_click (struct widget *w, int x, int y, int modifiers) struct tab *tab = g.help_tab; int i = 0; LIST_FOR_EACH (struct tab, iter, g.tabs) - if (++i == w->subid) + if (++i == w->userdata) tab = iter; app_switch_tab (tab); @@ -2863,7 +2922,7 @@ app_process_left_mouse_click (struct widget *w, int x, int y, int modifiers) case WIDGET_LIST: { struct tab *tab = g.active_tab; - int row_index = y / g.ui_vunit; + int row_index = y / g_xui.vunit; if (row_index < 0 || row_index >= (int) tab->item_count - tab->item_top) return false; @@ -2877,7 +2936,7 @@ app_process_left_mouse_click (struct widget *w, int x, int y, int modifiers) tab->item_selected = row_index + tab->item_top; app_ensure_selection_visible (); - app_invalidate (); + xui_invalidate (); if (modifiers & APP_KEYMOD_DOUBLE_CLICK) app_process_action (ACTION_CHOOSE); @@ -2889,7 +2948,8 @@ app_process_left_mouse_click (struct widget *w, int x, int y, int modifiers) int visible_items = app_visible_items (); tab->item_top = (double) y / w->height * (int) tab->item_count - visible_items / 2; - app_invalidate (); + xui_invalidate (); + app_fix_view_range (); break; } case WIDGET_MESSAGE: @@ -2917,11 +2977,7 @@ app_process_mouse (termo_mouse_event_t type, int x, int y, int button, && g.ui_dragging != WIDGET_SCROLLBAR) return true; - struct widget *target = NULL; - LIST_FOR_EACH (struct widget, w, g.widgets.head) - if (w->id == g.ui_dragging) - target = w; - + struct widget *target = app_widget_by_id (g.ui_dragging); x -= target->x; y -= target->y; return app_process_left_mouse_click (target, x, y, modifiers); @@ -2930,11 +2986,11 @@ app_process_mouse (termo_mouse_event_t type, int x, int y, int button, if (g.editor.line) { line_editor_abort (&g.editor, false); - app_invalidate (); + xui_invalidate (); } struct widget *target = NULL; - LIST_FOR_EACH (struct widget, w, g.widgets.head) + LIST_FOR_EACH (struct widget, w, g_xui.widgets) if (x >= w->x && x < w->x + w->width && y >= w->y && y < w->y + w->height) target = w; @@ -2949,12 +3005,34 @@ app_process_mouse (termo_mouse_event_t type, int x, int y, int button, g.ui_dragging = target->id; return app_process_left_mouse_click (target, x, y, modifiers); case 4: - if (target->id == WIDGET_LIST) + switch (target->id) + { + case WIDGET_LIST: return app_process_action (ACTION_SCROLL_UP); + case WIDGET_VOLUME: + return app_process_action ( +#ifdef WITH_PULSE + g.pulse_control_requested ? ACTION_PULSE_VOLUME_UP : +#endif // WITH_PULSE + ACTION_MPD_VOLUME_UP); + case WIDGET_GAUGE: + return app_process_action (ACTION_MPD_FORWARD); + } break; case 5: - if (target->id == WIDGET_LIST) + switch (target->id) + { + case WIDGET_LIST: return app_process_action (ACTION_SCROLL_DOWN); + case WIDGET_VOLUME: + return app_process_action ( +#ifdef WITH_PULSE + g.pulse_control_requested ? ACTION_PULSE_VOLUME_DOWN : +#endif // WITH_PULSE + ACTION_MPD_VOLUME_DOWN); + case WIDGET_GAUGE: + return app_process_action (ACTION_MPD_BACKWARD); + } break; } return false; @@ -2978,9 +3056,9 @@ static struct binding_default } g_normal_defaults[] = { - { "Escape", ACTION_QUIT }, { "q", ACTION_QUIT }, { "C-l", ACTION_REDRAW }, + { "Escape", ACTION_ABORT }, { "M-Tab", ACTION_TAB_LAST }, { "F1", ACTION_TAB_HELP }, { "S-Tab", ACTION_TAB_PREVIOUS }, @@ -2990,6 +3068,7 @@ g_normal_defaults[] = { "C-PageUp", ACTION_TAB_PREVIOUS }, { "C-PageDown", ACTION_TAB_NEXT }, + { "o", ACTION_GOTO_PLAYING }, { "Home", ACTION_GOTO_TOP }, { "End", ACTION_GOTO_BOTTOM }, { "M-<", ACTION_GOTO_TOP }, @@ -3041,11 +3120,15 @@ g_normal_defaults[] = { "Space", ACTION_MPD_TOGGLE }, { "C-Space", ACTION_MPD_STOP }, { "u", ACTION_MPD_UPDATE_DB }, - { "M-PageUp", ACTION_MPD_VOLUME_UP }, - { "M-PageDown", ACTION_MPD_VOLUME_DOWN }, + { "+", ACTION_MPD_VOLUME_UP }, + { "-", ACTION_MPD_VOLUME_DOWN }, }, g_editor_defaults[] = { + { "C-g", ACTION_ABORT }, + { "Escape", ACTION_ABORT }, + { "Enter", ACTION_EDITOR_CONFIRM }, + { "Left", ACTION_EDITOR_B_CHAR }, { "Right", ACTION_EDITOR_F_CHAR }, { "C-b", ACTION_EDITOR_B_CHAR }, @@ -3069,17 +3152,13 @@ g_editor_defaults[] = { "C-u", ACTION_EDITOR_B_KILL_LINE }, { "C-k", ACTION_EDITOR_F_KILL_LINE }, { "C-w", ACTION_EDITOR_B_KILL_WORD }, - - { "C-g", ACTION_QUIT }, - { "Escape", ACTION_QUIT }, - { "Enter", ACTION_EDITOR_CONFIRM }, }; static int app_binding_cmp (const void *a, const void *b) { const struct binding *aa = a, *bb = b; - int cmp = termo_keycmp (g.tk, &aa->decoded, &bb->decoded); + int cmp = termo_keycmp (g_xui.tk, &aa->decoded, &bb->decoded); return cmp ? cmp : bb->order - aa->order; } @@ -3090,7 +3169,7 @@ app_next_binding (struct str_map_iter *iter, termo_key_t *key, int *action) while ((v = str_map_iter_next (iter))) { *action = ACTION_NONE; - if (*termo_strpkey_utf8 (g.tk, + if (*termo_strpkey_utf8 (g_xui.tk, iter->link->key, key, TERMO_FORMAT_ALTISMETA)) print_error ("%s: invalid binding", iter->link->key); else if (v->type == CONFIG_ITEM_NULL) @@ -3119,7 +3198,7 @@ app_init_bindings (const char *keymap, termo_key_t decoded; for (size_t i = 0; i < defaults_len; i++) { - hard_assert (!*termo_strpkey_utf8 (g.tk, + hard_assert (!*termo_strpkey_utf8 (g_xui.tk, defaults[i].key, &decoded, TERMO_FORMAT_ALTISMETA)); a[a_len++] = (struct binding) { decoded, defaults[i].action, order++ }; } @@ -3141,7 +3220,8 @@ app_init_bindings (const char *keymap, for (size_t in = 0; in < a_len; in++) { a[in].order = 0; - if (!out || termo_keycmp (g.tk, &a[in].decoded, &a[out - 1].decoded)) + if (!out + || termo_keycmp (g_xui.tk, &a[in].decoded, &a[out - 1].decoded)) a[out++] = a[in]; } @@ -3153,14 +3233,15 @@ static char * app_strfkey (const termo_key_t *key) { // For display purposes, this is highly desirable - int flags = termo_get_flags (g.tk); - termo_set_flags (g.tk, flags | TERMO_FLAG_SPACESYMBOL); + int flags = termo_get_flags (g_xui.tk); + termo_set_flags (g_xui.tk, flags | TERMO_FLAG_SPACESYMBOL); termo_key_t fixed = *key; - termo_canonicalise (g.tk, &fixed); - termo_set_flags (g.tk, flags); + termo_canonicalise (g_xui.tk, &fixed); + termo_set_flags (g_xui.tk, flags); char buf[16] = ""; - termo_strfkey_utf8 (g.tk, buf, sizeof buf, &fixed, TERMO_FORMAT_ALTISMETA); + termo_strfkey_utf8 (g_xui.tk, + buf, sizeof buf, &fixed, TERMO_FORMAT_ALTISMETA); return xstrdup (buf); } @@ -3174,8 +3255,7 @@ app_process_termo_event (termo_key_t *event) bool handled = false; if ((handled = event->type == TERMO_TYPE_FOCUS)) { - g.ui_focused = !!event->code.focused; - app_invalidate (); + xui_invalidate (); // Senseless fall-through } @@ -3194,7 +3274,7 @@ app_process_termo_event (termo_key_t *event) return handled; line_editor_insert (&g.editor, event->code.codepoint); - app_invalidate (); + xui_invalidate (); return true; } if ((binding = bsearch (&dummy, g_normal_keys, g_normal_keys_len, @@ -3312,6 +3392,13 @@ current_tab_on_action (enum action action) switch (action) { const char *id; + case ACTION_GOTO_PLAYING: + if (g.song < 0 || (size_t) g.song >= tab->item_count) + return false; + + tab->item_selected = g.song; + app_ensure_selection_visible (); + return true; case ACTION_MOVE_UP: return current_tab_move_selection (-1); case ACTION_MOVE_DOWN: @@ -3356,7 +3443,7 @@ current_tab_update (void) g_current_tab.item_count = g.playlist.len; g_current_tab.item_mark = MIN ((int) g.playlist.len - 1, g_current_tab.item_mark); - app_invalidate (); + xui_invalidate (); } static struct tab * @@ -3618,7 +3705,7 @@ library_tab_load_data (const struct strv *data) if (g_library_tab.super.item_selected >= (int) len) app_move_selection (0); - app_invalidate (); + xui_invalidate (); } static void @@ -3662,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); } @@ -3781,7 +3905,7 @@ library_tab_on_action (enum action action) library_tab_load_data (&empty); strv_free (&empty); - app_invalidate (); + xui_invalidate (); return true; } case ACTION_MPD_ADD: @@ -3906,8 +4030,13 @@ streams_tab_parse_playlist (const char *playlist, const char *content_type, || (content_type && is_content_type (content_type, "audio", "x-scpls"))) extract_re = "^File[^=]*=(.+)"; else if ((lines.len && !strcasecmp_ascii (lines.vector[0], "#EXTM3U")) + || (content_type && is_content_type (content_type, "audio", "mpegurl")) || (content_type && is_content_type (content_type, "audio", "x-mpegurl"))) - extract_re = "^([^#].*)"; + // This could be "^([^#].*)", however 1. we would need to resolve + // relative URIs, and 2. relative URIs probably mean a Media Playlist, + // which must be passed to MPD. The better thing to do here would be to + // reject anything with EXT-X-TARGETDURATION, and to resolve the URIs. + extract_re = "^(https?://.+)"; regex_t *re = regex_compile (extract_re, REG_EXTENDED, NULL); hard_assert (re != NULL); @@ -3930,7 +4059,7 @@ streams_tab_extract_links (struct str *data, const char *content_type, } streams_tab_parse_playlist (data->str, content_type, out); - return true; + return out->len != 0; } static void @@ -4133,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; } @@ -4163,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; } @@ -4286,7 +4407,11 @@ info_tab_format_decode_toggle (char c) case '\x01': return A_BOLD; case '\x02': +#ifdef A_ITALIC return A_ITALIC; +#else + return A_UNDERLINE; +#endif default: return 0; } @@ -4325,7 +4450,7 @@ info_tab_on_item_layout (size_t item_index) { char *prefix = xstrdup_printf ("%s:", item->prefix); app_push (&l, g.ui->label (A_BOLD, prefix)) - ->width = 8 * g.ui_hunit; + ->width = 8 * g_xui.hunit; app_push (&l, g.ui->padding (0, 0.5, 1)); } @@ -4456,7 +4581,7 @@ info_tab_on_plugin_stdout (const struct pollfd *fd, void *user_data) case SOCKET_IO_EOF: info_tab_plugin_abort (); info_tab_update (); - app_invalidate (); + xui_invalidate (); } } @@ -4547,7 +4672,7 @@ info_tab_on_action (enum action action) case ACTION_CHOOSE: info_tab_plugin_run (item->plugin, item_list_get (&g.playlist, g.song)); info_tab_update (); - app_invalidate (); + xui_invalidate (); return true; default: return false; @@ -4598,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 */) @@ -4623,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++) @@ -4635,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; @@ -4646,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); } } @@ -4682,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"); @@ -4761,7 +4889,7 @@ debug_tab_push (char *message, chtype attrs) item->attrs = attrs; item->timestamp = clock_msec (CLOCK_REALTIME); - app_invalidate (); + xui_invalidate (); } static struct tab * @@ -4785,14 +4913,11 @@ spectrum_redraw (void) { // A full refresh would be too computationally expensive, // let's hack around it in this case - struct widget *spectrum = NULL; - LIST_FOR_EACH (struct widget, w, g.widgets.head) - if (w->id == WIDGET_SPECTRUM) - spectrum = w; + struct widget *spectrum = app_widget_by_id (WIDGET_SPECTRUM); if (spectrum) spectrum->on_render (spectrum); - poller_idle_set (&g.flip_event); + poller_idle_set (&g_xui.flip_event); } // When any problem occurs with the FIFO, we'll just give up on it completely @@ -4806,7 +4931,7 @@ spectrum_discard_fifo (void) g.spectrum_fd = -1; spectrum_free (&g.spectrum); - app_invalidate (); + xui_invalidate (); } } @@ -4878,7 +5003,7 @@ spectrum_setup_fifo (void) if (!path) print_error ("spectrum: %s", "FIFO path could not be resolved"); - else if (!g.locale_is_utf8) + else if (!g_xui.locale_is_utf8) print_error ("spectrum: %s", "UTF-8 locale required"); else if (!spectrum_init (&g.spectrum, (char *) spectrum_format, spectrum_bars->value.integer, spectrum_fps->value.integer, &e)) @@ -4951,10 +5076,10 @@ mpd_on_outputs_response (const struct mpd_response *response, else { pulse_init (&g.pulse, &g.poller); - g.pulse.on_update = app_invalidate; + g.pulse.on_update = xui_invalidate; } - app_invalidate (); + xui_invalidate (); } static void @@ -4974,7 +5099,7 @@ static void pulse_disable (void) { pulse_free (&g.pulse); - app_invalidate (); + xui_invalidate (); } #else // ! WITH_PULSE @@ -5083,7 +5208,7 @@ mpd_update_playback_state (void) if (g.playlist_version != last_playlist_version) mpd_update_playlist_time (); - app_invalidate (); + xui_invalidate (); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -5221,7 +5346,7 @@ mpd_on_elapsed_time_tick (void *user_data) // Try to get called on the next round second of playback poller_timer_set (&g.elapsed_event, 1000 - elapsed_msec); - app_invalidate (); + xui_invalidate (); } static void @@ -5431,72 +5556,17 @@ app_on_reconnect (void *user_data) mpd_queue_reconnect (); } free (address); - app_invalidate (); + xui_invalidate (); } // --- TUI --------------------------------------------------------------------- -static void -tui_flush_buffer (struct widget *self, struct row_buffer *buf) -{ - move (self->y, self->x); - - int space = MIN (self->width, g.ui_width - self->x); - row_buffer_align (buf, space, self->attrs); - row_buffer_flush (buf); - row_buffer_free (buf); -} - -static void -tui_render_padding (struct widget *self) -{ - struct row_buffer buf = row_buffer_make (); - tui_flush_buffer (self, &buf); -} - -static struct widget * -tui_make_padding (chtype attrs, float width, float height) -{ - struct widget *w = xcalloc (1, sizeof *w + 2); - w->text[0] = ' '; - w->on_render = tui_render_padding; - w->attrs = attrs; - w->width = width * 2; - w->height = height; - return w; -} - -static void -tui_render_label (struct widget *self) -{ - struct row_buffer buf = row_buffer_make (); - row_buffer_append (&buf, self->text, self->attrs); - tui_flush_buffer (self, &buf); -} - -static struct widget * -tui_make_label (chtype attrs, const char *label) -{ - size_t len = strlen (label); - struct widget *w = xcalloc (1, sizeof *w + len + 1); - w->on_render = tui_render_label; - w->attrs = attrs; - memcpy (w + 1, label, len); - - struct row_buffer buf = row_buffer_make (); - row_buffer_append (&buf, w->text, w->attrs); - w->width = buf.total_width; - w->height = 1; - row_buffer_free (&buf); - return w; -} - static struct widget * tui_make_button (chtype attrs, const char *label, enum action a) { - struct widget *w = tui_make_label (attrs, label); + struct widget *w = tui_make_label (attrs, 0, label); w->id = WIDGET_BUTTON; - w->subid = a; + w->userdata = a; return w; } @@ -5636,24 +5706,12 @@ tui_make_scrollbar (chtype attrs) return w; } -static void -tui_render_list (struct widget *self) -{ - LIST_FOR_EACH (struct widget, w, self->on_sublayout (self)) - { - w->on_render (w); - free (w); - } -} - static struct widget * tui_make_list (void) { struct widget *w = xcalloc (1, sizeof *w + 1); w->width = -1; w->height = g.active_tab->item_count; - w->on_render = tui_render_list; - w->on_sublayout = app_sublayout_list; return w; } @@ -5661,10 +5719,37 @@ static void tui_render_editor (struct widget *self) { struct row_buffer buf = row_buffer_make (); - int caret = line_editor_write (&g.editor, &buf, self->width, self->attrs); + const struct line_editor *e = &g.editor; + int width = self->width; + if (e->prompt) + { + hard_assert (e->prompt < 127); + row_buffer_append_c (&buf, e->prompt, self->attrs); + width--; + } + + int following = 0; + for (size_t i = e->point; i < e->len; i++) + following += e->w[i]; + + int preceding = 0; + size_t start = e->point; + while (start && preceding < width / 2) + preceding += e->w[--start]; + + // There can be one extra space at the end of the line but this way we + // don't need to care about non-spacing marks following full-width chars + while (start && width - preceding - following > 2 /* widest char */) + preceding += e->w[--start]; + + // XXX: we should also show < > indicators for overflow but it'd probably + // considerably complicate this algorithm + for (; start < e->len; start++) + row_buffer_append_c (&buf, e->line[start], self->attrs); tui_flush_buffer (self, &buf); // FIXME: This should be at the end of of tui_render(). + int caret = !!e->prompt + preceding; move (self->y, self->x + caret); curs_set (1); } @@ -5681,322 +5766,22 @@ tui_make_editor (chtype attrs) return w; } -static void -tui_render (void) -{ - erase (); - curs_set (0); - - LIST_FOR_EACH (struct widget, w, g.widgets.head) - if (w->width >= 0 && w->height >= 0) - w->on_render (w); -} - -static void -tui_flip (void) -{ - // Curses handles double-buffering for us automatically. - refresh (); -} - -static void -tui_winch (void) -{ - // The standard endwin/refresh sequence makes the terminal flicker -#if defined HAVE_RESIZETERM && defined TIOCGWINSZ - struct winsize size; - if (!ioctl (STDOUT_FILENO, TIOCGWINSZ, (char *) &size)) - { - char *row = getenv ("LINES"); - char *col = getenv ("COLUMNS"); - unsigned long tmp; - resizeterm ( - (row && xstrtoul (&tmp, row, 10)) ? tmp : size.ws_row, - (col && xstrtoul (&tmp, col, 10)) ? tmp : size.ws_col); - } -#else // HAVE_RESIZETERM && TIOCGWINSZ - endwin (); - refresh (); -#endif // HAVE_RESIZETERM && TIOCGWINSZ - - g.ui_width = COLS; - g.ui_height = LINES; - app_invalidate (); -} - -static void -tui_destroy (void) -{ - endwin (); -} - -static struct ui tui_ui = +static struct app_ui app_tui_ui = { .padding = tui_make_padding, - .label = tui_make_label, + .label = app_make_label, .button = tui_make_button, .gauge = tui_make_gauge, .spectrum = tui_make_spectrum, .scrollbar = tui_make_scrollbar, .list = tui_make_list, .editor = tui_make_editor, - - .render = tui_render, - .flip = tui_flip, - .winch = tui_winch, - .destroy = tui_destroy, }; -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -static void -tui_on_tty_event (termo_key_t *event, int64_t event_ts) -{ - // Simple double click detection via release--press delay, only a bit - // complicated by the fact that we don't know what's being released - static termo_key_t last_event; - static int64_t last_event_ts; - static int last_button; - - int y, x, button, y_last, x_last, modifiers = 0; - termo_mouse_event_t type, type_last; - if (termo_interpret_mouse (g.tk, event, &type, &button, &y, &x)) - { - if (termo_interpret_mouse - (g.tk, &last_event, &type_last, NULL, &y_last, &x_last) - && event_ts - last_event_ts < 500 - && type_last == TERMO_MOUSE_RELEASE && type == TERMO_MOUSE_PRESS - && y_last == y && x_last == x && last_button == button) - { - modifiers |= APP_KEYMOD_DOUBLE_CLICK; - // Prevent interpreting triple clicks as two double clicks. - last_button = 0; - } - else if (type == TERMO_MOUSE_PRESS) - last_button = button; - - if (!app_process_mouse (type, x, y, button, modifiers)) - beep (); - } - else if (!app_process_termo_event (event)) - beep (); - - last_event = *event; - last_event_ts = event_ts; -} - -static void -tui_on_tty_readable (const struct pollfd *fd, void *user_data) -{ - (void) user_data; - if (fd->revents & ~(POLLIN | POLLHUP | POLLERR)) - print_debug ("fd %d: unexpected revents: %d", fd->fd, fd->revents); - - poller_timer_reset (&g.tk_timer); - termo_advisereadable (g.tk); - - termo_key_t event = {}; - int64_t event_ts = clock_msec (CLOCK_BEST); - termo_result_t res; - while ((res = termo_getkey (g.tk, &event)) == TERMO_RES_KEY) - tui_on_tty_event (&event, event_ts); - - if (res == TERMO_RES_AGAIN) - poller_timer_set (&g.tk_timer, termo_get_waittime (g.tk)); - else if (res == TERMO_RES_ERROR || res == TERMO_RES_EOF) - app_quit (); -} - -static void -tui_on_key_timer (void *user_data) -{ - (void) user_data; - - termo_key_t event; - if (termo_getkey_force (g.tk, &event) == TERMO_RES_KEY) - if (!app_process_termo_event (&event)) - beep (); -} - -static void -tui_init (void) -{ - poller_fd_set (&g.tty_event, POLLIN); - if (!termo_start (g.tk) || !initscr () || nonl () == ERR) - exit_fatal ("failed to set up the terminal"); - - termo_set_mouse_tracking_mode (g.tk, TERMO_MOUSE_TRACKING_DRAG); - - g.ui = &tui_ui; - g.ui_width = COLS; - g.ui_height = LINES; - g.ui_vunit = 1; - g.ui_hunit = 1; - - // By default we don't use any colors so they're not required... - if (start_color () == ERR - || use_default_colors () == ERR - || COLOR_PAIRS <= ATTRIBUTE_COUNT) - return; - - for (int a = 0; a < ATTRIBUTE_COUNT; a++) - { - // ...thus we can reset back to defaults even after initializing some - // FIXME: that's a lie now, MULTISELECT requires a colour - if (g.attrs[a].fg >= COLORS || g.attrs[a].fg < -1 - || g.attrs[a].bg >= COLORS || g.attrs[a].bg < -1) - { - app_init_attributes (); - return; - } - - init_pair (a + 1, g.attrs[a].fg, g.attrs[a].bg); - g.attrs[a].attrs |= COLOR_PAIR (a + 1); - } -} - // --- X11 --------------------------------------------------------------------- #ifdef WITH_X11 -static XRenderColor x11_default_fg = { .alpha = 0xffff }; -static XRenderColor x11_default_bg = { 0xffff, 0xffff, 0xffff, 0xffff }; -static XErrorHandler x11_default_error_handler; - -static XftFont * -x11_font (struct widget *self) -{ - if (self->attrs & A_BOLD) - return g.xft_bold; - if (self->attrs & A_ITALIC) - return g.xft_italic; - return g.xft_regular; -} - -static XRenderColor * -x11_fg_attrs (chtype attrs) -{ - int pair = PAIR_NUMBER (attrs); - if (!pair--) - return &x11_default_fg; - return (attrs & A_REVERSE) ? &g.x_bg[pair] : &g.x_fg[pair]; -} - -static XRenderColor * -x11_fg (struct widget *self) -{ - return x11_fg_attrs (self->attrs); -} - -static XRenderColor * -x11_bg_attrs (chtype attrs) -{ - int pair = PAIR_NUMBER (attrs); - if (!pair--) - return &x11_default_bg; - return (attrs & A_REVERSE) ? &g.x_fg[pair] : &g.x_bg[pair]; -} - -static XRenderColor * -x11_bg (struct widget *self) -{ - return x11_bg_attrs (self->attrs); -} - -static void -x11_render_padding (struct widget *self) -{ - if (PAIR_NUMBER (self->attrs)) - { - XRenderFillRectangle (g.dpy, PictOpSrc, g.x11_pixmap_picture, - x11_bg (self), self->x, self->y, self->width, self->height); - } - if (self->attrs & A_UNDERLINE) - { - XRenderFillRectangle (g.dpy, PictOpSrc, g.x11_pixmap_picture, - x11_fg (self), self->x, self->y + self->height - 1, self->width, 1); - } -} - -static struct widget * -x11_make_padding (chtype attrs, float width, float height) -{ - struct widget *w = xcalloc (1, sizeof *w + 2); - w->text[0] = ' '; - w->on_render = x11_render_padding; - w->attrs = attrs; - w->width = g.ui_vunit * width; - w->height = g.ui_vunit * height; - return w; -} - -static void -x11_render_label (struct widget *self) -{ - x11_render_padding (self); - - int space = MIN (self->width, g.ui_width - self->x); - if (space <= 0) - return; - - // TODO: Try to avoid re-measuring on each render. - XftFont *font = x11_font (self); - XGlyphInfo extents = {}; - XftTextExtentsUtf8 (g.dpy, font, - (const FcChar8 *) self->text, strlen (self->text), &extents); - if (extents.xOff <= space) - { - XftColor color = { .color = *x11_fg (self) }; - XftDrawStringUtf8 (g.xft_draw, &color, font, - self->x, self->y + font->ascent, - (const FcChar8 *) self->text, strlen (self->text)); - return; - } - - // XRender doesn't extend gradients beyond their end stops. - XRenderColor solid = *x11_fg (self), colors[3] = { solid, solid, solid }; - colors[2].alpha = 0; - - double portion = MIN (1, 2.0 * font->height / space); - XFixed stops[3] = { 0, XDoubleToFixed (1 - portion), XDoubleToFixed (1) }; - XLinearGradient gradient = { {}, { XDoubleToFixed (space), 0 } }; - - // Note that this masking is a very expensive operation. - Picture source = - XRenderCreateLinearGradient (g.dpy, &gradient, stops, colors, 3); - XftTextRenderUtf8 (g.dpy, PictOpOver, source, font, g.x11_pixmap_picture, - -self->x, 0, self->x, self->y + font->ascent, - (const FcChar8 *) self->text, strlen (self->text)); - XRenderFreePicture (g.dpy, source); -} - -static struct widget * -x11_make_label (chtype attrs, const char *label) -{ - // Xft renders combining marks by themselves, NFC improves it a bit. - size_t label_len = strlen (label) + 1, normalized_len = 0; - uint8_t *normalized = u8_normalize (UNINORM_NFC, - (const uint8_t *) label, label_len, NULL, &normalized_len); - if (!normalized) - { - normalized = memcpy (xmalloc (label_len), label, label_len); - normalized_len = label_len; - } - - struct widget *w = xcalloc (1, sizeof *w + normalized_len); - w->on_render = x11_render_label; - w->attrs = attrs; - memcpy (w + 1, normalized, normalized_len); - - XftFont *font = x11_font (w); - XGlyphInfo extents = {}; - XftTextExtentsUtf8 (g.dpy, font, normalized, normalized_len - 1, &extents); - w->width = extents.xOff; - w->height = font->height; - free (normalized); - return w; -} - // On a 20x20 raster to make it feasible to design on paper. #define X11_STOP {INFINITY, INFINITY} static const XPointDouble @@ -6081,7 +5866,7 @@ x11_render_button (struct widget *self) { x11_render_padding (self); - const XPointDouble *icon = x11_icon_for_action (self->subid); + const XPointDouble *icon = x11_icon_for_action (self->userdata); if (!icon) { x11_render_label (self); @@ -6102,9 +5887,9 @@ x11_render_button (struct widget *self) color.blue /= 2; } - Picture source = XRenderCreateSolidFill (g.dpy, &color); + Picture source = XRenderCreateSolidFill (g_xui.dpy, &color); const XRenderPictFormat *format - = XRenderFindStandardFormat (g.dpy, PictStandardA8); + = XRenderFindStandardFormat (g_xui.dpy, PictStandardA8); int x = self->x, y = self->y + (self->height - self->width) / 2; XPointDouble buffer[total], *p = buffer; @@ -6117,27 +5902,27 @@ x11_render_button (struct widget *self) } else if (p != buffer) { - XRenderCompositeDoublePoly (g.dpy, PictOpOver, - source, g.x11_pixmap_picture, format, + XRenderCompositeDoublePoly (g_xui.dpy, PictOpOver, + source, g_xui.x11_pixmap_picture, format, 0, 0, 0, 0, buffer, p - buffer, EvenOddRule); p = buffer; } - XRenderFreePicture (g.dpy, source); + XRenderFreePicture (g_xui.dpy, source); } static struct widget * x11_make_button (chtype attrs, const char *label, enum action a) { - struct widget *w = x11_make_label (attrs, label); + struct widget *w = x11_make_label (attrs, 0, label); w->id = WIDGET_BUTTON; - w->subid = a; + w->userdata = a; if (x11_icon_for_action (a)) { w->on_render = x11_render_button; // It should be padded by the caller horizontally. - w->height = g.ui_vunit; + w->height = g_xui.vunit; w->width = w->height * 3 / 4; } return w; @@ -6151,13 +5936,13 @@ x11_render_gauge (struct widget *self) return; int part = (float) g.song_elapsed / g.song_duration * self->width; - XRenderFillRectangle (g.dpy, PictOpSrc, g.x11_pixmap_picture, + XRenderFillRectangle (g_xui.dpy, PictOpSrc, g_xui.x11_pixmap_picture, x11_bg_attrs (APP_ATTR (ELAPSED)), self->x, self->y + self->height / 8, part, self->height * 3 / 4); - XRenderFillRectangle (g.dpy, PictOpSrc, g.x11_pixmap_picture, + XRenderFillRectangle (g_xui.dpy, PictOpSrc, g_xui.x11_pixmap_picture, x11_bg_attrs (APP_ATTR (REMAINS)), self->x + part, self->y + self->height / 8, @@ -6173,7 +5958,7 @@ x11_make_gauge (chtype attrs) w->on_render = x11_render_gauge; w->attrs = attrs; w->width = -1; - w->height = g.ui_vunit; + w->height = g_xui.vunit; return w; } @@ -6197,13 +5982,13 @@ x11_render_spectrum (struct widget *self) }; } - XRenderFillRectangles (g.dpy, PictOpSrc, g.x11_pixmap_picture, + XRenderFillRectangles (g_xui.dpy, PictOpSrc, g_xui.x11_pixmap_picture, x11_fg (self), rectangles, N_ELEMENTS (rectangles)); #endif // WITH_FFTW // Enable the spectrum_redraw() hack. XRectangle r = { self->x, self->y, self->width, self->height }; - XUnionRectWithRegion (&r, g.x11_clip, g.x11_clip); + XUnionRectWithRegion (&r, g_xui.x11_clip, g_xui.x11_clip); } static struct widget * @@ -6212,8 +5997,8 @@ x11_make_spectrum (chtype attrs, int width) struct widget *w = xcalloc (1, sizeof *w + 1); w->on_render = x11_render_spectrum; w->attrs = attrs; - w->width = width * g.ui_vunit / 2; - w->height = g.ui_vunit; + w->width = width * g_xui.vunit / 2; + w->height = g_xui.vunit; return w; } @@ -6224,9 +6009,9 @@ x11_render_scrollbar (struct widget *self) struct tab *tab = g.active_tab; struct scrollbar bar = - app_compute_scrollbar (tab, app_visible_items_height (), g.ui_vunit); + app_compute_scrollbar (tab, app_visible_items_height (), g_xui.vunit); - XRenderFillRectangle (g.dpy, PictOpSrc, g.x11_pixmap_picture, + XRenderFillRectangle (g_xui.dpy, PictOpSrc, g_xui.x11_pixmap_picture, x11_fg_attrs (self->attrs), self->x, self->y + bar.start, @@ -6240,34 +6025,15 @@ x11_make_scrollbar (chtype attrs) struct widget *w = xcalloc (1, sizeof *w + 1); w->on_render = x11_render_scrollbar; w->attrs = attrs; - w->width = g.ui_vunit / 2; + w->width = g_xui.vunit / 2; return w; } -static void -x11_render_list (struct widget *self) -{ - // We could do that for all widgets, but it would be kind-of pointless. - // 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); - - x11_render_padding (self); - LIST_FOR_EACH (struct widget, w, self->on_sublayout (self)) - { - w->on_render (w); - free (w); - } - - XftDrawSetClip (g.xft_draw, None); -} - static struct widget * x11_make_list (void) { struct widget *w = xcalloc (1, sizeof *w + 1); - w->on_render = x11_render_list; - w->on_sublayout = app_sublayout_list; + w->on_render = x11_render_padding; return w; } @@ -6276,27 +6042,36 @@ x11_render_editor (struct widget *self) { x11_render_padding (self); - XftFont *font = x11_font (self); + 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.dpy, font, g.editor.prompt); - XftDrawGlyphs (g.xft_draw, &color, font, x, y, &i, 1); - XftGlyphExtents (g.dpy, font, &i, 1, &extents); - x += extents.xOff + g.ui_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: Make this scroll around the caret, and fade like labels. - XftDrawString32 (g.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.dpy, font, g.editor.line, g.editor.point, &extents); - XRenderFillRectangle (g.dpy, PictOpSrc, g.x11_pixmap_picture, - &color.color, x + extents.xOff, self->y, 2, self->height); + XRenderFillRectangle (g_xui.dpy, PictOpSrc, g_xui.x11_pixmap_picture, + &color.color, caret, self->y, 2, self->height); } static struct widget * @@ -6307,64 +6082,14 @@ x11_make_editor (chtype attrs) w->on_render = x11_render_editor; w->attrs = attrs; w->width = -1; - w->height = g.ui_vunit; + w->height = g_xui.vunit; return w; } -static void -x11_render (void) -{ - XRenderFillRectangle (g.dpy, PictOpSrc, g.x11_pixmap_picture, - &x11_default_bg, 0, 0, g.ui_width, g.ui_height); - LIST_FOR_EACH (struct widget, w, g.widgets.head) - if (w->width && w->height) - w->on_render (w); - - XRectangle r = { 0, 0, g.ui_width, g.ui_height }; - XUnionRectWithRegion (&r, g.x11_clip, g.x11_clip); - poller_idle_set (&g.xpending_event); -} - -static void -x11_flip (void) -{ - // This exercise in futility doesn't seem to affect CPU usage much. - XRectangle r = {}; - XClipBox (g.x11_clip, &r); - XCopyArea (g.dpy, g.x11_pixmap, g.x11_window, - DefaultGC (g.dpy, DefaultScreen (g.dpy)), - r.x, r.y, r.width, r.height, r.x, r.y); - - XSubtractRegion (g.x11_clip, g.x11_clip, g.x11_clip); - poller_idle_set (&g.xpending_event); -} - -static void -x11_destroy (void) -{ - XDestroyIC (g.x11_ic); - XCloseIM (g.x11_im); - XDestroyRegion (g.x11_clip); - XDestroyWindow (g.dpy, g.x11_window); - XRenderFreePicture (g.dpy, g.x11_pixmap_picture); - XFreePixmap (g.dpy, g.x11_pixmap); - XftDrawDestroy (g.xft_draw); - XftFontClose (g.dpy, g.xft_regular); - XftFontClose (g.dpy, g.xft_bold); - XftFontClose (g.dpy, g.xft_italic); - cstr_set (&g.x11_selection, NULL); - - poller_fd_reset (&g.x11_event); - XCloseDisplay (g.dpy); - - // Xft hooks called in XCloseDisplay() don't clean up everything. - FcFini (); -} - -static struct ui x11_ui = +static struct app_ui app_x11_ui = { .padding = x11_make_padding, - .label = x11_make_label, + .label = app_make_label, .button = x11_make_button, .gauge = x11_make_gauge, .spectrum = x11_make_spectrum, @@ -6372,623 +6097,9 @@ static struct ui x11_ui = .list = x11_make_list, .editor = x11_make_editor, - .render = x11_render, - .flip = x11_flip, - .destroy = x11_destroy, .have_icons = true, }; -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -static termo_sym_t -x11_convert_keysym (KeySym keysym) -{ - // Leaving out TERMO_TYPE_FUNCTION, TERMO_SYM_DEL (N/A), - // and TERMO_SYM_SPACE (governed by TERMO_FLAG_SPACESYMBOL, not in use). - switch (keysym) - { - case XK_BackSpace: return TERMO_SYM_BACKSPACE; - case XK_Tab: return TERMO_SYM_TAB; - case XK_ISO_Left_Tab: return TERMO_SYM_TAB; - case XK_Return: return TERMO_SYM_ENTER; - case XK_Escape: return TERMO_SYM_ESCAPE; - - case XK_Up: return TERMO_SYM_UP; - case XK_Down: return TERMO_SYM_DOWN; - case XK_Left: return TERMO_SYM_LEFT; - case XK_Right: return TERMO_SYM_RIGHT; - case XK_Begin: return TERMO_SYM_BEGIN; - case XK_Find: return TERMO_SYM_FIND; - case XK_Insert: return TERMO_SYM_INSERT; - case XK_Delete: return TERMO_SYM_DELETE; - case XK_Select: return TERMO_SYM_SELECT; - case XK_Page_Up: return TERMO_SYM_PAGEUP; - case XK_Page_Down: return TERMO_SYM_PAGEDOWN; - case XK_Home: return TERMO_SYM_HOME; - case XK_End: return TERMO_SYM_END; - - case XK_Cancel: return TERMO_SYM_CANCEL; - case XK_Clear: return TERMO_SYM_CLEAR; - // TERMO_SYM_CLOSE - // TERMO_SYM_COMMAND - // TERMO_SYM_COPY - // TERMO_SYM_EXIT - case XK_Help: return TERMO_SYM_HELP; - // TERMO_SYM_MARK - // TERMO_SYM_MESSAGE - // TERMO_SYM_MOVE - // TERMO_SYM_OPEN - // TERMO_SYM_OPTIONS - case XK_Print: return TERMO_SYM_PRINT; - case XK_Redo: return TERMO_SYM_REDO; - // TERMO_SYM_REFERENCE - // TERMO_SYM_REFRESH - // TERMO_SYM_REPLACE - // TERMO_SYM_RESTART - // TERMO_SYM_RESUME - // TERMO_SYM_SAVE - // TERMO_SYM_SUSPEND - case XK_Undo: return TERMO_SYM_UNDO; - - case XK_KP_0: return TERMO_SYM_KP0; - case XK_KP_1: return TERMO_SYM_KP1; - case XK_KP_2: return TERMO_SYM_KP2; - case XK_KP_3: return TERMO_SYM_KP3; - case XK_KP_4: return TERMO_SYM_KP4; - case XK_KP_5: return TERMO_SYM_KP5; - case XK_KP_6: return TERMO_SYM_KP6; - case XK_KP_7: return TERMO_SYM_KP7; - case XK_KP_8: return TERMO_SYM_KP8; - case XK_KP_9: return TERMO_SYM_KP9; - case XK_KP_Enter: return TERMO_SYM_KPENTER; - case XK_KP_Add: return TERMO_SYM_KPPLUS; - case XK_KP_Subtract: return TERMO_SYM_KPMINUS; - case XK_KP_Multiply: return TERMO_SYM_KPMULT; - case XK_KP_Divide: return TERMO_SYM_KPDIV; - case XK_KP_Separator: return TERMO_SYM_KPCOMMA; - case XK_KP_Decimal: return TERMO_SYM_KPPERIOD; - case XK_KP_Equal: return TERMO_SYM_KPEQUALS; - } - return TERMO_SYM_UNKNOWN; -} - -static bool -on_x11_keypress (XEvent *e) -{ - // A kibibyte long buffer will have to suffice for anyone. - XKeyEvent *ev = &e->xkey; - char buf[1 << 10] = {}, *p = buf; - KeySym keysym = None; - Status status = 0; - int len = Xutf8LookupString - (g.x11_ic, ev, buf, sizeof buf, &keysym, &status); - if (status == XBufferOverflow) - print_warning ("input method overflow"); - - termo_key_t key = {}; - if (ev->state & ShiftMask) - key.modifiers |= TERMO_KEYMOD_SHIFT; - if (ev->state & ControlMask) - key.modifiers |= TERMO_KEYMOD_CTRL; - if (ev->state & Mod1Mask) - key.modifiers |= TERMO_KEYMOD_ALT; - - if (keysym >= XK_F1 && keysym <= XK_F35) - { - key.type = TERMO_TYPE_FUNCTION; - key.code.number = 1 + keysym - XK_F1; - return app_process_termo_event (&key); - } - if ((key.code.sym = x11_convert_keysym (keysym)) != TERMO_SYM_UNKNOWN) - { - key.type = TERMO_TYPE_KEYSYM; - return app_process_termo_event (&key); - } - - bool result = true; - if (len) - { - key.type = TERMO_TYPE_KEY; - key.modifiers &= ~TERMO_KEYMOD_SHIFT; - - int32_t cp = 0; - struct utf8_iter iter = { .s = buf, .len = len }; - size_t cp_len = 0; - while ((cp = utf8_iter_next (&iter, &cp_len)) >= 0) - { - termo_key_t k = key; - memcpy (k.multibyte, p, MIN (cp_len, sizeof k.multibyte - 1)); - p += cp_len; - - // This is all unfortunate, but probably in the right place. - if (!cp) - { - k.code.codepoint = ' '; - if (ev->state & ShiftMask) - k.modifiers |= TERMO_KEYMOD_SHIFT; - } - else if (cp >= 32) - k.code.codepoint = cp; - else if (ev->state & ShiftMask) - k.code.codepoint = cp + 64; - else - k.code.codepoint = cp + 96; - if (!app_process_termo_event (&k)) - result = false; - } - } - return result; -} - -static void -x11_init_pixmap (void) -{ - int screen = DefaultScreen (g.dpy); - g.x11_pixmap = XCreatePixmap (g.dpy, g.x11_window, - g.ui_width, g.ui_height, DefaultDepth (g.dpy, screen)); - - Visual *visual = DefaultVisual (g.dpy, screen); - XRenderPictFormat *format = XRenderFindVisualFormat (g.dpy, visual); - g.x11_pixmap_picture - = XRenderCreatePicture (g.dpy, g.x11_pixmap, format, 0, NULL); -} - -static char * -x11_find_text (struct widget *list, int x, int y) -{ - struct widget *target = NULL; - LIST_FOR_EACH (struct widget, w, list) - if (x >= w->x && x < w->x + w->width - && y >= w->y && y < w->y + w->height) - target = w; - if (!target) - return NULL; - - if (target->on_sublayout) - { - struct widget *sublist = target->on_sublayout (target); - char *result = x11_find_text (sublist, x, y); - LIST_FOR_EACH (struct widget, w, sublist) - free (w); - if (result) - return result; - } - return xstrdup (target->text); -} - -// TODO: OSC 52 exists for terminals, so make it possible to enable that there. -static bool -x11_process_press (int x, int y, int button, int modifiers) -{ - if (button != Button3) - goto out; - - char *text = x11_find_text (g.widgets.head, x, y); - if (!text || !*(cstr_strip_in_place (text, " \t"))) - { - free (text); - goto out; - } - - cstr_set (&g.x11_selection, text); - XSetSelectionOwner (g.dpy, XInternAtom (g.dpy, "CLIPBOARD", False), - g.x11_window, CurrentTime); - app_show_message (xstrdup ("Text copied to clipboard: "), - xstrdup (g.x11_selection)); - return true; - -out: - return app_process_mouse (TERMO_MOUSE_PRESS, x, y, button, modifiers); -} - -static int -x11_state_to_modifiers (unsigned int state) -{ - int modifiers = 0; - if (state & ShiftMask) modifiers |= TERMO_KEYMOD_SHIFT; - if (state & ControlMask) modifiers |= TERMO_KEYMOD_CTRL; - if (state & Mod1Mask) modifiers |= TERMO_KEYMOD_ALT; - return modifiers; -} - -static bool -on_x11_input_event (XEvent *ev) -{ - static XEvent last_press_event; - if (ev->type == KeyPress) - { - last_press_event = (XEvent) {}; - return on_x11_keypress (ev); - } - if (ev->type == MotionNotify) - { - return app_process_mouse (TERMO_MOUSE_DRAG, - ev->xmotion.x, ev->xmotion.y, 1 /* Button1MotionMask */, - x11_state_to_modifiers (ev->xmotion.state)); - } - - // This is nearly the same as tui_on_tty_event(). - int x = ev->xbutton.x, y = ev->xbutton.y; - unsigned int button = ev->xbutton.button; - int modifiers = x11_state_to_modifiers (ev->xbutton.state); - if (ev->type == ButtonPress - && ev->xbutton.time - last_press_event.xbutton.time < 500 - && abs (last_press_event.xbutton.x - x) < 5 - && abs (last_press_event.xbutton.y - y) < 5 - && last_press_event.xbutton.button == button) - { - modifiers |= APP_KEYMOD_DOUBLE_CLICK; - // Prevent interpreting triple clicks as two double clicks. - last_press_event = (XEvent) {}; - } - else if (ev->type == ButtonPress) - last_press_event = *ev; - - if (ev->type == ButtonPress) - return x11_process_press (x, y, button, modifiers); - if (ev->type == ButtonRelease) - return app_process_mouse - (TERMO_MOUSE_RELEASE, x, y, button, modifiers); - return false; -} - -static void -on_x11_selection_request (XSelectionRequestEvent *ev) -{ - Atom xa_targets = XInternAtom (g.dpy, "TARGETS", False); - Atom xa_compound_text = XInternAtom (g.dpy, "COMPOUND_TEXT", False); - Atom xa_utf8 = XInternAtom (g.dpy, "UTF8_STRING", False); - Atom targets[] = { xa_targets, XA_STRING, xa_compound_text, xa_utf8 }; - - XEvent response = {}; - bool ok = false; - Atom property = ev->property ? ev->property : ev->target; - if (!g.x11_selection) - goto out; - - XICCEncodingStyle style = 0; - if ((ok = ev->target == xa_targets)) - { - XChangeProperty (g.dpy, ev->requestor, property, - XA_ATOM, 32, PropModeReplace, - (const unsigned char *) targets, N_ELEMENTS (targets)); - goto out; - } - else if (ev->target == XA_STRING) - style = XStringStyle; - else if (ev->target == xa_compound_text) - style = XCompoundTextStyle; - else if (ev->target == xa_utf8) - style = XUTF8StringStyle; - else - goto out; - - // XXX: We let it crash us with BadLength, but we may, e.g., use INCR. - XTextProperty text = {}; - if ((ok = !Xutf8TextListToTextProperty - (g.dpy, &g.x11_selection, 1, style, &text))) - { - XChangeProperty (g.dpy, ev->requestor, property, - text.encoding, text.format, PropModeReplace, - text.value, text.nitems); - } - XFree (text.value); - -out: - response.xselection.type = SelectionNotify; - // XXX: We should check it against the event causing XSetSelectionOwner(). - response.xselection.time = ev->time; - response.xselection.requestor = ev->requestor; - response.xselection.selection = ev->selection; - response.xselection.target = ev->target; - response.xselection.property = ok ? property : None; - XSendEvent (g.dpy, ev->requestor, False, 0, &response); -} - -static void -on_x11_event (XEvent *ev) -{ - termo_key_t key = {}; - switch (ev->type) - { - case Expose: - { - XRectangle r = { ev->xexpose.x, ev->xexpose.y, - ev->xexpose.width, ev->xexpose.height }; - XUnionRectWithRegion (&r, g.x11_clip, g.x11_clip); - poller_idle_set (&g.flip_event); - break; - } - case ConfigureNotify: - if (g.ui_width == ev->xconfigure.width - && g.ui_height == ev->xconfigure.height) - break; - - g.ui_width = ev->xconfigure.width; - g.ui_height = ev->xconfigure.height; - - XRenderFreePicture (g.dpy, g.x11_pixmap_picture); - XFreePixmap (g.dpy, g.x11_pixmap); - x11_init_pixmap (); - XftDrawChange (g.xft_draw, g.x11_pixmap); - app_invalidate (); - break; - case SelectionRequest: - on_x11_selection_request (&ev->xselectionrequest); - break; - case SelectionClear: - cstr_set (&g.x11_selection, NULL); - break; - case UnmapNotify: - app_quit (); - break; - case FocusIn: - key.type = TERMO_TYPE_FOCUS; - key.code.focused = true; - app_process_termo_event (&key); - break; - case FocusOut: - key.type = TERMO_TYPE_FOCUS; - key.code.focused = false; - app_process_termo_event (&key); - break; - case KeyPress: - case ButtonPress: - case ButtonRelease: - case MotionNotify: - if (!on_x11_input_event (ev)) - XkbBell (g.dpy, ev->xany.window, 0, None); - } -} - -static void -on_x11_pending (void *user_data) -{ - (void) user_data; - - XkbEvent ev; - while (XPending (g.dpy)) - { - if (XNextEvent (g.dpy, &ev.core)) - exit_fatal ("XNextEvent returned non-zero"); - if (XFilterEvent (&ev.core, None)) - continue; - - on_x11_event (&ev.core); - } - - poller_idle_reset (&g.xpending_event); -} - -static void -on_x11_ready (const struct pollfd *pfd, void *user_data) -{ - (void) pfd; - on_x11_pending (user_data); -} - -static int -on_x11_error (Display *dpy, XErrorEvent *event) -{ - // Without opting for WM_DELETE_WINDOW, this window can become destroyed - // and hence invalid at any time. We don't use the Window much, - // so we should be fine ignoring these errors. - if ((event->error_code == BadWindow && event->resourceid == g.x11_window) - || (event->error_code == BadDrawable && event->resourceid == g.x11_window)) - return app_quit (), 0; - - // XXX: The simplest possible way of discarding selection management errors. - // XCB would be a small win here, but it is a curse at the same time. - if (event->error_code == BadWindow && event->resourceid != g.x11_window) - return 0; - - return x11_default_error_handler (dpy, event); -} - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -static XRenderColor -x11_convert_color (int color) -{ - hard_assert (color >= 0 && color <= 255); - - static const uint16_t base[16] = - { - 0x000, 0x800, 0x080, 0x880, 0x008, 0x808, 0x088, 0xccc, - 0x888, 0xf00, 0x0f0, 0xff0, 0x00f, 0xf0f, 0x0ff, 0xfff, - }; - - XRenderColor c = { .alpha = 0xffff }; - if (color < 16) - { - c.red = 0x1111 * (base[color] >> 8); - c.green = 0x1111 * (0xf & (base[color] >> 4)); - c.blue = 0x1111 * (0xf & (base[color])); - } - else if (color >= 232) - c.red = c.green = c.blue = 0x0101 * (8 + (color - 232) * 10); - else - { - color -= 16; - - int r = color / 36; - int g = (color / 6) % 6; - int b = (color % 6); - c.red = 0x0101 * !!r * (55 + 40 * r); - c.green = 0x0101 * !!g * (55 + 40 * g); - c.blue = 0x0101 * !!b * (55 + 40 * b); - } - return c; -} - -static void -x11_init_attributes (void) -{ - for (int a = 0; a < ATTRIBUTE_COUNT; a++) - { - g.x_fg[a] = x11_default_fg; - g.x_bg[a] = x11_default_bg; - if (g.attrs[a].fg >= 256 || g.attrs[a].fg < -1 - || g.attrs[a].bg >= 256 || g.attrs[a].bg < -1) - continue; - - if (g.attrs[a].fg != -1) - g.x_fg[a] = x11_convert_color (g.attrs[a].fg); - if (g.attrs[a].bg != -1) - g.x_bg[a] = x11_convert_color (g.attrs[a].bg); - - g.attrs[a].attrs |= COLOR_PAIR (a + 1); - } -} - -static void -x11_init_fonts (void) -{ - // TODO: Try to use Gtk/FontName from the _XSETTINGS_S%d selection, - // as well as Net/DoubleClick*. See the XSETTINGS proposal for details. - // https://www.freedesktop.org/wiki/Specifications/XSettingsRegistry/ - const char *name = get_config_string (g.config.root, "settings.x11_font"); - int screen = DefaultScreen (g.dpy); - FcResult result = 0; - - FcPattern *query_regular = FcNameParse ((const FcChar8 *) name); - FcPattern *query_bold = FcPatternDuplicate (query_regular); - FcPatternAdd (query_bold, FC_STYLE, (FcValue) { - .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); - FcPatternDestroy (query_regular); - if (!regular) - exit_fatal ("cannot open font: %s (%d)", name, result); - if (!(g.xft_regular = XftFontOpenPattern (g.dpy, regular))) - { - FcPatternDestroy (regular); - exit_fatal ("cannot open font: %s", name); - } - - FcPattern *bold = XftFontMatch (g.dpy, screen, query_bold, &result); - FcPatternDestroy (query_bold); - if (bold && !(g.xft_bold = XftFontOpenPattern (g.dpy, bold))) - FcPatternDestroy (bold); - if (!g.xft_bold) - 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 -x11_init (void) -{ - // https://tedyin.com/posts/a-brief-intro-to-linux-input-method-framework/ - if (!XSupportsLocale ()) - print_warning ("locale not supported by Xlib"); - XSetLocaleModifiers (""); - - if (!(g.dpy = XkbOpenDisplay - (NULL, &g.xkb_base_event_code, NULL, NULL, NULL, NULL))) - exit_fatal ("cannot open display"); - if (!XftDefaultHasRender (g.dpy)) - exit_fatal ("XRender is not supported"); - if (!(g.x11_im = XOpenIM (g.dpy, NULL, NULL, NULL))) - exit_fatal ("failed to open an input method"); - - x11_default_error_handler = XSetErrorHandler (on_x11_error); - - set_cloexec (ConnectionNumber (g.dpy)); - g.x11_event = poller_fd_make (&g.poller, ConnectionNumber (g.dpy)); - g.x11_event.dispatcher = on_x11_ready; - poller_fd_set (&g.x11_event, POLLIN); - - // Whenever something causes Xlib to read its socket, it can make - // the I/O event above fail to trigger for whatever might have ended up - // in its queue. So always use this instead of XSync: - g.xpending_event = poller_idle_make (&g.poller); - g.xpending_event.dispatcher = on_x11_pending; - poller_idle_set (&g.xpending_event); - - x11_init_attributes (); - x11_init_fonts (); - - int screen = DefaultScreen (g.dpy); - Colormap cmap = DefaultColormap (g.dpy, screen); - XColor default_bg = - { - .red = x11_default_bg.red, - .green = x11_default_bg.green, - .blue = x11_default_bg.blue, - }; - if (!XAllocColor (g.dpy, cmap, &default_bg)) - exit_fatal ("X11 setup failed"); - - XSetWindowAttributes attrs = - { - .event_mask = StructureNotifyMask | ExposureMask | FocusChangeMask - | KeyPressMask | ButtonPressMask | ButtonReleaseMask - | Button1MotionMask, - .bit_gravity = NorthWestGravity, - .background_pixel = default_bg.pixel, - }; - - // Approximate the average width of a character to half of the em unit. - g.ui_vunit = g.xft_regular->height; - g.ui_hunit = g.ui_vunit / 2; - // Base the window's size on the regular font size. - // Roughly trying to match the 80x24 default dimensions of terminals. - g.ui_height = 24 * g.ui_vunit; - g.ui_width = g.ui_height * 4 / 3; - - long im_event_mask = 0; - if (!XGetIMValues (g.x11_im, XNFilterEvents, &im_event_mask, NULL)) - attrs.event_mask |= im_event_mask; - - Visual *visual = DefaultVisual (g.dpy, screen); - g.x11_window = XCreateWindow (g.dpy, RootWindow (g.dpy, screen), 100, 100, - g.ui_width, g.ui_height, 0, CopyFromParent, InputOutput, visual, - CWEventMask | CWBackPixel | CWBitGravity, &attrs); - g.x11_clip = XCreateRegion (); - - XTextProperty prop = {}; - char *name = PROGRAM_NAME; - if (!Xutf8TextListToTextProperty (g.dpy, &name, 1, XUTF8StringStyle, &prop)) - XSetWMName (g.dpy, g.x11_window, &prop); - XFree (prop.value); - - // TODO: It is possible to do, e.g., on-the-spot. - XIMStyle im_style = XIMPreeditNothing | XIMStatusNothing; - XIMStyles *im_styles = NULL; - bool im_style_found = false; - if (!XGetIMValues (g.x11_im, XNQueryInputStyle, &im_styles, NULL) - && im_styles) - { - for (unsigned i = 0; i < im_styles->count_styles; i++) - im_style_found |= im_styles->supported_styles[i] == im_style; - XFree (im_styles); - } - if (!im_style_found) - print_warning ("failed to find the desired input method style"); - if (!(g.x11_ic = XCreateIC (g.x11_im, - XNInputStyle, im_style, - XNClientWindow, g.x11_window, - NULL))) - exit_fatal ("failed to open an input context"); - - XSetICFocus (g.x11_ic); - - x11_init_pixmap (); - g.xft_draw = XftDrawCreate (g.dpy, g.x11_pixmap, visual, cmap); - g.ui = &x11_ui; - - XMapWindow (g.dpy, g.x11_window); -} - #endif // WITH_X11 // --- Signals ----------------------------------------------------------------- @@ -7076,8 +6187,8 @@ app_on_signal_pipe_readable (const struct pollfd *fd, void *user_data) if (g_winch_received) { g_winch_received = false; - if (g.ui->winch) - g.ui->winch (); + if (g_xui.ui->winch) + g_xui.ui->winch (); } } @@ -7086,9 +6197,7 @@ app_on_message_timer (void *user_data) { (void) user_data; - cstr_set (&g.message, NULL); - cstr_set (&g.message_detail, NULL); - app_invalidate (); + app_hide_message (); } static void @@ -7113,7 +6222,7 @@ app_log_handler (void *user_data, const char *quote, const char *fmt, app_show_message (xstrndup (message.str, quote_len), xstrdup (message.str + quote_len)); - if (g_verbose_mode && (g.ui != &tui_ui || !isatty (STDERR_FILENO))) + if (g_verbose_mode && (g_xui.ui != &tui_ui || !isatty (STDERR_FILENO))) fprintf (stderr, "%s\n", message.str); if (g_debug_tab.active) debug_tab_push (str_steal (&message), @@ -7133,12 +6242,6 @@ app_init_poller_events (void) g.message_timer = poller_timer_make (&g.poller); g.message_timer.dispatcher = app_on_message_timer; - // Always initialized, but only activated with the TUI. - g.tty_event = poller_fd_make (&g.poller, STDIN_FILENO); - g.tty_event.dispatcher = tui_on_tty_readable; - g.tk_timer = poller_timer_make (&g.poller); - g.tk_timer.dispatcher = tui_on_key_timer; - g.connect_event = poller_timer_make (&g.poller); g.connect_event.dispatcher = app_on_reconnect; poller_timer_set (&g.connect_event, 0); @@ -7147,12 +6250,34 @@ app_init_poller_events (void) g.elapsed_event.dispatcher = g.elapsed_poll ? mpd_on_elapsed_time_tick_poll : mpd_on_elapsed_time_tick; +} + +static void +app_init_ui (bool requested_x11) +{ + xui_preinit (); + + g_normal_keys = app_init_bindings ("normal", + g_normal_defaults, N_ELEMENTS (g_normal_defaults), &g_normal_keys_len); + g_editor_keys = app_init_bindings ("editor", + g_editor_defaults, N_ELEMENTS (g_editor_defaults), &g_editor_keys_len); + + // It doesn't work 100% (e.g. incompatible with undelining in urxvt) + // TODO: make this configurable + g.use_partial_boxes = g_xui.locale_is_utf8; + +#ifdef WITH_X11 + g_xui.x11_fontname = get_config_string (g.config.root, "settings.x11_font"); +#endif // WITH_X11 - g.refresh_event = poller_idle_make (&g.poller); - g.refresh_event.dispatcher = app_on_refresh; + xui_start (&g.poller, requested_x11, g.attrs, N_ELEMENTS (g.attrs)); - g.flip_event = poller_idle_make (&g.poller); - g.flip_event.dispatcher = app_on_flip; +#ifdef WITH_X11 + if (g_xui.ui == &x11_ui) + g.ui = &app_x11_ui; + else +#endif // WITH_X11 + g.ui = &app_tui_ui; } static void @@ -7234,18 +6359,7 @@ main (int argc, char *argv[]) app_load_configuration (); signals_setup_handlers (); app_init_poller_events (); - -#ifdef WITH_X11 - if (requested_x11 || (!isatty (STDIN_FILENO) && getenv ("DISPLAY"))) - x11_init (); - else -#endif // WITH_X11 - tui_init (); - - g_normal_keys = app_init_bindings ("normal", - g_normal_defaults, N_ELEMENTS (g_normal_defaults), &g_normal_keys_len); - g_editor_keys = app_init_bindings ("editor", - g_editor_defaults, N_ELEMENTS (g_editor_defaults), &g_editor_keys_len); + app_init_ui (requested_x11); if (g_debug_mode) app_prepend_tab (debug_tab_init ()); @@ -7269,7 +6383,7 @@ main (int argc, char *argv[]) while (g.polling) poller_run (&g.poller); - g.ui->destroy (); + xui_stop (); g_log_message_real = log_message_stdio; app_free_context (); return 0; diff --git a/nncmpp.desktop b/nncmpp.desktop new file mode 100644 index 0000000..4ba2668 --- /dev/null +++ b/nncmpp.desktop @@ -0,0 +1,9 @@ +[Desktop Entry] +Type=Application +Name=nncmpp +GenericName=MPD client +Icon=nncmpp +Exec=nncmpp %U +StartupNotify=false +# Not registering a MimeType, because that depends on MPD. +Categories=AudioVideo;Audio;Player; diff --git a/nncmpp.svg b/nncmpp.svg new file mode 100644 index 0000000..8960736 --- /dev/null +++ b/nncmpp.svg @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg version="1.0" width="48" height="48" viewBox="0 0 48 48" + xmlns="http://www.w3.org/2000/svg"> + <g transform="translate(5 4) scale(2 2)"> + <!-- From x11_icon_play, with a stroke for contrast. --> + <path d="M 0 0 20 10 0 20 Z" stroke="#eee" stroke-width="2" fill="#000" + stroke-linejoin="round" /> + </g> +</svg> diff --git a/termo b/termo -Subproject 8265f075b176b33680012094aa1ced5721e55ac +Subproject f9a102456fa6a0b43a916ceaf031f21ea5665e6 |