diff options
-rw-r--r-- | CMakeLists.txt | 26 | ||||
-rw-r--r-- | LICENSE | 2 | ||||
-rw-r--r-- | NEWS | 25 | ||||
-rw-r--r-- | README.adoc | 10 | ||||
m--------- | liberty | 0 | ||||
-rw-r--r-- | nncmpp.actions.awk | 4 | ||||
-rw-r--r-- | nncmpp.adoc | 14 | ||||
-rw-r--r-- | nncmpp.c | 335 | ||||
m--------- | termo | 0 |
9 files changed, 334 insertions, 82 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index c49574e..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) @@ -68,7 +68,7 @@ if (WITH_PULSE) list (APPEND extra_libraries ${libpulse_LIBRARIES}) endif () -pkg_check_modules (x11 x11 xrender xft fontconfig) +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) @@ -121,17 +121,33 @@ 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 () @@ -1,4 +1,4 @@ -Copyright (c) 2016 - 2023, Přemysl Eric Janouch <p@janouch.name> +Copyright (c) 2016 - 2024, Přemysl Eric Janouch <p@janouch.name> Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. @@ -1,5 +1,26 @@ Unreleased + * Made global search indicate the search terms, and match on filenames + + * Added ability to configure bindable user-defined actions; + these can launch arbitrary shell commands + + * Prevented crashes when the daemon disconnects during search + + * X11: added support for font fallbacks to the editor as well + + +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 @@ -13,10 +34,14 @@ Unreleased * 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 % diff --git a/README.adoc b/README.adoc index c5b9e15..775c5a3 100644 --- a/README.adoc +++ b/README.adoc @@ -33,6 +33,8 @@ You can get a package with the latest development version using Arch Linux's https://aur.archlinux.org/packages/nncmpp-git[AUR], or as a https://git.janouch.name/p/nixexprs[Nix derivation]. +Stable versions are present in: OpenBSD ports. + Documentation ------------- See the link:nncmpp.adoc[man page] for information about usage. @@ -40,10 +42,12 @@ The rest of this README will concern itself with externalities. Building -------- -Build dependencies: CMake, pkg-config, awk, liberty (included), - termo (included), asciidoctor or asciidoc (recommended but optional) + +Build-only dependencies: CMake, pkg-config, awk, liberty (included), + termo (included), asciidoctor or asciidoc (recommended but optional), + rsvg-convert (X11) + Runtime dependencies: ncursesw, libunistring, cURL + -Optional runtime dependencies: fftw3, libpulse, x11, xft, Perl + cURL (lyrics) +Optional runtime dependencies: fftw3, libpulse, x11 + xft + libpng (X11), + Perl + cURL (lyrics) $ git clone --recursive https://git.janouch.name/p/nncmpp.git $ mkdir nncmpp/build diff --git a/liberty b/liberty -Subproject 4c2874649d4b1d2414793d60915d309f0bf6711 +Subproject 0f20cce9c8cbda57b95f789e325686ee9c1c53f diff --git a/nncmpp.actions.awk b/nncmpp.actions.awk index b4d7eaf..8cd18e7 100644 --- a/nncmpp.actions.awk +++ b/nncmpp.actions.awk @@ -1,6 +1,6 @@ # nncmpp.actions.awk: produce C code for a list of user actions # -# Copyright (c) 2022, Přemysl Eric Janouch <p@janouch.name> +# Copyright (c) 2022 - 2024, Přemysl Eric Janouch <p@janouch.name> # SPDX-License-Identifier: 0BSD # # Usage: env LC_ALL=C A=0 B=1 awk -f nncmpp.actions.awk \ @@ -91,7 +91,7 @@ END { print "enum action {" for (i in Constants) print "\t" "ACTION_" Constants[i] "," - print "\t" "ACTION_COUNT" + print "\t" "ACTION_USER_0" print "};" print "" print "static const char *g_action_names[] = {" diff --git a/nncmpp.adoc b/nncmpp.adoc index 2fc8361..eaebfc3 100644 --- a/nncmpp.adoc +++ b/nncmpp.adoc @@ -69,7 +69,7 @@ colors = { scrollbar = "" } streams = { - "dnbradio.com" = "http://www.dnbradio.com/hi.m3u" + "dnbradio.com" = "https://dnbradio.com/hi.pls" "BassDrive.com" = "http://bassdrive.com/v2/streams/BassDrive.pls" } .... @@ -85,6 +85,18 @@ To adjust key bindings, put them within a *normal* or *editor* object. Run *nncmpp* with the *--debug* option to find out key combinations names. Press *?* in the help tab to learn the action identifiers to use. +You may also define and bind your own actions, launching arbitrary +shell commands. Note that you cannot override internal actions in this manner. + +.... +actions = { + "pioneer-on-off" = { + description = "Pioneer amplifier: turn on/off" + command = "elksmart-comm --nec A538" + } +} +.... + Spectrum visualiser ------------------- When built against the FFTW library, *nncmpp* can make use of MPD's "fifo" @@ -1,7 +1,7 @@ /* * nncmpp -- the MPD client you never knew you needed * - * Copyright (c) 2016 - 2023, Přemysl Eric Janouch <p@janouch.name> + * Copyright (c) 2016 - 2024, Přemysl Eric Janouch <p@janouch.name> * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted. @@ -522,7 +522,7 @@ static struct item_list item_list_make (void) { struct item_list self = {}; - self.items = xcalloc (sizeof *self.items, (self.alloc = 16)); + self.items = xcalloc ((self.alloc = 16), sizeof *self.items); return self; } @@ -814,9 +814,9 @@ spectrum_init (struct spectrum *s, char *format, int bars, int fps, s->useful_bins = s->bins / 2; int used_bins = necessary_bins / 2; - s->rendered = xcalloc (sizeof *s->rendered, s->bars * 3 + 1); - s->spectrum = xcalloc (sizeof *s->spectrum, s->bars); - s->top_bins = xcalloc (sizeof *s->top_bins, s->bars); + s->rendered = xcalloc (s->bars * 3 + 1, sizeof *s->rendered); + s->spectrum = xcalloc (s->bars, sizeof *s->spectrum); + s->top_bins = xcalloc (s->bars, sizeof *s->top_bins); for (int bar = 0; bar < s->bars; bar++) { int top_bin = floor (pow (used_bins + 1, (bar + 1.) / s->bars)) - 1; @@ -839,7 +839,7 @@ spectrum_init (struct spectrum *s, char *format, int bars, int fps, s->buffer = xcalloc (1, s->buffer_size); // Prepare the window - s->window = xcalloc (sizeof *s->window, s->bins); + s->window = xcalloc (s->bins, sizeof *s->window); window_hann (s->window, s->bins); // Multiply by 2 for only using half of the DFT's result, then adjust to @@ -849,11 +849,11 @@ spectrum_init (struct spectrum *s, char *format, int bars, int fps, float coherent_gain = window_coherent_gain (s->window, s->bins); s->accumulator_scale = 2 * 2 / coherent_gain / coherent_gain / s->samples; - s->data = xcalloc (sizeof *s->data, s->bins); - s->windowed = fftw_malloc (sizeof *s->windowed * s->bins); - s->out = fftw_malloc (sizeof *s->out * (s->useful_bins + 1)); + s->data = xcalloc (s->bins, sizeof *s->data); + s->windowed = fftw_malloc (s->bins * sizeof *s->windowed); + s->out = fftw_malloc ((s->useful_bins + 1) * sizeof *s->out); s->p = fftwf_plan_dft_r2c_1d (s->bins, s->windowed, s->out, FFTW_MEASURE); - s->accumulator = xcalloc (sizeof *s->accumulator, s->useful_bins); + s->accumulator = xcalloc (s->useful_bins, sizeof *s->accumulator); return true; } @@ -1256,6 +1256,9 @@ static struct app_context struct config config; ///< Program configuration struct strv streams; ///< List of "name NUL URI NUL" struct strv enqueue; ///< Items to enqueue once connected + struct strv action_names; ///< User-defined action names + struct strv action_descriptions; ///< User-defined action descriptions + struct strv action_commands; ///< User-defined action commands struct tab *help_tab; ///< Special help tab struct tab *tabs; ///< All other tabs @@ -1337,7 +1340,7 @@ on_pulseaudio_changed (struct config_item *item) g.pulse_control_requested = item->value.boolean; } -static struct config_schema g_config_settings[] = +static const struct config_schema g_config_settings[] = { { .name = "address", .comment = "Address to connect to the MPD server", @@ -1401,7 +1404,7 @@ static struct config_schema g_config_settings[] = {} }; -static struct config_schema g_config_colors[] = +static const struct config_schema g_config_colors[] = { #define XX(name_, config, fg_, bg_, attrs_) \ { .name = #config, .type = CONFIG_ITEM_STRING }, @@ -1410,6 +1413,17 @@ static struct config_schema g_config_colors[] = {} }; +static const struct config_schema g_config_actions[] = +{ + { .name = "description", + .comment = "Human-readable description of the action", + .type = CONFIG_ITEM_STRING }, + { .name = "command", + .comment = "Shell command to run", + .type = CONFIG_ITEM_STRING }, + {} +}; + static const char * get_config_string (struct config_item *root, const char *key) { @@ -1485,12 +1499,43 @@ load_config_streams (struct config_item *subtree, void *user_data) } static void +load_config_actions (struct config_item *subtree, void *user_data) +{ + (void) user_data; + + struct str_map_iter iter = str_map_iter_make (&subtree->value.object); + while (str_map_iter_next (&iter)) + strv_append (&g.action_names, iter.link->key); + qsort (g.action_names.vector, g.action_names.len, + sizeof *g.action_names.vector, strv_sort_utf8_cb); + + for (size_t i = 0; i < g.action_names.len; i++) + { + const char *name = g.action_names.vector[i]; + struct config_item *item = config_item_get (subtree, name, NULL); + hard_assert (item != NULL); + if (item->type != CONFIG_ITEM_OBJECT) + exit_fatal ("`%s': invalid user action, expected an object", name); + + config_schema_apply_to_object (g_config_actions, item, NULL); + config_schema_call_changed (item); + + const char *description = get_config_string (item, "description"); + const char *command = get_config_string (item, "command"); + strv_append (&g.action_descriptions, description ? description : name); + strv_append (&g.action_commands, command ? command : ""); + } +} + +static void app_load_configuration (void) { struct config *config = &g.config; config_register_module (config, "settings", load_config_settings, NULL); config_register_module (config, "colors", load_config_colors, NULL); config_register_module (config, "streams", load_config_streams, NULL); + // This must run before bindings are parsed in app_init_ui(). + config_register_module (config, "actions", load_config_actions, NULL); // Bootstrap configuration, so that we can access schema items at all config_load (config, config_item_object ()); @@ -1548,6 +1593,9 @@ app_init_context (void) g.config = config_make (); g.streams = strv_make (); g.enqueue = strv_make (); + g.action_names = strv_make (); + g.action_descriptions = strv_make (); + g.action_commands = strv_make (); g.playback_info = str_map_make (free); g.playback_info.key_xfrm = tolower_ascii_strxfrm; @@ -1570,6 +1618,9 @@ app_free_context (void) str_map_free (&g.playback_info); strv_free (&g.streams); strv_free (&g.enqueue); + strv_free (&g.action_names); + strv_free (&g.action_descriptions); + strv_free (&g.action_commands); item_list_free (&g.playlist); #ifdef WITH_FFTW @@ -2379,12 +2430,52 @@ app_goto_tab (int tab_index) static int action_resolve (const char *name) { - for (int i = 0; i < ACTION_COUNT; i++) + for (int i = 0; i < ACTION_USER_0; i++) if (!strcasecmp_ascii (g_action_names[i], name)) return i; + + // We could put this lookup first, and accordingly adjust + // app_init_bindings() to do action_resolve(action_name(action)), + // however the ability to override internal actions seems pointless. + for (size_t i = 0; i < g.action_names.len; i++) + if (!strcasecmp_ascii (g.action_names.vector[i], name)) + return ACTION_USER_0 + i; return -1; } +static const char * +action_name (enum action action) +{ + if (action < ACTION_USER_0) + return g_action_names[action]; + + size_t user_action = action - ACTION_USER_0; + hard_assert (user_action < g.action_names.len); + return g.action_names.vector[user_action]; +} + +static const char * +action_description (enum action action) +{ + if (action < ACTION_USER_0) + return g_action_descriptions[action]; + + size_t user_action = action - ACTION_USER_0; + hard_assert (user_action < g.action_descriptions.len); + return g.action_descriptions.vector[user_action]; +} + +static const char * +action_command (enum action action) +{ + if (action < ACTION_USER_0) + return NULL; + + size_t user_action = action - ACTION_USER_0; + hard_assert (user_action < g.action_commands.len); + return g.action_commands.vector[user_action]; +} + // --- User input handling ----------------------------------------------------- static void @@ -2517,6 +2608,64 @@ incremental_search_on_end (bool confirmed) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static bool +run_command (const char *command, struct str *output, struct error **e) +{ + char *adjusted = xstrdup_printf ("2>&1 %s", command); + print_debug ("running command: %s", adjusted); + + FILE *fp = popen (adjusted, "r"); + free (adjusted); + if (!fp) + return error_set (e, "%s", strerror (errno)); + + char buf[BUFSIZ]; + size_t len; + while ((len = fread (buf, 1, sizeof buf, fp)) == sizeof buf) + str_append_data (output, buf, len); + str_append_data (output, buf, len); + + int status = pclose (fp); + if (status < 0) + return error_set (e, "%s", strerror (errno)); + if (WIFEXITED (status) && WEXITSTATUS (status)) + return error_set (e, "exit status %d", WEXITSTATUS (status)); + if (WIFSIGNALED (status)) + return error_set (e, "terminated on signal %d", WTERMSIG (status)); + if (WIFSTOPPED (status)) + return error_set (e, "stopped on signal %d", WSTOPSIG (status)); + return true; +} + +static bool +app_process_action_command (enum action action) +{ + const char *command = action_command (action); + if (!command) + return false; + + struct str output = str_make (); + struct error *error = NULL; + (void) run_command (command, &output, &error); + str_enforce_utf8 (&output); + + struct strv lines = strv_make (); + cstr_split (output.str, "\r\n", false, &lines); + str_free (&output); + while (lines.len && !*lines.vector[lines.len - 1]) + free (strv_steal (&lines, lines.len - 1)); + for (size_t i = 0; i < lines.len; i++) + print_debug ("output: %s", lines.vector[i]); + strv_free (&lines); + + if (error) + { + print_error ("\"%s\": %s", action_description (action), error->message); + error_free (error); + } + return true; +} + +static bool app_mpd_toggle (const char *name) { const char *s = str_map_find (&g.playback_info, name); @@ -2562,10 +2711,6 @@ app_process_action (enum action action) xui_invalidate (); app_hide_message (); return true; - default: - print_error ("\"%s\" is not allowed here", - g_action_descriptions[action]); - return false; case ACTION_MULTISELECT: if (!tab->can_multiselect @@ -2677,8 +2822,14 @@ app_process_action (enum action action) case ACTION_GOTO_VIEW_BOTTOM: g.active_tab->item_selected = g.active_tab->item_top; return app_move_selection (MAX (0, app_visible_items () - 1)); + + default: + if (app_process_action_command (action)) + return true; + + print_error ("\"%s\" is not allowed here", action_description (action)); + return false; } - return false; } static bool @@ -2696,8 +2847,7 @@ app_editor_process_action (enum action action) g.editor.on_end = NULL; return true; default: - print_error ("\"%s\" is not allowed here", - g_action_descriptions[action]); + print_error ("\"%s\" is not allowed here", action_description (action)); return false; case ACTION_EDITOR_B_CHAR: @@ -3599,30 +3749,67 @@ static void library_tab_on_search_data (const struct mpd_response *response, const struct strv *data, void *user_data) { - (void) user_data; + char *filter = user_data; if (!g_library_tab.searching) - return; + goto out; if (!response->success) - { print_error ("cannot search: %s", response->message_text); - return; + else + { + cstr_set (&g_library_tab.super.header, + xstrdup_printf ("%s: %s", "Global search", filter)); + library_tab_load_data (data); } - library_tab_load_data (data); +out: + free (filter); +} + +static char * +mpd_quoted_filter_string (const char *value) +{ + struct str quoted = str_make (); + str_append_c ("ed, '\''); + for (const char *p = value; *p; p++) + { + if (mpd_client_must_escape_in_quote (*p)) + str_append_c ("ed, '\\'); + str_append_c ("ed, *p); + } + str_append_c ("ed, '\''); + return str_steal ("ed); } static void search_on_changed (void) { struct mpd_client *c = &g.client; + if (c->state != MPD_CONNECTED) + return; size_t len; char *u8 = (char *) u32_to_u8 (g.editor.line, g.editor.len + 1, NULL, &len); + mpd_client_list_begin (c); mpd_client_send_command (c, "search", "any", u8, NULL); - free (u8); - mpd_client_add_task (c, library_tab_on_search_data, NULL); + // Just tag search doesn't consider filenames. + // Older MPD can do `search any X file X` but without the negation, + // which is necessary to avoid duplicates. Neither syntax supports OR. + // XXX: We should parse this, but it's probably not going to reach 100 soon, + // and it is not really documented what this should even look like. + if (strcmp (c->got_hello, "0.21.") > 1) + { + char *quoted = mpd_quoted_filter_string (u8); + char *expression = xstrdup_printf ("((!(any contains %s)) AND " + "(file contains %s))", quoted, quoted); + mpd_client_send_command (c, "search", expression, NULL); + free (expression); + free (quoted); + } + + mpd_client_list_end (c); + mpd_client_add_task (c, library_tab_on_search_data, u8); mpd_client_idle (c, 0); } @@ -3843,8 +4030,13 @@ streams_tab_parse_playlist (const char *playlist, const char *content_type, || (content_type && is_content_type (content_type, "audio", "x-scpls"))) extract_re = "^File[^=]*=(.+)"; else if ((lines.len && !strcasecmp_ascii (lines.vector[0], "#EXTM3U")) + || (content_type && is_content_type (content_type, "audio", "mpegurl")) || (content_type && is_content_type (content_type, "audio", "x-mpegurl"))) - extract_re = "^([^#].*)"; + // This could be "^([^#].*)", however 1. we would need to resolve + // relative URIs, and 2. relative URIs probably mean a Media Playlist, + // which must be passed to MPD. The better thing to do here would be to + // reject anything with EXT-X-TARGETDURATION, and to resolve the URIs. + extract_re = "^(https?://.+)"; regex_t *re = regex_compile (extract_re, REG_EXTENDED, NULL); hard_assert (re != NULL); @@ -3867,7 +4059,7 @@ streams_tab_extract_links (struct str *data, const char *content_type, } streams_tab_parse_playlist (data->str, content_type, out); - return true; + return out->len != 0; } static void @@ -4070,24 +4262,16 @@ info_tab_plugin_load (const char *path) // Shell quoting is less annoying than process management. struct str escaped = str_make (); shell_quote (path, &escaped); - FILE *fp = popen (escaped.str, "r"); - str_free (&escaped); - if (!fp) - { - print_error ("%s: %s", path, strerror (errno)); - return NULL; - } struct str description = str_make (); - char buf[BUFSIZ]; - size_t len; - while ((len = fread (buf, 1, sizeof buf, fp)) == sizeof buf) - str_append_data (&description, buf, len); - str_append_data (&description, buf, len); - if (pclose (fp)) + struct error *error = NULL; + (void) run_command (escaped.str, &description, &error); + str_free (&escaped); + if (error) { + print_error ("%s: %s", path, error->message); + error_free (error); str_free (&description); - print_error ("%s: %s", path, strerror (errno)); return NULL; } @@ -4100,8 +4284,8 @@ info_tab_plugin_load (const char *path) str_enforce_utf8 (&description); if (!description.len) { - str_free (&description); print_error ("%s: %s", path, "missing description"); + str_free (&description); return NULL; } @@ -4539,7 +4723,7 @@ help_tab_on_action (enum action action) if (action == ACTION_DESCRIBE) { app_show_message (xstrdup ("Configuration name: "), - xstrdup (g_action_names[a])); + xstrdup (action_name (a))); return true; } if (action != ACTION_CHOOSE || a == ACTION_CHOOSE /* avoid recursion */) @@ -4564,9 +4748,9 @@ help_tab_assign_action (enum action action) static void help_tab_group (struct binding *keys, size_t len, struct strv *out, - bool bound[ACTION_COUNT]) + bool bound[], size_t action_count) { - for (enum action i = 0; i < ACTION_COUNT; i++) + for (enum action i = 0; i < action_count; i++) { struct strv ass = strv_make (); for (size_t k = 0; k < len; k++) @@ -4576,7 +4760,7 @@ help_tab_group (struct binding *keys, size_t len, struct strv *out, { char *joined = strv_join (&ass, ", "); strv_append_owned (out, xstrdup_printf - (" %s%c%s", g_action_descriptions[i], 0, joined)); + (" %s%c%s", action_description (i), 0, joined)); free (joined); bound[i] = true; @@ -4587,13 +4771,13 @@ help_tab_group (struct binding *keys, size_t len, struct strv *out, } static void -help_tab_unbound (struct strv *out, bool bound[ACTION_COUNT]) +help_tab_unbound (struct strv *out, bool bound[], size_t action_count) { - for (enum action i = 0; i < ACTION_COUNT; i++) + for (enum action i = 0; i < action_count; i++) if (!bound[i]) { strv_append_owned (out, - xstrdup_printf (" %s%c", g_action_descriptions[i], 0)); + xstrdup_printf (" %s%c", action_description (i), 0)); help_tab_assign_action (i); } } @@ -4623,27 +4807,30 @@ help_tab_init (void) struct strv *lines = &g_help_tab.lines; *lines = strv_make (); - bool bound[ACTION_COUNT] = { [ACTION_NONE] = true }; + size_t bound_len = ACTION_USER_0 + g.action_names.len; + bool *bound = xcalloc (bound_len, sizeof *bound); + bound[ACTION_NONE] = true; strv_append (lines, "Normal mode actions"); - help_tab_group (g_normal_keys, g_normal_keys_len, lines, bound); + help_tab_group (g_normal_keys, g_normal_keys_len, lines, bound, bound_len); strv_append (lines, ""); strv_append (lines, "Editor mode actions"); - help_tab_group (g_editor_keys, g_editor_keys_len, lines, bound); + help_tab_group (g_editor_keys, g_editor_keys_len, lines, bound, bound_len); strv_append (lines, ""); bool have_unbound = false; - for (enum action i = 0; i < ACTION_COUNT; i++) + for (size_t i = 0; i < bound_len; i++) if (!bound[i]) have_unbound = true; if (have_unbound) { strv_append (lines, "Unbound actions"); - help_tab_unbound (lines, bound); + help_tab_unbound (lines, bound, bound_len); strv_append (lines, ""); } + free (bound); struct tab *super = &g_help_tab.super; tab_init (super, "Help"); @@ -5855,28 +6042,36 @@ x11_render_editor (struct widget *self) { x11_render_padding (self); - XftFont *font = x11_widget_font (self)->list->font; + struct x11_font *font = x11_widget_font (self); XftColor color = { .color = *x11_fg (self) }; - // A simplistic adaptation of line_editor_write() follows. - int x = self->x, y = self->y + font->ascent; - XGlyphInfo extents = {}; - if (g.editor.prompt) + // A simplistic adaptation of tui_render_editor() follows. + const struct line_editor *e = &g.editor; + int x = self->x; + if (e->prompt) { - FT_UInt i = XftCharIndex (g_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; + hard_assert (e->prompt < 127); + x += x11_font_draw (font, &color, x, self->y, + (char[2]) { e->prompt, 0 }) + g_xui.vunit / 4; } - // TODO: Adapt x11_font_{hadvance,draw}(). // TODO: Make this scroll around the caret, and fade like labels. - XftDrawString32 (g_xui.xft_draw, &color, font, x, y, - g.editor.line, g.editor.len); + size_t len; + ucs4_t *buf = xcalloc (e->len + 1, sizeof *buf); + u32_cpy (buf, e->line, e->point); + char *a = (char *) u32_to_u8 (buf, u32_strlen (buf) + 1, NULL, &len); + u32_cpy (buf, e->line + e->point, e->len - e->point + 1); + char *b = (char *) u32_to_u8 (buf, u32_strlen (buf) + 1, NULL, &len); + free (buf); + + x += x11_font_draw (font, &color, x, self->y, a); + int caret = x; + x += x11_font_draw (font, &color, x, self->y, b); + free (a); + free (b); - XftTextExtents32 (g_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); + &color.color, caret, self->y, 2, self->height); } static struct widget * diff --git a/termo b/termo -Subproject 2518b53e5ae4579bf84ed58fa7a62806f64e861 +Subproject f9a102456fa6a0b43a916ceaf031f21ea5665e6 |