From e101afab380d3a2253dd726719c1057191bbd6d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=99emysl=20Janouch?= Date: Fri, 25 Dec 2015 04:21:18 +0100 Subject: [PATCH] degesch: allow launching an editor for input Useful for editing multiline text (such as making it single-line). Some refactoring and cleanup. --- NEWS | 2 + common.c | 43 +++++++++ degesch.c | 254 +++++++++++++++++++++++++++++++++++++++++++----------- 3 files changed, 251 insertions(+), 48 deletions(-) diff --git a/NEWS b/NEWS index 97de82a..8c4fb3b 100644 --- a/NEWS +++ b/NEWS @@ -11,6 +11,8 @@ * degesch: libedit backend works again + * degesch: added capability to edit the input line using VISUAL/EDITOR + * degesch: correctly respond to stopping and resuming (SIGTSTP) * degesch: fixed decoding of text formatting diff --git a/common.c b/common.c index 8470171..0d34591 100644 --- a/common.c +++ b/common.c @@ -54,6 +54,49 @@ str_vector_find (const struct str_vector *v, const char *s) return -1; } +/// 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; +} + // --- Logging ----------------------------------------------------------------- static void diff --git a/degesch.c b/degesch.c index 287a60d..09f5d69 100644 --- a/degesch.c +++ b/degesch.c @@ -149,8 +149,6 @@ struct input int saved_mark; ///< Saved mark #elif defined HAVE_EDITLINE EditLine *editline; ///< The EditLine object - char *(*saved_prompt) (EditLine *); ///< Saved prompt function - char saved_char; ///< Saved char for the prompt #endif // HAVE_EDITLINE char *prompt; ///< The prompt we use @@ -220,14 +218,22 @@ input_set_prompt (struct input *self, char *prompt) rl_redisplay (); } +static void +input_erase_content (struct input *self) +{ + (void) self; + + rl_replace_line ("", false); + rl_redisplay (); +} + static void input_erase (struct input *self) { (void) self; rl_set_prompt (""); - rl_replace_line ("", false); - rl_redisplay (); + input_erase_content (self); } static void @@ -269,6 +275,13 @@ input_insert (struct input *self, const char *s) return true; } +static char * +input_get_content (struct input *self) +{ + (void) self; + return rl_copy_text (0, rl_end); +} + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static int app_readline_init (void); @@ -560,29 +573,39 @@ input_make_empty_prompt (EditLine *editline) } static void -input_erase (struct input *self) +input_erase_content (struct input *self) { 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); - - // XXX: this doesn't seem to save the escape character - el_get (self->editline, EL_PROMPT, &self->saved_prompt, &self->saved_char); - el_set (self->editline, EL_PROMPT, input_make_empty_prompt); input_redisplay (self); } +static void +input_erase (struct input *self) +{ + el_set (self->editline, EL_PROMPT, input_make_empty_prompt); + input_erase_content (self); +} + static bool input_insert (struct input *self, const char *s) { - bool success = !el_insertstr (self->editline, s); + bool success = !*s || !el_insertstr (self->editline, s); if (self->prompt_shown > 0) input_redisplay (self); return success; } +static char * +input_get_content (struct input *self) +{ + const LineInfo *info = el_line (self->editline); + return xstrndup (info->buffer, info->lastchar - info->buffer); +} + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static void @@ -699,8 +722,7 @@ input_show (struct input *self) return; input_restore (self); - // Would have used "saved_char" but it doesn't seem to work. - // And it doesn't even when it does anyway (it seems to just strip it). + // XXX: the ignore doesn't quite work, see https://gnats.netbsd.org/47539 el_set (self->editline, EL_PROMPT_ESC, input_make_prompt, INPUT_START_IGNORE); input_redisplay (self); @@ -1518,6 +1540,8 @@ struct app_context struct str input_buffer; ///< Buffered pasted content bool running_backlog_helper; ///< Running a backlog helper + bool running_editor; ///< Running editor for the input + char *editor_filename; ///< The file being edited by user int terminal_suspended; ///< Terminal suspension level struct plugin *plugins; ///< Loaded plugins @@ -1624,6 +1648,8 @@ app_context_free (struct app_context *self) input_free (&self->input); str_free (&self->input_buffer); + + free (self->editor_filename); } static void refresh_prompt (struct app_context *ctx); @@ -10107,47 +10133,52 @@ jump_to_buffer (struct app_context *ctx, int n) return true; } -static void -exec_backlog_helper (const char *command, FILE *backlog) +static pid_t +spawn_helper_child (struct app_context *ctx) { - dup2 (fileno (backlog), STDIN_FILENO); - - // Put the child into a new foreground process group - hard_assert (setpgid (0, 0)!= -1); - hard_assert (tcsetpgrp (STDOUT_FILENO, getpgid (0)) != -1); - - execl ("/bin/sh", "/bin/sh", "-c", command, NULL); - print_error ("%s: %s", "Failed to launch backlog helper", strerror (errno)); - _exit (EXIT_FAILURE); + 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 launch_backlog_helper (struct app_context *ctx, FILE *backlog) { hard_assert (!ctx->running_backlog_helper); - suspend_terminal (ctx); - - pid_t child = fork (); - if (child == 0) - exec_backlog_helper (get_config_string - (ctx->config.root, "behaviour.backlog_helper"), backlog); - - if (child == -1) + switch (spawn_helper_child (ctx)) { - int saved_errno = errno; - resume_terminal (ctx); + case 0: + dup2 (fileno (backlog), STDIN_FILENO); + execl ("/bin/sh", "/bin/sh", "-c", get_config_string + (ctx->config.root, "behaviour.backlog_helper"), NULL); + print_error ("%s: %s", + "Failed to launch backlog helper", strerror (errno)); + _exit (EXIT_FAILURE); + case -1: log_global_error (ctx, "#s: #s", - "Failed to launch backlog helper", strerror (saved_errno)); - } - else - { - // Make sure the child has its own process group - (void) setpgid (child, child); - + "Failed to launch backlog helper", strerror (errno)); + break; + default: ctx->running_backlog_helper = true; } - - fclose (backlog); } static void @@ -10168,6 +10199,7 @@ display_backlog (struct app_context *ctx) rewind (backlog); set_cloexec (fileno (backlog)); launch_backlog_helper (ctx, backlog); + fclose (backlog); } static void @@ -10189,6 +10221,103 @@ display_full_log (struct app_context *ctx) set_cloexec (fileno (full_log)); launch_backlog_helper (ctx, full_log); + fclose (full_log); +} + +static bool +dump_input_to_file (struct app_context *ctx, char *template, struct error **e) +{ + int fd = mkstemp (template); + if (fd < 0) + FAIL ("%s", strerror (errno)); + + char *input = input_get_content (&ctx->input); + bool success = xwrite (fd, input, strlen (input), e); + free (input); + + if (!success) + (void) unlink (template); + + xclose (fd); + return success; +} + +static char * +try_dump_input_to_file (struct app_context *ctx) +{ + char *template = resolve_filename + ("input.XXXXXX", resolve_relative_runtime_unique_filename); + + struct error *e = NULL; + if (dump_input_to_file (ctx, template, &e)) + return template; + + log_global_error (ctx, "#s: #s", + "Failed to create a temporary file for editing", e->message); + error_free (e); + free (template); + return NULL; +} + +static void +launch_input_editor (struct app_context *ctx) +{ + char *filename; + if (!(filename = try_dump_input_to_file (ctx))) + return; + + const char *command; + if (!(command = getenv ("VISUAL")) + && !(command = getenv ("EDITOR"))) + command = "vi"; + + hard_assert (!ctx->running_editor); + 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: + log_global_error (ctx, "#s: #s", + "Failed to launch editor", strerror (errno)); + free (filename); + break; + default: + ctx->running_editor = true; + 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)) + { + log_global_error (ctx, "#s: #s", "Input editing failed", e->message); + error_free (e); + } + else + input_erase_content (&ctx->input); + + if (!input_insert (&ctx->input, input.str)) + log_global_error (ctx, "#s: #s", "Input editing failed", + "could not re-insert the modified text"); + + if (unlink (ctx->editor_filename)) + log_global_error (ctx, "Could not unlink `#s': #s", + ctx->editor_filename, strerror (errno)); + + free (ctx->editor_filename); + ctx->editor_filename = NULL; + str_free (&input); + + ctx->running_editor = false; } static void @@ -10204,6 +10333,7 @@ bind_common_keys (struct app_context *ctx) input_bind_meta (self, 'm', "insert-attribute"); input_bind_meta (self, 'h', "display-full-log"); + input_bind_meta (self, 'e', "edit-input"); if (key_f5) input_bind (self, key_f5, "previous-buffer"); @@ -10275,6 +10405,17 @@ on_readline_display_full_log (int count, int key) return 0; } +static int +on_readline_edit_input (int count, int key) +{ + (void) count; + (void) key; + + struct app_context *ctx = g_ctx; + launch_input_editor (ctx); + return 0; +} + static int on_readline_redraw_screen (int count, int key) { @@ -10380,6 +10521,7 @@ app_readline_init (void) rl_add_defun ("goto-buffer", on_readline_goto_buffer, -1); rl_add_defun ("display-backlog", on_readline_display_backlog, -1); rl_add_defun ("display-full-log", on_readline_display_full_log, -1); + rl_add_defun ("edit-input", on_readline_edit_input, -1); rl_add_defun ("redraw-screen", on_readline_redraw_screen, -1); rl_add_defun ("insert-attribute", on_readline_insert_attribute, -1); rl_add_defun ("start-paste-mode", on_readline_start_paste_mode, -1); @@ -10460,6 +10602,16 @@ on_editline_display_full_log (EditLine *editline, int key) return CC_NORM; } +static unsigned char +on_editline_edit_input (EditLine *editline, int key) +{ + (void) editline; + (void) key; + + launch_input_editor (g_ctx); + return CC_NORM; +} + static unsigned char on_editline_redraw_screen (EditLine *editline, int key) { @@ -10588,6 +10740,7 @@ app_editline_init (struct input *self) { "next-buffer", "Next buffer", on_editline_next_buffer }, { "display-backlog", "Show backlog", on_editline_display_backlog }, { "display-full-log", "Show full log", on_editline_display_full_log }, + { "edit-input", "Edit input", on_editline_edit_input }, { "redraw-screen", "Redraw screen", on_editline_redraw_screen }, { "insert-attribute", "mIRC formatting", on_editline_insert_attribute }, { "start-paste-mode", "Bracketed paste", on_editline_start_paste_mode }, @@ -10846,19 +10999,22 @@ try_reap_child (struct app_context *ctx) if (!zombie) return false; - if (!ctx->running_backlog_helper) - { - print_debug ("an unknown child has died"); - return true; - } if (WIFSTOPPED (status)) { // We could also send SIGCONT but what's the point + print_debug ("a child has been stopped, killing its process group"); kill (-zombie, SIGKILL); return true; } - ctx->running_backlog_helper = false; + if (ctx->running_backlog_helper) + ctx->running_backlog_helper = false; + else if (!ctx->running_editor) + { + print_debug ("an unknown child has died"); + return true; + } + hard_assert (tcsetpgrp (STDOUT_FILENO, getpgid (0)) != -1); resume_terminal (ctx); @@ -10868,6 +11024,8 @@ try_reap_child (struct app_context *ctx) else if (WIFEXITED (status) && WEXITSTATUS (status) != 0) log_global_error (ctx, "Child returned status #d", WEXITSTATUS (status)); + else if (ctx->running_editor) + process_edited_input (ctx); return true; }