Implement drop data transmission

This commit is contained in:
Kovid Goyal 2026-03-07 14:54:07 +05:30
parent 4f404f8c85
commit 5ad55ec936
No known key found for this signature in database
GPG key ID: 06BC317B515ACE7C
9 changed files with 188 additions and 46 deletions

View file

@ -98,6 +98,22 @@ mandatory, terminals must send the full list of MIME types available in
the drop. The client program can now request data for the MIME types
it is interested in.
Requesting data is done by sending an escape code of the form::
OSC _dnd_code ; t=r ; MIME type ST
This will request data for the specifid MIME type. The terminal must respond
with a series of escape codes of the form::
OSC _dnd_code ; t=r ; data ST
End of data is indicated by an empty payload. If some error occures while
getting the data, the terminal must send an escape code of the form::
OSC _dnd_code ; t=R ; POSIX error name ST
Here POSIX error name is a POSIX symbolic error name such as ``ENOENT`` or
``EIO`` or the value ``EUNKNOWN`` for an unknown error.
Metadata reference
---------------------------
@ -113,6 +129,7 @@ Key Value Default Description
)`` ``A`` - stop accepting drops
``m`` - a drop move event
``M`` - a drop dropped event
``r`` - request dropped data
``m`` Chunking indicator ``0`` ``0`` or ``i``

View file

