More work on the dnd kitten

This commit is contained in:
Kovid Goyal 2026-05-02 15:18:11 +05:30
parent ff7c6425e6
commit d3114ec948
No known key found for this signature in database
GPG key ID: 06BC317B515ACE7C
6 changed files with 80 additions and 23 deletions

View file

@ -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
}

View file

@ -14,6 +14,7 @@
#include <dirent.h>
#include <fcntl.h>
#include <limits.h>
#include <stddef.h>
#include <sys/stat.h>
#include <unistd.h>
#include <errno.h>
@ -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}
};

View file

@ -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);

View file

@ -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);

View file

@ -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;

View file

@ -1,6 +1,7 @@
#!/usr/bin/env python
# License: GPLv3 Copyright: 2026, Kovid Goyal <kovid at kovidgoyal.net>
import errno
import fnmatch
import itertools
import os
@ -22,6 +23,7 @@
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 @@ def assert_files_have_same_content(self, a, b):
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 @@ def end_drag(canceled=True):
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)