From 366641e9188df09d7bef6c091e916d277ddc37a1 Mon Sep 17 00:00:00 2001 From: Harald Hoyer Date: Fri, 4 Jul 2025 14:47:45 +0200 Subject: [PATCH] terminal: Add image and file path pasting support for Shift+Ctrl+V MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enhance the paste functionality to detect different clipboard content types: - File URIs are pasted as shell-quoted file paths - Images are saved to temporary files and their paths are pasted - Plain text continues to work as before with security checks This provides a consistent experience with the drag-and-drop functionality and allows users to easily paste file paths from file managers or temporary paths for copied images. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/kgx-terminal.c | 160 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 155 insertions(+), 5 deletions(-) diff --git a/src/kgx-terminal.c b/src/kgx-terminal.c index fffbb92..fd9f936 100644 --- a/src/kgx-terminal.c +++ b/src/kgx-terminal.c @@ -28,10 +28,13 @@ #include "kgx-config.h" #include +#include +#include #include #define PCRE2_CODE_UNIT_WIDTH 0 #include +#include #include "kgx-despatcher.h" #include "kgx-marshals.h" @@ -41,6 +44,16 @@ #include "kgx-terminal.h" +/* Translators: The user ctrl-clicked, or used ‘Open Link’, on a URI that, + * for whatever reason, we were unable to open. */ +#define URI_FAILED_MESSAGE C_("toast-message", "Couldn't Open Link") + +/* MIME types for clipboard content detection */ +#define MIME_TEXT_URI_LIST "text/uri-list" +#define MIME_TEXT_PLAIN "text/plain;charset=utf-8" +#define MIME_PORTAL_FILES "application/vnd.portal.filetransfer" +#define MIME_PORTAL_FILES_OLD "application/vnd.portal.files" + /* Regex adapted from TerminalWidget.vala in Pantheon Terminal */ #define USERCHARS "-[:alnum:]" @@ -365,6 +378,110 @@ copy_activated (KgxTerminal *self) } +static char * +quote_file_path (GFile *file) +{ + g_autofree char *path = g_file_get_path (file); + + if (G_LIKELY (path)) { + return g_shell_quote (path); + } else { + /* Fall back to URI if path extraction fails */ + g_autofree char *uri = g_file_get_uri (file); + return g_strdup (uri); + } +} + + +static void +got_file_list (GObject *source, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr (KgxTerminal) self = user_data; + g_autoptr (GError) error = NULL; + g_autoptr (GStrvBuilder) builder = NULL; + g_auto (GStrv) items = NULL; + g_autofree char *text = NULL; + const GValue *value; + GSList *files; + + /* Get the resulting file list */ + value = gdk_clipboard_read_value_finish (GDK_CLIPBOARD (source), result, &error); + + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) { + return; + } else if (error) { + g_critical ("Couldn't paste files: %s\n", error->message); + return; + } + + builder = g_strv_builder_new (); + files = g_value_get_boxed (value); + + for (GSList *l = files; l; l = g_slist_next (l)) { + g_autofree char *quoted = quote_file_path (G_FILE (l->data)); + g_strv_builder_add (builder, quoted); + } + + /* Add empty string to facilitate consecutive pastes */ + g_strv_builder_add (builder, ""); + items = g_strv_builder_end (builder); + text = g_strjoinv (" ", items); + + kgx_terminal_accept_paste (self, text); +} + + +static void +got_image (GObject *source, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr (KgxTerminal) self = user_data; + g_autoptr (GError) error = NULL; + g_autoptr (GdkTexture) texture = NULL; + g_autofree char *text = NULL; + g_autofree char *temp_path = NULL; + g_autofree char *quoted_path = NULL; + GBytes *bytes; + int fd; + + /* Get the resulting texture */ + texture = gdk_clipboard_read_texture_finish (GDK_CLIPBOARD (source), result, &error); + + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) { + return; + } else if (error) { + g_critical ("Couldn't paste image: %s\n", error->message); + return; + } + + /* Create a temporary file for the image */ + fd = g_file_open_tmp ("kgx-clipboard-XXXXXX.png", &temp_path, &error); + if (fd == -1) { + g_critical ("Failed to create temporary file for clipboard image: %s\n", error->message); + return; + } + close (fd); + + /* Save texture data directly */ + bytes = gdk_texture_save_to_png_bytes (texture); + if (!g_file_set_contents (temp_path, g_bytes_get_data (bytes, NULL), + g_bytes_get_size (bytes), &error)) { + g_bytes_unref (bytes); + g_unlink (temp_path); + g_critical ("Failed to save clipboard image to temporary file: %s\n", error->message); + return; + } + g_bytes_unref (bytes); + + /* Quote the path and paste it */ + quoted_path = g_shell_quote (temp_path); + kgx_terminal_accept_paste (self, quoted_path); +} + + static void got_text (GObject *source, GAsyncResult *result, @@ -392,11 +509,44 @@ static void paste_activated (KgxTerminal *self) { GdkClipboard *cb = gtk_widget_get_clipboard (GTK_WIDGET (self)); - - gdk_clipboard_read_text_async (cb, - self->cancellable, - got_text, - g_object_ref (self)); + GdkContentFormats *formats = gdk_clipboard_get_formats (cb); + const char *const *mimes = gdk_content_formats_get_mime_types (formats, NULL); + + /* Check clipboard content types in priority order */ + if (G_LIKELY (g_strv_contains (mimes, MIME_PORTAL_FILES)) || + g_strv_contains (mimes, MIME_PORTAL_FILES_OLD)) { + /* File list from modern applications */ + gdk_clipboard_read_value_async (cb, + GDK_TYPE_FILE_LIST, + G_PRIORITY_DEFAULT, + self->cancellable, + got_file_list, + g_object_ref (self)); + } else if (g_strv_contains (mimes, MIME_TEXT_URI_LIST)) { + /* URI list from older applications */ + gdk_clipboard_read_value_async (cb, + GDK_TYPE_FILE_LIST, + G_PRIORITY_DEFAULT, + self->cancellable, + got_file_list, + g_object_ref (self)); + } else if (g_strv_contains (mimes, "image/png") || + g_strv_contains (mimes, "image/jpeg") || + g_strv_contains (mimes, "image/gif") || + g_strv_contains (mimes, "image/bmp") || + g_strv_contains (mimes, "image/webp")) { + /* Image data - save to temp file */ + gdk_clipboard_read_texture_async (cb, + self->cancellable, + got_image, + g_object_ref (self)); + } else { + /* Default: try to read as text */ + gdk_clipboard_read_text_async (cb, + self->cancellable, + got_text, + g_object_ref (self)); + } } -- 2.49.0