diff options
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | CMakeLists.txt | 38 | ||||
| -rw-r--r-- | LICENSE | 2 | ||||
| -rw-r--r-- | NEWS | 29 | ||||
| -rw-r--r-- | README.adoc | 18 | ||||
| -rw-r--r-- | config.h.in | 1 | ||||
| m--------- | liberty | 0 | ||||
| -rw-r--r-- | nncmpp.actions.awk | 4 | ||||
| -rw-r--r-- | nncmpp.adoc | 14 | ||||
| -rw-r--r-- | nncmpp.c | 759 | ||||
| m--------- | termo | 0 |
11 files changed, 699 insertions, 167 deletions
@@ -2,6 +2,7 @@ /build # Qt Creator files +/.qtcreator /CMakeLists.txt.user* /nncmpp.config /nncmpp.files diff --git a/CMakeLists.txt b/CMakeLists.txt index c49574e..7e0f6c1 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,16 @@ if (WITH_PULSE) list (APPEND extra_libraries ${libpulse_LIBRARIES}) endif () -pkg_check_modules (x11 x11 xrender xft fontconfig) +add_option (WITH_APPKIT "Build with AppKit support" "${APPLE}") +if (WITH_APPKIT) + enable_language (OBJC) + set (CMAKE_OBJC_FLAGS + "${CMAKE_OBJC_FLAGS} -std=gnu99 -Wall -Wextra -Wno-unused-function") + list (APPEND extra_libraries + "-framework AppKit" "-framework CoreFoundation") +endif () + +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) @@ -120,18 +129,37 @@ add_custom_command (OUTPUT ${actions} # Build the main executable and link it add_executable (${PROJECT_NAME} ${PROJECT_NAME}.c ${actions}) +if (WITH_APPKIT) + set_source_files_properties (${PROJECT_NAME}.c PROPERTIES LANGUAGE OBJC) +endif () 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 - 2026, 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,30 @@ Unreleased + * Made global search indicate the search terms, and match on filenames + + * Added ability to configure bindable user-defined actions; + these can launch arbitrary shell commands + + * Prevented crashes when the daemon disconnects during search + + * X11: added support for font fallbacks to the editor as well + + * X11: fixed that XSettings had to be present + + * X11: fixed a new Fontconfig warning + + +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 +38,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..4fdebbe 100644 --- a/README.adoc +++ b/README.adoc @@ -2,8 +2,8 @@ nncmpp ====== 'nncmpp' is yet another MPD client. Its specialty is running equally well in -the terminal, or as an X11 client--it will provide the same keyboard- and -mouse-friendly interface. +the terminal, or as an X11 client or macOS application--it will provide the same +keyboard- and mouse-friendly interface. This project began its life as a simplified TUI version of Sonata. I had already written a lot of the required code before, so I had the perfect @@ -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 @@ -70,8 +74,8 @@ however, tricky to get consistent results on, so be aware of the following: - Xterm needs `XTerm*metaSendsEscape: true` for the default bindings to work - urxvt's 'vtwheel' plugin sabotages scrolling -The X11 graphical interface is a second-class citizen, so some limitations of -terminals carry over, such as the plain default theme. +The X11 and macOS graphical interfaces are second-class citizens, so some +limitations of terminals carry over, such as the plain default theme. Contributing and Support ------------------------ diff --git a/config.h.in b/config.h.in index 77296dd..f9fbebd 100644 --- a/config.h.in +++ b/config.h.in @@ -10,6 +10,7 @@ #cmakedefine HAVE_RESIZETERM #cmakedefine WITH_FFTW #cmakedefine WITH_PULSE +#cmakedefine WITH_APPKIT #cmakedefine WITH_X11 #endif /* ! CONFIG_H */ diff --git a/liberty b/liberty -Subproject 4c2874649d4b1d2414793d60915d309f0bf6711 +Subproject c0eda1c23fe35e5fcd345874fc27a20ac31352f 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 - 2026, 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. @@ -74,6 +74,9 @@ enum #ifdef WITH_X11 #define LIBERTY_XUI_WANT_X11 #endif // WITH_X11 +#ifdef WITH_APPKIT +#define LIBERTY_XUI_WANT_APPKIT +#endif // WITH_APPKIT #include "liberty/liberty-xui.c" #include <dirent.h> @@ -522,7 +525,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 +817,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 +842,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 +852,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 +1259,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 +1343,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 +1407,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 +1416,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 +1502,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 +1596,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 +1621,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 @@ -1859,7 +1913,7 @@ app_layout_status (struct layout *out) if (!stopped && g.song_elapsed >= 0 && g.song_duration >= 1) app_push (&l, g.ui->gauge (attrs[0])) - ->id = WIDGET_GAUGE; + ->widget_id = WIDGET_GAUGE; else app_push_fill (&l, g.ui->padding (attrs[0], 0, 1)); @@ -1867,7 +1921,7 @@ app_layout_status (struct layout *out) { app_push (&l, g.ui->padding (attrs[0], 1, 1)); app_push (&l, g.ui->label (attrs[0], volume.str)) - ->id = WIDGET_VOLUME; + ->widget_id = WIDGET_VOLUME; } str_free (&volume); @@ -1883,20 +1937,20 @@ app_layout_tabs (struct layout *out) // The help tab is disguised so that it's not too intruding app_push (&l, g.ui->padding (attrs[g.active_tab == g.help_tab], 0.25, 1)) - ->id = WIDGET_TAB; + ->widget_id = WIDGET_TAB; app_push (&l, g.ui->label (attrs[g.active_tab == g.help_tab], APP_TITLE)) - ->id = WIDGET_TAB; + ->widget_id = WIDGET_TAB; // XXX: attrs[0]? app_push (&l, g.ui->padding (attrs[g.active_tab == g.help_tab], 0.5, 1)) - ->id = WIDGET_TAB; + ->widget_id = WIDGET_TAB; int i = 0; LIST_FOR_EACH (struct tab, iter, g.tabs) { struct widget *w = app_push (&l, g.ui->label (attrs[iter == g.active_tab], iter->name)); - w->id = WIDGET_TAB; + w->widget_id = WIDGET_TAB; w->userdata = ++i; } @@ -1907,7 +1961,7 @@ app_layout_tabs (struct layout *out) if (g.spectrum_fd != -1) { app_push (&l, g.ui->spectrum (attrs[0], g.spectrum.bars)) - ->id = WIDGET_SPECTRUM; + ->widget_id = WIDGET_SPECTRUM; } #endif // WITH_FFTW @@ -2009,7 +2063,7 @@ 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->widget_id = WIDGET_LIST; list->height = height; list->width = g_xui.width; @@ -2018,7 +2072,7 @@ app_layout_view (struct layout *out, int height) { struct widget *scrollbar = g.ui->scrollbar (APP_ATTR (SCROLLBAR)); list->width -= scrollbar->width; - app_push (&l, scrollbar)->id = WIDGET_SCROLLBAR; + app_push (&l, scrollbar)->widget_id = WIDGET_SCROLLBAR; } int to_show = MIN ((int) tab->item_count - tab->item_top, @@ -2157,7 +2211,7 @@ app_layout_statusbar (struct layout *out) app_flush_layout (&l, out); LIST_FOR_EACH (struct widget, w, l.head) - w->id = WIDGET_MESSAGE; + w->widget_id = WIDGET_MESSAGE; } else if (g.editor.line) { @@ -2179,10 +2233,10 @@ app_layout_statusbar (struct layout *out) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static struct widget * -app_widget_by_id (int id) +app_widget_by_id (int widget_id) { LIST_FOR_EACH (struct widget, w, g_xui.widgets) - if (w->id == id) + if (w->widget_id == widget_id) return w; return NULL; } @@ -2379,12 +2433,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 +2611,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 +2714,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 +2825,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 +2850,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: @@ -2741,7 +2894,7 @@ enum { APP_KEYMOD_DOUBLE_CLICK = 1 << 15 }; static bool app_process_left_mouse_click (struct widget *w, int x, int y, int modifiers) { - switch (w->id) + switch (w->widget_id) { case WIDGET_BUTTON: app_process_action (w->userdata); @@ -2852,10 +3005,10 @@ app_process_mouse (termo_mouse_event_t type, int x, int y, int button, switch (button) { case 1: - g.ui_dragging = target->id; + g.ui_dragging = target->widget_id; return app_process_left_mouse_click (target, x, y, modifiers); case 4: - switch (target->id) + switch (target->widget_id) { case WIDGET_LIST: return app_process_action (ACTION_SCROLL_UP); @@ -2870,7 +3023,7 @@ app_process_mouse (termo_mouse_event_t type, int x, int y, int button, } break; case 5: - switch (target->id) + switch (target->widget_id) { case WIDGET_LIST: return app_process_action (ACTION_SCROLL_DOWN); @@ -3193,13 +3346,13 @@ static void current_tab_move (int from, int to) { compact_map_t map; - const char *id; + const char *item_id; if (!(map = item_list_get (&g.playlist, from)) - || !(id = compact_map_find (map, "id"))) + || !(item_id = compact_map_find (map, "id"))) return; char *target_str = xstrdup_printf ("%d", to); - mpd_client_send_command (&g.client, "moveid", id, target_str, NULL); + mpd_client_send_command (&g.client, "moveid", item_id, target_str, NULL); free (target_str); } @@ -3241,7 +3394,7 @@ current_tab_on_action (enum action action) compact_map_t map = item_list_get (&g.playlist, tab->item_selected); switch (action) { - const char *id; + const char *item_id; case ACTION_GOTO_PLAYING: if (g.song < 0 || (size_t) g.song >= tab->item_count) return false; @@ -3255,13 +3408,13 @@ current_tab_on_action (enum action action) return current_tab_move_selection (+1); case ACTION_CHOOSE: tab->item_mark = -1; - return map && (id = compact_map_find (map, "id")) - && MPD_SIMPLE ("playid", id); + return map && (item_id = compact_map_find (map, "id")) + && MPD_SIMPLE ("playid", item_id); case ACTION_DESCRIBE: - if (!map || !(id = compact_map_find (map, "file"))) + if (!map || !(item_id = compact_map_find (map, "file"))) return false; - app_show_message (xstrdup ("Path: "), xstrdup (id)); + app_show_message (xstrdup ("Path: "), xstrdup (item_id)); return true; case ACTION_DELETE: { @@ -3274,8 +3427,8 @@ current_tab_on_action (enum action action) for (int i = range.from; i <= range.upto; i++) { if ((map = item_list_get (&g.playlist, i)) - && (id = compact_map_find (map, "id"))) - mpd_client_send_command (c, "deleteid", id, NULL); + && (item_id = compact_map_find (map, "id"))) + mpd_client_send_command (c, "deleteid", item_id, NULL); } mpd_client_list_end (c); mpd_client_add_task (c, mpd_on_simple_response, NULL); @@ -3599,30 +3752,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 +4033,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 +4062,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 +4265,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 +4287,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 +4726,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 +4751,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 +4763,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 +4774,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 +4810,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"); @@ -5066,12 +5256,12 @@ static ssize_t mpd_find_pos_of_id (const char *desired_id) { compact_map_t map; - const char *id; + const char *item_id; for (size_t i = 0; i < g.playlist.len; i++) { if ((map = item_list_get (&g.playlist, i)) - && (id = compact_map_find (map, "id")) - && !strcmp (id, desired_id)) + && (item_id = compact_map_find (map, "id")) + && !strcmp (item_id, desired_id)) return i; } return -1; @@ -5378,7 +5568,7 @@ static struct widget * tui_make_button (chtype attrs, const char *label, enum action a) { struct widget *w = tui_make_label (attrs, 0, label); - w->id = WIDGET_BUTTON; + w->widget_id = WIDGET_BUTTON; w->userdata = a; return w; } @@ -5591,95 +5781,340 @@ static struct app_ui app_tui_ui = .editor = tui_make_editor, }; -// --- X11 --------------------------------------------------------------------- +// --- Shared GUI Icons -------------------------------------------------------- -#ifdef WITH_X11 +#if defined WITH_X11 || defined WITH_APPKIT + +struct app_icon_point +{ + double x; + double y; +}; // On a 20x20 raster to make it feasible to design on paper. -#define X11_STOP {INFINITY, INFINITY} -static const XPointDouble - x11_icon_previous[] = +#define APP_ICON_STOP {INFINITY, INFINITY} +static const struct app_icon_point + app_icon_previous[] = { - {10, 0}, {0, 10}, {10, 20}, X11_STOP, - {20, 0}, {10, 10}, {20, 20}, X11_STOP, X11_STOP, + {10, 0}, {0, 10}, {10, 20}, APP_ICON_STOP, + {20, 0}, {10, 10}, {20, 20}, APP_ICON_STOP, APP_ICON_STOP, }, - x11_icon_pause[] = + app_icon_pause[] = { - {1, 0}, {7, 0}, {7, 20}, {1, 20}, X11_STOP, - {13, 0}, {19, 0}, {19, 20}, {13, 20}, X11_STOP, X11_STOP, + {1, 0}, {7, 0}, {7, 20}, {1, 20}, APP_ICON_STOP, + {13, 0}, {19, 0}, {19, 20}, {13, 20}, APP_ICON_STOP, APP_ICON_STOP, }, - x11_icon_play[] = + app_icon_play[] = { - {0, 0}, {20, 10}, {0, 20}, X11_STOP, X11_STOP, + {0, 0}, {20, 10}, {0, 20}, APP_ICON_STOP, APP_ICON_STOP, }, - x11_icon_stop[] = + app_icon_stop[] = { - {0, 0}, {20, 0}, {20, 20}, {0, 20}, X11_STOP, X11_STOP, + {0, 0}, {20, 0}, {20, 20}, {0, 20}, APP_ICON_STOP, APP_ICON_STOP, }, - x11_icon_next[] = + app_icon_next[] = { - {0, 0}, {10, 10}, {0, 20}, X11_STOP, - {10, 0}, {20, 10}, {10, 20}, X11_STOP, X11_STOP, + {0, 0}, {10, 10}, {0, 20}, APP_ICON_STOP, + {10, 0}, {20, 10}, {10, 20}, APP_ICON_STOP, APP_ICON_STOP, }, - x11_icon_repeat[] = + app_icon_repeat[] = { {0, 12}, {0, 6}, {3, 3}, {13, 3}, {13, 0}, {20, 4.5}, - {13, 9}, {13, 6}, {3, 6}, {3, 10}, X11_STOP, + {13, 9}, {13, 6}, {3, 6}, {3, 10}, APP_ICON_STOP, {0, 15.5}, {7, 11}, {7, 14}, {17, 14}, {17, 10}, {20, 8}, - {20, 14}, {17, 17}, {7, 17}, {7, 20}, X11_STOP, X11_STOP, + {20, 14}, {17, 17}, {7, 17}, {7, 20}, APP_ICON_STOP, APP_ICON_STOP, }, - x11_icon_random[] = + app_icon_random[] = { - {0, 6}, {0, 3}, {5, 3}, {6, 4.5}, {4, 7.5}, {3, 6}, X11_STOP, + {0, 6}, {0, 3}, {5, 3}, {6, 4.5}, {4, 7.5}, {3, 6}, APP_ICON_STOP, {9, 15.5}, {11, 12.5}, {12, 14}, {13, 14}, {13, 11}, {20, 15.5}, - {13, 20}, {13, 17}, {10, 17}, X11_STOP, + {13, 20}, {13, 17}, {10, 17}, APP_ICON_STOP, {0, 17}, {0, 14}, {3, 14}, {10, 3}, {13, 3}, {13, 0}, {20, 4.5}, - {13, 9}, {13, 6}, {12, 6}, {5, 17}, X11_STOP, X11_STOP, + {13, 9}, {13, 6}, {12, 6}, {5, 17}, APP_ICON_STOP, APP_ICON_STOP, }, - x11_icon_single[] = + app_icon_single[] = { {7, 6}, {7, 4}, {9, 2}, {12, 2}, {12, 15}, {14, 15}, {14, 18}, - {7, 18}, {7, 15}, {9, 15}, {9, 6}, X11_STOP, X11_STOP, + {7, 18}, {7, 15}, {9, 15}, {9, 6}, APP_ICON_STOP, APP_ICON_STOP, }, - x11_icon_consume[] = + app_icon_consume[] = { {0, 13}, {0, 7}, {4, 3}, {10, 3}, {14, 7}, {5, 10}, {14, 13}, - {10, 17}, {4, 17}, X11_STOP, - {16, 12}, {16, 8}, {20, 8}, {20, 12}, X11_STOP, X11_STOP, + {10, 17}, {4, 17}, APP_ICON_STOP, + {16, 12}, {16, 8}, {20, 8}, {20, 12}, APP_ICON_STOP, APP_ICON_STOP, }; -static const XPointDouble * -x11_icon_for_action (enum action action) +static const struct app_icon_point * +app_icon_for_action (enum action action) { switch (action) { case ACTION_MPD_PREVIOUS: - return x11_icon_previous; + return app_icon_previous; case ACTION_MPD_TOGGLE: - return g.state == PLAYER_PLAYING ? x11_icon_pause : x11_icon_play; + return g.state == PLAYER_PLAYING ? app_icon_pause : app_icon_play; case ACTION_MPD_STOP: - return x11_icon_stop; + return app_icon_stop; case ACTION_MPD_NEXT: - return x11_icon_next; + return app_icon_next; case ACTION_MPD_REPEAT: - return x11_icon_repeat; + return app_icon_repeat; case ACTION_MPD_RANDOM: - return x11_icon_random; + return app_icon_random; case ACTION_MPD_SINGLE: - return x11_icon_single; + return app_icon_single; case ACTION_MPD_CONSUME: - return x11_icon_consume; + return app_icon_consume; default: return NULL; } } +#endif // WITH_X11 || WITH_APPKIT + +// --- AppKit ------------------------------------------------------------------ + +#ifdef WITH_APPKIT + +static void +appkit_render_button (struct widget *self) +{ + appkit_render_padding (self); + + const struct app_icon_point *icon = app_icon_for_action (self->userdata); + if (!icon) + { + appkit_render_label (self); + return; + } + + NSColor *color = appkit_fg (self); + if (!(self->attrs & A_BOLD)) + { + CGFloat r = 0, g_ = 0, b = 0, a = 1; + NSColor *converted = + [color colorUsingColorSpace:[NSColorSpace deviceRGBColorSpace]]; + if (converted) + { + [converted getRed:&r green:&g_ blue:&b alpha:&a]; + color = [NSColor colorWithDeviceRed:r * 0.5 + green:g_ * 0.5 blue:b * 0.5 alpha:a * 0.5]; + } + } + + [color setFill]; + NSBezierPath *path = [NSBezierPath bezierPath]; + [path setWindingRule:NSWindingRuleEvenOdd]; + + int x = self->x, y = self->y + (self->height - self->width) / 2; + bool started = false; + for (size_t i = 0; ; i++) + { + const struct app_icon_point *p = &icon[i]; + if (isinf (p->x)) + { + if (!started) + break; + [path closePath]; + started = false; + continue; + } + + NSPoint point = NSMakePoint + (x + p->x / 20.0 * self->width, y + p->y / 20.0 * self->width); + if (!started) + [path moveToPoint:point]; + else + [path lineToPoint:point]; + started = true; + } + [path fill]; +} + +static struct widget * +appkit_make_button (chtype attrs, const char *label, enum action a) +{ + struct widget *w = appkit_make_label (attrs, 0, label); + w->widget_id = WIDGET_BUTTON; + w->userdata = a; + + if (app_icon_for_action (a)) + { + w->on_render = appkit_render_button; + + // It should be padded by the caller horizontally. + w->height = g_xui.vunit; + w->width = w->height * 3 / 4; + } + return w; +} + +static void +appkit_render_gauge (struct widget *self) +{ + appkit_render_padding (self); + if (g.state == PLAYER_STOPPED || g.song_elapsed < 0 || g.song_duration < 1) + return; + + int part = (float) g.song_elapsed / g.song_duration * self->width; + [appkit_bg_attrs (APP_ATTR (ELAPSED)) setFill]; + NSRectFill (NSMakeRect + (self->x, self->y + self->height / 8, part, self->height * 3 / 4)); + [appkit_bg_attrs (APP_ATTR (REMAINS)) setFill]; + NSRectFill (NSMakeRect (self->x + part, self->y + self->height / 8, + self->width - part, self->height * 3 / 4)); +} + +static struct widget * +appkit_make_gauge (chtype attrs) +{ + struct widget *w = xcalloc (1, sizeof *w + 1); + w->on_render = appkit_render_gauge; + w->attrs = attrs; + w->width = -1; + w->height = g_xui.vunit; + return w; +} + +static void +appkit_render_spectrum (struct widget *self) +{ + appkit_render_padding (self); + +#ifdef WITH_FFTW + int bars = g.spectrum.bars; + if (bars < 1) + return; + + int step = MAX (1, self->width / bars); + [appkit_fg (self) setFill]; + for (int i = 0; i < bars; i++) + { + int height = round ((self->height - 2) * g.spectrum.spectrum[i]); + NSRectFill (NSMakeRect (self->x + i * step, + self->y + self->height - 1 - height, + step, height)); + } +#endif // WITH_FFTW +} + +static struct widget * +appkit_make_spectrum (chtype attrs, int width) +{ + struct widget *w = xcalloc (1, sizeof *w + 1); + w->on_render = appkit_render_spectrum; + w->attrs = attrs; + w->width = width * g_xui.vunit / 2; + w->height = g_xui.vunit; + return w; +} + +static void +appkit_render_scrollbar (struct widget *self) +{ + appkit_render_padding (self); + + struct tab *tab = g.active_tab; + struct scrollbar bar = + app_compute_scrollbar (tab, app_visible_items_height (), g_xui.vunit); + + [appkit_fg_attrs (self->attrs) setFill]; + NSRectFill (NSMakeRect + (self->x, self->y + bar.start, self->width, bar.length)); +} + +static struct widget * +appkit_make_scrollbar (chtype attrs) +{ + struct widget *w = xcalloc (1, sizeof *w + 1); + w->on_render = appkit_render_scrollbar; + w->attrs = attrs; + w->width = g_xui.vunit / 2; + return w; +} + +static struct widget * +appkit_make_list (void) +{ + struct widget *w = xcalloc (1, sizeof *w + 1); + w->on_render = appkit_render_padding; + return w; +} + +static void +appkit_render_editor (struct widget *self) +{ + appkit_render_padding (self); + + NSFont *font = appkit_widget_font (self); + NSColor *color = appkit_fg (self); + + const struct line_editor *e = &g.editor; + int x = self->x; + if (e->prompt) + { + hard_assert (e->prompt < 127); + x += appkit_font_draw (font, color, x, self->y, + (char[2]) { e->prompt, 0 }, self->width) + g_xui.vunit / 4; + } + + 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 += appkit_font_draw (font, color, x, self->y, a, + MAX (0, self->width - (x - self->x))); + int caret = x; + x += appkit_font_draw (font, color, x, self->y, b, + MAX (0, self->width - (x - self->x))); + free (a); + free (b); + + [color setFill]; + NSRectFill (NSMakeRect (caret, self->y, 2, self->height)); +} + +static struct widget * +appkit_make_editor (chtype attrs) +{ + struct widget *w = xcalloc (1, sizeof *w + 1); + w->on_render = appkit_render_editor; + w->attrs = attrs; + w->width = -1; + w->height = g_xui.vunit; + return w; +} + +static struct app_ui app_appkit_ui = +{ + .padding = appkit_make_padding, + .label = app_make_label, + .button = appkit_make_button, + .gauge = appkit_make_gauge, + .spectrum = appkit_make_spectrum, + .scrollbar = appkit_make_scrollbar, + .list = appkit_make_list, + .editor = appkit_make_editor, + + .have_icons = true, +}; + +#endif // WITH_APPKIT + +// --- X11 --------------------------------------------------------------------- + +#ifdef WITH_X11 + static void x11_render_button (struct widget *self) { x11_render_padding (self); - const XPointDouble *icon = x11_icon_for_action (self->userdata); + const struct app_icon_point *icon = app_icon_for_action (self->userdata); if (!icon) { x11_render_label (self); @@ -5687,8 +6122,15 @@ x11_render_button (struct widget *self) } size_t total = 0; - for (size_t i = 0; icon[i].x != INFINITY || icon[i - 1].x != INFINITY; i++) - total++; + for (; ; total++) + { + if (isinf (icon[total].x) && isinf (icon[total].y) + && isinf (icon[total + 1].x) && isinf (icon[total + 1].y)) + { + total++; + break; + } + } // TODO: There should be an attribute for buttons, to handle this better. XRenderColor color = *x11_fg (self); @@ -5727,10 +6169,10 @@ static struct widget * x11_make_button (chtype attrs, const char *label, enum action a) { struct widget *w = x11_make_label (attrs, 0, label); - w->id = WIDGET_BUTTON; + w->widget_id = WIDGET_BUTTON; w->userdata = a; - if (x11_icon_for_action (a)) + if (app_icon_for_action (a)) { w->on_render = x11_render_button; @@ -5855,28 +6297,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 * @@ -5917,10 +6367,10 @@ static volatile sig_atomic_t g_termination_requested; static volatile sig_atomic_t g_winch_received; static void -signals_postpone_handling (char id) +signals_postpone_handling (char signal_id) { int original_errno = errno; - if (write (g_signal_pipe[1], &id, 1) == -1) + if (write (g_signal_pipe[1], &signal_id, 1) == -1) soft_assert (errno == EAGAIN); errno = original_errno; } @@ -5981,8 +6431,8 @@ app_on_signal_pipe_readable (const struct pollfd *fd, void *user_data) { (void) user_data; - char id = 0; - (void) read (fd->fd, &id, 1); + char signal_id = 0; + (void) read (fd->fd, &signal_id, 1); if (g_termination_requested && !g.quitting) app_quit (); @@ -6082,6 +6532,11 @@ app_init_ui (bool requested_x11) g.ui = &app_x11_ui; else #endif // WITH_X11 +#ifdef WITH_APPKIT + if (g_xui.ui == &appkit_ui) + g.ui = &app_appkit_ui; + else +#endif // WITH_APPKIT g.ui = &app_tui_ui; } @@ -6113,9 +6568,9 @@ main (int argc, char *argv[]) static const struct opt opts[] = { { 'd', "debug", NULL, 0, "run in debug mode" }, -#ifdef WITH_X11 - { 'x', "x11", NULL, 0, "use X11 even when run from a terminal" }, -#endif // WITH_X11 +#if defined WITH_X11 || defined WITH_APPKIT + { 'x', "x11", NULL, 0, "use a graphical frontend even from a terminal" }, +#endif // WITH_X11 || WITH_APPKIT { 'h', "help", NULL, 0, "display this help and exit" }, { 'v', "verbose", NULL, 0, "log messages on standard error" }, { 'V', "version", NULL, 0, "output version information and exit" }, @@ -6133,9 +6588,11 @@ main (int argc, char *argv[]) case 'd': g_debug_mode = true; break; +#if defined WITH_X11 || defined WITH_APPKIT case 'x': requested_x11 = true; break; +#endif // WITH_X11 || WITH_APPKIT case 'v': g_verbose_mode = true; break; diff --git a/termo b/termo -Subproject 2518b53e5ae4579bf84ed58fa7a62806f64e861 +Subproject f9a102456fa6a0b43a916ceaf031f21ea5665e6 |
