Skip to content

Commit

Permalink
Checkpoint some work on a libbu editor selector
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
starseeker committed Dec 9, 2024
1 parent d7f01ce commit 49be552
Show file tree
Hide file tree
Showing 2 changed files with 171 additions and 0 deletions.
45 changes: 45 additions & 0 deletions include/bu/env.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
126 changes: 126 additions & 0 deletions src/libbu/env.c
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,11 @@
# include <mach/mach_host.h>
#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
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 49be552

Please sign in to comment.