From 49be552d3cae2f7d0f0b45dcc98e7942f38d61d1 Mon Sep 17 00:00:00 2001 From: Clifford Yapp <238416+starseeker@users.noreply.github.com> Date: Mon, 9 Dec 2024 14:41:23 -0500 Subject: [PATCH] Checkpoint some work on a libbu editor selector We've got this code in a couple places, in one form or another, and there's nothing app or lib specific about it, so try to think about what a canonical implementation would be that could cover all our potential use cases. At least one of the current implementations also encapsulates logic to launch command line programs with xterm, but that feels like it should be a separate consideration so leaving it out of this function. Not hooking it up to anything yet - need to write unit tests and try on multiple platforms first. --- include/bu/env.h | 45 +++++++++++++++++ src/libbu/env.c | 126 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 171 insertions(+) diff --git a/include/bu/env.h b/include/bu/env.h index f51df7f6a2b..bd94891f3b9 100644 --- a/include/bu/env.h +++ b/include/bu/env.h @@ -54,6 +54,51 @@ BU_EXPORT extern int bu_setenv(const char *name, const char *value, int overwrit */ BU_EXPORT extern ssize_t bu_mem(int type, size_t *sz); + +/** + * Select an editor. + * + * Returns a string naming an editor to be used. If an option is needed for + * invoking the editor, it is supplied via the editor_opt output. + * + * By default editors requiring a graphical environment will be considered - to + * avoid this, supply 1 to the no_gui input parameter. + * + * If the EDITOR environment variable is set, that takes priority. If EDITOR + * holds a full, valid path it will be used as-is. If not, bu_which will + * attempt to find the full path to $EDITOR. If that fails, $EDITOR will be + * used as-is without modification and it will be up to the user's environment + * to make it succeed at runtime. If EDITOR is unset, then libbu will attempt + * to find a viable editor option by looking for various common editors. + * + * If the optional check_for_editors array is provided, libbu will first + * attempt to use the contents of that array to find an editor. Unlike EDITOR, + * the list contents WILL be checked. The main purpose of check_for_editors is + * to allow applications to define their own preferred precedence order in case + * there are specific advantages to using some editors over others. It is also + * useful if an app wishes to list some specialized editor not part of the + * normal listings. If an application wishes to use ONLY a check_for_editors + * list and not fall back on libbu's internal list if it fails, they should + * assign the last entry of check_for_editors to be NULL to signal libbu to + * stop looking for an editor there: + * + * int check_for_cnt = 3; const char *check_for_editors[3] = {"editor1", "editor2", NULL}; + * + * We are deliberately NOT documenting the libbu's own internal editor list as + * public API, nor do we make any guarantees about the presence of any editor + * that is on the list will take relative to other editors. What editors are + * popular in various environments can change over time, and the purpose of + * this function is to provide *some* editor, rather than locking in any + * particular precedence. check_for_editors should be used if an app needs + * more guaranteed stability in lookup behaviors. + * + * Caller should NOT free either the main string return from bu_editor or the + * pointer assigned to editor_opt. They may both change contents from one + * call to the next, so caller should duplicate the string outputs if they + * wish to preserve them beyond the next bu_editor call. + */ +BU_EXPORT const char *bu_editor(const char **editor_opt, int no_gui, int check_for_cnt, const char **check_for_editors); + /** @} */ __END_DECLS diff --git a/src/libbu/env.c b/src/libbu/env.c index f4bc4bca3ea..aae876ef4e0 100644 --- a/src/libbu/env.c +++ b/src/libbu/env.c @@ -53,8 +53,11 @@ # include #endif +#include "bu/app.h" #include "bu/env.h" +#include "bu/file.h" #include "bu/malloc.h" +#include "bu/str.h" /* strict c89 doesn't declare setenv() */ #ifndef HAVE_DECL_SETENV @@ -322,6 +325,129 @@ bu_mem(int type, size_t *sz) return -1; } +/* editors to test, in order of discovery preference (EDITOR overrides) */ +#define WIN_EDITOR "\"c:/Program Files/Windows NT/Accessories/wordpad\"" +#define MAC_EDITOR "/Applications/TextEdit.app/Contents/MacOS/TextEdit" +#define EMACS_EDITOR "emacs" +#define GVIM_EDITOR "gvim" +#define KATE_EDITOR "kate" +#define GEDIT_EDITOR "gedit" +#define VIM_EDITOR "vim" +#define VI_EDITOR "vi" +#define NANO_EDITOR "nano" +#define MICRO_EDITOR "micro" + +// TODO - long ago, BRL-CAD bundled jove to always guarantee basic text editing +// capabilities. We haven't done that for a while (jove didn't end up getting +// much development momentum), but maybe bext's availability would make it +// worth considering including a last-resort fallback again. Particular +// motivation this time around is the lack on Windows of a built-in console +// editor - all the solutions seem to involved requiring the user be able to +// install a 3rd party solution themselves with something like winget. +// Apparently this can be a real nuisance for people doing remote ssh into to +// Windows servers. Microsoft seems to be considering proving a default CLI +// editor again, similar to what the used to provide with edit.exe, but that +// doesn't seem to have materialized yet. See discussion at +// https://github.com/microsoft/terminal/discussions/16440 +// +// https://github.com/malxau/yori does already implement a modern MIT licensed +// clone of the old edit.exe editor which would have been a fallback on older +// Windows systems - even if it doesn't ultimately get included in newer +// Windows systems, we may be able to build the necessary pieces in bext to +// provide yedit.exe ourselves... As far as I know every other environment we +// target has at least vi available by default - Windows is the outlier. + +const char * +bu_editor(const char **editor_opt, int no_gui, int check_for_cnt, const char **check_for_editors) +{ + int i; + static char bu_editor[MAXPATHLEN] = {0}; + static char bu_editor_opt[MAXPATHLEN] = {0}; + static char bu_editor_tmp[MAXPATHLEN] = {0}; + const char *which_str = NULL; + const char *e_str = NULL; + const char *gui_editor_list[] = { + WIN_EDITOR, MAC_EDITOR, EMACS_EDITOR, GVIM_EDITOR, GEDIT_EDITOR, KATE_EDITOR, NULL + }; + const char *nongui_editor_list[] = { + EMACS_EDITOR, VIM_EDITOR, VI_EDITOR, NANO_EDITOR, MICRO_EDITOR, NULL + }; + const char **editor_list = (no_gui) ? nongui_editor_list : gui_editor_list; + + + // EDITOR environment variable takes precedence, if set + const char *env_editor = getenv("EDITOR"); + if (env_editor && env_editor[0] != '\0') { + // If EDITOR is a full, valid path we are done + if (bu_file_exists(env_editor, NULL)) { + bu_strlcpy(bu_editor, env_editor, MAXPATHLEN); + goto do_opt; + } + // Doesn't exist as-is - try bu_which + which_str = bu_which(env_editor); + if (which_str) { + bu_strlcpy(bu_editor, which_str, MAXPATHLEN); + goto do_opt; + } + // Neither of the above worked - just pass through $EDITOR as-is + bu_strlcpy(bu_editor, env_editor, MAXPATHLEN); + goto do_opt; + } + + // The app wants us to check some things, before investigating our default + // set - handle them first. + if (check_for_cnt && check_for_editors) { + for (i = 0; i < check_for_cnt; i++) { + // If it exists as-is, go with that. + if (bu_file_exists(check_for_editors[i], NULL)) { + bu_strlcpy(bu_editor, check_for_editors[i], MAXPATHLEN); + goto do_opt; + } + // Doesn't exist as-is - try bu_which + which_str = bu_which(env_editor); + if (which_str) { + bu_strlcpy(bu_editor, which_str, MAXPATHLEN); + goto do_opt; + } + } + } + + // No environment variable and no application-provided list - use + // our internal list + i = 0; + e_str = editor_list[i]; + while (e_str) { + which_str = bu_which(e_str); + if (which_str) { + bu_strlcpy(bu_editor, which_str, MAXPATHLEN); + goto do_opt; + } + e_str = editor_list[i++]; + } + + // If we have nothing after all that, we're done + if (editor_opt) + *editor_opt = NULL; + return NULL; + +do_opt: + // If the caller didn't supply an option pointer, just return the editor + // string + if (!editor_opt) + return (const char *)bu_editor; + + // Supply any options needed (normally graphical editor needing to be + // non-graphical due to no_gui being set, for example.) + + snprintf(bu_editor_tmp, MAXPATHLEN, bu_which("emacs")); + if (BU_STR_EQUAL(bu_editor, bu_editor_tmp) && no_gui) { + // Non-graphical emacs requires an option + sprintf(bu_editor_opt, "-nw"); + } + + return (const char *)bu_editor; +} + /* * Local Variables: * tab-width: 8