Cleanup new chunked drop code and use it in kitty

Currently the chunking is useful but it will become useful for a future
drag and drop TUI protocol
This commit is contained in:
Kovid Goyal 2026-02-04 20:34:10 +05:30
parent ed5eb8f45c
commit 9a2ddc887b
No known key found for this signature in database
GPG key ID: 06BC317B515ACE7C
9 changed files with 144 additions and 93 deletions

View file

@ -1521,11 +1521,8 @@ - (BOOL)performDragOperation:(id <NSDraggingInfo>)sender
drop_data->eof_reached = false;
drop_data->current_data = NULL;
drop_data->data_offset = 0;
_glfwInputDrop(window, drop_data);
// Note: drop_data is NOT freed here - application must call glfwFinishDrop
return YES;
}
@ -3884,9 +3881,7 @@ void _glfwPlatformUpdateDragState(_GLFWwindow* window) {
ssize_t
_glfwPlatformReadDropData(GLFWDropData* drop, const char* mime, void* buffer, size_t capacity, monotonic_t timeout UNUSED) {
if (!drop || !mime || !buffer || capacity == 0) return -EINVAL;
NSPasteboard* pasteboard = (__bridge NSPasteboard*)drop->platform_data;
NSPasteboard* pasteboard = (NSPasteboard*)drop->platform_data;
if (!pasteboard) return -EINVAL;
// Check if the MIME type is available
@ -3906,14 +3901,12 @@ void _glfwPlatformUpdateDragState(_GLFWwindow* window) {
drop->current_data = NULL;
}
drop->data_offset = 0;
free(drop->current_mime); drop->current_mime = NULL;
}
// If we need to fetch data for this MIME type
if (drop->current_data == NULL || drop->current_mime == NULL ||
strcmp(drop->current_mime, mime) != 0) {
if (drop->current_data == NULL || drop->current_mime == NULL) {
NSData* data = nil;
// Handle special MIME types
if (strcmp(mime, "text/uri-list") == 0) {
NSDictionary* options = @{NSPasteboardURLReadingFileURLsOnlyKey:@YES};
@ -3943,11 +3936,9 @@ void _glfwPlatformUpdateDragState(_GLFWwindow* window) {
}
}
}
if (!data) return -ENOENT;
drop->current_data = [data retain];
drop->current_mime = mime;
drop->current_mime = _glfw_strdup(mime);
drop->data_offset = 0;
}
@ -3955,23 +3946,19 @@ void _glfwPlatformUpdateDragState(_GLFWwindow* window) {
NSData* data = (NSData*)drop->current_data;
NSUInteger dataLength = [data length];
if (drop->data_offset >= dataLength) {
return 0; // EOF
}
if (drop->data_offset >= dataLength) return 0; // EOF
NSUInteger remaining = dataLength - drop->data_offset;
NSUInteger to_read = (remaining < capacity) ? remaining : capacity;
[data getBytes:buffer range:NSMakeRange(drop->data_offset, to_read)];
drop->data_offset += to_read;
return (ssize_t)to_read;
}
void
_glfwPlatformFinishDrop(GLFWDropData* drop, GLFWDragOperationType operation UNUSED, bool success UNUSED) {
if (!drop) return;
free(drop->current_mime); drop->current_mime = NULL;
// Release the retained current data
if (drop->current_data) {
[(NSData*)drop->current_data release];

6
glfw/input.c vendored
View file

@ -1161,11 +1161,7 @@ GLFWAPI const char** glfwGetDropMimeTypes(GLFWDropData* drop, int* count)
GLFWAPI ssize_t glfwReadDropData(GLFWDropData* drop, const char* mime, void* buffer, size_t capacity, monotonic_t timeout)
{
assert(drop != NULL);
assert(mime != NULL);
assert(buffer != NULL);
assert(capacity > 0);
if (drop == NULL || mime == NULL || buffer == NULL || capacity < 1) return -EINVAL;
_GLFW_REQUIRE_INIT_OR_RETURN(-1);
return _glfwPlatformReadDropData(drop, mime, buffer, capacity, timeout);
}

2
glfw/internal.h vendored
View file

@ -90,7 +90,7 @@ struct GLFWDropData {
const char** mime_types; // Array of available MIME types (owned by drop object)
int mime_count; // Number of MIME types
int mime_array_size; // Original array size for proper cleanup
const char* current_mime; // Currently being read MIME type
char* current_mime; // Currently being read MIME type
int read_fd; // File descriptor for reading data (Wayland/X11)
size_t bytes_read; // Total bytes read so far for current mime
void* platform_data; // Platform-specific data (offer for Wayland, pasteboard for Cocoa)

62
glfw/wl_window.c vendored
View file

@ -2522,14 +2522,14 @@ static void drag_leave(void *data UNUSED, struct wl_data_device *wl_data_device
}
}
static void drop(void *data UNUSED, struct wl_data_device *wl_data_device UNUSED) {
static void
drop(void *data UNUSED, struct wl_data_device *wl_data_device UNUSED) {
for (size_t i = 0; i < arraysz(_glfw.wl.dataOffers); i++) {
if (_glfw.wl.dataOffers[i].offer_type == DRAG_AND_DROP) {
_GLFWWaylandDataOffer *offer = &_glfw.wl.dataOffers[i];
_GLFWwindow* window = _glfw.windowListHead;
while (window)
{
while (window) {
if (window->wl.surface == offer->surface) {
// Heap-allocate drop data structure for chunked reading
// The application is responsible for freeing this via glfwFinishDrop
@ -2543,26 +2543,20 @@ static void drop(void *data UNUSED, struct wl_data_device *wl_data_device UNUSED
drop_data->mime_types = offer->mimes;
drop_data->mime_count = (int)offer->mimes_count;
drop_data->mime_array_size = (int)offer->mimes_count;
// Clear offer's references since drop object now owns the mimes
offer->mimes = NULL;
offer->mimes_count = 0;
drop_data->current_mime = NULL;
drop_data->read_fd = -1;
drop_data->bytes_read = 0;
drop_data->platform_data = offer; // Store the offer for later use
drop_data->platform_data = offer->id; // Store the offer for later use
offer->mimes = NULL; offer->id = NULL;
destroy_data_offer(offer);
drop_data->eof_reached = false;
memset(offer, 0, sizeof(offer[0]));
_glfwInputDrop(window, drop_data);
// Note: drop_data is NOT freed here - application must call glfwFinishDrop
break;
}
window = window->next;
}
// Note: We no longer destroy the offer here as the drop_data holds a reference
// The offer will be destroyed when glfwFinishDrop is called
break;
}
}
@ -3189,10 +3183,7 @@ _glfwPlatformGetDropMimeTypes(GLFWDropData* drop, int* count) {
ssize_t
_glfwPlatformReadDropData(GLFWDropData* drop, const char* mime, void* buffer, size_t capacity, monotonic_t timeout) {
if (!drop || !mime || !buffer || capacity == 0) return -EINVAL;
_GLFWWaylandDataOffer *offer = (_GLFWWaylandDataOffer*)drop->platform_data;
if (!offer || !offer->id) return -EINVAL;
if (!drop->platform_data) return -EINVAL;
// Check if the MIME type is available
bool mime_found = false;
@ -3212,27 +3203,23 @@ _glfwPlatformReadDropData(GLFWDropData* drop, const char* mime, void* buffer, si
}
drop->bytes_read = 0;
drop->eof_reached = false;
free(drop->current_mime); drop->current_mime = NULL;
}
// If we've reached EOF for this MIME type, return 0
if (drop->eof_reached && drop->current_mime && strcmp(drop->current_mime, mime) == 0) {
return 0;
}
if (drop->eof_reached && drop->current_mime && strcmp(drop->current_mime, mime) == 0) return 0;
// Open a new pipe if we don't have one for this MIME type
if (drop->read_fd < 0 || drop->current_mime == NULL || strcmp(drop->current_mime, mime) != 0) {
if (drop->read_fd < 0 || drop->current_mime == NULL) {
int pipefd[2];
if (pipe2(pipefd, O_CLOEXEC | O_NONBLOCK) != 0) return -EIO;
wl_data_offer_receive(offer->id, mime, pipefd[1]);
wl_data_offer_receive(drop->platform_data, mime, pipefd[1]);
close(pipefd[1]);
// Round trip to ensure the compositor processes the receive request
wl_display_roundtrip(_glfw.wl.display);
// Flush to ensure the compositor processes the receive request
wl_display_flush(_glfw.wl.display);
drop->read_fd = pipefd[0];
// mime points to drop->mime_types entry, valid for duration of drop callback
drop->current_mime = mime;
drop->current_mime = _glfw_strdup(mime);
drop->bytes_read = 0;
drop->eof_reached = false;
}
@ -3264,20 +3251,20 @@ _glfwPlatformReadDropData(GLFWDropData* drop, const char* mime, void* buffer, si
}
// Read data from the pipe
ssize_t bytes_read = read(drop->read_fd, buffer, capacity);
ssize_t bytes_read = -1;
errno = EINTR;
while (errno == EINTR) { errno = 0; bytes_read = read(drop->read_fd, buffer, capacity); }
if (bytes_read < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
return -ETIME; // No data available yet
}
return -EIO;
}
if (bytes_read == 0) {
// EOF reached
drop->eof_reached = true;
return 0;
}
drop->bytes_read += bytes_read;
return bytes_read;
}
@ -3285,6 +3272,7 @@ _glfwPlatformReadDropData(GLFWDropData* drop, const char* mime, void* buffer, si
void
_glfwPlatformFinishDrop(GLFWDropData* drop, GLFWDragOperationType operation UNUSED, bool success UNUSED) {
if (!drop) return;
free(drop->current_mime); drop->current_mime = NULL;
// Close any open file descriptor
if (drop->read_fd >= 0) {
@ -3293,19 +3281,15 @@ _glfwPlatformFinishDrop(GLFWDropData* drop, GLFWDragOperationType operation UNUS
}
// Destroy the associated data offer
// Note: Wayland doesn't have a way to report the operation type or success back to the source
// in the same way as X11, as the source is notified through other means
_GLFWWaylandDataOffer* offer = (_GLFWWaylandDataOffer*)drop->platform_data;
if (offer) {
destroy_data_offer(offer);
}
if (drop->platform_data) wl_data_offer_destroy(drop->platform_data);
drop->platform_data = NULL;
// Free the mime types array (owned by drop object)
if (drop->mime_types) {
for (int i = 0; i < drop->mime_array_size; i++) {
if (drop->mime_types[i])
free((char*)drop->mime_types[i]);
}
for (int i = 0; i < drop->mime_array_size; i++) free((char*)drop->mime_types[i]);
free(drop->mime_types);
drop->mime_types = NULL;
}

19
glfw/x11_window.c vendored
View file

@ -3883,8 +3883,6 @@ _glfwPlatformGetDropMimeTypes(GLFWDropData* drop, int* count) {
ssize_t
_glfwPlatformReadDropData(GLFWDropData* drop, const char* mime, void* buffer, size_t capacity, monotonic_t timeout) {
if (!drop || !mime || !buffer || capacity == 0) return -EINVAL;
// Check if the MIME type is available
bool mime_found = false;
for (int i = 0; i < drop->mime_count; i++) {
@ -3903,15 +3901,12 @@ _glfwPlatformReadDropData(GLFWDropData* drop, const char* mime, void* buffer, si
}
drop->x11_data_size = 0;
drop->data_offset = 0;
drop->current_mime = NULL;
free(drop->current_mime); drop->current_mime = NULL;
}
// If we need to fetch data for this MIME type
if (drop->current_data == NULL || drop->current_mime == NULL ||
strcmp(drop->current_mime, mime) != 0) {
if (drop->current_data == NULL || drop->current_mime == NULL) {
Atom target_atom = XInternAtom(_glfw.x11.display, mime, False);
// Request the data via XConvertSelection
XConvertSelection(_glfw.x11.display,
_glfw.x11.XdndSelection,
@ -3963,7 +3958,7 @@ _glfwPlatformReadDropData(GLFWDropData* drop, const char* mime, void* buffer, si
drop->x11_data_size = size;
drop->data_offset = 0;
// mime points to drop->mime_types entry, valid for duration of drop callback
drop->current_mime = mime;
drop->current_mime = _glfw_strdup(mime);
got_data = true;
} else {
if (data) XFree(data);
@ -3983,22 +3978,18 @@ _glfwPlatformReadDropData(GLFWDropData* drop, const char* mime, void* buffer, si
}
// Read data from buffer
if (drop->data_offset >= drop->x11_data_size) {
return 0; // EOF
}
if (drop->data_offset >= drop->x11_data_size) return 0; // EOF
size_t remaining = drop->x11_data_size - drop->data_offset;
size_t to_read = (remaining < capacity) ? remaining : capacity;
memcpy(buffer, (unsigned char*)drop->current_data + drop->data_offset, to_read);
drop->data_offset += to_read;
return (ssize_t)to_read;
}
void
_glfwPlatformFinishDrop(GLFWDropData* drop, GLFWDragOperationType operation, bool success) {
if (!drop) return;
free(drop->current_mime); drop->current_mime = NULL;
// Free current data if any
if (drop->current_data) {

View file

@ -1883,20 +1883,28 @@ def update_tab_bar_data(self, os_window_id: int) -> None:
if tm is not None:
tm.update_tab_bar_data()
def on_drop(self, os_window_id: int, mime: str, data: bytes) -> None:
def on_drop(self, os_window_id: int, drop: dict[str, bytes] | Exception) -> None:
if isinstance(drop, Exception):
self.show_error(_('Drop failed'), str(drop))
return
tm = self.os_window_map.get(os_window_id)
if tm is not None:
w = tm.active_window
if w is not None:
text = data.decode('utf-8', 'replace')
if mime == 'text/uri-list':
urls = parse_uri_list(text)
text = ''
if uri_list := drop.pop('text/uri-list', b''):
urls = parse_uri_list(uri_list.decode('utf-8', 'replace'))
if w.at_prompt:
import shlex
text = ' '.join(map(shlex.quote, urls))
else:
text = '\n'.join(urls)
w.paste_text(text)
elif tp := drop.pop('text/plain', b''):
text = tp.decode('utf-8', 'replace')
elif tp := drop.pop('text/plain;charset=utf-8', b''):
text = tp.decode('utf-8', 'replace')
if text:
w.paste_text(text)
@ac('win', '''
Focus the nth OS window if positive or the previously active OS windows if negative. When the number is larger

9
kitty/glfw-wrapper.c generated
View file

@ -368,6 +368,15 @@ load_glfw(const char* path) {
*(void **) (&glfwUpdateDragState_impl) = dlsym(handle, "glfwUpdateDragState");
if (glfwUpdateDragState_impl == NULL) fail("Failed to load glfw function glfwUpdateDragState with error: %s", dlerror());
*(void **) (&glfwGetDropMimeTypes_impl) = dlsym(handle, "glfwGetDropMimeTypes");
if (glfwGetDropMimeTypes_impl == NULL) fail("Failed to load glfw function glfwGetDropMimeTypes with error: %s", dlerror());
*(void **) (&glfwReadDropData_impl) = dlsym(handle, "glfwReadDropData");
if (glfwReadDropData_impl == NULL) fail("Failed to load glfw function glfwReadDropData with error: %s", dlerror());
*(void **) (&glfwFinishDrop_impl) = dlsym(handle, "glfwFinishDrop");
if (glfwFinishDrop_impl == NULL) fail("Failed to load glfw function glfwFinishDrop with error: %s", dlerror());
*(void **) (&glfwJoystickPresent_impl) = dlsym(handle, "glfwJoystickPresent");
if (glfwJoystickPresent_impl == NULL) fail("Failed to load glfw function glfwJoystickPresent with error: %s", dlerror());

49
kitty/glfw-wrapper.h generated
View file

@ -1005,6 +1005,22 @@ typedef struct GLFWwindow GLFWwindow;
*/
typedef struct GLFWcursor GLFWcursor;
/*! @brief Opaque drop data object.
*
* Opaque drop data object representing data from a drag and drop operation.
* This object is passed to the drop callback and can be used to query
* available MIME types and read the dropped data in chunks.
*
* @see @ref path_drop
* @see @ref glfwGetDropMimeTypes
* @see @ref glfwReadDropData
*
* @since Added in version 4.0.
*
* @ingroup input
*/
typedef struct GLFWDropData GLFWDropData;
typedef enum {
GLFW_RELEASE = 0,
GLFW_PRESS = 1,
@ -1499,25 +1515,30 @@ typedef void (* GLFWkeyboardfun)(GLFWwindow*, GLFWkeyevent*);
* This is the function pointer type for drop callbacks. A drop
* callback function has the following signature:
* @code
* void function_name(GLFWwindow* window, const char* mime, const char* data, size_t sz)
* void function_name(GLFWwindow* window, GLFWDropData* drop)
* @endcode
*
* @param[in] window The window that received the event.
* @param[in] mime The UTF-8 encoded drop mime-type
* @param[in] data The dropped data.
* @param[in] sz The size of the dropped data
* @param[in] drop A heap-allocated opaque pointer representing the dropped data. Use
* @ref glfwGetDropMimeTypes to get available MIME types and
* @ref glfwReadDropData to read the data in chunks.
*
* @pointer_lifetime The data is valid until the
* callback function returns.
* @note The drop object is heap-allocated and remains valid until the
* application calls @ref glfwFinishDrop to free it. The application is
* responsible for calling glfwFinishDrop when it has finished reading
* the dropped data, even if reading fails or is not needed.
*
* @sa @ref path_drop
* @sa @ref glfwSetDropCallback
* @sa @ref glfwGetDropMimeTypes
* @sa @ref glfwReadDropData
* @sa @ref glfwFinishDrop
*
* @since Added in version 3.1.
* @since Changed in version 4.0 to receive opaque drop data pointer.
*
* @ingroup input
*/
typedef void (* GLFWdropfun)(GLFWwindow*, const char *, const char*, size_t);
typedef void (* GLFWdropfun)(GLFWwindow*, GLFWDropData*);
/*! @brief Drag event types.
*
@ -2302,6 +2323,18 @@ typedef void (*glfwUpdateDragState_func)(GLFWwindow*);
GFW_EXTERN glfwUpdateDragState_func glfwUpdateDragState_impl;
#define glfwUpdateDragState glfwUpdateDragState_impl
typedef const char** (*glfwGetDropMimeTypes_func)(GLFWDropData*, int*);
GFW_EXTERN glfwGetDropMimeTypes_func glfwGetDropMimeTypes_impl;
#define glfwGetDropMimeTypes glfwGetDropMimeTypes_impl
typedef ssize_t (*glfwReadDropData_func)(GLFWDropData*, const char*, void*, size_t, monotonic_t);
GFW_EXTERN glfwReadDropData_func glfwReadDropData_impl;
#define glfwReadDropData glfwReadDropData_impl
typedef void (*glfwFinishDrop_func)(GLFWDropData*, GLFWDragOperationType, bool);
GFW_EXTERN glfwFinishDrop_func glfwFinishDrop_impl;
#define glfwFinishDrop glfwFinishDrop_impl
typedef int (*glfwJoystickPresent_func)(int);
GFW_EXTERN glfwJoystickPresent_func glfwJoystickPresent_impl;
#define glfwJoystickPresent glfwJoystickPresent_impl

View file

@ -700,10 +700,53 @@ end:
return ret;
}
static PyObject*
read_drop_data(GLFWDropData *drop, const char *mime) {
RAII_PyObject(ans, PyBytes_FromStringAndSize(NULL, 8192));
if (!ans) return NULL;
size_t pos = 0;
monotonic_t timeout = s_double_to_monotonic_t(2);
while (true) {
int ret = glfwReadDropData(drop, mime, PyBytes_AS_STRING(ans) + pos, PyBytes_GET_SIZE(ans) - pos, timeout);
if (ret > 0) {
pos += ret;
if (pos >= (size_t)PyBytes_GET_SIZE(ans)) {
if (_PyBytes_Resize(&ans, pos * 2) != 0) return NULL;
}
} else if (ret == 0) {
if (_PyBytes_Resize(&ans, pos) != 0) return NULL;
return Py_NewRef(ans);
}
else {
errno = -ret;
PyErr_SetFromErrno(PyExc_OSError);
return NULL;
}
}
}
static void
drop_callback(GLFWwindow *w, const char *mime, const char *data, size_t sz) {
get_mime_data(GLFWDropData *drop, const char **mimes, int mime_count, PyObject *ans) {
for (int i = 0; i < mime_count; i++) {
if (is_droppable_mime(mimes[i])) {
RAII_PyObject(data, read_drop_data(drop, mimes[i]));
if (data == NULL) return ;
if (PyDict_SetItemString(ans, mimes[i], data) != 0) return ;
}
}
}
static void
drop_callback(GLFWwindow *w, GLFWDropData *drop) {
int num_mimes;
const char** mimes = glfwGetDropMimeTypes(drop, &num_mimes);
RAII_PyObject(ans, PyDict_New());
get_mime_data(drop, mimes, num_mimes, ans);
RAII_PyObject(exc, PyErr_GetRaisedException());
glfwFinishDrop(drop, GLFW_DRAG_OPERATION_COPY, true);
if (!set_callback_window(w)) return;
WINDOW_CALLBACK(on_drop, "sy#", mime, data, (Py_ssize_t)sz);
if (exc != NULL) { WINDOW_CALLBACK(on_drop, "O", exc); }
else if (PyDict_Size(ans)) WINDOW_CALLBACK(on_drop, "O", ans);
request_tick_callback();
global_state.callback_os_window = NULL;
}