From a9ef000328b0f6ac8a9980e3bf9d26dad5ffc911 Mon Sep 17 00:00:00 2001 From: Michael Vetter Date: Tue, 24 Mar 2026 21:20:24 +0000 Subject: [PATCH 1/2] fix: Improve status reporting and filename handling for /url save Use the original aesgcm:// URL when downloading OMEMO downloads. Then in the final message use the decrypted file destination instead of the internal temporary path. Use the unique download ID instead of the URL for message updates to have proper progress reporting. Unify the final transfer status to "done" across both downloads and uploads. Fixes: https://github.com/profanity-im/profanity/issues/1939 Signed-off-by: Michael Vetter --- src/command/cmd_funcs.c | 28 ++++++++++++++++++++-------- src/tools/aesgcm_download.c | 8 ++++++-- src/tools/http_download.c | 31 +++++++++++++++++++------------ src/tools/http_download.h | 2 ++ src/tools/http_upload.c | 10 ++++++++-- 5 files changed, 55 insertions(+), 24 deletions(-) diff --git a/src/command/cmd_funcs.c b/src/command/cmd_funcs.c index dbf92d21..68d6188a 100644 --- a/src/command/cmd_funcs.c +++ b/src/command/cmd_funcs.c @@ -9209,18 +9209,30 @@ _prepare_filename(ProfWin* window, gchar* url, gchar* path) break; } - // Ensure that the downloads directory exists for saving cleartexts. - auto_gchar gchar* downloads_dir = path ? get_expanded_path(path) : files_get_download_path(jid); - if (g_mkdir_with_parents(downloads_dir, S_IRWXU) != 0) { - cons_show_error("Failed to create download directory " - "at '%s' with error '%s'", - downloads_dir, strerror(errno)); - return NULL; + auto_gchar gchar* expanded_path = path ? get_expanded_path(path) : files_get_download_path(jid); + + // If path is provided and doesn't look like a directory, create its parent. + // Otherwise, create the directory itself. + if (path && !g_str_has_suffix(expanded_path, G_DIR_SEPARATOR_S) && !g_file_test(expanded_path, G_FILE_TEST_IS_DIR)) { + auto_gchar gchar* dirname = g_path_get_dirname(expanded_path); + if (g_mkdir_with_parents(dirname, S_IRWXU) != 0) { + cons_show_error("Failed to create download directory " + "at '%s' with error '%s'", + dirname, strerror(errno)); + return NULL; + } + } else { + if (g_mkdir_with_parents(expanded_path, S_IRWXU) != 0) { + cons_show_error("Failed to create download directory " + "at '%s' with error '%s'", + expanded_path, strerror(errno)); + return NULL; + } } // Generate an unique filename from the URL that should be stored in the // downloads directory. - return unique_filename_from_url(url, downloads_dir); + return unique_filename_from_url(url, expanded_path); } #ifdef HAVE_OMEMO diff --git a/src/tools/aesgcm_download.c b/src/tools/aesgcm_download.c index 1c34074e..e6daae71 100644 --- a/src/tools/aesgcm_download.c +++ b/src/tools/aesgcm_download.c @@ -80,9 +80,11 @@ aesgcm_file_get(void* userdata) http_dl->worker = aesgcm_dl->worker; http_dl->id = strdup(aesgcm_dl->id); http_dl->url = strdup(https_url); + http_dl->display_url = strdup(aesgcm_dl->url); http_dl->filename = strdup(tmpname); http_dl->cmd_template = NULL; http_dl->silent = FALSE; + http_dl->silent_done = TRUE; http_dl->return_bytes_received = TRUE; aesgcm_dl->http_dl = http_dl; @@ -113,10 +115,12 @@ aesgcm_file_get(void* userdata) http_print_transfer_update(aesgcm_dl->window, aesgcm_dl->id, "Downloading '%s' failed: Failed to decrypt " "file (%s).", - https_url, gcry_strerror(crypt_res)); + aesgcm_dl->url, gcry_strerror(crypt_res)); } else { http_print_transfer_update(aesgcm_dl->window, aesgcm_dl->id, - "Decrypted file saved to '%s'", aesgcm_dl->filename); + "Downloading '%s': done\nSaved to '%s'", + aesgcm_dl->url, aesgcm_dl->filename); + win_mark_received(aesgcm_dl->window, aesgcm_dl->id); } if (aesgcm_dl->cmd_template != NULL) { diff --git a/src/tools/http_download.c b/src/tools/http_download.c index 720cc40c..8e14fa66 100644 --- a/src/tools/http_download.c +++ b/src/tools/http_download.c @@ -32,7 +32,6 @@ #include "common.h" GSList* download_processes = NULL; -gboolean silent = FALSE; static int _xferinfo(void* userdata, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow) @@ -58,9 +57,16 @@ _xferinfo(void* userdata, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultot dlperc = (100 * dlnow) / dltotal; } - if (!silent) - http_print_transfer_update(download->window, download->url, - "Downloading '%s': %d%%", download->url, dlperc); + if (!download->silent) { + const char* url = download->display_url ? download->display_url : download->url; + if (dlnow == dltotal && dltotal > 0) { + http_print_transfer_update(download->window, download->id, + "Downloading '%s': done", url); + } else { + http_print_transfer_update(download->window, download->id, + "Downloading '%s': %d%%", url, dlperc); + } + } pthread_mutex_unlock(&lock); @@ -85,15 +91,15 @@ http_file_get(void* userdata) CURL* curl; CURLcode res; - silent = download->silent; download->cancel = 0; download->bytes_received = 0; pthread_mutex_lock(&lock); - if (!silent) { + const char* display_url = download->display_url ? download->display_url : download->url; + if (!download->silent) { http_print_transfer(download->window, download->id, - "Downloading '%s': 0%%", download->url); + "Downloading '%s': 0%%", display_url); } FILE* outfh = fopen(download->filename, "wb"); @@ -172,18 +178,18 @@ http_file_get(void* userdata) http_print_transfer_update(download->window, download->id, "Downloading '%s' failed: " "Download was canceled", - download->url); + display_url); } else { http_print_transfer_update(download->window, download->id, "Downloading '%s' failed: %s", - download->url, err); + display_url, err); } free(err); } else { - if (!download->cancel && !silent) { + if (!download->cancel && !download->silent && !download->silent_done) { http_print_transfer_update(download->window, download->id, "Downloading '%s': done\nSaved to '%s'", - download->url, download->filename); + display_url, download->filename); win_mark_received(download->window, download->id); if (download->return_bytes_received) { ret = malloc(sizeof(*ret)); @@ -204,7 +210,7 @@ http_file_get(void* userdata) http_print_transfer_update(download->window, download->id, "Downloading '%s' failed: Unable to call " "command '%s' with file at '%s' (%s).", - download->url, + display_url, download->cmd_template, download->filename, "TODO: Log the error"); @@ -221,6 +227,7 @@ out: free(download->filename); free(download->url); + free(download->display_url); free(download->id); free(download); diff --git a/src/tools/http_download.h b/src/tools/http_download.h index 4b7a40e7..c020e864 100644 --- a/src/tools/http_download.h +++ b/src/tools/http_download.h @@ -25,6 +25,7 @@ typedef struct http_download_t { char* id; char* url; + char* display_url; char* filename; char* cmd_template; curl_off_t bytes_received; @@ -32,6 +33,7 @@ typedef struct http_download_t pthread_t worker; int cancel; gboolean silent; + gboolean silent_done; gboolean return_bytes_received; } HTTPDownload; diff --git a/src/tools/http_upload.c b/src/tools/http_upload.c index 6162034f..919c7d87 100644 --- a/src/tools/http_upload.c +++ b/src/tools/http_upload.c @@ -65,7 +65,13 @@ _xferinfo(void* userdata, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultot ulperc = (100 * ulnow) / ultotal; } - gchar* msg = g_strdup_printf("Uploading '%s': %d%%", upload->filename, ulperc); + gchar* msg = NULL; + if (ulnow == ultotal && ultotal > 0) { + msg = g_strdup_printf("Uploading '%s': done", upload->filename); + } else { + msg = g_strdup_printf("Uploading '%s': %d%%", upload->filename, ulperc); + } + if (!msg) { msg = g_strdup(FALLBACK_MSG); } @@ -288,7 +294,7 @@ http_file_put(void* userdata) cons_show_error(err_msg); } else { if (!upload->cancel) { - auto_gchar gchar* status_msg = g_strdup_printf("Uploading '%s': 100%%", upload->filename); + auto_gchar gchar* status_msg = g_strdup_printf("Uploading '%s': done", upload->filename); if (!status_msg) { status_msg = g_strdup(FALLBACK_MSG); } From 63af72773c13d0e2a2e3c696aed46c230280049a Mon Sep 17 00:00:00 2001 From: Michael Vetter Date: Tue, 24 Mar 2026 23:11:03 +0100 Subject: [PATCH 2/2] feat: Implement color coded status messages for file transfers Introduce meaningful color feedback for background tasks like file downloads and uploads. Progress is displayed in a neutral color, while successful completions turn green and failures turn red. We reuse the existing THEME colors for now. Add new UI flags ENTRY_COMPLETED and ENTRY_ERROR to the buffer entry system. So we don't misuse delivery receipts anymore. Fixes: https://github.com/profanity-im/profanity/issues/1758 Signed-off-by: Michael Vetter --- src/tools/aesgcm_download.c | 14 +++++++------- src/tools/http_common.c | 8 ++++---- src/tools/http_common.h | 4 ++-- src/tools/http_download.c | 30 +++++++++++++++++------------- src/tools/http_upload.c | 13 +++++++++---- src/ui/ui.h | 14 ++++++++------ src/ui/window.c | 29 ++++++++++++++++++++++++++++- src/ui/window.h | 2 ++ 8 files changed, 77 insertions(+), 37 deletions(-) diff --git a/src/tools/aesgcm_download.c b/src/tools/aesgcm_download.c index e6daae71..b7e7cd85 100644 --- a/src/tools/aesgcm_download.c +++ b/src/tools/aesgcm_download.c @@ -43,7 +43,7 @@ aesgcm_file_get(void* userdata) // and tag stored in the URL fragment. if (omemo_parse_aesgcm_url(aesgcm_dl->url, &https_url, &fragment) != 0) { cons_show_error("Download failed: Cannot parse URL '%s'.", aesgcm_dl->url); - http_print_transfer_update(aesgcm_dl->window, aesgcm_dl->id, + http_print_transfer_update(aesgcm_dl->window, aesgcm_dl->id, THEME_ERROR, ENTRY_ERROR, "Download failed: Cannot parse URL '%s'.", aesgcm_dl->url); return NULL; @@ -54,7 +54,7 @@ aesgcm_file_get(void* userdata) auto_gchar gchar* tmpname = NULL; auto_gfd gint tmpfd = 0; if ((tmpfd = g_file_open_tmp("profanity.XXXXXX", &tmpname, NULL)) == -1) { - http_print_transfer_update(aesgcm_dl->window, aesgcm_dl->id, + http_print_transfer_update(aesgcm_dl->window, aesgcm_dl->id, THEME_ERROR, ENTRY_ERROR, "Downloading '%s' failed: Unable to create " "temporary ciphertext file for writing " "(%s).", @@ -65,7 +65,7 @@ aesgcm_file_get(void* userdata) // Open the target file for storing the cleartext. auto_FILE FILE* outfh = fopen(aesgcm_dl->filename, "wb"); if (outfh == NULL) { - http_print_transfer_update(aesgcm_dl->window, aesgcm_dl->id, + http_print_transfer_update(aesgcm_dl->window, aesgcm_dl->id, THEME_ERROR, ENTRY_ERROR, "Downloading '%s' failed: Unable to open " "output file at '%s' for writing (%s).", https_url, aesgcm_dl->filename, @@ -97,7 +97,7 @@ aesgcm_file_get(void* userdata) FILE* tmpfh = fopen(tmpname, "rb"); if (tmpfh == NULL) { - http_print_transfer_update(aesgcm_dl->window, aesgcm_dl->id, + http_print_transfer_update(aesgcm_dl->window, aesgcm_dl->id, THEME_ERROR, ENTRY_ERROR, "Downloading '%s' failed: Unable to open " "temporary file at '%s' for reading (%s).", aesgcm_dl->url, tmpname, @@ -112,12 +112,12 @@ aesgcm_file_get(void* userdata) remove(tmpname); if (crypt_res != GPG_ERR_NO_ERROR) { - http_print_transfer_update(aesgcm_dl->window, aesgcm_dl->id, + http_print_transfer_update(aesgcm_dl->window, aesgcm_dl->id, THEME_ERROR, ENTRY_ERROR, "Downloading '%s' failed: Failed to decrypt " "file (%s).", aesgcm_dl->url, gcry_strerror(crypt_res)); } else { - http_print_transfer_update(aesgcm_dl->window, aesgcm_dl->id, + http_print_transfer_update(aesgcm_dl->window, aesgcm_dl->id, THEME_ONLINE, ENTRY_COMPLETED, "Downloading '%s': done\nSaved to '%s'", aesgcm_dl->url, aesgcm_dl->filename); win_mark_received(aesgcm_dl->window, aesgcm_dl->id); @@ -130,7 +130,7 @@ aesgcm_file_get(void* userdata) // TODO: Log the error. if (!call_external(argv)) { - http_print_transfer_update(aesgcm_dl->window, aesgcm_dl->id, + http_print_transfer_update(aesgcm_dl->window, aesgcm_dl->id, THEME_ERROR, ENTRY_ERROR, "Downloading '%s' failed: Unable to call " "command '%s' with file at '%s' (%s).", aesgcm_dl->url, diff --git a/src/tools/http_common.c b/src/tools/http_common.c index cfec1bcf..0386e08d 100644 --- a/src/tools/http_common.c +++ b/src/tools/http_common.c @@ -19,7 +19,7 @@ #define FALLBACK_MSG "" void -http_print_transfer_update(ProfWin* window, char* id, const char* fmt, ...) +http_print_transfer_update(ProfWin* window, char* id, theme_item_t theme_item, int flags, const char* fmt, ...) { va_list args; @@ -29,7 +29,7 @@ http_print_transfer_update(ProfWin* window, char* id, const char* fmt, ...) va_end(args); if (window->type != WIN_CONSOLE) { - win_update_entry_message(window, id, msg->str); + win_update_entry(window, id, msg->str, theme_item, flags); } else { cons_show("%s", msg->str); } @@ -38,7 +38,7 @@ http_print_transfer_update(ProfWin* window, char* id, const char* fmt, ...) } void -http_print_transfer(ProfWin* window, char* id, const char* fmt, ...) +http_print_transfer(ProfWin* window, char* id, theme_item_t theme_item, const char* fmt, ...) { va_list args; @@ -47,7 +47,7 @@ http_print_transfer(ProfWin* window, char* id, const char* fmt, ...) g_string_vprintf(msg, fmt, args); va_end(args); - win_print_http_transfer(window, msg->str, id); + win_print_status_with_id(window, msg->str, id, theme_item, 0); g_string_free(msg, TRUE); } diff --git a/src/tools/http_common.h b/src/tools/http_common.h index 529c05c9..f1f9b56a 100644 --- a/src/tools/http_common.h +++ b/src/tools/http_common.h @@ -12,7 +12,7 @@ #include "ui/window.h" -void http_print_transfer(ProfWin* window, char* id, const char* fmt, ...); -void http_print_transfer_update(ProfWin* window, char* id, const char* fmt, ...); +void http_print_transfer(ProfWin* window, char* id, theme_item_t theme_item, const char* fmt, ...); +void http_print_transfer_update(ProfWin* window, char* id, theme_item_t theme_item, int flags, const char* fmt, ...); #endif diff --git a/src/tools/http_download.c b/src/tools/http_download.c index 8e14fa66..bc561233 100644 --- a/src/tools/http_download.c +++ b/src/tools/http_download.c @@ -60,10 +60,12 @@ _xferinfo(void* userdata, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultot if (!download->silent) { const char* url = download->display_url ? download->display_url : download->url; if (dlnow == dltotal && dltotal > 0) { - http_print_transfer_update(download->window, download->id, - "Downloading '%s': done", url); + if (!download->silent_done) { + http_print_transfer_update(download->window, download->id, THEME_ONLINE, ENTRY_COMPLETED, + "Downloading '%s': done", url); + } } else { - http_print_transfer_update(download->window, download->id, + http_print_transfer_update(download->window, download->id, THEME_DEFAULT, 0, "Downloading '%s': %d%%", url, dlperc); } } @@ -98,13 +100,13 @@ http_file_get(void* userdata) pthread_mutex_lock(&lock); const char* display_url = download->display_url ? download->display_url : download->url; if (!download->silent) { - http_print_transfer(download->window, download->id, + http_print_transfer(download->window, download->id, THEME_DEFAULT, "Downloading '%s': 0%%", display_url); } FILE* outfh = fopen(download->filename, "wb"); if (outfh == NULL) { - http_print_transfer_update(download->window, download->id, + http_print_transfer_update(download->window, download->id, THEME_ERROR, ENTRY_ERROR, "Downloading '%s' failed: Unable to open " "output file at '%s' for writing (%s).", download->url, download->filename, @@ -175,22 +177,24 @@ http_file_get(void* userdata) g_free(cert_path); if (err) { if (download->cancel) { - http_print_transfer_update(download->window, download->id, + http_print_transfer_update(download->window, download->id, THEME_ERROR, ENTRY_ERROR, "Downloading '%s' failed: " "Download was canceled", display_url); } else { - http_print_transfer_update(download->window, download->id, + http_print_transfer_update(download->window, download->id, THEME_ERROR, ENTRY_ERROR, "Downloading '%s' failed: %s", display_url, err); } free(err); } else { - if (!download->cancel && !download->silent && !download->silent_done) { - http_print_transfer_update(download->window, download->id, - "Downloading '%s': done\nSaved to '%s'", - display_url, download->filename); - win_mark_received(download->window, download->id); + if (!download->cancel) { + if (!download->silent && !download->silent_done) { + http_print_transfer_update(download->window, download->id, THEME_ONLINE, ENTRY_COMPLETED, + "Downloading '%s': done\nSaved to '%s'", + display_url, download->filename); + win_mark_received(download->window, download->id); + } if (download->return_bytes_received) { ret = malloc(sizeof(*ret)); if (ret) { @@ -207,7 +211,7 @@ http_file_get(void* userdata) // TODO: Log the error. if (!call_external(argv)) { - http_print_transfer_update(download->window, download->id, + http_print_transfer_update(download->window, download->id, THEME_ERROR, ENTRY_ERROR, "Downloading '%s' failed: Unable to call " "command '%s' with file at '%s' (%s).", display_url, diff --git a/src/tools/http_upload.c b/src/tools/http_upload.c index 919c7d87..3cd7cb33 100644 --- a/src/tools/http_upload.c +++ b/src/tools/http_upload.c @@ -66,8 +66,12 @@ _xferinfo(void* userdata, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultot } gchar* msg = NULL; + theme_item_t theme = THEME_DEFAULT; + int flags = 0; if (ulnow == ultotal && ultotal > 0) { msg = g_strdup_printf("Uploading '%s': done", upload->filename); + theme = THEME_ONLINE; + flags = ENTRY_COMPLETED; } else { msg = g_strdup_printf("Uploading '%s': %d%%", upload->filename, ulperc); } @@ -75,7 +79,7 @@ _xferinfo(void* userdata, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultot if (!msg) { msg = g_strdup(FALLBACK_MSG); } - win_update_entry_message(upload->window, upload->put_url, msg); + win_update_entry(upload->window, upload->put_url, msg, theme, flags); g_free(msg); pthread_mutex_unlock(&lock); @@ -156,7 +160,7 @@ http_file_put(void* userdata) if (!msg) { msg = g_strdup(FALLBACK_MSG); } - win_print_http_transfer(upload->window, msg, upload->put_url); + win_print_status_with_id(upload->window, msg, upload->put_url, THEME_DEFAULT, 0); auto_gchar gchar* cert_path = prefs_get_string(PREF_TLS_CERTPATH); auto_gchar gchar* cafile = cafile_get_name(); @@ -284,12 +288,13 @@ http_file_put(void* userdata) if (!err_msg) { err_msg = g_strdup(FALLBACK_MSG); } + win_update_entry(upload->window, upload->put_url, err_msg, THEME_ERROR, ENTRY_ERROR); } else { err_msg = g_strdup_printf("Uploading '%s' failed: %s", upload->filename, err); if (!err_msg) { err_msg = g_strdup(FALLBACK_MSG); } - win_update_entry_message(upload->window, upload->put_url, err_msg); + win_update_entry(upload->window, upload->put_url, err_msg, THEME_ERROR, ENTRY_ERROR); } cons_show_error(err_msg); } else { @@ -298,7 +303,7 @@ http_file_put(void* userdata) if (!status_msg) { status_msg = g_strdup(FALLBACK_MSG); } - win_update_entry_message(upload->window, upload->put_url, status_msg); + win_update_entry(upload->window, upload->put_url, status_msg, THEME_ONLINE, ENTRY_COMPLETED); win_mark_received(upload->window, upload->put_url); char* url = NULL; diff --git a/src/ui/ui.h b/src/ui/ui.h index f47bb906..ea0777db 100644 --- a/src/ui/ui.h +++ b/src/ui/ui.h @@ -25,12 +25,14 @@ #include "otr/otr.h" #endif -#define NO_ME 1 -#define NO_DATE 2 -#define NO_EOL 4 -#define NO_COLOUR_FROM 8 -#define NO_COLOUR_DATE 16 -#define UNTRUSTED 32 +#define NO_ME 1 +#define NO_DATE 2 +#define NO_EOL 4 +#define NO_COLOUR_FROM 8 +#define NO_COLOUR_DATE 16 +#define UNTRUSTED 32 +#define ENTRY_COMPLETED 64 +#define ENTRY_ERROR 128 // core UI void ui_init(void); diff --git a/src/ui/window.c b/src/ui/window.c index 218e252d..2ee55176 100644 --- a/src/ui/window.c +++ b/src/ui/window.c @@ -1659,7 +1659,7 @@ win_appendln_highlight(ProfWin* window, theme_item_t theme_item, const char* con void win_print_http_transfer(ProfWin* window, const char* const message, char* id) { - win_print_outgoing_with_receipt(window, "!", NULL, message, id, NULL); + win_print_status_with_id(window, message, id, THEME_DEFAULT, 0); } void @@ -1708,6 +1708,33 @@ win_update_entry_message(ProfWin* window, const char* const id, const char* cons } } +void +win_update_entry(ProfWin* window, const char* const id, const char* const message, theme_item_t theme_item, int flags) +{ + if (window->type == WIN_CONSOLE) + return; + ProfBuffEntry* entry = buffer_get_entry_by_id(window->layout->buffer, id); + if (entry) { + if (message) { + free(entry->message); + entry->message = strdup(message); + } + entry->theme_item = theme_item; + entry->flags |= flags; + win_redraw(window); + } +} + +void +win_print_status_with_id(ProfWin* window, const char* const message, char* id, theme_item_t theme_item, int flags) +{ + GDateTime* time = g_date_time_new_now_local(); + int y_start_pos = getcury(window->layout->win); + _win_print_internal(window, "!", 0, time, flags, theme_item, NULL, message, NULL); + buffer_append(window->layout->buffer, "!", 0, time, flags, theme_item, NULL, NULL, message, NULL, id, y_start_pos, getcury(window->layout->win)); + g_date_time_unref(time); +} + void win_remove_entry_message(ProfWin* window, const char* const id) { diff --git a/src/ui/window.h b/src/ui/window.h index 08e3e8e3..55665511 100644 --- a/src/ui/window.h +++ b/src/ui/window.h @@ -56,6 +56,8 @@ void win_sub_print(WINDOW* win, char* msg, gboolean newline, gboolean wrap, int void win_sub_newline_lazy(WINDOW* win); void win_mark_received(ProfWin* window, const char* const id); void win_update_entry_message(ProfWin* window, const char* const id, const char* const message); +void win_update_entry(ProfWin* window, const char* const id, const char* const message, theme_item_t theme_item, int flags); +void win_print_status_with_id(ProfWin* window, const char* const message, char* id, theme_item_t theme_item, int flags); gboolean win_has_active_subwin(ProfWin* window);