From e0aa42fb994b4c0b74f7d5a34d80cf4586151721 Mon Sep 17 00:00:00 2001
From: Přemysl Janouch
Date: Sat, 26 Dec 2015 04:10:17 +0100
Subject: Allow line editing with VISUAL/EDITOR/vi
Let's pray I haven't broken anything so far.
---
json-rpc-shell.c | 386 ++++++++++++++++++++++++++++++++++++++++++++++++++-----
1 file changed, 357 insertions(+), 29 deletions(-)
diff --git a/json-rpc-shell.c b/json-rpc-shell.c
index 0e90e01..8bf50bd 100644
--- a/json-rpc-shell.c
+++ b/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;
}
--
cgit v1.2.3-70-g09d2