diff options
-rw-r--r-- | CMakeLists.txt | 68 | ||||
-rw-r--r-- | LICENSE | 2 | ||||
-rw-r--r-- | NEWS | 31 | ||||
-rw-r--r-- | README.adoc | 14 | ||||
m--------- | liberty | 0 | ||||
-rw-r--r-- | line-editor.c | 327 | ||||
-rw-r--r-- | nncmpp.actions | 3 | ||||
-rw-r--r-- | nncmpp.adoc | 6 | ||||
-rw-r--r-- | nncmpp.c | 1956 | ||||
-rw-r--r-- | nncmpp.desktop | 9 | ||||
-rw-r--r-- | nncmpp.svg | 9 | ||||
m--------- | termo | 0 |
12 files changed, 562 insertions, 1863 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 - 2023, 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,39 @@ -Unreleased +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..1a0f199 100644 --- a/README.adoc +++ b/README.adoc @@ -28,8 +28,10 @@ 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]. Documentation ------------- @@ -38,10 +40,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 969a4cfc3ea1c4d7c0327907385fc64906ed5d4 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.adoc b/nncmpp.adoc index 5b1cfdb..33e2834 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" } .... @@ -117,8 +117,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 - 2023, 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) { @@ -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; @@ -1363,14 +1264,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 +1278,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]; @@ -1661,6 +1530,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) { @@ -1684,22 +1560,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 (); } @@ -1732,9 +1592,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 +1604,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 +1693,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 +1713,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 +1796,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 +1866,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 +1897,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 +1911,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 +1974,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 +2004,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; -} + list->children = children.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; - - 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 +2080,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 +2133,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 +2155,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 +2164,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 +2228,27 @@ app_fix_view_range (void) } static void -app_on_flip (void *user_data) +app_layout (void) { - (void) user_data; - poller_idle_reset (&g.flip_event); + struct layout top = {}, bottom = {}; + app_layout_header (&top); + app_layout_statusbar (&bottom); - // Waste of time, and may cause X11 to render uninitialised pixmaps. - if (g.polling && !g.refresh_event.active) - g.ui->flip (); -} + 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; -static void -app_on_refresh (void *user_data) -{ - (void) user_data; - poller_idle_reset (&g.refresh_event); - - LIST_FOR_EACH (struct widget, w, g.widgets.head) - free (w); - - g.widgets = (struct layout) {}; - - 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 +2258,7 @@ static bool app_scroll (int n) { g.active_tab->item_top += n; - app_invalidate (); + xui_invalidate (); return app_fix_view_range (); } @@ -2407,7 +2302,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 +2314,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 +2326,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 +2347,7 @@ static void app_prepend_tab (struct tab *tab) { LIST_PREPEND (g.tabs, tab); - app_invalidate (); + xui_invalidate (); } static void @@ -2451,7 +2358,7 @@ app_switch_tab (struct tab *tab) g.last_tab = g.active_tab; g.active_tab = tab; - app_invalidate (); + xui_invalidate (); } static bool @@ -2581,7 +2488,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; @@ -2624,7 +2531,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,24 +2540,26 @@ 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: @@ -2663,7 +2572,7 @@ app_process_action (enum action action) || !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 +2582,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 +2626,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 +2645,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 +2654,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; @@ -2775,10 +2684,10 @@ app_process_action (enum action action) 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; @@ -2835,7 +2744,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 +2763,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 +2772,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 +2786,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 +2798,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 +2827,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 +2836,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 +2855,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 +2906,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 +2918,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 +2970,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 +3002,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 +3019,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 +3048,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 +3070,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 +3083,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 +3105,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 +3124,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 +3242,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 +3293,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 +3555,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 @@ -3781,7 +3718,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 +3843,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 +3872,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 @@ -4286,7 +4228,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 +4271,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 +4402,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 +4493,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; @@ -4761,7 +4707,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 +4731,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 +4749,7 @@ spectrum_discard_fifo (void) g.spectrum_fd = -1; spectrum_free (&g.spectrum); - app_invalidate (); + xui_invalidate (); } } @@ -4878,7 +4821,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 +4894,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 +4917,7 @@ static void pulse_disable (void) { pulse_free (&g.pulse); - app_invalidate (); + xui_invalidate (); } #else // ! WITH_PULSE @@ -5083,7 +5026,7 @@ mpd_update_playback_state (void) if (g.playlist_version != last_playlist_version) mpd_update_playlist_time (); - app_invalidate (); + xui_invalidate (); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -5221,7 +5164,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 +5374,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 +5524,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 +5537,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 +5584,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 +5684,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 +5705,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 +5720,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 +5754,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 +5776,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 +5800,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 +5815,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 +5827,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 +5843,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,7 +5860,7 @@ x11_render_editor (struct widget *self) { x11_render_padding (self); - XftFont *font = x11_font (self); + XftFont *font = x11_widget_font (self)->list->font; XftColor color = { .color = *x11_fg (self) }; // A simplistic adaptation of line_editor_write() follows. @@ -6284,18 +5868,19 @@ x11_render_editor (struct widget *self) XGlyphInfo extents = {}; if (g.editor.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; + FT_UInt i = XftCharIndex (g_xui.dpy, font, g.editor.prompt); + XftDrawGlyphs (g_xui.xft_draw, &color, font, x, y, &i, 1); + XftGlyphExtents (g_xui.dpy, font, &i, 1, &extents); + x += extents.xOff + g_xui.vunit / 4; } + // TODO: Adapt x11_font_{hadvance,draw}(). // TODO: Make this scroll around the caret, and fade like labels. - XftDrawString32 (g.xft_draw, &color, font, x, y, + XftDrawString32 (g_xui.xft_draw, &color, font, x, y, g.editor.line, g.editor.len); - XftTextExtents32 (g.dpy, font, g.editor.line, g.editor.point, &extents); - XRenderFillRectangle (g.dpy, PictOpSrc, g.x11_pixmap_picture, + XftTextExtents32 (g_xui.dpy, font, g.editor.line, g.editor.point, &extents); + XRenderFillRectangle (g_xui.dpy, PictOpSrc, g_xui.x11_pixmap_picture, &color.color, x + extents.xOff, self->y, 2, self->height); } @@ -6307,64 +5892,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 +5907,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 +5997,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 +6007,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 +6032,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 +6052,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 +6060,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; - g.refresh_event = poller_idle_make (&g.poller); - g.refresh_event.dispatcher = app_on_refresh; +#ifdef WITH_X11 + g_xui.x11_fontname = get_config_string (g.config.root, "settings.x11_font"); +#endif // WITH_X11 + + 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 +6169,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 +6193,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 2518b53e5ae4579bf84ed58fa7a62806f64e861 |