From e101afab380d3a2253dd726719c1057191bbd6d1 Mon Sep 17 00:00:00 2001
From: Přemysl Janouch
Date: Fri, 25 Dec 2015 04:21:18 +0100
Subject: 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 | 252 ++++++++++++++++++++++++++++++++++++++++++++++++++------------
3 files changed, 250 insertions(+), 47 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
@@ -221,15 +219,23 @@ input_set_prompt (struct input *self, char *prompt)
}
static void
-input_erase (struct input *self)
+input_erase_content (struct input *self)
{
(void) self;
- rl_set_prompt ("");
rl_replace_line ("", false);
rl_redisplay ();
}
+static void
+input_erase (struct input *self)
+{
+ (void) self;
+
+ rl_set_prompt ("");
+ input_erase_content (self);
+}
+
static void
input_bind (struct input *self, const char *seq, const char *function_name)
{
@@ -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);
+ input_redisplay (self);
+}
- // XXX: this doesn't seem to save the escape character
- el_get (self->editline, EL_PROMPT, &self->saved_prompt, &self->saved_char);
+static void
+input_erase (struct input *self)
+{
el_set (self->editline, EL_PROMPT, input_make_empty_prompt);
- input_redisplay (self);
+ 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)
-{
- 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);
-}
-
-static void
-launch_backlog_helper (struct app_context *ctx, FILE *backlog)
+static pid_t
+spawn_helper_child (struct app_context *ctx)
{
- 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 (child)
+ {
+ case -1:
{
int saved_errno = errno;
resume_terminal (ctx);
- log_global_error (ctx, "#s: #s",
- "Failed to launch backlog helper", strerror (saved_errno));
+ errno = saved_errno;
+ break;
}
- else
- {
- // Make sure the child has its own process group
+ 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);
+ switch (spawn_helper_child (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 (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;
}
--
cgit v1.2.3-70-g09d2