Compare commits

..

5 Commits

Author SHA1 Message Date
dc66dab7f5 refactor: rename argument to avoid C++ keyword conflict
All checks were successful
CI / Linux (debian) (pull_request) Successful in 9m38s
CI / Check coding style (pull_request) Successful in 36s
CI / Check spelling (pull_request) Successful in 18s
CI / Linux (ubuntu) (pull_request) Successful in 9m50s
CI / Linux (arch) (pull_request) Successful in 14m26s
The parameter `template` in format_call_external_argv() was renamed
to `template_fmt` to avoid collision with the `template` keyword in
C++. This improves compatibility when the header is included in
C++ code.
2025-08-26 09:47:02 +00:00
ed4f272ae5 fix: add missing parameter types to function declarations
Some functions were declared without parameters in header files,
despite their implementations specifying arguments. This mismatch
is deprecated in all versions of C and is disallowed in C23.

This change updates the declarations to match their definitions,
ensuring compatibility with C standards, avoiding potential
undefined behavior, and improving support in tools and editors.
2025-08-26 09:47:02 +00:00
b91a8b53f1 build: add missing includes to header files
This change adds necessary `#include` directives that were previously
omitted in some header files. Without these includes, compilation
could fail or produce undefined behavior when the headers are used
in isolation (i.e., before their dependencies are included elsewhere).

Ensures better modularity and reliability of header usage.
2025-08-26 09:47:02 +00:00
a3e23c3514 Temporarily disable Fedora tests
All checks were successful
CI / Check spelling (pull_request) Successful in 15s
CI / Check coding style (pull_request) Successful in 28s
CI / Linux (arch) (pull_request) Successful in 11m52s
CI / Linux (ubuntu) (pull_request) Successful in 11m59s
CI / Linux (debian) (pull_request) Successful in 12m2s
CI / Check spelling (push) Successful in 17s
CI / Check coding style (push) Successful in 30s
CI / Linux (ubuntu) (push) Successful in 10m9s
CI / Linux (arch) (push) Successful in 12m4s
CI / Linux (debian) (push) Successful in 13m0s
They are failing on our runners due to issues with dependency resolution.
2025-08-07 03:27:14 +02:00
ad294cdd6d feat(editor): add asynchronous external editor support
Some checks failed
CI / Check spelling (pull_request) Successful in 18s
CI / Check coding style (pull_request) Successful in 31s
CI / Linux (fedora) (pull_request) Failing after 1m14s
CI / Linux (debian) (pull_request) Successful in 10m19s
CI / Linux (ubuntu) (pull_request) Successful in 10m42s
CI / Linux (arch) (pull_request) Successful in 13m2s
The synchronous `get_message_from_editor` blocked the main loop (`prof_run`) while launching an external editor like `vim`, halting network I/O in `session_process_events` and preventing incoming message reception.

Introduced `get_message_from_editor_async` for `cmd_editor` to run the editor asynchronously. It forks and execs the editor in a thread (`editor_thread`), suspending NCurses to free the terminal. The main loop skips `inp_readline` and `ui_update` via a new `background_mode` flag while the editor runs, allowing `session_process_events` to keep the connection alive.

On editor completion, `editor_process` (called per loop iteration) resumes NCurses with `ui_resize`, inserts the result into the readline buffer and clears `background_mode`.

Retained synchronous `get_message_from_editor` for ~20 existing code paths (e.g., `vcard_nickname`) to avoid breaking them.

Tested with `vim` and `nano`: confirms no rendering conflicts, messages received during editing, and seamless resume. Edge cases like editor crashes handled via error logging and seamless resume.
2025-08-07 03:03:43 +02:00
7 changed files with 251 additions and 53 deletions

View File

@@ -16,7 +16,7 @@ jobs:
strategy:
matrix:
flavor: [arch, debian, fedora, ubuntu]
flavor: [arch, debian, ubuntu]
name: Linux
steps:

View File

@@ -9527,16 +9527,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();
get_message_from_editor_async(NULL);
return TRUE;
}

View File

@@ -171,6 +171,8 @@ typedef enum {
RESOURCE_XA
} resource_presence_t;
extern gboolean background_mode;
gboolean string_to_verbosity(const char* cmd, int* verbosity, gchar** err_msg);
gboolean create_dir(const char* name);

View File

