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