From b0e57b4dce23bf7a415dd03e2b9976c19b4789e3 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 6 Mar 2026 10:24:40 +0530 Subject: [PATCH] Send drop move events to child --- docs/dnd-protocol.rst | 4 +- gen/apc_parsers.py | 1 + kitty/child-monitor.c | 70 ++++++++++++++---- kitty/data-types.h | 5 +- kitty/dnd.c | 146 ++++++++++++++++++++++++++++++++++++++ kitty/dnd.h | 12 ++++ kitty/glfw.c | 14 +++- kitty/parse-dnd-command.h | 13 ++-- kitty/screen.c | 4 +- kitty/screen.h | 3 +- kitty/state.c | 2 + kitty/state.h | 17 ++++- 12 files changed, 263 insertions(+), 28 deletions(-) create mode 100644 kitty/dnd.c create mode 100644 kitty/dnd.h diff --git a/docs/dnd-protocol.rst b/docs/dnd-protocol.rst index 55863cd80..6798cbdcb 100644 --- a/docs/dnd-protocol.rst +++ b/docs/dnd-protocol.rst @@ -24,7 +24,9 @@ their metadata. Each chunk must have a payload of no more than 4096 base64 encoded bytes without trailing padding, except the last chunk which may optionally have trailing padding. Only the first chunk is guaranteed to have metadata other than the ``m`` key. Subsequent chunks may optionally omit all -metadata except the ``m`` and ``i`` keys. +metadata except the ``m`` and ``i`` keys. While a chunked transfer is in +progress it is a protocol error to for the sending side to +send any protocol related escape codes other than chunked ones. All integer values used in this escape code must be 32-bit signed or unsigned integers encoded in decimal representation. diff --git a/gen/apc_parsers.py b/gen/apc_parsers.py index 91adfd059..24ce5e373 100755 --- a/gen/apc_parsers.py +++ b/gen/apc_parsers.py @@ -333,6 +333,7 @@ def parsers() -> None: keymap = { 't': ('type', flag('aA')), 'm': ('more', 'uint'), + 'i': ('client_id', 'uint'), } text = generate( 'parse_dnd_code', 'screen_handle_dnd_command', 'dnd_command', keymap, 'DnDCommand', diff --git a/kitty/child-monitor.c b/kitty/child-monitor.c index fbf655ab9..2cb3dac55 100644 --- a/kitty/child-monitor.c +++ b/kitty/child-monitor.c @@ -321,9 +321,10 @@ add_child(ChildMonitor *self, PyObject *args) { Py_RETURN_NONE; } -#define schedule_write_to_child_generic(id, num, va_start, get_next_arg, va_end) \ +static const unsigned write_buf_limit = 100 * 1024 * 1024; + +#define schedule_write_to_child_generic(id, num, va_start, get_next_arg, va_end, found, too_much_data) \ ChildMonitor *self = the_monitor; \ - bool found = false; \ const char *data; \ size_t szval, sz = 0; \ va_start(ap, num); \ @@ -339,8 +340,8 @@ add_child(ChildMonitor *self, PyObject *args) { screen_mutex(lock, write); \ size_t space_left = screen->write_buf_sz - screen->write_buf_used; \ if (space_left < sz) { \ - if (screen->write_buf_used + sz > 100 * 1024 * 1024) { \ - log_error("Too much data being sent to child with id: %lu, ignoring it", id); \ + if (screen->write_buf_used + sz > write_buf_limit) { \ + too_much_data = true; \ screen_mutex(unlock, write); \ break; \ } \ @@ -366,19 +367,57 @@ add_child(ChildMonitor *self, PyObject *args) { break; \ } \ } \ - children_mutex(unlock); \ - return found; + children_mutex(unlock); -bool -schedule_write_to_child(unsigned long id, unsigned int num, ...) { - va_list ap; -#define get_next_arg(ap) data = va_arg(ap, const char*); szval = va_arg(ap, size_t); - schedule_write_to_child_generic(id, num, va_start, get_next_arg, va_end); -#undef get_next_arg +void +schedule_write_to_child_if_possible(id_type id, const char *data, size_t sz, bool *found, bool *too_much_data) { + children_mutex(lock); + ChildMonitor *self = the_monitor; + *found = false; *too_much_data = false; + for (size_t i = 0; i < self->count; i++) { + if (children[i].id == id) { + Screen *screen = children[i].screen; + screen_mutex(lock, write); + size_t space_left = screen->write_buf_sz - screen->write_buf_used; + if (space_left < sz) { + if (screen->write_buf_used + sz > write_buf_limit) { + *too_much_data = true; + screen_mutex(unlock, write); + break; + } + screen->write_buf_sz = screen->write_buf_used + sz; + screen->write_buf = PyMem_RawRealloc(screen->write_buf, screen->write_buf_sz); + if (screen->write_buf == NULL) { fatal("Out of memory."); } + } + *found = true; + memcpy(screen->write_buf + screen->write_buf_used, data, sz); + screen->write_buf_used += sz; + if (screen->write_buf_sz > BUFSIZ && screen->write_buf_used < BUFSIZ) { + screen->write_buf_sz = BUFSIZ; + screen->write_buf = PyMem_RawRealloc(screen->write_buf, screen->write_buf_sz); + if (screen->write_buf == NULL) { fatal("Out of memory."); } + } + if (screen->write_buf_used) wakeup_io_loop(self, false); + screen_mutex(unlock, write); + break; + } + } + children_mutex(unlock); } bool -schedule_write_to_child_python(unsigned long id, const char *prefix, PyObject *ap, const char *suffix) { +schedule_write_to_child(id_type id, unsigned num, ...) { + va_list ap; + bool too_much_data = false, found = false; +#define get_next_arg(ap) data = va_arg(ap, const char*); szval = va_arg(ap, size_t); + schedule_write_to_child_generic(id, num, va_start, get_next_arg, va_end, found, too_much_data); +#undef get_next_arg + if (too_much_data) log_error("Too much data being written to child with id: %llu dropping it", id); + return found; +} + +bool +schedule_write_to_child_python(id_type id, const char *prefix, PyObject *ap, const char *suffix) { if (!PyTuple_Check(ap)) return false; bool has_prefix = prefix && prefix[0], has_suffix = suffix && suffix[0]; const size_t extra = (has_prefix ? 1 : 0) + (has_suffix ? 1 : 0); @@ -403,7 +442,10 @@ schedule_write_to_child_python(unsigned long id, const char *prefix, PyObject *a } \ } \ } - schedule_write_to_child_generic(id, num, py_start, get_next_arg, py_end); + bool found = false, too_much_data = false; + schedule_write_to_child_generic(id, num, py_start, get_next_arg, py_end, found, too_much_data); + if (too_much_data) log_error("Too much data being written to child with id: %llu dropping it", id); + return found; #undef py_start #undef py_end #undef get_next_arg diff --git a/kitty/data-types.h b/kitty/data-types.h index 600980a7a..eeb49ddd3 100644 --- a/kitty/data-types.h +++ b/kitty/data-types.h @@ -311,8 +311,9 @@ void cursor_from_sgr(Cursor *self, int *params, unsigned int count, bool is_grou const char* cursor_as_sgr(const Cursor *); PyObject* cm_thread_write(PyObject *self, PyObject *args); -bool schedule_write_to_child(unsigned long id, unsigned int num, ...); -bool schedule_write_to_child_python(unsigned long id, const char *prefix, PyObject* tuple_of_str_or_bytes, const char *suffix); +bool schedule_write_to_child(id_type id, unsigned int num, ...); +bool schedule_write_to_child_python(id_type id, const char *prefix, PyObject* tuple_of_str_or_bytes, const char *suffix); +void schedule_write_to_child_if_possible(id_type id, const char *data, size_t sz, bool *found, bool *too_much_data); bool set_iutf8(int, bool); DynamicColor colorprofile_to_color(const ColorProfile *self, DynamicColor entry, DynamicColor defval); diff --git a/kitty/dnd.c b/kitty/dnd.c new file mode 100644 index 000000000..93fbd97e3 --- /dev/null +++ b/kitty/dnd.c @@ -0,0 +1,146 @@ +/* + * dnd.c + * Copyright (C) 2026 Kovid Goyal + * + * Distributed under terms of the GPL3 license. + */ + +#include "dnd.h" +#include "base64.h" + +static void +drop_free_offered_mimes(Window *w) { + if (w->drop.offerred_mimes) { + for (size_t i = 0; i < w->drop.num_offerred_mimes; i++) free((void*)w->drop.offerred_mimes[i]); + free(w->drop.offerred_mimes); w->drop.offerred_mimes = NULL; + } + w->drop.num_offerred_mimes = 0; +} + +static void +free_pending(PendingData *pending) { + if (pending->items) { + for (size_t i = 0; i < pending->count; i++) free(pending->items[i].buf); + free(pending->items); + } + zero_at_ptr(pending); +} + +void +drop_free_data(Window *w) { + drop_free_offered_mimes(w); + free_pending(&w->drop.pending); +} + +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; + for (size_t i = 0; i < an; i++) { + int ret = strcmp(a[i], b[i]); + if (ret != 0) return ret; + } + return 0; +} + +static size_t +send_payload_to_child(id_type id, const char *header, size_t header_sz, const char *data, size_t data_sz) { + size_t offset = 0; + char buf[4096 + 1024]; + memcpy(buf, header, header_sz); + buf[header_sz++] = ':'; buf[header_sz++] = 'm'; buf[header_sz++] = '='; + while (offset < data_sz) { + size_t chunk = data_sz - offset; + size_t p = header_sz; + buf[p++] = offset + 3072 >= data_sz ? '0' : '1'; + buf[p++] = ';'; + size_t b64_len = sizeof(buf) - p; + base64_encode8((const uint8_t*)data + offset, chunk, (uint8_t*)buf + p, &b64_len, false); + p += b64_len; + buf[p++] = 0x1b; buf[p++] = '\\'; + bool found, too_much_data; + schedule_write_to_child_if_possible(id, buf, p, &found, &too_much_data); + if (too_much_data) break; + if (!found) return data_sz; + offset += chunk; + } + return offset; +} + +static bool +flush_pending(id_type id, PendingData *pending) { + while (pending->count) { + PendingEntry *e = pending->items; + size_t written = send_payload_to_child(id, e->buf, e->header_sz, e->buf + e->header_sz, e->data_sz); + if (written < e->data_sz) { + if (written) { + e->data_sz -= written; + memmove(e->buf + e->header_sz, e->buf + e->header_sz + written, e->data_sz); + } + break; + } else { + free(e->buf); + remove_i_from_array(pending->items, 0, pending->count); + } + } + return pending->count > 0; +} + +static void +queue_payload_to_child(id_type id, PendingData *pending, const char *header, size_t header_sz, const char *data, size_t data_sz) { + size_t offset = 0; + if (flush_pending(id, pending)) offset = send_payload_to_child(id, header, header_sz, data, data_sz); + if (offset < data_sz) { + ensure_space_for(pending, items, PendingEntry, pending->count + 1, capacity, 32, true); + char *buf = malloc(header_sz + data_sz - offset); + if (!buf) fatal("Out of memory"); + memcpy(buf, header, header_sz); memcpy(buf + header_sz, data, data_sz - offset); + PendingEntry *e = &pending->items[pending->count++]; + e->buf = buf; e->header_sz = header_sz; e->data_sz = data_sz - offset; + } +} + +void +drop_move_on_child(Window *w, const char** mimes, size_t num_mimes) { + if (!w->drop.hovered) { + drop_free_offered_mimes(w); + w->drop.hovered = true; + } + size_t mimes_total_size = 0; + if (mimes && (w->drop.offerred_mimes == NULL || string_arrays_cmp(mimes, num_mimes, w->drop.offerred_mimes, w->drop.num_offerred_mimes) != 0)) { + drop_free_offered_mimes(w); + w->drop.offerred_mimes = malloc(num_mimes * sizeof(char*)); + if (w->drop.offerred_mimes) { + for (size_t i = 0; i < num_mimes; i++) { + size_t l = strlen(mimes[i]); + mimes_total_size += 1 + l; + char *p = malloc(l + 1); + if (!p) fatal("Out of memory"); + memcpy(p, mimes[i], l); p[l] = 0; + w->drop.offerred_mimes[i] = p; + } + } + w->drop.num_offerred_mimes = num_mimes; + } + // we simply drop this event if there is too much data being written to the child + if (w->drop.pending.count) return; + char buf[128]; + int header_size = snprintf(buf, sizeof(buf), "\x1b]%d;i=%u:t=m:x=%u:y=%u:X=%d:Y=%d", w->drop.client_id, + w->mouse_pos.cell_x, w->mouse_pos.cell_y, (int)w->mouse_pos.global_x, (int)w->mouse_pos.global_y); + if (mimes_total_size) { + mimes_total_size += 1; + RAII_ALLOC(char, mbuf, malloc(mimes_total_size)); + if (mbuf) { + size_t pos = 0; + for (size_t i = 0; i < w->drop.num_offerred_mimes && pos < mimes_total_size; i++) { + int n = snprintf(mbuf, mimes_total_size - pos, mbuf + pos, "%s ", w->drop.offerred_mimes[i]); + if (n < 0) break; + pos += n; + } + queue_payload_to_child(w->id, &w->drop.pending, buf, header_size, mbuf, pos); + } + } else { + buf[header_size++] = 0x1b; buf[header_size++] = '\\'; + bool found, too_much_data; + schedule_write_to_child_if_possible(w->id, buf, header_size, &found, &too_much_data); + } +} diff --git a/kitty/dnd.h b/kitty/dnd.h new file mode 100644 index 000000000..2b04664ea --- /dev/null +++ b/kitty/dnd.h @@ -0,0 +1,12 @@ +/* + * dnd.h + * Copyright (C) 2026 Kovid Goyal + * + * Distributed under terms of the GPL3 license. + */ +#pragma once + +#include "state.h" + +void drop_move_on_child(Window *w, const char **mimes, size_t num_mimes); +void drop_free_data(Window *w); diff --git a/kitty/glfw.c b/kitty/glfw.c index 9710a0d9d..7d4d741c9 100644 --- a/kitty/glfw.c +++ b/kitty/glfw.c @@ -7,6 +7,7 @@ #include "state.h" #include "cleanup.h" #include "monotonic.h" +#include "dnd.h" #include "charsets.h" #include "control-codes.h" #include @@ -766,12 +767,14 @@ read_drop_data(GLFWwindow *window, GLFWDropEvent *ev) { } void -register_drop_window(id_type window_id, const uint8_t *payload, size_t payload_sz, bool on) { +register_drop_window(id_type window_id, const uint8_t *payload, size_t payload_sz, bool on, uint32_t client_id) { Window *w = window_for_window_id(window_id); OSWindow *osw = os_window_for_kitty_window(window_id); if (w && osw && osw->handle) { - w->accepts_drops = on; - if (on && payload && payload_sz) { + w->drop.allowed = on; + w->drop.client_id = client_id; + if (!on) { drop_free_data(w); zero_at_ptr(&w->drop); } + else if (payload && payload_sz) { #ifdef __APPLE__ RAII_ALLOC(char, copy, malloc(payload_sz + 1)); if (!copy) return; RAII_ALLOC(const char*, mimes, calloc(payload_sz, sizeof(char*))); if (!mimes) return; @@ -792,12 +795,17 @@ static void on_drop(GLFWwindow *window, GLFWDropEvent *ev) { if (!set_callback_window(window)) return; OSWindow *os_window = global_state.callback_os_window; + Window *w = NULL; switch (ev->type) { case GLFW_DROP_ENTER: case GLFW_DROP_MOVE: os_window->last_drag_event.x = (int)(ev->xpos * os_window->viewport_x_ratio); os_window->last_drag_event.y = (int)(ev->ypos * os_window->viewport_y_ratio); on_mouse_position_update(ev->xpos, ev->ypos); + if (global_state.mouse_hover_in_window && (w = window_for_window_id(global_state.mouse_hover_in_window)) && w->drop.allowed) { + drop_move_on_child(w, ev->mimes, ev->num_mimes); + return; + } call_boss(on_drop_move, "KiiOO", os_window->id, os_window->last_drag_event.x, os_window->last_drag_event.y, ev->from_self ? Py_True : Py_False, Py_False); diff --git a/kitty/parse-dnd-command.h b/kitty/parse-dnd-command.h index 41497be0e..f80998f08 100644 --- a/kitty/parse-dnd-command.h +++ b/kitty/parse-dnd-command.h @@ -18,7 +18,7 @@ static inline void parse_dnd_code(PS *self, uint8_t *parser_buf, (void)is_negative; size_t sz; - enum KEYS { type = 't', more = 'm' }; + enum KEYS { type = 't', more = 'm', client_id = 'i' }; enum KEYS key = 'a'; if (parser_buf[pos] == ';') @@ -36,6 +36,9 @@ static inline void parse_dnd_code(PS *self, uint8_t *parser_buf, case more: value_state = UINT; break; + case client_id: + value_state = UINT; + break; default: REPORT_ERROR( "Malformed DnDCommand control block, invalid key character: 0x%x", @@ -59,7 +62,7 @@ static inline void parse_dnd_code(PS *self, uint8_t *parser_buf, case type: { g.type = parser_buf[pos++]; - if (g.type != 'a' && g.type != 'e') { + if (g.type != 'A' && g.type != 'a') { REPORT_ERROR("Malformed DnDCommand control block, unknown flag value " "for type: 0x%x", g.type); @@ -121,6 +124,7 @@ static inline void parse_dnd_code(PS *self, uint8_t *parser_buf, break switch (key) { U(more); + U(client_id); default: break; } @@ -178,11 +182,12 @@ static inline void parse_dnd_code(PS *self, uint8_t *parser_buf, break; } - REPORT_VA_COMMAND("K s {sc sI ss#}", self->window_id, "dnd_command", + REPORT_VA_COMMAND("K s {sc sI sI ss#}", self->window_id, "dnd_command", "type", g.type, - "more", (unsigned int)g.more, + "more", (unsigned int)g.more, "client_id", + (unsigned int)g.client_id, "", (char *)parser_buf, g.payload_sz); diff --git a/kitty/screen.c b/kitty/screen.c index b912750c2..5bca56d0c 100644 --- a/kitty/screen.c +++ b/kitty/screen.c @@ -1511,8 +1511,8 @@ void screen_handle_dnd_command(Screen *self, const DnDCommand *cmd, const uint8_t *payload) { if (!self->window_id) return; switch(cmd->type) { - case 'a': register_drop_window(self->window_id, payload, cmd->payload_sz, true); break; - case 'A': register_drop_window(self->window_id, NULL, 0, false); break; + case 'a': register_drop_window(self->window_id, payload, cmd->payload_sz, true, cmd->client_id); break; + case 'A': register_drop_window(self->window_id, NULL, 0, false, cmd->client_id); break; } } diff --git a/kitty/screen.h b/kitty/screen.h index b2b05315a..b8e15806c 100644 --- a/kitty/screen.h +++ b/kitty/screen.h @@ -17,6 +17,7 @@ typedef enum ScrollTypes { SCROLL_LINE = -999999, SCROLL_PAGE, SCROLL_FULL } Scr typedef struct DnDCommand { char type; unsigned more; + uint32_t client_id; size_t payload_sz; } DnDCommand; @@ -323,7 +324,7 @@ bool screen_pause_rendering(Screen *self, bool pause, int for_in_ms); void screen_check_pause_rendering(Screen *self, monotonic_t now); void screen_designate_charset(Screen *self, uint32_t which, uint32_t as); void screen_multi_cursor(Screen *self, int queried_shape, int *params, unsigned num_params); -void register_drop_window(id_type window_id, const uint8_t *payload, size_t payload_sz, bool on); +void register_drop_window(id_type window_id, const uint8_t *payload, size_t payload_sz, bool on, uint32_t client_id); #define DECLARE_CH_SCREEN_HANDLER(name) void screen_##name(Screen *screen); DECLARE_CH_SCREEN_HANDLER(bell) DECLARE_CH_SCREEN_HANDLER(backspace) diff --git a/kitty/state.c b/kitty/state.c index 364e0bef6..8e40fc913 100644 --- a/kitty/state.c +++ b/kitty/state.c @@ -6,6 +6,7 @@ */ #include "cleanup.h" +#include "dnd.h" #include "options/to-c-generated.h" #include #include @@ -351,6 +352,7 @@ update_os_window_title(OSWindow *os_window) { static void destroy_window(Window *w) { + drop_free_data(w); free(w->pending_clicks.clicks); zero_at_ptr(&w->pending_clicks); free(w->buffered_keys.key_data); zero_at_ptr(&w->buffered_keys); Py_CLEAR(w->render_data.screen); Py_CLEAR(w->title); diff --git a/kitty/state.h b/kitty/state.h index d51dc98e2..b32202c1b 100644 --- a/kitty/state.h +++ b/kitty/state.h @@ -204,9 +204,18 @@ typedef struct WindowBarData { bool needs_render; } WindowBarData; +typedef struct PendingEntry { + char *buf; size_t header_sz; + size_t data_sz; +} PendingEntry; + +typedef struct PendingData { + PendingEntry *items; size_t count, capacity; +} PendingData; + typedef struct Window { id_type id; - bool visible, accepts_drops; + bool visible; PyObject *title; WindowRenderData render_data; WindowRenderData window_title_render_data; @@ -236,6 +245,12 @@ typedef struct Window { double drag_start_scrolled_by; bool is_hovering; } scrollbar; + struct { + bool allowed, hovered; + uint32_t client_id; + const char **offerred_mimes; size_t num_offerred_mimes; + PendingData pending; + } drop; } Window; typedef struct BorderRect {