From 36ec2b0ae1fa7afcdfe6f166323955b8bd121b0f Mon Sep 17 00:00:00 2001 From: Michael Vetter Date: Thu, 2 Apr 2026 21:30:31 +0200 Subject: [PATCH] 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 --- src/command/cmd_funcs.c | 106 ++++++++++++++++++++++-------------- src/tools/editor.c | 117 ++++++++++++++++++++++++++-------------- src/tools/editor.h | 2 +- src/ui/core.c | 15 ++++++ src/ui/inputwin.c | 27 ++++++---- 5 files changed, 178 insertions(+), 89 deletions(-) diff --git a/src/command/cmd_funcs.c b/src/command/cmd_funcs.c index efcea89f..25649a95 100644 --- a/src/command/cmd_funcs.c +++ b/src/command/cmd_funcs.c @@ -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; } diff --git a/src/tools/editor.c b/src/tools/editor.c index 03d94ed7..bf9e9c4c 100644 --- a/src/tools/editor.c +++ b/src/tools/editor.c @@ -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; } diff --git a/src/tools/editor.h b/src/tools/editor.h index f38d2c62..f7994125 100644 --- a/src/tools/editor.h +++ b/src/tools/editor.h @@ -13,6 +13,6 @@ #include -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 diff --git a/src/ui/core.c b/src/ui/core.c index ac287383..4e8a7606 100644 --- a/src/ui/core.c +++ b/src/ui/core.c @@ -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(); diff --git a/src/ui/inputwin.c b/src/ui/inputwin.c index 60c58894..f53b7889 100644 --- a/src/ui/inputwin.c +++ b/src/ui/inputwin.c @@ -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; }