From ce041fab840a3d1be30a5963a8d86c0da83a33bc Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 4 Apr 2026 13:32:18 +0530 Subject: [PATCH] More work on DnD protocol implementation --- docs/dnd-protocol.rst | 1 + kitty/dnd.c | 82 +++++++++++++++++++++++++++++++++++++++---- kitty/dnd.h | 2 ++ kitty/glfw.c | 15 ++++++++ kitty/screen.c | 5 +++ kitty/state.h | 17 +++++---- 6 files changed, 106 insertions(+), 16 deletions(-) diff --git a/docs/dnd-protocol.rst b/docs/dnd-protocol.rst index 5fcfa02e5..57c97a2ce 100644 --- a/docs/dnd-protocol.rst +++ b/docs/dnd-protocol.rst @@ -259,6 +259,7 @@ image by sending:: OSC _dnd_code ; t=P:x=idx ST Where ``idx`` is now a zero based index with zero being the first image and so on. +Sending an ``idx`` out of bounds means the drag image should be removed. Once the terminal program has sent all data and images for the drag operation, it indicates the drag should be started by sending ``t=P:x=-1``. At diff --git a/kitty/dnd.c b/kitty/dnd.c index 0b1766011..70aafce1f 100644 --- a/kitty/dnd.c +++ b/kitty/dnd.c @@ -10,6 +10,7 @@ #include "control-codes.h" #include "safe-wrappers.h" #include "iqsort.h" +#include "png-reader.h" #include #include #include @@ -865,7 +866,7 @@ void drag_free_offer(Window *w) { free(ds.mimes_buf); ds.mimes_buf = NULL; ds.allowed_operations = 0; - ds.offer_being_built = false; + ds.state = DRAG_SOURCE_NONE; if (ds.items) { for (size_t i=0; i < ds.num_mimes; i++) free(ds.items[i].optional_data); free(ds.items); @@ -879,13 +880,20 @@ drag_free_offer(Window *w) { ds.images_sent_total_sz = 0; } +static void +cancel_drag(Window *w, int error_code) { + if (error_code) drop_send_error(w, error_code); + if (global_state.drag_source.is_active && global_state.drag_source.from_window == w->id) cancel_current_drag_source(); + drag_free_offer(w); +} + void drag_add_mimes(Window *w, int allowed_operations, const char *data, size_t sz, bool has_more) { -#define abrt(code) { drop_send_error(w, code); drag_free_offer(w); return; } - if (allowed_operations && ds.offer_being_built) drag_free_offer(w); +#define abrt(code) { cancel_drag(w, code); return; } + if (allowed_operations && ds.state != DRAG_SOURCE_NONE) cancel_drag(w, 0); if (allowed_operations && !ds.allowed_operations) ds.allowed_operations = allowed_operations; if (!ds.allowed_operations) { abrt(EINVAL); } - ds.offer_being_built = true; + ds.state = DRAG_SOURCE_BEING_BUILT; size_t new_sz = ds.bufsz + sz; if (new_sz > MIME_LIST_SIZE_CAP) abrt(EFBIG); ds.mimes_buf = realloc(ds.mimes_buf, ds.bufsz + sz + 1); @@ -917,7 +925,7 @@ drag_add_mimes(Window *w, int allowed_operations, const char *data, size_t sz, b void drag_add_pre_sent_data(Window *w, unsigned idx, const uint8_t *payload, size_t sz) { - if (!ds.offer_being_built || idx >= ds.num_mimes) abrt(EINVAL); + if (ds.state != DRAG_SOURCE_BEING_BUILT || idx >= ds.num_mimes) abrt(EINVAL); if (sz + ds.pre_sent_total_sz > PRESENT_DATA_CAP) abrt(EFBIG); ds.pre_sent_total_sz += sz; #define item ds.items[idx] @@ -937,12 +945,14 @@ drag_add_pre_sent_data(Window *w, unsigned idx, const uint8_t *payload, size_t s #undef item } +#define img ds.images[idx] + void drag_add_image(Window *w, unsigned idx, int fmt, int width, int height, const uint8_t *payload, size_t sz) { + if (ds.state != DRAG_SOURCE_BEING_BUILT) abrt(EINVAL); if (idx + 1 >= arraysz(ds.images)) abrt(EFBIG); if (ds.images_sent_total_sz + sz > PRESENT_DATA_CAP) abrt(EFBIG); ds.images_sent_total_sz += sz; -#define img ds.images[idx] if (!img.started) { if (fmt != 24 && fmt != 32 && fmt != 100) abrt(EINVAL); if (width < 1 || height < 1) abrt(EINVAL); @@ -960,8 +970,66 @@ drag_add_image(Window *w, unsigned idx, int fmt, int width, int height, const ui size_t outlen = img.capacity - img.sz; if (!base64_decode_stream(&img.base64_state, payload, sz, img.data + img.sz, &outlen)) abrt(EINVAL); img.sz += outlen; -#undef img } +void +drag_change_image(Window *w, unsigned idx) { + ds.img_idx = idx; + if (ds.state == DRAG_SOURCE_STARTED) change_drag_image(idx); +} + +static bool +expand_rgb_data(Window *w, size_t idx) { +#define fail(code) { cancel_drag(w, code); return false; } + if (img.sz != (size_t)img.width * (size_t)img.height * 3) fail(EINVAL); + const size_t sz = img.width * img.height * 4; + RAII_ALLOC(uint8_t, expanded, malloc(sz)); + if (!expanded) fail(ENOMEM); + memset(expanded, 0xff, sz); + for (int r = 0; r < img.height; r++) { + uint8_t *src_row = img.data + r * img.width * 3, *dest_row = expanded + r * img.width * 4; + for (int c = 0; c < img.width; c++) memcpy(dest_row + c * 4, src_row + c * 3, 3); + } + SWAP(img.data, expanded); img.sz = sz; img.fmt = 32; + return true; +} + +static bool +expand_png_data(Window *w, size_t idx) { + png_read_data d = {0}; + inflate_png_inner(&d, img.data, img.sz, 2000); + if (d.ok) { + free(img.data); + img.data = d.decompressed; + img.sz = d.sz; + img.width = d.width; img.height = d.height; + } else free(d.decompressed); + free(d.row_pointers); + return d.ok; +} +#undef fail + +void +drag_start(Window *w) { + if (ds.state != DRAG_SOURCE_BEING_BUILT) abrt(EINVAL); + size_t total_size = 0; + for (size_t idx = 0; idx < arraysz(ds.images); idx++) { + if (img.sz) { + switch (img.fmt) { + case 24: + if (!expand_rgb_data(w, idx)) return; + break; + case 100: + if (!expand_png_data(w, idx)) return; + break; + } + total_size += img.sz; + if (total_size > 2 * PRESENT_DATA_CAP) abrt(EFBIG); + if (img.sz != (size_t)img.width * (size_t)img.height * 4u) abrt(EINVAL); + } + } +} + +#undef img #undef abrt #undef ds diff --git a/kitty/dnd.h b/kitty/dnd.h index 2d03198ed..43e01b181 100644 --- a/kitty/dnd.h +++ b/kitty/dnd.h @@ -27,3 +27,5 @@ void drag_free_offer(Window *w); void drag_add_mimes(Window *w, int allowed_operations, const char *data, size_t sz, bool has_more); void drag_add_pre_sent_data(Window *w, unsigned idx, const uint8_t *payload, size_t sz); void drag_add_image(Window *w, unsigned idx_, int fmt, int width, int height, const uint8_t *payload, size_t sz); +void drag_change_image(Window *w, unsigned idx); +void drag_start(Window *w); diff --git a/kitty/glfw.c b/kitty/glfw.c index e20d4ccb6..9ebb37453 100644 --- a/kitty/glfw.c +++ b/kitty/glfw.c @@ -3110,6 +3110,21 @@ get_thumbnail(PyObject *thumbnails, GLFWimage *thumbnail, int idx) { return true; } +bool +change_drag_image(int idx) { + if (!global_state.drag_source.from_os_window) return false; + OSWindow *w = os_window_for_id(global_state.drag_source.from_os_window); + if (!w || !w->handle) { errno = EINVAL; return false; } + if (global_state.drag_source.thumbnail_idx == idx) return true; + GLFWimage thumbnail = {0}; + if (idx >=0 && global_state.drag_source.thumbnails && idx < PySequence_Size(global_state.drag_source.thumbnails)) { + if (!get_thumbnail(global_state.drag_source.thumbnails, &thumbnail, idx)) { errno = ENOMEM; PyErr_Clear(); return NULL; } + global_state.drag_source.thumbnail_idx = idx; + } else global_state.drag_source.thumbnail_idx = -1; + errno = glfwStartDrag(w->handle, NULL, 0, thumbnail.pixels ? &thumbnail : NULL, -2, false); + return errno == 0; +} + static PyObject* change_drag_thumbnail(PyObject *self UNUSED, PyObject *args) { unsigned long long os_window_id; int idx = -1; diff --git a/kitty/screen.c b/kitty/screen.c index c484af24c..73bcf3a68 100644 --- a/kitty/screen.c +++ b/kitty/screen.c @@ -1552,6 +1552,11 @@ screen_handle_dnd_command(Screen *self, const DnDCommand *cmd, const uint8_t *pa if (cmd->cell_x >= 0) drag_add_pre_sent_data(w, cmd->cell_x, payload, cmd->payload_sz); else drag_add_image(w, -cmd->cell_x, cmd->cell_y, cmd->pixel_x, cmd->pixel_y, payload, cmd->payload_sz); } break; + case 'P': { + if (cmd->cell_x >= 0) drag_change_image(w, cmd->cell_x); + else drag_start(w); + } break; + } } diff --git a/kitty/state.h b/kitty/state.h index 7d0b0c83f..c513646ad 100644 --- a/kitty/state.h +++ b/kitty/state.h @@ -234,13 +234,7 @@ typedef struct DirHandle { uint32_t id; /* handle id, 1-based; 0 = invalid */ } DirHandle; -typedef struct DragSourceItem { - const char *mime_type; - uint8_t *optional_data; - size_t data_size, data_capacity; - base64_state base64_state; - bool data_decode_initialized; -} DragSourceItem; +typedef enum { DRAG_SOURCE_NONE, DRAG_SOURCE_BEING_BUILT, DRAG_SOURCE_STARTED, DRAG_SOURCE_DROPPED } DragSourceState; typedef struct Window { id_type id; @@ -298,13 +292,17 @@ typedef struct Window { bool can_offer; struct { double x, y; monotonic_t at; } initial_left_press; char *mimes_buf; size_t num_mimes, bufsz; - DragSourceItem *items; + struct { + const char *mime_type; uint8_t *optional_data; size_t data_size, data_capacity; base64_state base64_state; + bool data_decode_initialized; + } *items; struct { int width, height, fmt; uint8_t *data; size_t sz, capacity; bool started; base64_state base64_state; } images[16]; size_t pre_sent_total_sz, images_sent_total_sz; + unsigned img_idx; int allowed_operations; - bool offer_being_built; + DragSourceState state; } drag_source; } Window; @@ -595,3 +593,4 @@ void request_drop_status_update(OSWindow *osw); void register_mimes_for_drop(OSWindow *w, const char **mimes, size_t sz); void request_drop_data(OSWindow *w, id_type wid, const char* mime); void cancel_current_drag_source(void); +bool change_drag_image(int idx);