Allow line editing with VISUAL/EDITOR/vi
Let's pray I haven't broken anything so far.
This commit is contained in:
parent
dd9bfbe37e
commit
e0aa42fb99
386
json-rpc-shell.c
386
json-rpc-shell.c
@ -136,6 +136,8 @@ struct input
|
||||
|
||||
/// Process a single line input by the user
|
||||
void (*on_input) (char *line, void *user_data);
|
||||
/// User requested external line editing
|
||||
void (*on_run_editor) (const char *line, void *user_data);
|
||||
};
|
||||
|
||||
struct input_vtable
|
||||
@ -144,6 +146,8 @@ struct input_vtable
|
||||
void (*start) (struct input *input, const char *program_name);
|
||||
/// Stop the interface
|
||||
void (*stop) (struct input *input);
|
||||
/// Prepare or unprepare terminal for our needs
|
||||
void (*prepare) (struct input *input, bool enabled);
|
||||
/// Destroy the object
|
||||
void (*destroy) (struct input *input);
|
||||
|
||||
@ -153,6 +157,8 @@ struct input_vtable
|
||||
void (*show) (struct input *input);
|
||||
/// Change the prompt string; takes ownership
|
||||
void (*set_prompt) (struct input *input, char *prompt);
|
||||
/// Change the current line input
|
||||
bool (*replace_line) (struct input *input, const char *line);
|
||||
/// Ring the terminal bell
|
||||
void (*ding) (struct input *input);
|
||||
|
||||
@ -225,6 +231,26 @@ input_rl_on_input (char *line)
|
||||
self->prompt_shown++;
|
||||
}
|
||||
|
||||
static int
|
||||
input_rl_on_run_editor (int count, int key)
|
||||
{
|
||||
(void) count;
|
||||
(void) key;
|
||||
|
||||
struct input_rl *self = g_input_rl;
|
||||
if (self->super.on_run_editor)
|
||||
self->super.on_run_editor (rl_line_buffer, self->super.user_data);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
input_rl_on_startup (void)
|
||||
{
|
||||
rl_add_defun ("run-editor", input_rl_on_run_editor, -1);
|
||||
rl_bind_keyseq ("\\ee", rl_named_function ("run-editor"));
|
||||
return 0;
|
||||
}
|
||||
|
||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
static void
|
||||
@ -237,6 +263,7 @@ input_rl_start (struct input *input, const char *program_name)
|
||||
|
||||
const char *slash = strrchr (program_name, '/');
|
||||
rl_readline_name = slash ? ++slash : program_name;
|
||||
rl_startup_hook = input_rl_on_startup;
|
||||
rl_catch_sigwinch = false;
|
||||
|
||||
hard_assert (self->prompt != NULL);
|
||||
@ -262,6 +289,17 @@ input_rl_stop (struct input *input)
|
||||
g_input_rl = NULL;
|
||||
}
|
||||
|
||||
static void
|
||||
input_rl_prepare (struct input *input, bool enabled)
|
||||
{
|
||||
(void) input;
|
||||
|
||||
if (enabled)
|
||||
rl_prep_terminal (true);
|
||||
else
|
||||
rl_deprep_terminal ();
|
||||
}
|
||||
|
||||
static void
|
||||
input_rl_destroy (struct input *input)
|
||||
{
|
||||
@ -332,6 +370,20 @@ input_rl_set_prompt (struct input *input, char *prompt)
|
||||
rl_redisplay ();
|
||||
}
|
||||
|
||||
static bool
|
||||
input_rl_replace_line (struct input *input, const char *line)
|
||||
{
|
||||
struct input_rl *self = (struct input_rl *) input;
|
||||
if (!self->active || self->prompt_shown < 1)
|
||||
return false;
|
||||
|
||||
rl_point = rl_mark = 0;
|
||||
rl_replace_line (line, false);
|
||||
rl_point = strlen (line);
|
||||
rl_redisplay ();
|
||||
return true;
|
||||
}
|
||||
|
||||
static void
|
||||
input_rl_ding (struct input *input)
|
||||
{
|
||||
@ -392,11 +444,13 @@ static struct input_vtable input_rl_vtable =
|
||||
{
|
||||
.start = input_rl_start,
|
||||
.stop = input_rl_stop,
|
||||
.prepare = input_rl_prepare,
|
||||
.destroy = input_rl_destroy,
|
||||
|
||||
.hide = input_rl_hide,
|
||||
.show = input_rl_show,
|
||||
.set_prompt = input_rl_set_prompt,
|
||||
.replace_line = input_rl_replace_line,
|
||||
.ding = input_rl_ding,
|
||||
|
||||
.load_history = input_rl_load_history,
|
||||
@ -542,6 +596,22 @@ input_el_on_return (EditLine *editline, int key)
|
||||
return CC_NEWLINE;
|
||||
}
|
||||
|
||||
static unsigned char
|
||||
input_el_on_run_editor (EditLine *editline, int key)
|
||||
{
|
||||
(void) key;
|
||||
|
||||
struct input_el *self;
|
||||
el_get (editline, EL_CLIENTDATA, &self);
|
||||
|
||||
const LineInfo *info = el_line (editline);
|
||||
char *line = xstrndup (info->buffer, info->lastchar - info->buffer);
|
||||
if (self->super.on_run_editor)
|
||||
self->super.on_run_editor (line, self->super.user_data);
|
||||
free (line);
|
||||
return CC_NORM;
|
||||
}
|
||||
|
||||
static void
|
||||
input_el_install_prompt (struct input_el *self)
|
||||
{
|
||||
@ -574,6 +644,11 @@ input_el_start (struct input *input, const char *program_name)
|
||||
"send-line", "Send line", input_el_on_return);
|
||||
el_set (self->editline, EL_BIND, "\n", "send-line", NULL);
|
||||
|
||||
// It's probably better to handle this ourselves
|
||||
el_set (self->editline, EL_ADDFN,
|
||||
"run-editor", "Run editor to edit line", input_el_on_run_editor);
|
||||
el_set (self->editline, EL_BIND, "M-e", "run-editor", NULL);
|
||||
|
||||
// Source the user's defaults file
|
||||
el_source (self->editline, NULL);
|
||||
|
||||
@ -595,6 +670,13 @@ input_el_stop (struct input *input)
|
||||
self->prompt_shown = 0;
|
||||
}
|
||||
|
||||
static void
|
||||
input_el_prepare (struct input *input, bool enabled)
|
||||
{
|
||||
struct input_el *self = (struct input_el *) input;
|
||||
el_set (self->editline, EL_PREP_TERM, enabled);
|
||||
}
|
||||
|
||||
static void
|
||||
input_el_destroy (struct input *input)
|
||||
{
|
||||
@ -667,6 +749,24 @@ input_el_set_prompt (struct input *input, char *prompt)
|
||||
input_el_redisplay (self);
|
||||
}
|
||||
|
||||
static bool
|
||||
input_el_replace_line (struct input *input, const char *line)
|
||||
{
|
||||
struct input_el *self = (struct input_el *) input;
|
||||
if (!self->active || self->prompt_shown < 1)
|
||||
return false;
|
||||
|
||||
const LineInfoW *info = el_wline (self->editline);
|
||||
int len = info->lastchar - info->buffer;
|
||||
int point = info->cursor - info->buffer;
|
||||
el_cursor (self->editline, len - point);
|
||||
el_wdeletestr (self->editline, len);
|
||||
|
||||
bool success = !*line || !el_insertstr (self->editline, line);
|
||||
input_el_redisplay (self);
|
||||
return success;
|
||||
}
|
||||
|
||||
static void
|
||||
input_el_ding (struct input *input)
|
||||
{
|
||||
@ -761,11 +861,13 @@ static struct input_vtable input_el_vtable =
|
||||
{
|
||||
.start = input_el_start,
|
||||
.stop = input_el_stop,
|
||||
.prepare = input_el_prepare,
|
||||
.destroy = input_el_destroy,
|
||||
|
||||
.hide = input_el_hide,
|
||||
.show = input_el_show,
|
||||
.set_prompt = input_el_set_prompt,
|
||||
.replace_line = input_el_replace_line,
|
||||
.ding = input_el_ding,
|
||||
|
||||
.load_history = input_el_load_history,
|
||||
@ -801,11 +903,18 @@ enum color_mode
|
||||
|
||||
static struct app_context
|
||||
{
|
||||
ev_child child_watcher; ///< SIGCHLD watcher
|
||||
ev_signal winch_watcher; ///< SIGWINCH watcher
|
||||
ev_signal term_watcher; ///< SIGTERM watcher
|
||||
ev_signal int_watcher; ///< SIGINT watcher
|
||||
ev_io tty_watcher; ///< Terminal watcher
|
||||
|
||||
struct input *input; ///< Input interface
|
||||
char *attrs_defaults[ATTR_COUNT]; ///< Default terminal attributes
|
||||
char *attrs[ATTR_COUNT]; ///< Terminal attributes
|
||||
|
||||
struct backend *backend; ///< Our current backend
|
||||
char *editor_filename; ///< File for input line editor
|
||||
|
||||
struct config config; ///< Program configuration
|
||||
enum color_mode color_mode; ///< Colour output mode
|
||||
@ -2574,10 +2683,219 @@ fail:
|
||||
free (input);
|
||||
}
|
||||
|
||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
// The ability to use an external editor on the input line has been shamelessly
|
||||
// copypasted from degesch with minor changes only.
|
||||
|
||||
static void
|
||||
suspend_terminal (struct app_context *ctx)
|
||||
{
|
||||
ctx->input->vtable->hide (ctx->input);
|
||||
ev_io_stop (EV_DEFAULT_ &ctx->tty_watcher);
|
||||
ctx->input->vtable->prepare (ctx->input, false);
|
||||
}
|
||||
|
||||
static void
|
||||
resume_terminal (struct app_context *ctx)
|
||||
{
|
||||
ctx->input->vtable->prepare (ctx->input, true);
|
||||
ev_io_start (EV_DEFAULT_ &ctx->tty_watcher);
|
||||
ctx->input->vtable->show (ctx->input);
|
||||
}
|
||||
|
||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
/// This differs from the non-unique version in that we expect the filename
|
||||
/// to be something like a pattern for mkstemp(), so the resulting path can
|
||||
/// reside in a system-wide directory with no risk of a conflict.
|
||||
static char *
|
||||
resolve_relative_runtime_unique_filename (const char *filename)
|
||||
{
|
||||
struct str path;
|
||||
str_init (&path);
|
||||
|
||||
const char *runtime_dir = getenv ("XDG_RUNTIME_DIR");
|
||||
if (runtime_dir && *runtime_dir == '/')
|
||||
str_append (&path, runtime_dir);
|
||||
else
|
||||
str_append (&path, "/tmp");
|
||||
str_append_printf (&path, "/%s/%s", PROGRAM_NAME, filename);
|
||||
|
||||
// Try to create the file's ancestors;
|
||||
// typically the user will want to immediately create a file in there
|
||||
const char *last_slash = strrchr (path.str, '/');
|
||||
if (last_slash && last_slash != path.str)
|
||||
{
|
||||
char *copy = xstrndup (path.str, last_slash - path.str);
|
||||
(void) mkdir_with_parents (copy, NULL);
|
||||
free (copy);
|
||||
}
|
||||
return str_steal (&path);
|
||||
}
|
||||
|
||||
static bool
|
||||
xwrite (int fd, const char *data, size_t len, struct error **e)
|
||||
{
|
||||
size_t written = 0;
|
||||
while (written < len)
|
||||
{
|
||||
ssize_t res = write (fd, data + written, len - written);
|
||||
if (res >= 0)
|
||||
written += res;
|
||||
else if (errno != EINTR)
|
||||
FAIL ("%s", strerror (errno));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
dump_line_to_file (const char *line, char *template, struct error **e)
|
||||
{
|
||||
int fd = mkstemp (template);
|
||||
if (fd < 0)
|
||||
FAIL ("%s", strerror (errno));
|
||||
|
||||
bool success = xwrite (fd, line, strlen (line), e);
|
||||
if (!success)
|
||||
(void) unlink (template);
|
||||
|
||||
xclose (fd);
|
||||
return success;
|
||||
}
|
||||
|
||||
static char *
|
||||
try_dump_line_to_file (const char *line)
|
||||
{
|
||||
char *template = resolve_filename
|
||||
("input.XXXXXX", resolve_relative_runtime_unique_filename);
|
||||
|
||||
struct error *e = NULL;
|
||||
if (dump_line_to_file (line, template, &e))
|
||||
return template;
|
||||
|
||||
print_error ("%s: %s",
|
||||
"failed to create a temporary file for editing", e->message);
|
||||
error_free (e);
|
||||
free (template);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static pid_t
|
||||
spawn_helper_child (struct app_context *ctx)
|
||||
{
|
||||
suspend_terminal (ctx);
|
||||
pid_t child = fork ();
|
||||
switch (child)
|
||||
{
|
||||
case -1:
|
||||
{
|
||||
int saved_errno = errno;
|
||||
resume_terminal (ctx);
|
||||
errno = saved_errno;
|
||||
break;
|
||||
}
|
||||
case 0:
|
||||
// Put the child in a new foreground process group
|
||||
hard_assert (setpgid (0, 0) != -1);
|
||||
hard_assert (tcsetpgrp (STDOUT_FILENO, getpgid (0)) != -1);
|
||||
break;
|
||||
default:
|
||||
// Make sure of it in the parent as well before continuing
|
||||
(void) setpgid (child, child);
|
||||
}
|
||||
return child;
|
||||
}
|
||||
|
||||
static void
|
||||
run_editor (const char *line, void *user_data)
|
||||
{
|
||||
struct app_context *ctx = user_data;
|
||||
hard_assert (!ctx->editor_filename);
|
||||
|
||||
char *filename;
|
||||
if (!(filename = try_dump_line_to_file (line)))
|
||||
return;
|
||||
|
||||
const char *command;
|
||||
if (!(command = getenv ("VISUAL"))
|
||||
&& !(command = getenv ("EDITOR")))
|
||||
command = "vi";
|
||||
|
||||
switch (spawn_helper_child (ctx))
|
||||
{
|
||||
case 0:
|
||||
execlp (command, command, filename, NULL);
|
||||
print_error ("%s: %s", "failed to launch editor", strerror (errno));
|
||||
_exit (EXIT_FAILURE);
|
||||
case -1:
|
||||
print_error ("%s: %s", "failed to launch editor", strerror (errno));
|
||||
free (filename);
|
||||
break;
|
||||
default:
|
||||
ctx->editor_filename = filename;
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
process_edited_input (struct app_context *ctx)
|
||||
{
|
||||
struct str input;
|
||||
str_init (&input);
|
||||
|
||||
struct error *e = NULL;
|
||||
if (!read_file (ctx->editor_filename, &input, &e))
|
||||
{
|
||||
print_error ("%s: %s", "input editing failed", e->message);
|
||||
error_free (e);
|
||||
}
|
||||
else if (!ctx->input->vtable->replace_line (ctx->input, input.str))
|
||||
print_error ("%s: %s", "input editing failed",
|
||||
"could not re-insert modified text");
|
||||
|
||||
if (unlink (ctx->editor_filename))
|
||||
print_error ("could not unlink `%s': %s",
|
||||
ctx->editor_filename, strerror (errno));
|
||||
|
||||
free (ctx->editor_filename);
|
||||
ctx->editor_filename = NULL;
|
||||
str_free (&input);
|
||||
}
|
||||
|
||||
static void
|
||||
on_child (EV_P_ ev_child *handle, int revents)
|
||||
{
|
||||
(void) revents;
|
||||
struct app_context *ctx = ev_userdata (loop);
|
||||
|
||||
// I am not a shell, stopping not allowed
|
||||
int status = handle->rstatus;
|
||||
if (WIFSTOPPED (status)
|
||||
|| WIFCONTINUED (status))
|
||||
{
|
||||
kill (-handle->rpid, SIGKILL);
|
||||
return;
|
||||
}
|
||||
// I don't recognize this child (we should also check PID)
|
||||
if (!ctx->editor_filename)
|
||||
return;
|
||||
|
||||
hard_assert (tcsetpgrp (STDOUT_FILENO, getpgid (0)) != -1);
|
||||
resume_terminal (ctx);
|
||||
|
||||
if (WIFSIGNALED (status))
|
||||
print_error ("editor died from signal %d", WTERMSIG (status));
|
||||
else if (WIFEXITED (status) && WEXITSTATUS (status) != 0)
|
||||
print_error ("editor returned status %d", WEXITSTATUS (status));
|
||||
else
|
||||
process_edited_input (ctx);
|
||||
}
|
||||
|
||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
static void
|
||||
on_winch (EV_P_ ev_signal *handle, int revents)
|
||||
{
|
||||
(void) loop;
|
||||
(void) handle;
|
||||
(void) revents;
|
||||
|
||||
@ -2605,6 +2923,39 @@ on_tty_readable (EV_P_ ev_io *handle, int revents)
|
||||
ctx->input->vtable->on_tty_readable (ctx->input);
|
||||
}
|
||||
|
||||
static void
|
||||
init_watchers (struct app_context *ctx)
|
||||
{
|
||||
if (!EV_DEFAULT)
|
||||
exit_fatal ("libev initialization failed");
|
||||
|
||||
// So that if the remote end closes the connection, attempts to write to
|
||||
// the socket don't terminate the program
|
||||
(void) signal (SIGPIPE, SIG_IGN);
|
||||
|
||||
// So that we can write to the terminal while we're running a backlog
|
||||
// helper. This is also inherited by the child so that it doesn't stop
|
||||
// when it calls tcsetpgrp().
|
||||
(void) signal (SIGTTOU, SIG_IGN);
|
||||
|
||||
ev_child_init (&ctx->child_watcher, on_child, 0, true);
|
||||
ev_child_start (EV_DEFAULT_ &ctx->child_watcher);
|
||||
|
||||
ev_signal_init (&ctx->winch_watcher, on_winch, SIGWINCH);
|
||||
ev_signal_start (EV_DEFAULT_ &ctx->winch_watcher);
|
||||
|
||||
ev_signal_init (&ctx->term_watcher, on_terminated, SIGTERM);
|
||||
ev_signal_start (EV_DEFAULT_ &ctx->term_watcher);
|
||||
|
||||
ev_signal_init (&ctx->int_watcher, on_terminated, SIGINT);
|
||||
ev_signal_start (EV_DEFAULT_ &ctx->int_watcher);
|
||||
|
||||
ev_io_init (&ctx->tty_watcher, on_tty_readable, STDIN_FILENO, EV_READ);
|
||||
ev_io_start (EV_DEFAULT_ &ctx->tty_watcher);
|
||||
}
|
||||
|
||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
static void
|
||||
parse_program_arguments (struct app_context *ctx, int argc, char **argv,
|
||||
char **origin, char **endpoint)
|
||||
@ -2762,6 +3113,7 @@ main (int argc, char *argv[])
|
||||
g_ctx.input = input_new ();
|
||||
g_ctx.input->user_data = &g_ctx;
|
||||
g_ctx.input->on_input = process_input;
|
||||
g_ctx.input->on_run_editor = run_editor;
|
||||
|
||||
char *history_path =
|
||||
xstrdup_printf ("%s/" PROGRAM_NAME "/history", data_home);
|
||||
@ -2781,35 +3133,11 @@ main (int argc, char *argv[])
|
||||
INPUT_END_IGNORE));
|
||||
}
|
||||
|
||||
// So that if the remote end closes the connection, attempts to write to
|
||||
// the socket don't terminate the program
|
||||
(void) signal (SIGPIPE, SIG_IGN);
|
||||
|
||||
struct ev_loop *loop = EV_DEFAULT;
|
||||
if (!loop)
|
||||
exit_fatal ("libev initialization failed");
|
||||
|
||||
ev_signal winch_watcher;
|
||||
ev_signal term_watcher;
|
||||
ev_signal int_watcher;
|
||||
ev_io tty_watcher;
|
||||
|
||||
ev_signal_init (&winch_watcher, on_winch, SIGWINCH);
|
||||
ev_signal_start (EV_DEFAULT_ &winch_watcher);
|
||||
|
||||
ev_signal_init (&term_watcher, on_terminated, SIGTERM);
|
||||
ev_signal_start (EV_DEFAULT_ &term_watcher);
|
||||
|
||||
ev_signal_init (&int_watcher, on_terminated, SIGINT);
|
||||
ev_signal_start (EV_DEFAULT_ &int_watcher);
|
||||
|
||||
ev_io_init (&tty_watcher, on_tty_readable, STDIN_FILENO, EV_READ);
|
||||
ev_io_start (EV_DEFAULT_ &tty_watcher);
|
||||
|
||||
init_watchers (&g_ctx);
|
||||
g_ctx.input->vtable->start (g_ctx.input, PROGRAM_NAME);
|
||||
|
||||
ev_set_userdata (loop, &g_ctx);
|
||||
ev_run (loop, 0);
|
||||
ev_set_userdata (EV_DEFAULT_ &g_ctx);
|
||||
ev_run (EV_DEFAULT_ 0);
|
||||
|
||||
// User has terminated the program, let's save the history and clean up
|
||||
struct error *e = NULL;
|
||||
@ -2833,6 +3161,6 @@ main (int argc, char *argv[])
|
||||
iconv_close (g_ctx.term_to_utf8);
|
||||
config_free (&g_ctx.config);
|
||||
free_terminal ();
|
||||
ev_loop_destroy (loop);
|
||||
ev_loop_destroy (EV_DEFAULT);
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user