From d3114ec948c49ca837475bd27eba30cf0eb73ea3 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 2 May 2026 15:18:11 +0530 Subject: [PATCH] More work on the dnd kitten --- kittens/dnd/drag.go | 7 +++++ kitty/dnd.c | 65 +++++++++++++++++++++++++++++---------- kitty/dnd.h | 1 - kitty/glfw.c | 4 +-- kitty/state.h | 2 +- kitty_tests/dnd_kitten.py | 24 ++++++++++++++- 6 files changed, 80 insertions(+), 23 deletions(-) diff --git a/kittens/dnd/drag.go b/kittens/dnd/drag.go index ee1df9fea..9631a6395 100644 --- a/kittens/dnd/drag.go +++ b/kittens/dnd/drag.go @@ -123,9 +123,16 @@ func (dnd *dnd) handle_data_request(idx int, send_remote_data bool) (err error) mime := dnd.drag_status.offered_mimes[idx] ds := dnd.drag_sources[mime] send_remote_data = send_remote_data && mime == "text/uri-list" && len(ds.uri_list) > 0 + for _, dr := range dnd.drag_status.data_requests { + if dr.index == idx { + dnd.finish_drag("EINVAL") + return fmt.Errorf("terminal sent a duplicate drag data request") + } + } dr := &data_request{drag_source: ds, send_remote_data: send_remote_data, index: idx} if ds.path == "" { dnd.lp.QueueDnDData(DC{Type: 'e', Y: idx, Payload: utils.UnsafeStringToBytes(base64.RawStdEncoding.EncodeToString(ds.data))}) + dnd.lp.QueueDnDData(DC{Type: 'e', Y: idx}) // EOF if !dr.send_remote_data { return } diff --git a/kitty/dnd.c b/kitty/dnd.c index fd0edd8b4..1f3529606 100644 --- a/kitty/dnd.c +++ b/kitty/dnd.c @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -100,6 +101,11 @@ rmtree_best_effort(const char *relpath, int dirfd) { safe_close(dirfd, __FILE__, __LINE__); } +static bool +dnd_is_test_mode(void) { + return g_dnd_test_write_func != NULL; +} + static char* mktempdir_in_cache(const char *prefix, int *fd) { char *ans = NULL; @@ -176,11 +182,6 @@ dnd_set_test_write_func(PyObject *func, size_t mime_list_size_cap, size_t presen REMOTE_DRAG_LIMIT = remote_drag_limit ? remote_drag_limit : DEFAULT_REMOTE_DRAG_LIMIT; } -bool -dnd_is_test_mode(void) { - return g_dnd_test_write_func != NULL; -} - static int string_arrays_cmp(const char **a, size_t an, const char **b, size_t bn) { if (an != bn) return (int)an - (int)bn; @@ -1521,6 +1522,7 @@ drag_get_data(Window *w, const char *mime_type, size_t *sz, int *err_code) { // No unread data if (!ds.items[i].data_decode_initialized) { // Transfer complete and all data read + ds.items[i].data_requested_from_client = false; *err_code = 0; return NULL; } @@ -1529,11 +1531,14 @@ drag_get_data(Window *w, const char *mime_type, size_t *sz, int *err_code) { return NULL; } // No fd yet, request data from the client - char buf[128]; - ds.items[i].requested_remote_files = ds.is_remote_client && ds.items[i].is_uri_list; - int header_sz = snprintf(buf, sizeof(buf), "\x1b]%d;t=e:x=%d:y=%zu:Y=%d", - DND_CODE, DRAG_NOTIFY_FINISHED + 2, i, ds.items[i].requested_remote_files); - queue_payload_to_child(w->id, w->drag_source.client_id, &w->drag_source.pending, buf, header_sz, NULL, 0, false); + if (!ds.items[i].data_requested_from_client) { + char buf[128]; + ds.items[i].requested_remote_files = ds.is_remote_client && ds.items[i].is_uri_list; + int header_sz = snprintf(buf, sizeof(buf), "\x1b]%d;t=e:x=%d:y=%zu:Y=%d", + DND_CODE, DRAG_NOTIFY_FINISHED + 2, i, ds.items[i].requested_remote_files); + queue_payload_to_child(w->id, w->drag_source.client_id, &w->drag_source.pending, buf, header_sz, NULL, 0, false); + ds.items[i].data_requested_from_client = true; + } *err_code = EAGAIN; return NULL; } @@ -1582,8 +1587,10 @@ drag_process_item_data(Window *w, size_t idx, int has_more, const uint8_t *paylo int err = parse_errno_name(payload, payload_sz); ds.items[idx].fd_plus_one = -err; ds.items[idx].data_decode_initialized = false; - int ret = notify_drag_data_ready(global_state.drag_source.from_os_window, ds.items[idx].mime_type); - if (ret) cancel_drag(w, ret); + if (!dnd_is_test_mode()) { + int ret = notify_drag_data_ready(global_state.drag_source.from_os_window, ds.items[idx].mime_type); + if (ret) cancel_drag(w, ret); + } return; } @@ -1592,8 +1599,10 @@ drag_process_item_data(Window *w, size_t idx, int has_more, const uint8_t *paylo ds.items[idx].data_decode_initialized = false; if (ds.items[idx].fd_plus_one > 0) { if (!ds.items[idx].requested_remote_files) { - int ret = notify_drag_data_ready(global_state.drag_source.from_os_window, ds.items[idx].mime_type); - if (ret) cancel_drag(w, ret); + if (!dnd_is_test_mode()) { + int ret = notify_drag_data_ready(global_state.drag_source.from_os_window, ds.items[idx].mime_type); + if (ret) cancel_drag(w, ret); + } } } return; @@ -1631,8 +1640,10 @@ drag_process_item_data(Window *w, size_t idx, int has_more, const uint8_t *paylo ds.items[idx].data_capacity += outlen; // Notify as soon as any data is available if (!ds.items[idx].requested_remote_files) { - int ret = notify_drag_data_ready(global_state.drag_source.from_os_window, ds.items[idx].mime_type); - if (ret) cancel_drag(w, ret); + if (!dnd_is_test_mode()) { + int ret = notify_drag_data_ready(global_state.drag_source.from_os_window, ds.items[idx].mime_type); + if (ret) cancel_drag(w, ret); + } } } } @@ -1727,7 +1738,7 @@ finish_remote_data(Window *w, size_t item_idx) { if ((ret = write_all(fd, "\r\n", 2))) abrt(ret); } free(ds.items[item_idx].uri_list); ds.items[item_idx].uri_list = NULL; ds.items[item_idx].num_uris = 0; - int ret = notify_drag_data_ready(global_state.drag_source.from_os_window, ds.items[item_idx].mime_type); + int ret = dnd_is_test_mode() ? 0 : notify_drag_data_ready(global_state.drag_source.from_os_window, ds.items[item_idx].mime_type); abrt(ret); } @@ -2262,6 +2273,25 @@ dnd_test_start_drag_offer(PyObject *self UNUSED, PyObject *args) { Py_RETURN_NONE; } +static PyObject* +dnd_test_drag_get_data(PyObject *self UNUSED, PyObject *args) { + const char *mime; unsigned long long window_id; + if (!PyArg_ParseTuple(args, "Ks", &window_id, &mime)) return NULL; + Window *w = window_for_window_id((id_type)window_id); + if (!w) { PyErr_SetString(PyExc_ValueError, "Window not found"); return NULL; } + int err; size_t sz; + const char *data = drag_get_data(w, mime, &sz, &err); + if (err != 0) { + if (data) drag_free_data(w, mime, data, sz); + errno = err; + PyErr_SetFromErrno(PyExc_OSError); + return NULL; + } + PyObject *ans = PyBytes_FromStringAndSize(data ? data : "", sz); + if (data) drag_free_data(w, mime, data, sz); + return ans; +} + static PyMethodDef dnd_methods[] = { {"dnd_set_test_write_func", (PyCFunction)py_dnd_set_test_write_func, METH_VARARGS, ""}, METHODB(dnd_test_create_fake_window, METH_NOARGS), @@ -2275,6 +2305,7 @@ static PyMethodDef dnd_methods[] = { METHODB(dnd_test_drag_notify, METH_VARARGS), METHODB(dnd_test_drag_finish, METH_VARARGS), METHODB(dnd_test_probe_state, METH_VARARGS), + METHODB(dnd_test_drag_get_data, METH_VARARGS), {NULL, NULL, 0, NULL} }; diff --git a/kitty/dnd.h b/kitty/dnd.h index 998c06ee4..ec5fb2a5e 100644 --- a/kitty/dnd.h +++ b/kitty/dnd.h @@ -21,7 +21,6 @@ void drop_set_status(Window *w, int operation, const char *payload, size_t paylo size_t drop_update_mimes(Window *w, const char **allowed_mimes, size_t allowed_mimes_count); void drop_dispatch_data(Window *w, const char *mime_type, const char *data, ssize_t sz); void drop_finish(Window *w); -bool dnd_is_test_mode(void); typedef enum { DRAG_NOTIFY_ACCEPTED, DRAG_NOTIFY_ACTION_CHANGED, DRAG_NOTIFY_DROPPED, DRAG_NOTIFY_FINISHED } DragNotifyType; void drag_free_offer(Window *w); diff --git a/kitty/glfw.c b/kitty/glfw.c index a9c3ad134..697d25e15 100644 --- a/kitty/glfw.c +++ b/kitty/glfw.c @@ -946,7 +946,6 @@ drag_source_callback(GLFWwindow *window UNUSED, GLFWDragEvent *ev) { switch (ev->type) { case GLFW_DRAG_DATA_REQUEST: - ev->err_num = ENOENT; if (is_client_drag) { ev->err_num = 0; if (ev->data_sz) { @@ -954,7 +953,7 @@ drag_source_callback(GLFWwindow *window UNUSED, GLFWDragEvent *ev) { } else { ev->data = drag_get_data(w, ev->mime_type, &ev->data_sz, &ev->err_num); } - } + } else ev->err_num = ENOENT; break; case GLFW_DRAG_ACCEPTED: free(ds.accepted_mime_type); @@ -994,7 +993,6 @@ drag_source_callback(GLFWwindow *window UNUSED, GLFWDragEvent *ev) { int notify_drag_data_ready(id_type os_window_id, const char *mime_type) { - if (dnd_is_test_mode()) return 0; // In test mode, always succeed OSWindow *w = os_window_for_id(os_window_id); GLFWDragSourceItem item = {.mime_type = mime_type}; if (w && w->handle) return glfwStartDrag(w->handle, &item, 1, NULL, -1, false); diff --git a/kitty/state.h b/kitty/state.h index 586781207..7a4d58f36 100644 --- a/kitty/state.h +++ b/kitty/state.h @@ -324,7 +324,7 @@ typedef struct Window { struct { int32_t x, y, X, Y; bool active; } in_flight_remote_file_data; struct { const char *mime_type; uint8_t *optional_data; size_t data_size, data_capacity; base64_state base64_state; - bool data_decode_initialized, is_uri_list, requested_remote_files; + bool data_decode_initialized, is_uri_list, requested_remote_files, data_requested_from_client; int fd_plus_one; char** uri_list; size_t num_uris; DragRemoteItem *remote_items; size_t num_remote_items; diff --git a/kitty_tests/dnd_kitten.py b/kitty_tests/dnd_kitten.py index 55d972347..ecb91286b 100644 --- a/kitty_tests/dnd_kitten.py +++ b/kitty_tests/dnd_kitten.py @@ -1,6 +1,7 @@ #!/usr/bin/env python # License: GPLv3 Copyright: 2026, Kovid Goyal +import errno import fnmatch import itertools import os @@ -22,6 +23,7 @@ from kitty.fast_data_types import ( dnd_test_cleanup_fake_window, dnd_test_create_fake_window, dnd_test_drag_finish, + dnd_test_drag_get_data, dnd_test_drag_notify, dnd_test_fake_drop_data, dnd_test_fake_drop_event, @@ -328,11 +330,29 @@ class TestDnDKitten(BaseTest): def test_dnd_kitten_drag(self): img_drag_path = 'image.png' with open(os.path.join(self.kitten_wd, img_drag_path), 'wb') as f: - f.write(os.urandom(1113)) + self.img_drag_data = os.urandom(10113) + f.write(self.img_drag_data) create_fs(self.src_data_dir) self.finish_setup(cli_args=(f'--drag=image/png:{img_drag_path}', self.src_data_dir)) # ))) self.dnd_kitten_drag(False, img_drag_path) self.exit_kitten() + self.img_drag_data = None + + def read_drag_data(self, mime): + ans = b'' + while True: + try: + chunk = dnd_test_drag_get_data(self.capture.window_id, mime) + if not chunk: + break + ans += chunk + except OSError as err: + if err.errno == errno.EAGAIN: + self.pty.process_input_from_child() + continue + del chunk, ans + raise + return ans def dnd_kitten_drag(self, remote_client, img_drop_path): # self.pty.log_data_flow = True @@ -365,4 +385,6 @@ class TestDnDKitten(BaseTest): dnd_test_drag_notify(self.capture.window_id, 2) self.send_dnd_command_to_kitten('DRAG_STATUS') self.wait_for_responses('text/uri-list:2:true') + self.assertEqual(self.img_drag_data, self.read_drag_data('image/png')) + # self.read_drag_data('text/uri-list') end_drag(False)