@@ -59,6 +59,7 @@
#include "config/tlscerts.h"
#include "config/scripts.h"
#include "command/cmd_defs.h"
#include "tools/editor.h"
#include "plugins/plugins.h"
#include "event/client_events.h"
#include "ui/ui.h"
@@ -93,6 +94,9 @@ static void _connect_default(const char* const account);
pthread_mutex_t lock;
static gboolean force_quit = FALSE;
// main.c (prof_run section)
gboolean background_mode = FALSE;
void
prof_run(gchar* log_level, gchar* account_name, gchar* config_file, gchar* log_file, gchar* theme_name, gchar** commands)
{
@@ -118,42 +122,44 @@ prof_run(gchar* log_level, gchar* account_name, gchar* config_file, gchar* log_f
log_stderr_handler();
session_check_autoaway();
line = commands ? *commands : inp_readline();
if (commands && line && memcmp(line, "/sleep", 6) == 0) {
if (!g_timer_is_active(waittimer)) {
gchar* err_msg;
if (strtoi_range(line + 7, &waittime, 0, 300, &err_msg)) {
g_timer_start(waittimer);
/* Increase the minimal runtime by the waiting time
* so we can be sure there's runtime left after executing
* the last command.
*/
min_runtime += waittime;
} else {
log_error(err_msg);
g_free(err_msg);
commands = NULL;
if (!background_mode) {
line = commands ? *commands : inp_readline();
if (commands && line && memcmp(line, "/sleep", 6) == 0) {
if (!g_timer_is_active(waittimer)) {
gchar* err_msg;
if (strtoi_range(line + 7, &waittime, 0, 300, &err_msg)) {
g_timer_start(waittimer);
/* Increase the minimal runtime by the waiting time
* so we can be sure there's runtime left after executing
* the last command.
*/
min_runtime += waittime;
} else {
log_error(err_msg);
g_free(err_msg);
commands = NULL;
}
} else if (g_timer_elapsed(waittimer, NULL) >= waittime) {
g_timer_stop(waittimer);
commands++;
if (!(*commands))
commands = NULL;
}
cont = TRUE;
} else if (line) {
ProfWin* window = wins_get_current();
cont = cmd_process_input(window, line);
if (commands) {
commands++;
if (!(*commands))
commands = NULL;
} else {
free(line);
line = NULL;
}
} else if (g_timer_elapsed(waittimer, NULL) >= waittime) {
g_timer_stop(waittimer);
commands++;
if (!(*commands))
commands = NULL;
}
cont = TRUE;
} else if (line) {
ProfWin* window = wins_get_current();
cont = cmd_process_input(window, line);
if (commands) {
commands++;
if (!(*commands))
commands = NULL;
} else {
free(line);
line = NULL;
cont = TRUE;
}
} else {
cont = TRUE;
}
#ifdef HAVE_LIBOTR
@@ -163,7 +169,12 @@ prof_run(gchar* log_level, gchar* account_name, gchar* config_file, gchar* log_f
notify_remind();
session_process_events();
iq_autoping_check();
ui_update();
editor_process(wins_get_current());
if (!background_mode) {
ui_update();
}
#ifdef HAVE_GTK
tray_update();
#endif

View File

@@ -38,15 +38,200 @@
#include <fcntl.h>
#include <glib.h>
#include <errno.h>
#include <stdio.h> // necessary for readline
#include <readline/readline.h>
#include <sys/wait.h>
#include <unistd.h>
#include <pthread.h>
#include "config/files.h"
#include "config/preferences.h"
#include "editor.h"
#include "log.h"
#include "common.h"
#include "ncurses.h"
#include "ui/ui.h"
#include "xmpp/xmpp.h"
EditorTask editor_task = { 0, FALSE, NULL, NULL, PTHREAD_MUTEX_INITIALIZER, 0, NULL, NULL };
void*
editor_thread(void* arg)
{
EditorTask* task = (EditorTask*)arg;
auto_gcharv gchar** argv = g_strsplit(task->editor_cmd, " ", 0);
pid_t pid = fork();
if (pid == 0) {
execvp(argv[0], argv);
log_error("[Editor] Failed to exec %s", argv[0]);
_exit(EXIT_FAILURE);
}
pthread_mutex_lock(&task->mutex);
if (pid > 0) {
task->pid = pid;
int status;
// Unlock mutex to avoid deadlock on the main loop
pthread_mutex_unlock(&task->mutex);
waitpid(pid, &status, 0);
pthread_mutex_lock(&task->mutex);
gsize length;
if (!g_file_get_contents(task->filename, &task->result, &length, &task->error)) {
log_error("[Editor] could not read from %s: %s", task->filename,
task->error ? task->error->message : "No GLib error given");
task->result = g_strdup(""); // Ensure valid result
} else {
g_strchomp(task->result);
if (remove(task->filename) != 0) {
log_error("[Editor] error during file deletion of %s", task->filename);
} else {
log_debug("[Editor] deleted file: %s", task->filename);
}
}
if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
task->error = g_error_new(G_FILE_ERROR, G_FILE_ERROR_FAILED,
"Editor process failed with status %d", WEXITSTATUS(status));
}
} else {
task->error = g_error_new(G_FILE_ERROR, G_FILE_ERROR_FAILED, "Fork failed: %s", strerror(errno));
task->result = g_strdup(""); // Ensure valid result
}
task->active = FALSE;
pthread_mutex_unlock(&task->mutex);
return NULL;
}
// Returns true if an error occurred
gboolean
get_message_from_editor_async(gchar* message)
{
// Check if editor is already running
pthread_mutex_lock(&editor_task.mutex);
if (editor_task.active) {
pthread_mutex_unlock(&editor_task.mutex);
log_error("[Editor] Editor already running");
return TRUE;
}
pthread_mutex_unlock(&editor_task.mutex);
// Create temporary file
auto_gchar gchar* filename = NULL;
GError* glib_error = NULL;
const char* jid = connection_get_barejid();
if (jid) {
filename = files_file_in_account_data_path(DIR_EDITOR, jid, "compose.md");
} else {
log_debug("[Editor] could not get JID");
auto_gchar gchar* data_dir = files_get_data_path(DIR_EDITOR);
if (!create_dir(data_dir)) {
log_error("[Editor] could not create directory %s", data_dir);
return TRUE;
}
filename = g_strdup_printf("%s/compose.md", data_dir);
}
if (!filename) {
log_error("[Editor] something went wrong while creating compose file");
return TRUE;
}
// Write message to file
gsize messagelen = message ? strlen(message) : 0;
if (!g_file_set_contents(filename, message ? message : "", messagelen, &glib_error)) {
log_error("[Editor] could not write to %s: %s", filename,
glib_error ? glib_error->message : "No GLib error given");
if (glib_error) {
g_error_free(glib_error);
}
return TRUE;
}
// Prepare editor command
auto_gchar gchar* editor = prefs_get_string(PREF_COMPOSE_EDITOR);
auto_gchar gchar* editor_with_filename = g_strdup_printf("%s %s", editor, filename);
// Suspend NCurses
def_prog_mode();
endwin();
// Initialize thread task
pthread_mutex_lock(&editor_task.mutex);
editor_task.active = TRUE;
editor_task.result = NULL;
editor_task.error = NULL;
g_free(editor_task.filename);
g_free(editor_task.editor_cmd);
editor_task.filename = g_strdup(filename);
editor_task.editor_cmd = g_strdup(editor_with_filename);
pthread_mutex_unlock(&editor_task.mutex);
// Set background mode
background_mode = TRUE;
log_debug("[Editor] Entering background mode");
// Start editor thread
if (pthread_create(&editor_task.thread, NULL, editor_thread, &editor_task) != 0) {
pthread_mutex_lock(&editor_task.mutex);
editor_task.active = FALSE;
editor_task.error = g_error_new(G_FILE_ERROR, G_FILE_ERROR_FAILED,
"Failed to create editor thread: %s", strerror(errno));
g_free(editor_task.filename);
g_free(editor_task.editor_cmd);
editor_task.filename = NULL;
editor_task.editor_cmd = NULL;
pthread_mutex_unlock(&editor_task.mutex);
background_mode = FALSE;
log_debug("[Editor] Exiting background mode");
reset_prog_mode();
doupdate();
log_error("[Editor] Failed to create editor thread");
return TRUE;
}
return FALSE;
}
void
editor_process(ProfWin* window)
{
pthread_mutex_lock(&editor_task.mutex);
if (editor_task.active) {
pthread_mutex_unlock(&editor_task.mutex);
return;
}
if (!editor_task.result) {
pthread_mutex_unlock(&editor_task.mutex);
return;
}
g_strchomp(editor_task.result);
rl_replace_line(editor_task.result, 0);
rl_point = rl_end;
rl_forced_update_display();
reset_prog_mode();
doupdate();
ui_resize();
g_free(editor_task.result);
g_free(editor_task.filename);
g_free(editor_task.editor_cmd);
editor_task.result = NULL;
editor_task.filename = NULL;
editor_task.editor_cmd = NULL;
if (editor_task.error) {
log_error("[Editor] %s", editor_task.error->message);
g_error_free(editor_task.error);
editor_task.error = NULL;
}
pthread_mutex_unlock(&editor_task.mutex);
background_mode = FALSE;
log_debug("[Editor] Exiting background mode");
}
// Deprecated synchronous editor call. Returns a message as returned_message.
// Please avoid using it, since it blocks execution of the message handling and other important functionality until the editor is closed.
// Returns true if an error occurred
gboolean
get_message_from_editor(gchar* message, gchar** returned_message)

View File

@@ -37,8 +37,24 @@
#ifndef TOOLS_EDITOR_H
#define TOOLS_EDITOR_H
#include "ui/win_types.h"
#include <glib.h>
typedef struct
{
pid_t pid; // Editor process PID
gboolean active; // Thread running
gchar* result; // Edited message
GError* error; // Error if any
pthread_mutex_t mutex; // Protect access
pthread_t thread; // Editor thread
gchar* filename; // Temporary file path
gchar* editor_cmd; // Editor command with filename
} EditorTask;
extern EditorTask editor_task;
gboolean get_message_from_editor(gchar* message, gchar** returned_message);
gboolean get_message_from_editor_async(gchar* message);
void editor_process(ProfWin* window);
#endif

View File

@@ -974,14 +974,7 @@ _inp_rl_send_to_editor(int count, int key)
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();
get_message_from_editor_async(rl_line_buffer);
return 0;
}