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