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