xF: enable running external editors on input
And also on the transcript. This is an easy way of resolving text selection, and the mediocre input editor.
This commit is contained in:
+1
-1
Submodule liberty updated: 7f2156c3d7...c0ea24d92d
@@ -19,6 +19,9 @@
|
||||
#include "config.h"
|
||||
#define PROGRAM_NAME "xF"
|
||||
|
||||
// mkstemps
|
||||
#define _DEFAULT_SOURCE
|
||||
|
||||
#include "common.c"
|
||||
#include "xC-proto.c"
|
||||
|
||||
@@ -259,6 +262,8 @@ enum action
|
||||
ACTION_BUFFER_10,
|
||||
ACTION_TOGGLE_UNIMPORTANT,
|
||||
ACTION_LOG,
|
||||
ACTION_EDIT_INPUT,
|
||||
ACTION_VIEW_TRANSCRIPT,
|
||||
ACTION_FORMAT,
|
||||
ACTION_FORMAT_BOLD,
|
||||
ACTION_FORMAT_ITALIC,
|
||||
@@ -467,13 +472,17 @@ static struct
|
||||
// User interface:
|
||||
|
||||
struct attrs attrs[ATTRIBUTE_COUNT];
|
||||
struct line_editor editor; ///< Message editor
|
||||
int64_t last_active; ///< Last ACTIVE command timestamp
|
||||
int buffer_list_offset; ///< XUI pixels from the top of the list
|
||||
enum app_widget_id dragged_scrollbar;
|
||||
int transcript_width; ///< Width used by buffer line wrapping
|
||||
int transcript_height; ///< Current buffer content height
|
||||
|
||||
struct line_editor editor; ///< Message editor
|
||||
char *editor_running_for; ///< Buffer name being edited externally
|
||||
char *editor_filename; ///< File being edited by the user
|
||||
bool editor_shows_transcript; ///< External editor shows transcript
|
||||
|
||||
struct binding *keys; ///< Parsed xC-style key bindings
|
||||
size_t keys_len;
|
||||
|
||||
@@ -2527,16 +2536,23 @@ app_make_mark (const struct buffer_line *line)
|
||||
app_line_mark (line->rendition), false);
|
||||
}
|
||||
|
||||
static const char *
|
||||
app_format_line_timestamp (const struct buffer_line *line)
|
||||
{
|
||||
static char timestamp[16];
|
||||
time_t when = line->when / 1000;
|
||||
struct tm tm = {};
|
||||
|
||||
*timestamp = '\0';
|
||||
if (localtime_r (&when, &tm))
|
||||
strftime (timestamp, sizeof timestamp, "%H:%M:%S", &tm);
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
static struct widget *
|
||||
app_make_line_timestamp (const struct buffer_line *line)
|
||||
{
|
||||
time_t when = line->when / 1000;
|
||||
struct tm tm = {};
|
||||
char timestamp[16] = "";
|
||||
if (localtime_r (&when, &tm))
|
||||
strftime (timestamp, sizeof timestamp, "%H:%M:%S", &tm);
|
||||
|
||||
return app_label (ATTRIBUTE_TIME, timestamp, false);
|
||||
return app_label (ATTRIBUTE_TIME, app_format_line_timestamp (line), false);
|
||||
}
|
||||
|
||||
static void
|
||||
@@ -2921,6 +2937,17 @@ static struct widget *
|
||||
app_make_input (void)
|
||||
{
|
||||
struct layout row = {};
|
||||
if (g.editor_running_for)
|
||||
{
|
||||
app_push (&row, app_padding (ATTRIBUTE_NORMAL, 0.25));
|
||||
app_push (&row, app_label (ATTRIBUTE_NORMAL,
|
||||
g.editor_shows_transcript
|
||||
? "Running external transcript viewer"
|
||||
: "Running external input editor", true));
|
||||
app_push (&row, app_padding (ATTRIBUTE_NORMAL, 0.25));
|
||||
return xui_hbox (row.head);
|
||||
}
|
||||
|
||||
char *prompt = app_prompt ();
|
||||
if (*prompt)
|
||||
{
|
||||
@@ -2932,7 +2959,9 @@ app_make_input (void)
|
||||
else
|
||||
app_push (&row, app_padding (ATTRIBUTE_NORMAL, 0.25));
|
||||
free (prompt);
|
||||
|
||||
app_push (&row, g.backend.make_editor (g_attrs[ATTRIBUTE_NORMAL].attrs));
|
||||
app_push (&row, app_padding (ATTRIBUTE_NORMAL, 0.25));
|
||||
return xui_hbox (row.head);
|
||||
}
|
||||
|
||||
@@ -3283,6 +3312,246 @@ app_open (const char *object)
|
||||
;
|
||||
}
|
||||
|
||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
static char *
|
||||
app_external_editor_make_file (const char *template_name, const char *contents,
|
||||
size_t contents_len, struct error **e)
|
||||
{
|
||||
char *template = resolve_filename
|
||||
(template_name, resolve_relative_runtime_template);
|
||||
int fd = mkstemps (template, 4);
|
||||
if (fd < 0)
|
||||
{
|
||||
error_set (e, "creation failed: %s", strerror (errno));
|
||||
free (template);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// We should transcode it to the system encoding (hopefully LC_CTYPE)
|
||||
// but it's reasonable to expect everything to be in UTF-8 these days.
|
||||
struct error *err = NULL;
|
||||
bool written = xwrite (fd, contents, contents_len, &err);
|
||||
xclose (fd);
|
||||
if (!written)
|
||||
{
|
||||
error_set (e, "write failed: %s", err->message);
|
||||
error_free (err);
|
||||
(void) unlink (template);
|
||||
free (template);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return template;
|
||||
}
|
||||
|
||||
static void
|
||||
app_external_editor_append_line (struct str *output,
|
||||
const struct buffer_line *line, bool decorated)
|
||||
{
|
||||
if (decorated)
|
||||
{
|
||||
str_append (output, app_format_line_timestamp (line));
|
||||
str_append (output, app_line_mark (line->rendition));
|
||||
}
|
||||
LIST_FOR_EACH (struct buffer_line_item, item, line->items)
|
||||
str_append (output, item->text);
|
||||
str_append_c (output, '\n');
|
||||
}
|
||||
|
||||
static struct str
|
||||
app_external_editor_make_transcript (void)
|
||||
{
|
||||
struct str transcript = str_make ();
|
||||
if (g.log.visible)
|
||||
{
|
||||
for (size_t i = 0; i < g.log.lines_len; i++)
|
||||
app_external_editor_append_line
|
||||
(&transcript, g.log.lines[i], false);
|
||||
}
|
||||
else
|
||||
{
|
||||
struct buffer *buffer = g.buffer_current;
|
||||
LIST_FOR_EACH (struct buffer_line, line, buffer->lines)
|
||||
if (buffer_line_is_visible (buffer, line))
|
||||
app_external_editor_append_line (&transcript, line, true);
|
||||
}
|
||||
return transcript;
|
||||
}
|
||||
|
||||
static void
|
||||
app_external_editor_process_file (void)
|
||||
{
|
||||
struct str input = str_make ();
|
||||
struct error *e = NULL;
|
||||
if (!read_file (g.editor_filename, &input, &e))
|
||||
{
|
||||
print_error ("input editing failed: %s", e->message);
|
||||
error_free (e);
|
||||
}
|
||||
else if (!utf8_validate (input.str, input.len))
|
||||
print_error ("input editing failed: file is not valid UTF-8");
|
||||
else if (memchr (input.str, 0, input.len))
|
||||
print_error ("input editing failed: file contains a NUL byte");
|
||||
else
|
||||
{
|
||||
while (input.len && strchr ("\r\n", input.str[input.len - 1]))
|
||||
input.str[--input.len] = 0;
|
||||
|
||||
struct buffer *buffer = buffer_find (g.editor_running_for);
|
||||
if (buffer == g.buffer_current)
|
||||
app_editor_set (input.str);
|
||||
else if (buffer)
|
||||
{
|
||||
cstr_set (&buffer->input, xstrdup (input.str));
|
||||
buffer->input_point = 0;
|
||||
|
||||
struct utf8_iter iter = { .s = input.str, .len = input.len };
|
||||
while (utf8_iter_next (&iter, NULL) >= 0)
|
||||
buffer->input_point++;
|
||||
}
|
||||
}
|
||||
str_free (&input);
|
||||
}
|
||||
|
||||
static void
|
||||
app_external_editor_cleanup (void)
|
||||
{
|
||||
if (g_xui.ui == &tui_ui)
|
||||
{
|
||||
g_winch_received = false;
|
||||
xui_tui_resume ();
|
||||
}
|
||||
#ifdef WITH_X11
|
||||
else
|
||||
x11_embed_end ();
|
||||
#endif // WITH_X11
|
||||
|
||||
if (unlink (g.editor_filename))
|
||||
print_error ("failed to remove `%s': %s",
|
||||
g.editor_filename, strerror (errno));
|
||||
|
||||
cstr_set (&g.editor_running_for, NULL);
|
||||
cstr_set (&g.editor_filename, NULL);
|
||||
g.editor_shows_transcript = false;
|
||||
|
||||
xui_invalidate ();
|
||||
}
|
||||
|
||||
static bool
|
||||
app_external_editor_launch (struct error **e)
|
||||
{
|
||||
char xid[32] = "";
|
||||
if (g_xui.ui == &tui_ui)
|
||||
xui_tui_suspend ();
|
||||
#ifdef WITH_X11
|
||||
else
|
||||
{
|
||||
Window socket = x11_embed_start (0, g_xui.vunit, 0, 2 * g_xui.vunit);
|
||||
if (!socket)
|
||||
return error_set (e, "conflict");
|
||||
|
||||
// Emacs only accepts decimal notation, so it must be like this.
|
||||
snprintf (xid, sizeof xid, "%lu", (unsigned long) socket);
|
||||
}
|
||||
#endif // WITH_X11
|
||||
|
||||
const char *editor = getenv ("VISUAL");
|
||||
if (!editor)
|
||||
editor = getenv ("EDITOR");
|
||||
if (!editor)
|
||||
editor = "vi";
|
||||
|
||||
pid_t child = fork ();
|
||||
if (child < 0)
|
||||
return error_set (e, "%s", strerror (errno));
|
||||
|
||||
bool run_in_terminal = g_xui.ui == &tui_ui;
|
||||
if (!child)
|
||||
{
|
||||
if (run_in_terminal)
|
||||
{
|
||||
hard_assert (setpgid (0, 0) != -1);
|
||||
hard_assert (tcsetpgrp (STDOUT_FILENO, getpgrp ()) != -1);
|
||||
execlp (editor, editor, g.editor_filename, NULL);
|
||||
}
|
||||
#ifdef WITH_X11
|
||||
#define LAUNCH(x, ...) execlp (x, x, __VA_ARGS__, g.editor_filename, NULL)
|
||||
else
|
||||
{
|
||||
// This list is nearly exhaustive, so it's about priorities
|
||||
// and option customisation.
|
||||
LAUNCH ("gvim", "-f", "--socketid", xid);
|
||||
LAUNCH ("emacs", "--parent-id", xid);
|
||||
LAUNCH ("urxvt", "-embed", xid, "-e", editor);
|
||||
LAUNCH ("st", "-w", xid, "-e", editor);
|
||||
|
||||
// Note that -into doesn't follow XEmbed focus;
|
||||
// XTerm will work better with "XTerm*allowSendEvents: true".
|
||||
LAUNCH ("xterm", "-into", xid, "-e", editor);
|
||||
}
|
||||
#undef LAUNCH
|
||||
#endif // WITH_X11
|
||||
|
||||
// AppKit offers no similar mechanism.
|
||||
|
||||
// We should have a better mechanism to pass errors to the parent.
|
||||
perror ("failed to launch an editor");
|
||||
_exit (EXIT_FAILURE);
|
||||
}
|
||||
if (run_in_terminal)
|
||||
(void) setpgid (child, child);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
app_launch_external_editor (bool view_transcript)
|
||||
{
|
||||
if (g.editor_running_for || !g.buffer_current)
|
||||
return false;
|
||||
|
||||
char *contents = NULL;
|
||||
size_t contents_len = 0;
|
||||
const char *template_name = "input.XXXXXX.txt";
|
||||
if (view_transcript)
|
||||
{
|
||||
struct str transcript = app_external_editor_make_transcript ();
|
||||
contents_len = transcript.len;
|
||||
contents = str_steal (&transcript);
|
||||
template_name = "transcript.XXXXXX.txt";
|
||||
}
|
||||
else
|
||||
{
|
||||
contents = app_editor_text ();
|
||||
contents_len = strlen (contents);
|
||||
}
|
||||
|
||||
struct error *err = NULL;
|
||||
char *filename = app_external_editor_make_file (template_name,
|
||||
contents, contents_len, &err);
|
||||
free (contents);
|
||||
if (!filename)
|
||||
{
|
||||
print_error ("failed to prepare input for editing: %s", err->message);
|
||||
error_free (err);
|
||||
return false;
|
||||
}
|
||||
|
||||
g.editor_running_for = xstrdup (g.buffer_current->buffer_name);
|
||||
g.editor_filename = filename;
|
||||
g.editor_shows_transcript = view_transcript;
|
||||
xui_invalidate ();
|
||||
|
||||
if (!app_external_editor_launch (&err))
|
||||
{
|
||||
print_error ("failed to launch an editor: %s", err->message);
|
||||
error_free (err);
|
||||
app_external_editor_cleanup ();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ~~~ Action and mouse dispatch ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
static const enum line_editor_action g_editor_actions[] =
|
||||
@@ -3374,6 +3643,10 @@ app_process_action (enum action action)
|
||||
return true;
|
||||
case ACTION_LOG:
|
||||
return app_toggle_log ();
|
||||
case ACTION_EDIT_INPUT:
|
||||
return app_launch_external_editor (false);
|
||||
case ACTION_VIEW_TRANSCRIPT:
|
||||
return app_launch_external_editor (true);
|
||||
case ACTION_FORMAT:
|
||||
g.inserting_attribute = true;
|
||||
return true;
|
||||
@@ -3420,6 +3693,9 @@ app_process_mouse (termo_mouse_event_t type, int x, int y, int button,
|
||||
{
|
||||
(void) modifiers;
|
||||
|
||||
if (g.editor_running_for)
|
||||
return type == TERMO_MOUSE_RELEASE;
|
||||
|
||||
app_notify_active ();
|
||||
|
||||
if (type == TERMO_MOUSE_RELEASE)
|
||||
@@ -3523,6 +3799,8 @@ g_default_bindings[] =
|
||||
{ "M-a", ACTION_BUFFER_ACTIVITY },
|
||||
{ "M-H", ACTION_TOGGLE_UNIMPORTANT },
|
||||
{ "M-h", ACTION_LOG },
|
||||
{ "M-e", ACTION_EDIT_INPUT },
|
||||
{ "M-E", ACTION_VIEW_TRANSCRIPT },
|
||||
{ "M-m", ACTION_FORMAT },
|
||||
{ "PageUp", ACTION_SCROLL_UP },
|
||||
{ "PageDown", ACTION_SCROLL_DOWN },
|
||||
@@ -3657,6 +3935,9 @@ signals_superhandler (int signum)
|
||||
{
|
||||
switch (signum)
|
||||
{
|
||||
case SIGCHLD:
|
||||
signals_postpone_handling ('c');
|
||||
break;
|
||||
case SIGWINCH:
|
||||
g_winch_received = true;
|
||||
signals_postpone_handling ('w');
|
||||
@@ -3682,14 +3963,16 @@ signals_init (void)
|
||||
set_blocking (g_signal_pipe[0], false);
|
||||
set_blocking (g_signal_pipe[1], false);
|
||||
signal (SIGPIPE, SIG_IGN);
|
||||
signal (SIGTTOU, SIG_IGN);
|
||||
|
||||
struct sigaction sa;
|
||||
sa.sa_flags = SA_RESTART;
|
||||
sa.sa_handler = signals_superhandler;
|
||||
sigemptyset (&sa.sa_mask);
|
||||
if (sigaction (SIGWINCH, &sa, NULL) == -1
|
||||
|| sigaction (SIGINT, &sa, NULL) == -1
|
||||
|| sigaction (SIGTERM, &sa, NULL) == -1)
|
||||
|| sigaction (SIGINT, &sa, NULL) == -1
|
||||
|| sigaction (SIGTERM, &sa, NULL) == -1
|
||||
|| sigaction (SIGCHLD, &sa, NULL) == -1)
|
||||
exit_fatal ("sigaction: %s", strerror (errno));
|
||||
}
|
||||
|
||||
@@ -3699,7 +3982,9 @@ signals_free (void)
|
||||
soft_assert (signal (SIGWINCH, SIG_DFL) != SIG_ERR);
|
||||
soft_assert (signal (SIGINT, SIG_DFL) != SIG_ERR);
|
||||
soft_assert (signal (SIGTERM, SIG_DFL) != SIG_ERR);
|
||||
soft_assert (signal (SIGCHLD, SIG_DFL) != SIG_ERR);
|
||||
soft_assert (signal (SIGPIPE, SIG_DFL) != SIG_ERR);
|
||||
soft_assert (signal (SIGTTOU, SIG_DFL) != SIG_ERR);
|
||||
|
||||
poller_fd_reset (&g.signal_event);
|
||||
if (g_signal_pipe[0] != -1)
|
||||
@@ -3714,6 +3999,46 @@ signals_free (void)
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
signals_reap_editor (void)
|
||||
{
|
||||
if (!g.editor_running_for)
|
||||
return;
|
||||
|
||||
int status = 0;
|
||||
pid_t child;
|
||||
do
|
||||
child = waitpid (-1, &status, WNOHANG | WUNTRACED);
|
||||
while (child < 0 && errno == EINTR);
|
||||
|
||||
if (!child)
|
||||
return;
|
||||
if (child < 0)
|
||||
{
|
||||
if (errno != ECHILD)
|
||||
print_error ("failed to wait for editor: %s", strerror (errno));
|
||||
return;
|
||||
}
|
||||
if (WIFSTOPPED (status))
|
||||
{
|
||||
print_error ("editor has been stopped, killing its process group");
|
||||
(void) kill (-child, SIGKILL);
|
||||
return;
|
||||
}
|
||||
|
||||
if (g_xui.ui == &tui_ui)
|
||||
hard_assert (tcsetpgrp (STDOUT_FILENO, getpgrp ()) != -1);
|
||||
|
||||
if (WIFSIGNALED (status))
|
||||
print_error ("editor died from signal %d", WTERMSIG (status));
|
||||
else if (WIFEXITED (status) && WEXITSTATUS (status))
|
||||
print_error ("editor returned status %d", WEXITSTATUS (status));
|
||||
|
||||
if (!g.editor_shows_transcript)
|
||||
app_external_editor_process_file ();
|
||||
app_external_editor_cleanup ();
|
||||
}
|
||||
|
||||
static void
|
||||
signals_on_readable (const struct pollfd *fd, void *user_data)
|
||||
{
|
||||
@@ -3722,9 +4047,10 @@ signals_on_readable (const struct pollfd *fd, void *user_data)
|
||||
while (read (fd->fd, signals, sizeof signals) > 0)
|
||||
;
|
||||
|
||||
signals_reap_editor ();
|
||||
if (g_termination_requested)
|
||||
app_quit ();
|
||||
if (g_winch_received)
|
||||
if (g_winch_received && !g_xui.tui_suspended)
|
||||
{
|
||||
g_winch_received = false;
|
||||
if (g_xui.ui->winch)
|
||||
@@ -3782,6 +4108,10 @@ app_free_context (void)
|
||||
|
||||
free (g.keys);
|
||||
app_editor_free ();
|
||||
if (g.editor_filename)
|
||||
(void) unlink (g.editor_filename);
|
||||
free (g.editor_filename);
|
||||
free (g.editor_running_for);
|
||||
poller_free (&g.poller);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user