feat: implement asynchronous external editor support

Move from blocking fork/wait logic to nonblocking fork and
g_child_watch_add.

This ensures that the Profanity main loop continues to run while an
external editor is open. So we don't loose connection and react to
pings.

We change editor handling also in vcard and muc subject editing.
In the new implementation we are launching the editor and passing a
callback which we will use once the editor exited.

We use the recently added ui_susped() and ui_resume().

To not clutter the UI we need to check whether Profanity UI is suspended
and omit drawing in this case.

Fixes: https://github.com/profanity-im/profanity/issues/1888
Ref: 9b112904a9bc7250dc013d901187ca8622580d98
Signed-off-by: Michael Vetter <jubalh@iodoru.org>
This commit is contained in:
Michael Vetter
2026-04-02 21:30:31 +02:00
parent fc66ddc2c1
commit 36ec2b0ae1
5 changed files with 178 additions and 89 deletions

View File

@@ -109,19 +109,26 @@ static gboolean _cmd_execute_alias(ProfWin* window, const char* const inp, gbool
static gboolean
_download_install_plugin(ProfWin* window, gchar* url, gchar* path);
static void
_vcard_editor_finished_cb(gchar* message, void* user_data)
{
if (message) {
char** field = user_data;
if (*field) {
free(*field);
}
*field = g_strdup(message);
cons_show("Field updated. Remember to call /me save to apply changes.");
}
}
static gboolean
_update_vcard_field(char** field, char* value)
{
if (!value) {
gchar* editor_value;
if (get_message_from_editor(*field, &editor_value)) {
if (launch_editor(*field, _vcard_editor_finished_cb, field)) {
return TRUE;
}
if (*field) {
free(*field);
}
*field = editor_value;
} else {
if (*field) {
free(*field);
@@ -1640,6 +1647,51 @@ cmd_help(ProfWin* window, const char* const command, gchar** args)
return TRUE;
}
static void
_cmd_editor_finished_cb(gchar* message, void* user_data)
{
if (message) {
rl_insert_text(message);
rl_point = rl_end;
rl_forced_update_display();
}
}
typedef struct
{
ProfWin* window;
char* roomjid;
} EditorFinishedContext;
static void
_cmd_room_editor_finished_cb(gchar* message, void* user_data)
{
EditorFinishedContext* ctx = user_data;
if (message && ctx->roomjid) {
message_send_groupchat_subject(ctx->roomjid, message);
}
if (ctx) {
g_free(ctx->roomjid);
g_free(ctx);
}
}
static void
_cmd_correct_editor_finished_cb(gchar* message, void* user_data)
{
EditorFinishedContext* ctx = user_data;
if (message && ctx->window) {
if (ctx->window->type == WIN_CHAT) {
ProfChatWin* chatwin = (ProfChatWin*)ctx->window;
cl_ev_send_msg_correct(chatwin, message, FALSE, TRUE);
} else if (ctx->window->type == WIN_MUC) {
ProfMucWin* mucwin = (ProfMucWin*)ctx->window;
cl_ev_send_muc_msg_corrected(mucwin, message, FALSE, TRUE);
}
}
g_free(ctx);
}
gboolean
cmd_about(ProfWin* window, const char* const command, gchar** args)
{
@@ -4033,18 +4085,13 @@ cmd_subject(ProfWin* window, const char* const command, gchar** args)
}
if (g_strcmp0(args[0], "editor") == 0) {
gchar* message = NULL;
char* subject = muc_subject(mucwin->roomjid);
if (get_message_from_editor(subject, &message)) {
return TRUE;
}
EditorFinishedContext* ctx = g_new0(EditorFinishedContext, 1);
ctx->roomjid = g_strdup(mucwin->roomjid);
launch_editor(subject, _cmd_room_editor_finished_cb, ctx);
if (message) {
message_send_groupchat_subject(mucwin->roomjid, message);
} else {
cons_bad_cmd_usage(command);
}
return TRUE;
}
@@ -9567,16 +9614,7 @@ cmd_change_password(ProfWin* window, const char* const command, gchar** args)
gboolean
cmd_editor(ProfWin* window, const char* const command, gchar** args)
{
auto_gchar gchar* message = NULL;
if (get_message_from_editor(NULL, &message)) {
return TRUE;
}
rl_insert_text(message);
ui_resize();
rl_point = rl_end;
rl_forced_update_display();
launch_editor(NULL, _cmd_editor_finished_cb, NULL);
return TRUE;
}
@@ -9590,20 +9628,10 @@ cmd_correct_editor(ProfWin* window, const char* const command, gchar** args)
gchar* initial_message = win_get_last_sent_message(window);
auto_gchar gchar* message = NULL;
if (get_message_from_editor(initial_message, &message)) {
return TRUE;
}
EditorFinishedContext* ctx = g_new0(EditorFinishedContext, 1);
ctx->window = window;
if (window->type == WIN_CHAT) {
ProfChatWin* chatwin = (ProfChatWin*)window;
cl_ev_send_msg_correct(chatwin, message, FALSE, TRUE);
} else if (window->type == WIN_MUC) {
ProfMucWin* mucwin = (ProfMucWin*)window;
cl_ev_send_muc_msg_corrected(mucwin, message, FALSE, TRUE);
}
launch_editor(initial_message, _cmd_correct_editor_finished_cb, ctx);
return TRUE;
}

View File

@@ -20,14 +20,52 @@
#include "log.h"
#include "common.h"
#include "xmpp/xmpp.h"
#include "ui/ui.h"
typedef struct EditorContext
{
gchar* filename;
void (*callback)(gchar* content, void* user_data);
void* user_data;
} EditorContext;
static void
_editor_exit_cb(GPid pid, gint status, gpointer data)
{
EditorContext* ctx = data;
gchar* contents = NULL;
GError* error = NULL;
ui_resume();
ui_resize();
if (WIFEXITED(status) && WEXITSTATUS(status) == 0) {
if (g_file_get_contents(ctx->filename, &contents, NULL, &error)) {
g_strchomp(contents);
ctx->callback(contents, ctx->user_data);
g_free(contents);
} else {
log_error("[Editor] could not read from %s: %s", ctx->filename, error->message);
cons_show_error("Could not read edited content: %s", error->message);
g_error_free(error);
}
} else {
cons_show_error("Editor exited with error status %d", WEXITSTATUS(status));
}
if (remove(ctx->filename) != 0) {
log_error("[Editor] error during file deletion of %s", ctx->filename);
}
g_free(ctx->filename);
g_free(ctx);
g_spawn_close_pid(pid);
}
// Returns true if an error occurred
gboolean
get_message_from_editor(gchar* message, gchar** returned_message)
launch_editor(gchar* initial_content, void (*callback)(gchar* content, void* data), void* user_data)
{
/* Make sure that there's no junk in the return-pointer in error cases */
*returned_message = NULL;
auto_gchar gchar* filename = NULL;
auto_gerror GError* glib_error = NULL;
const char* jid = connection_get_barejid();
@@ -37,64 +75,65 @@ get_message_from_editor(gchar* message, gchar** returned_message)
log_debug("[Editor] could not get JID");
auto_gchar gchar* data_dir = files_get_data_path(DIR_EDITOR);
if (!create_dir(data_dir)) {
cons_show_error("Could not create editor directory.");
return TRUE;
}
filename = g_strdup_printf("%s/compose.md", data_dir);
}
if (!filename) {
log_error("[Editor] something went wrong while creating compose file");
cons_show_error("Could not create compose file.");
return TRUE;
}
gsize messagelen = 0;
if (message != NULL) {
messagelen = strlen(message);
if (initial_content != NULL) {
messagelen = strlen(initial_content);
}
if (!g_file_set_contents(filename, message, messagelen, &glib_error)) {
if (!g_file_set_contents(filename, initial_content, messagelen, &glib_error)) {
log_error("[Editor] could not write to %s: %s", filename, PROF_GERROR_MESSAGE(glib_error));
cons_show_error("Could not write to compose file: %s", PROF_GERROR_MESSAGE(glib_error));
return TRUE;
}
auto_gchar gchar* editor = prefs_get_string(PREF_COMPOSE_EDITOR);
auto_gchar gchar* editor_with_filename = g_strdup_printf("%s %s", editor, filename);
auto_gcharv gchar** editor_argv = g_strsplit(editor_with_filename, " ", 0);
gchar** editor_argv = NULL;
GError* error = NULL;
if (!editor_argv || !editor_argv[0]) {
log_error("[Editor] Failed to parse editor command: %s", editor);
auto_gchar gchar* full_cmd = g_strdup_printf("%s %s", editor, filename);
if (!g_shell_parse_argv(full_cmd, NULL, &editor_argv, &error)) {
log_error("[Editor] Failed to parse editor command: %s", error->message);
cons_show_error("Failed to parse editor command: %s", error->message);
g_error_free(error);
return TRUE;
}
// Fork / exec
pid_t pid = fork();
if (pid == 0) {
if (editor_argv && editor_argv[0]) {
int x = execvp(editor_argv[0], editor_argv);
if (x == -1)
log_error("[Editor] Failed to exec %s", editor);
}
_exit(EXIT_FAILURE);
} else {
if (pid == -1) {
return TRUE;
}
waitpid(pid, NULL, 0);
EditorContext* ctx = g_new0(EditorContext, 1);
ctx->filename = g_steal_pointer(&filename);
ctx->callback = callback;
ctx->user_data = user_data;
gchar* contents;
gsize length;
if (!g_file_get_contents(filename, &contents, &length, &glib_error)) {
log_error("[Editor] could not read from %s: %s", filename, PROF_GERROR_MESSAGE(glib_error));
return TRUE;
}
/* Remove all trailing new-line characters */
g_strchomp(contents);
*returned_message = contents;
if (remove(filename) != 0) {
log_error("[Editor] error during file deletion of %s", filename);
} else {
log_debug("[Editor] deleted file: %s", filename);
}
ui_suspend();
pid_t pid = fork();
if (pid == -1) {
log_error("[Editor] Failed to fork: %s", strerror(errno));
ui_resume();
ui_resize();
cons_show_error("Failed to start editor: %s", strerror(errno));
g_strfreev(editor_argv);
g_free(ctx->filename);
g_free(ctx);
return TRUE;
} else if (pid == 0) {
// Child process: Inherits TTY from parent
execvp(editor_argv[0], editor_argv);
_exit(EXIT_FAILURE);
}
// Parent process: Watch the child asynchronously
g_child_watch_add((GPid)pid, _editor_exit_cb, ctx);
g_strfreev(editor_argv);
return FALSE;
}

View File

@@ -13,6 +13,6 @@
#include <glib.h>
gboolean get_message_from_editor(gchar* message, gchar** returned_message);
gboolean launch_editor(gchar* initial_content, void (*callback)(gchar* content, void* data), void* user_data);
#endif

View File

@@ -120,6 +120,11 @@ ui_sigwinch_handler(int sig)
void
ui_update(void)
{
// UI is suspended
if (isendwin()) {
return;
}
ProfWin* current = wins_get_current();
if (current->layout->paged == 0) {
win_move_to_end(current);
@@ -186,6 +191,11 @@ ui_resume(void)
void
ui_resize(void)
{
// UI is suspended
if (isendwin()) {
return;
}
struct winsize w;
ioctl(STDOUT_FILENO, TIOCGWINSZ, &w);
erase();
@@ -204,6 +214,11 @@ ui_resize(void)
void
ui_redraw(void)
{
// UI is suspended
if (isendwin()) {
return;
}
title_bar_resize();
wins_resize_all();
status_bar_resize();

View File

@@ -171,6 +171,12 @@ _inp_slashguard_check(void)
char*
inp_readline(void)
{
// UI is suspended
if (isendwin()) {
g_usleep(100000); // 100ms
return NULL;
}
p_rl_timeout.tv_sec = inp_timeout / 1000;
p_rl_timeout.tv_usec = inp_timeout % 1000 * 1000;
FD_ZERO(&fds);
@@ -996,6 +1002,16 @@ _inp_rl_down_arrow_handler(int count, int key)
return 0;
}
static void
_editor_finished_cb(gchar* message, void* user_data)
{
if (message) {
rl_replace_line(message, 0);
rl_point = rl_end;
rl_forced_update_display();
}
}
static int
_inp_rl_send_to_editor(int count, int key)
{
@@ -1003,16 +1019,7 @@ _inp_rl_send_to_editor(int count, int key)
return 0;
}
auto_gchar gchar* message = NULL;
if (get_message_from_editor(rl_line_buffer, &message)) {
return 0;
}
rl_replace_line(message, 0);
ui_resize();
rl_point = rl_end;
rl_forced_update_display();
launch_editor(rl_line_buffer, _editor_finished_cb, NULL);
return 0;
}