degesch: allow launching an editor for input
Useful for editing multiline text (such as making it single-line). Some refactoring and cleanup.
This commit is contained in:
parent
37e9165548
commit
e101afab38
2
NEWS
2
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
|
||||
|
43
common.c
43
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
|
||||
|
254
degesch.c
254
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;
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user