@ -331,7 +331,7 @@ def parsers() -> None:
write_header(text, 'kitty/parse-multicell-command.h')
keymap = {
't': ('type', flag('aAmM')),
't': ('type', flag('aAmMrR')),
'm': ('more', 'uint'),
'i': ('client_id', 'uint'),
'o': ('operation', 'uint'),

View file

@ -39,6 +39,8 @@ drop_free_data(Window *w) {
drop_free_offered_mimes(w);
drop_free_accepted_mimes(w);
free_pending(&w->drop.pending);
free(w->drop.registered_mimes); w->drop.registered_mimes = NULL;
free(w->drop.getting_data_for_mime); w->drop.getting_data_for_mime = NULL;
}
static void
@ -52,6 +54,40 @@ reset_drop(Window *w) {
}
}
void
drop_register_window(Window *w, const uint8_t *payload, size_t payload_sz, bool on, uint32_t client_id, bool more) {
w->drop.wanted = on;
w->drop.client_id = client_id;
if (!on) { drop_free_data(w); zero_at_ptr(&w->drop); return; }
if (!payload || !payload_sz) return;
size_t sz = w->drop.registered_mimes ? strlen(w->drop.registered_mimes) : 0;
if (sz + payload_sz > 1024 * 1024) return;
w->drop.registered_mimes = realloc(w->drop.registered_mimes, sz + payload_sz + 1);
if (w->drop.registered_mimes) {
memcpy(w->drop.registered_mimes + sz, payload, payload_sz);
sz += payload_sz;
w->drop.registered_mimes[sz] = 0;
}
if (more) return;
if (w->drop.registered_mimes) {
OSWindow *osw = os_window_for_kitty_window(w->id);
if (osw) {
size_t num = 0;
RAII_ALLOC(const char*, mimes, malloc(sizeof(char*) * strlen(w->drop.registered_mimes)));
if (mimes) {
char* token = strtok(w->drop.registered_mimes, " ");
while (token != NULL) {
mimes[num++] = token;
token = strtok(NULL, " ");
}
register_mimes_for_drop(osw, mimes, num);
}
}
}
free(w->drop.registered_mimes); w->drop.registered_mimes = 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;
@ -237,6 +273,54 @@ drop_update_mimes(Window *w, const char **allowed_mimes, size_t allowed_mimes_co
return allowed_mimes_count;
}
static const char*
get_errno_name(int err) {
switch (err) {
case EPERM: return "EPERM";
case ENOENT: return "ENOENT";
case EIO: return "EIO";
default: return "EUNKNOWN";
}
}
static void
drop_send_error(Window *w, int error_code) {
char buf[128];
uint8_t ebuf[32];
size_t b64_len = sizeof(ebuf);
const char *e = get_errno_name(error_code);
base64_encode8((const uint8_t*)e, strlen(e), ebuf, &b64_len, false);
int header_size = snprintf(buf, sizeof(buf), "\x1b]%d;i=%u:t=R", DND_CODE, w->drop.client_id);
queue_payload_to_child(w->id, &w->drop.pending, buf, header_size, (char*)ebuf, b64_len);
}
void
drop_request_data(Window *w, const char *mime) {
if (w->drop.getting_data_for_mime) { free(w->drop.getting_data_for_mime); w->drop.getting_data_for_mime = NULL; }
OSWindow *osw = os_window_for_kitty_window(w->id);
if (!osw) return;
if (w->drop.offerred_mimes) {
for (size_t i = 0; i < w->drop.num_offerred_mimes; i++) {
if (strcmp(mime, w->drop.offerred_mimes[i]) == 0) {
w->drop.getting_data_for_mime = strdup(mime);
if (w->drop.getting_data_for_mime) request_drop_data(osw, w->id, mime);
return;
}
}
}
drop_send_error(w, ENOENT);
}
void
drop_dispatch_data(Window *w, const char *data, ssize_t sz) {
if (sz < 0) drop_send_error(w, -sz);
else {
char buf[128];
int header_size = snprintf(buf, sizeof(buf), "\x1b]%d;i=%u:t=r", DND_CODE, w->drop.client_id);
queue_payload_to_child(w->id, &w->drop.pending, buf, header_size, sz ? data : NULL, sz);
}
}
void
drop_left_child(Window *w) {
w->drop.hovered = false;

View file

@ -8,8 +8,11 @@
#include "state.h"
void drop_register_window(Window *w, const uint8_t *payload, size_t payload_sz, bool on, uint32_t client_id, bool more);
void drop_move_on_child(Window *w, const char **mimes, size_t num_mimes, bool is_drop);
void drop_left_child(Window *w);
void drop_free_data(Window *w);
void drop_request_data(Window *w, const char *mime);
void drop_set_status(Window *w, int operation, const char *payload, size_t payload_sz, bool more);
size_t drop_update_mimes(Window *w, const char **allowed_mimes, size_t allowed_mimes_count);
void drop_dispatch_data(Window *w, const char *data, ssize_t sz);

View file

@ -767,28 +767,17 @@ 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, 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->drop.wanted = on;
w->drop.client_id = client_id;
if (!on) { drop_free_data(w); zero_at_ptr(&w->drop); }
else if (payload && payload_sz) {
register_mimes_for_drop(OSWindow *w, const char **mimes, size_t sz) {
(void)w; (void)mimes; (void)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;
memcpy(copy, payload, payload_sz); copy[payload_sz] = 0;
size_t num = 0;
char* token = strtok(copy, " ");
while (token != NULL) {
mimes[num++] = token;
token = strtok(NULL, " ");
}
glfwCocoaRegisterMIMETypes(osw->handle, mimes, num);
if (w->handle) glfwCocoaRegisterMIMETypes(w->handle, mimes, sz);
#endif
}
}
}
void
request_drop_data(OSWindow *w, id_type wid, const char* mime) {
global_state.drop_dest.client_window_data_request = wid;
if (w->handle) glfwRequestDropData(w->handle, mime);
}
static void
@ -800,6 +789,7 @@ on_drop(GLFWwindow *window, GLFWDropEvent *ev) {
for (size_t i = 0; i < ev->num_mimes; i++) {
if (is_droppable_mime(ev->mimes[i]) >= TAB_DRAG_MIME_NUMBER) { is_kitty_ui_drag = true; break;}
}
bool is_client_drop = !is_kitty_ui_drag && wid && (w = window_for_window_id(wid)) && w->drop.wanted;
switch (ev->type) {
case GLFW_DROP_ENTER:
case GLFW_DROP_MOVE:
@ -807,7 +797,7 @@ on_drop(GLFWwindow *window, GLFWDropEvent *ev) {
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 (!is_kitty_ui_drag && wid && (w = window_for_window_id(wid)) && w->drop.wanted) {
if (is_client_drop) {
drop_move_on_child(w, ev->mimes, ev->num_mimes, false);
ev->num_mimes = drop_update_mimes(w, ev->mimes, ev->num_mimes);
return;
@ -817,9 +807,8 @@ on_drop(GLFWwindow *window, GLFWDropEvent *ev) {
ev->from_self ? Py_True : Py_False, Py_False);
/* fallthrough */
case GLFW_DROP_STATUS_UPDATE:
if (is_kitty_ui_drag) update_allowed_mimes_for_drop(ev);
else if (wid && (w = window_for_window_id(wid)) && w->drop.wanted && w->drop.hovered)
ev->num_mimes = drop_update_mimes(w, ev->mimes, ev->num_mimes);
if (is_client_drop) ev->num_mimes = drop_update_mimes(w, ev->mimes, ev->num_mimes);
else update_allowed_mimes_for_drop(ev);
break;
case GLFW_DROP_LEAVE:
for (size_t tc = 0; tc < os_window->num_tabs; tc++) {
@ -836,6 +825,7 @@ on_drop(GLFWwindow *window, GLFWDropEvent *ev) {
case GLFW_DROP_DROP:
Py_CLEAR(global_state.drop_dest.data);
global_state.drop_dest.drop_has_happened = true;
global_state.drop_dest.client_window_data_request = 0;
if (is_kitty_ui_drag) {
if (ev->from_self) {
if (global_state.drag_source.drag_data) {
@ -852,13 +842,20 @@ on_drop(GLFWwindow *window, GLFWDropEvent *ev) {
if (!global_state.drop_dest.num_left || !(global_state.drop_dest.data = PyDict_New())) {
ev->finish_drop(window, GLFW_DRAG_OPERATION_GENERIC);
}
} else if (wid && (w = window_for_window_id(wid)) && w->drop.wanted && w->drop.hovered) {
drop_move_on_child(w, ev->mimes, ev->num_mimes, true);
}
} else if (is_client_drop) drop_move_on_child(w, ev->mimes, ev->num_mimes, true);
break;
case GLFW_DROP_DATA_AVAILABLE:
if (!global_state.drop_dest.data) ev->finish_drop(window, GLFW_DRAG_OPERATION_GENERIC);
else read_drop_data(window, ev);
if (global_state.drop_dest.client_window_data_request) {
if ((w = window_for_window_id(global_state.drop_dest.client_window_data_request))) {
char buf[3072];
ssize_t ret = ev->read_data(window, ev, buf, sizeof(buf));
drop_dispatch_data(w, buf, ret);
if (ret <= 0) ev->finish_drop(window, GLFW_DRAG_OPERATION_GENERIC);
}
} else {
if (!global_state.drop_dest.data) ev->finish_drop(window, GLFW_DRAG_OPERATION_GENERIC);
else read_drop_data(window, ev);
}
break;
}
}

View file

@ -18,7 +18,16 @@ static inline void parse_dnd_code(PS *self, uint8_t *parser_buf,
(void)is_negative;
size_t sz;
enum KEYS { type = 't', more = 'm', client_id = 'i' };
enum KEYS {
type = 't',
more = 'm',
client_id = 'i',
operation = 'o',
cell_x = 'x',
cell_y = 'y',
pixel_x = 'X',
pixel_y = 'Y'
};
enum KEYS key = 'a';
if (parser_buf[pos] == ';')
@ -39,6 +48,21 @@ static inline void parse_dnd_code(PS *self, uint8_t *parser_buf,
case client_id:
value_state = UINT;
break;
case operation:
value_state = UINT;
break;
case cell_x:
value_state = INT;
break;
case cell_y:
value_state = INT;
break;
case pixel_x:
value_state = INT;
break;
case pixel_y:
value_state = INT;
break;
default:
REPORT_ERROR(
"Malformed DnDCommand control block, invalid key character: 0x%x",
@ -62,7 +86,8 @@ 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 != 'a') {
if (g.type != 'A' && g.type != 'M' && g.type != 'R' && g.type != 'a' &&
g.type != 'm' && g.type != 'r') {
REPORT_ERROR("Malformed DnDCommand control block, unknown flag value "
"for type: 0x%x",
g.type);
@ -109,7 +134,10 @@ static inline void parse_dnd_code(PS *self, uint8_t *parser_buf,
break
READ_UINT;
switch (key) {
;
I(cell_x);
I(cell_y);
I(pixel_x);
I(pixel_y);
default:
break;
}
@ -125,6 +153,7 @@ static inline void parse_dnd_code(PS *self, uint8_t *parser_buf,
switch (key) {
U(more);
U(client_id);
U(operation);
default:
break;
}
@ -182,14 +211,18 @@ static inline void parse_dnd_code(PS *self, uint8_t *parser_buf,
break;
}
REPORT_VA_COMMAND("K s {sc sI sI ss#}", self->window_id, "dnd_command",
REPORT_VA_COMMAND(
"K s {sc sI sI sI si si si si ss#}", self->window_id, "dnd_command",
"type", g.type,
"type", g.type,
"more", (unsigned int)g.more, "client_id",
(unsigned int)g.client_id,
"more", (unsigned int)g.more, "client_id", (unsigned int)g.client_id,
"operation", (unsigned int)g.operation,
"", (char *)parser_buf, g.payload_sz);
"cell_x", (int)g.cell_x, "cell_y", (int)g.cell_y, "pixel_x",
(int)g.pixel_x, "pixel_y", (int)g.pixel_y,
"", (char *)parser_buf, g.payload_sz);
screen_handle_dnd_command(self->screen, &g, parser_buf);
}

View file

@ -1511,12 +1511,18 @@ screen_dirty_line_graphics(Screen *self, const unsigned int top, const unsigned
void
screen_handle_dnd_command(Screen *self, const DnDCommand *cmd, const uint8_t *payload) {
if (!self->window_id) return;
Window *w = window_for_window_id(self->window_id);
if (!w) return;
switch(cmd->type) {
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;
case 'm': {
Window *w = window_for_window_id(self->window_id);
if (w) drop_set_status(w, cmd->operation, (const char*)payload, cmd->payload_sz, cmd->more);
case 'a': drop_register_window(w, payload, cmd->payload_sz, true, cmd->client_id, cmd->more); break;
case 'A': drop_register_window(w, NULL, 0, false, cmd->client_id, cmd->more); break;
case 'm': drop_set_status(w, cmd->operation, (const char*)payload, cmd->payload_sz, cmd->more); break;
case 'r': {
char buf[256];
if (w && cmd->payload_sz + 1 < sizeof(buf)) {
memcpy(buf, payload, cmd->payload_sz); buf[cmd->payload_sz] = 0;
drop_request_data(w, buf);
}
} break;
}
}

View file

@ -326,7 +326,6 @@ 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, 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

@ -248,13 +248,14 @@ typedef struct Window {
struct {
bool wanted, hovered, dropped;
uint32_t client_id;
char *registered_mimes;
PendingData pending;
const char **offerred_mimes; size_t num_offerred_mimes;
char *accepted_mimes; size_t accepted_mimes_sz;
int accepted_operation; bool accept_in_progress;
char *getting_data_for_mime;
} drop;
} Window;
@ -404,7 +405,7 @@ typedef struct GlobalState {
struct {
PyObject *data;
id_type os_window_id;
id_type os_window_id, client_window_data_request;
double x, y;
size_t num_left;
bool drop_has_happened;
@ -536,3 +537,5 @@ void swap_window_buffers(OSWindow *w);
void take_screenshot_of_rectangular_region(OSWindow *os_window, Region region, unsigned char *dst_buf, unsigned *thumb_w, unsigned *thumb_h);
bool current_framebuffer_is_ok(void);
void request_drop_status_update(OSWindow *osw);
void register_mimes_for_drop(OSWindow *w, const char **mimes, size_t sz);
void request_drop_data(OSWindow *w, id_type wid, const char* mime);