xF: enable running external editors on input
Alpine 3.24 Success
Arch Linux AUR Success
OpenBSD 7.8 Success

And also on the transcript.

This is an easy way of resolving text selection,
and the mediocre input editor.
This commit is contained in:
2026-08-05 12:08:15 +02:00
parent 65a98dfd7c
commit 41be32692c
2 changed files with 342 additions and 12 deletions
+1 -1
Submodule liberty updated: 7f2156c3d7...c0ea24d92d
+341 -11
View File
@@ -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);
}