Send drop move events to child

This commit is contained in:
Kovid Goyal 2026-03-06 10:24:40 +05:30
parent 8d069d3bcd
commit b0e57b4dce
No known key found for this signature in database
GPG key ID: 06BC317B515ACE7C
12 changed files with 263 additions and 28 deletions

View file

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

View file

@ -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',

View file

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

View file

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

146
kitty/dnd.c Normal file
View file

@ -0,0 +1,146 @@
/*
* dnd.c
* Copyright (C) 2026 Kovid Goyal <kovid at kovidgoyal.net>
*
* 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);
}
}

12
kitty/dnd.h Normal file
View file

@ -0,0 +1,12 @@
/*
* dnd.h
* Copyright (C) 2026 Kovid Goyal <kovid at kovidgoyal.net>
*
* 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);

View file

@ -7,6 +7,7 @@
#include "state.h"
#include "cleanup.h"
#include "monotonic.h"
#include "dnd.h"
#include "charsets.h"
#include "control-codes.h"
#include <structmember.h>
@ -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);

View file

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

View file

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

View file

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

View file

@ -6,6 +6,7 @@
*/
#include "cleanup.h"
#include "dnd.h"
#include "options/to-c-generated.h"
#include <math.h>
#include <sys/mman.h>
@ -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);

View file

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