diff --git a/include/bu/env.h b/include/bu/env.h index f51df7f6a2..bd94891f3b 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 f4bc4bca3e..aae876ef4e 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