kitty/kitty/dnd.c
2026-05-12 12:23:57 +05:30

2567 lines
106 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* dnd.c
* Copyright (C) 2026 Kovid Goyal <kovid at kovidgoyal.net>
*
* Distributed under terms of the GPL3 license.
*/
#include "dnd.h"
#include "base64.h"
#include "control-codes.h"
#include "safe-wrappers.h"
#include "iqsort.h"
#include "png-reader.h"
#define NAME lower_str_set
#define KEY_TY char*
#define KEY_DTOR_FN free
#include "kitty-verstable.h"
#include <ctype.h>
#include <dirent.h>
#include <fcntl.h>
#include <limits.h>
#include <stddef.h>
#include <sys/stat.h>
#include <unistd.h>
#include <errno.h>
#define DEFAULT_MIME_LIST_SIZE_CAP 1024u * 1024u
static size_t MIME_LIST_SIZE_CAP = DEFAULT_MIME_LIST_SIZE_CAP;
#define DEFAULT_PRESENT_DATA_CAP 64 * 1024 * 1024
static size_t PRESENT_DATA_CAP = DEFAULT_PRESENT_DATA_CAP;
#define DEFAULT_REMOTE_DRAG_LIMIT 1024 * 1024 * 1024
static size_t REMOTE_DRAG_LIMIT = DEFAULT_REMOTE_DRAG_LIMIT;
static PyObject *g_dnd_test_write_func = NULL;
static const unsigned file_permissions = 0644;
static const unsigned dir_permissions = 0755;
// Utils {{{
// In test mode, this callable is invoked instead of schedule_write_to_child_if_possible.
// It receives (window_id: int, data: bytes) and its return value is ignored.
static void drop_process_queue(Window *w);
static void drop_pop_request(Window *w);
static size_t
count_occurrences(const char *str, size_t len, char target) {
size_t count = 0;
const char *ptr = str;
while ((ptr = memchr(ptr, target, len - (ptr - str))) != NULL) {
count++; ptr++; // Move past the found character
if (ptr >= str + len) break;
}
return count;
}
static const char*
get_errno_name(int err) {
switch (err) {
case EPERM: return "EPERM";
case ENOENT: return "ENOENT";
case EIO: return "EIO";
case EINVAL: return "EINVAL";
case EMFILE: return "EMFILE";
case ENOMEM: return "ENOMEM";
case EFBIG: return "EFBIG";
case EISDIR: return "EISDIR";
case ENOSPC: return "ENOSPC";
case 0: return "OK";
default: return "EUNKNOWN";
}
}
static const char*
machine_id(void) {
static bool done = false;
static char ans[512] = {0};
if (!done) {
done = true;
RAII_PyObject(mname, PyUnicode_DecodeFSDefault("kitty.machine_id"));
if (mname) {
RAII_PyObject(module, PyImport_Import(mname));
if (module) {
RAII_PyObject(func, PyObject_GetAttrString(module, "machine_id"));
if (func) {
RAII_PyObject(ret, PyObject_CallFunction(func, "s", "tty-dnd-protocol-machine-id"));
if (ret) snprintf(ans, sizeof(ans), "%s", PyUnicode_AsUTF8(ret));
}
}
}
if (PyErr_Occurred()) PyErr_Print();
}
return ans;
}
static void
rmtree_best_effort(const char *relpath, int dirfd) {
RAII_PyObject(mname, PyUnicode_DecodeFSDefault("kitty.utils"));
if (mname) {
RAII_PyObject(module, PyImport_Import(mname));
if (module) {
RAII_PyObject(func, PyObject_GetAttrString(module, "rmtree_best_effort"));
if (func) {
RAII_PyObject(ret, PyObject_CallFunction(func, "si", relpath, dirfd));
}
}
}
if (PyErr_Occurred()) PyErr_Print();
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;
RAII_PyObject(mname, PyUnicode_DecodeFSDefault("kitty.utils"));
if (mname) {
RAII_PyObject(module, PyImport_Import(mname));
if (module) {
RAII_PyObject(func, PyObject_GetAttrString(module, "mktempdir_in_cache"));
if (func) {
RAII_PyObject(ret, PyObject_CallFunction(func, "sO", prefix, dnd_is_test_mode() ? Py_False : Py_True));
if (ret) {
if (PyArg_ParseTuple(ret, "si", &ans, fd)) {
if (*fd < 0) {
errno = -*fd;
return NULL;
}
ans = strdup(ans);
if (!ans) {
errno = ENOMEM; return NULL;
}
return ans;
}
}
}
}
}
if (PyErr_Occurred()) PyErr_Print();
errno = EIO;
return NULL;
}
// -1 = unknown, 0 = case-sensitive, 1 = case-insensitive
static int tempdir_case_insensitive = -1;
static void
detect_tempdir_case_sensitivity(const char *tempdir_path) {
if (tempdir_case_insensitive >= 0) return;
tempdir_case_insensitive = 0; // default: case-sensitive
const char *slash = strrchr(tempdir_path, '/');
if (!slash) return;
size_t dirname_len = (size_t)(slash - tempdir_path);
const char *basename = slash + 1;
size_t basename_len = strlen(basename);
if (!basename_len) return;
char upper_basename[PATH_MAX];
if (basename_len >= sizeof(upper_basename)) return;
for (size_t i = 0; i < basename_len; i++) upper_basename[i] = (char)toupper((unsigned char)basename[i]);
upper_basename[basename_len] = '\0';
if (strcmp(upper_basename, basename) == 0) return; // already all-uppercase, cannot distinguish
char upper_path[PATH_MAX];
if (dirname_len + 1 + basename_len + 1 > sizeof(upper_path)) return;
memcpy(upper_path, tempdir_path, dirname_len);
upper_path[dirname_len] = '/';
memcpy(upper_path + dirname_len + 1, upper_basename, basename_len + 1);
struct stat st;
if (stat(upper_path, &st) == 0) tempdir_case_insensitive = 1;
}
static char*
as_file_url(const char *wd, const char *middle, const char *filename) {
RAII_PyObject(mname, PyUnicode_DecodeFSDefault("kitty.utils"));
if (mname) {
RAII_PyObject(module, PyImport_Import(mname));
if (module) {
RAII_PyObject(func, PyObject_GetAttrString(module, "as_file_url"));
if (func) {
RAII_PyObject(ret, PyObject_CallFunction(func, "sss", wd, middle, filename));
if (ret) return strdup(PyUnicode_AsUTF8(ret));
}
}
}
if (PyErr_Occurred()) PyErr_Print();
return NULL;
}
static char*
sanitized_filename_from_url(const char *url) {
RAII_PyObject(mname, PyUnicode_DecodeFSDefault("kitty.utils"));
if (mname) {
RAII_PyObject(module, PyImport_Import(mname));
if (module) {
RAII_PyObject(func, PyObject_GetAttrString(module, "sanitized_filename_from_url"));
if (func) {
RAII_PyObject(ret, PyObject_CallFunction(func, "s", url));
if (ret) return strdup(PyUnicode_AsUTF8(ret));
}
}
}
if (PyErr_Occurred()) PyErr_Print();
return NULL;
}
static void
dnd_set_test_write_func(PyObject *func, size_t mime_list_size_cap, size_t present_data_cap, size_t remote_drag_limit) {
(void)machine_id;
Py_CLEAR(g_dnd_test_write_func);
g_dnd_test_write_func = Py_XNewRef(func);
MIME_LIST_SIZE_CAP = mime_list_size_cap ? mime_list_size_cap : DEFAULT_MIME_LIST_SIZE_CAP;
PRESENT_DATA_CAP = present_data_cap ? present_data_cap : DEFAULT_PRESENT_DATA_CAP;
REMOTE_DRAG_LIMIT = remote_drag_limit ? remote_drag_limit : DEFAULT_REMOTE_DRAG_LIMIT;
}
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 bool
test_write_chunk(id_type id, const char *buf, size_t sz) {
// In test mode, deliver the chunk to the registered Python callable.
// Returns true when the test interceptor consumed the data (no real write needed).
if (!g_dnd_test_write_func) return false;
RAII_PyObject(ret, PyObject_CallFunction(g_dnd_test_write_func, "Ky#", (unsigned long long)id, buf, (Py_ssize_t)sz));
if (!ret) PyErr_Print();
return true;
}
static size_t
send_payload_to_child(id_type id, uint32_t client_id, const char *header, size_t header_sz, const char *data, const size_t data_sz, bool as_base64) {
size_t offset = 0;
char buf[4096 + 1024];
memcpy(buf, header, header_sz);
if (client_id) header_sz += snprintf(buf + header_sz, sizeof(buf) - header_sz, ":i=%u", (unsigned)client_id);
if (!data_sz) {
buf[header_sz++] = 0x1b; buf[header_sz++] = '\\';
if (!test_write_chunk(id, buf, header_sz)) {
bool found, too_much_data;
schedule_write_to_child_if_possible(id, buf, header_sz, &found, &too_much_data);
if (too_much_data) return 0;
}
return 1;
}
buf[header_sz++] = ':'; buf[header_sz++] = 'm'; buf[header_sz++] = '=';
const size_t limit = as_base64 ? 3072 : 4096;
while (offset < data_sz) {
size_t chunk = data_sz - offset;
if (chunk > limit) chunk = limit;
size_t p = header_sz;
const bool is_last = offset + chunk >= data_sz;
buf[p++] = is_last ? '0' : '1'; buf[p++] = ';';
if (as_base64) {
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;
} else {
memcpy(buf + p, data + offset, chunk);
p += chunk;
}
buf[p++] = 0x1b; buf[p++] = '\\';
if (!test_write_chunk(id, 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->client_id, e->buf, e->header_sz, e->buf + e->header_sz, e->data_sz, e->as_base64);
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 {
if (!e->data_sz && !written) break;
free(e->buf); zero_at_ptr(e);
remove_i_from_array(pending->items, 0, pending->count);
}
}
return pending->count == 0;
}
#define check_for_pending_writes() \
add_main_loop_timer(ms_to_monotonic_t(20), false, flush_pending_payloads, (void*)(uintptr_t)id, NULL)
static void
flush_pending_payloads(id_type timer_id UNUSED, void *x) {
id_type id = (uintptr_t)x;
Window *w = window_for_window_id(id);
if (w && w->drop.wanted) {
if (!flush_pending(w->id, &w->drop.pending)) check_for_pending_writes();
}
}
static void
queue_payload_to_child(id_type id, uint32_t client_id, PendingData *pending, const char *header, size_t header_sz, const char *data, size_t data_sz, bool as_base64) {
size_t offset = 0;
if (flush_pending(id, pending)) offset = send_payload_to_child(id, client_id, header, header_sz, data, data_sz, as_base64);
if (offset < data_sz || (!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);
if (data_sz - offset) 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;
e->as_base64 = as_base64; e->client_id = client_id;
}
if (pending->count) check_for_pending_writes();
}
void
dnd_query(Window *w, uint32_t client_id) {
char buf[64];
ssize_t header_size = snprintf(buf, sizeof(buf), "\x1b]%d;t=q", DND_CODE);
send_payload_to_child(w->id, client_id, buf, header_size, NULL, 0, false);
}
static bool
is_same_machine(const char *client_machine_id, size_t sz) {
if (!sz || !client_machine_id) return true;
if (sz < 20) return false;
if (client_machine_id[0] != '1' || client_machine_id[1] != ':') return false;
client_machine_id = client_machine_id + 2; sz -= 2;
const char *host_machine_id = machine_id();
if (!host_machine_id) return true;
const size_t hsz = strlen(host_machine_id);
return sz == hsz && memcmp(client_machine_id, host_machine_id, sz) == 0;
}
static void
url_decode_inplace(char *str) {
char *src = str, *dst = str;
while (*src) {
if (*src == '%' && src[1] && src[2]) {
unsigned int hi = 0, lo = 0;
char c1 = src[1], c2 = src[2];
if (c1 >= '0' && c1 <= '9') hi = c1 - '0';
else if (c1 >= 'a' && c1 <= 'f') hi = c1 - 'a' + 10;
else if (c1 >= 'A' && c1 <= 'F') hi = c1 - 'A' + 10;
else { *dst++ = *src++; continue; }
if (c2 >= '0' && c2 <= '9') lo = c2 - '0';
else if (c2 >= 'a' && c2 <= 'f') lo = c2 - 'a' + 10;
else if (c2 >= 'A' && c2 <= 'F') lo = c2 - 'A' + 10;
else { *dst++ = *src++; continue; }
*dst++ = (char)((hi << 4) | lo);
src += 3;
} else {
*dst++ = *src++;
}
}
*dst = 0;
}
// }}}
// Dropping {{{
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;
w->drop.offered_mimes_total_size = 0;
}
static void
drop_free_accepted_mimes(Window *w) {
free(w->drop.accepted_mimes); w->drop.accepted_mimes = NULL;
w->drop.accepted_mimes_sz = 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);
}
static void
drop_free_dir_handle(DirHandle *h) {
free(h->path);
for (size_t i = 0; i < h->num_entries; i++) free(h->entries[i]);
free(h->entries);
zero_at_ptr(h);
}
static void
drop_free_dir_handles(Window *w) {
for (size_t i = 0; i < w->drop.num_dir_handles; i++)
drop_free_dir_handle(&w->drop.dir_handles[i]);
free(w->drop.dir_handles);
w->drop.dir_handles = NULL;
w->drop.num_dir_handles = 0;
w->drop.dir_handles_capacity = 0;
w->drop.next_dir_handle_id = 0;
}
static void
drop_close_file_fd(Window *w) {
if (w->drop.file_fd_plus_one) {
safe_close(w->drop.file_fd_plus_one - 1, __FILE__, __LINE__);
w->drop.file_fd_plus_one = 0;
}
if (w->drop.file_send_timer) {
remove_main_loop_timer(w->drop.file_send_timer);
w->drop.file_send_timer = 0;
}
}
static void
drop_free_request_queue(Window *w) {
w->drop.num_data_requests = 0;
w->drop.current_request_x = 0;
w->drop.current_request_y = 0;
w->drop.current_request_Y = 0;
}
void
drop_free_data(Window *w) {
drop_close_file_fd(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.uri_list); w->drop.uri_list = NULL;
free(w->drop.getting_data_for_mime); w->drop.getting_data_for_mime = NULL;
drop_free_dir_handles(w);
drop_free_request_queue(w);
}
static void
reset_drop(Window *w) {
bool wanted = w->drop.wanted; uint32_t cid = w->drop.client_id;
bool is_remote_client = w->drop.is_remote_client;
drop_free_data(w);
zero_at_ptr(&w->drop);
if (wanted) {
w->drop.wanted = wanted;
w->drop.client_id = cid;
w->drop.is_remote_client = is_remote_client;
}
}
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;
w->drop.is_remote_client = false;
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 > MIME_LIST_SIZE_CAP) return;
char *tmp = realloc(w->drop.registered_mimes, sz + payload_sz + 1);
if (tmp) {
w->drop.registered_mimes = tmp;
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;
}
void
drop_register_machine_id(Window *w, const uint8_t *machine_id, size_t sz) {
w->drop.is_remote_client = !is_same_machine((const char*)machine_id, sz);
}
void
drop_move_on_child(Window *w, const char** mimes, size_t num_mimes, bool is_drop) {
if (!w->drop.hovered) {
reset_drop(w);
w->drop.hovered = true;
}
if (is_drop) { w->drop.dropped = true; w->drop.hovered = false; }
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) {
w->drop.offered_mimes_total_size = 0;
for (size_t i = 0; i < num_mimes; i++) {
size_t l = strlen(mimes[i]);
w->drop.offered_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 && !is_drop) return;
char buf[128];
int header_size = snprintf(buf, sizeof(buf), "\x1b]%d;t=%c:x=%u:y=%u:X=%d:Y=%d", DND_CODE,
is_drop ? 'M' : 'm', w->mouse_pos.cell_x, w->mouse_pos.cell_y,
(int)w->mouse_pos.global_x, (int)w->mouse_pos.global_y);
if (w->drop.offered_mimes_total_size) {
const size_t mimes_total_size = 1 + w->drop.offered_mimes_total_size;
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 + pos, mimes_total_size - pos, "%s ", w->drop.offerred_mimes[i]);
if (n < 0) break;
pos += n;
}
queue_payload_to_child(w->id, w->drop.client_id, &w->drop.pending, buf, header_size, mbuf, pos, false);
}
} else {
buf[header_size++] = 0x1b; buf[header_size++] = '\\';
queue_payload_to_child(w->id, w->drop.client_id, &w->drop.pending, buf, header_size, NULL, 0, false);
}
}
static GLFWDragOperationType
drop_operation_to_enum(uint32_t op) {
switch(op) {
case 1: return GLFW_DRAG_OPERATION_COPY;
case 2: return GLFW_DRAG_OPERATION_MOVE;
default: return GLFW_DRAG_OPERATION_NONE;
}
}
static GLFWDragOperationType last_drop_finish_operation = GLFW_DRAG_OPERATION_NONE;
void
drop_set_status(Window *w, int operation, const char *payload, size_t payload_sz, bool more) {
last_drop_finish_operation = GLFW_DRAG_OPERATION_NONE;
if (!w->drop.accept_in_progress) {
drop_free_accepted_mimes(w); w->drop.accept_in_progress = true;
w->drop.accepted_operation = drop_operation_to_enum(operation);
}
if (payload_sz) {
if (w->drop.accepted_mimes_sz + payload_sz > MIME_LIST_SIZE_CAP) return;
char *new_buf = realloc(w->drop.accepted_mimes, w->drop.accepted_mimes_sz + payload_sz + 2);
if (!new_buf) return;
w->drop.accepted_mimes = new_buf;
memcpy(w->drop.accepted_mimes + w->drop.accepted_mimes_sz, payload, payload_sz);
w->drop.accepted_mimes_sz += payload_sz;
}
if (!more) {
w->drop.accept_in_progress = false;
if (w->drop.accepted_mimes) {
for (size_t i = 0; i < w->drop.accepted_mimes_sz; i++)
if (w->drop.accepted_mimes[i] == ' ') w->drop.accepted_mimes[i] = 0;
w->drop.accepted_mimes[w->drop.accepted_mimes_sz++] = 0;
}
OSWindow *osw = os_window_for_kitty_window(w->id);
if (osw) request_drop_status_update(osw);
}
}
void
drop_finish(Window *w) {
OSWindow *osw = os_window_for_kitty_window(w->id);
if (osw && osw->handle) {
glfwEndDrop(osw->handle, w->drop.accepted_operation);
}
}
size_t
drop_update_mimes(Window *w, const char **allowed_mimes, size_t allowed_mimes_count) {
if (w->drop.accept_in_progress) return allowed_mimes_count;
if (w->drop.accepted_operation == GLFW_DRAG_OPERATION_NONE) return 0;
typedef struct mime_sorter { const char *m; ssize_t key; } mime_sorter;
if (!w->drop.accepted_mimes) return allowed_mimes_count;
RAII_ALLOC(mime_sorter, ms, malloc(sizeof(mime_sorter) * allowed_mimes_count));
if (!ms) return allowed_mimes_count;
const ssize_t sentinel = allowed_mimes_count;
for (size_t i = 0; i < allowed_mimes_count; i++) {
ms[i].m = allowed_mimes[i];
const char *p = strstr(w->drop.accepted_mimes, ms[i].m);
ms[i].key = p ? p - w->drop.accepted_mimes : sentinel;
}
#define mimes_lt(a, b) ((a)->key < (b)->key)
QSORT(mime_sorter, ms, allowed_mimes_count, mimes_lt);
#undef mimes_lt
while(allowed_mimes_count && ms[allowed_mimes_count-1].key == sentinel) allowed_mimes_count--;
for (size_t i = 0; i < allowed_mimes_count; i++) allowed_mimes[i] = ms[i].m;
return allowed_mimes_count;
}
/* Append the current request disambiguation keys (x, y, Y) to a header buffer.
* Returns the number of bytes written. */
static int
drop_append_request_keys(Window *w, char *buf, size_t bufsize) {
int sz = 0;
if (w->drop.current_request_x && sz < (int)bufsize - 1)
sz += snprintf(buf + sz, bufsize - sz, ":x=%d", (int)w->drop.current_request_x);
if (w->drop.current_request_y && sz < (int)bufsize - 1)
sz += snprintf(buf + sz, bufsize - sz, ":y=%d", (int)w->drop.current_request_y);
if (w->drop.current_request_Y && sz < (int)bufsize - 1)
sz += snprintf(buf + sz, bufsize - sz, ":Y=%d", (int)w->drop.current_request_Y);
return sz;
}
static void
drop_send_error(Window *w, int error_code, const char *desc) {
char buf[128], details_buf[1024];
int n;
if (desc && desc[0]) n = snprintf(details_buf, sizeof(details_buf), "%s:%s", get_errno_name(error_code), desc);
else n = snprintf(details_buf, sizeof(details_buf), "%s:%s", get_errno_name(error_code), desc);
int header_size = snprintf(buf, sizeof(buf), "\x1b]%d;t=R", DND_CODE);
header_size += drop_append_request_keys(w, buf + header_size, sizeof(buf) - header_size);
queue_payload_to_child(w->id, w->drop.client_id, &w->drop.pending, buf, header_size, details_buf, n, false);
}
void
drop_send_einval(Window *w, const char *desc) {
drop_send_error(w, EINVAL, desc);
}
/* Returns true if the request completed synchronously (error, no-op),
* false if an async OS data fetch was started. */
static bool
do_drop_request_data(Window *w, int32_t idx) {
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 true;
/* idx is 1-based */
if (idx < 1 || !w->drop.offerred_mimes || (size_t)idx > w->drop.num_offerred_mimes) {
drop_send_error(w, ENOENT, "drop data request index out of bounds");
return true;
}
const char *mime = w->drop.offerred_mimes[idx - 1];
w->drop.getting_data_for_mime = strdup(mime);
if (w->drop.getting_data_for_mime) request_drop_data(osw, w->id, mime);
return false; /* async: completion via drop_dispatch_data */
}
void
drop_dispatch_data(Window *w, const char *mime, const char *data, ssize_t sz) {
if (sz < 0) {
drop_send_error(w, -sz, "drop data request failed to read data");
drop_pop_request(w);
drop_process_queue(w);
} else {
char buf[128];
int header_size = snprintf(buf, sizeof(buf), "\x1b]%d;t=r", DND_CODE);
const bool is_uri_list = strcmp(mime, "text/uri-list") == 0;
if (is_uri_list) header_size += snprintf(
buf + header_size, sizeof(buf) - header_size, ":X=%d", w->drop.is_remote_client);
header_size += drop_append_request_keys(w, buf + header_size, sizeof(buf) - header_size);
queue_payload_to_child(w->id, w->drop.client_id, &w->drop.pending, buf, header_size, sz ? data : NULL, sz, true);
if (is_uri_list) {
w->drop.uri_list_sz += sz;
char *tmp = realloc(w->drop.uri_list, w->drop.uri_list_sz);
if (tmp) { w->drop.uri_list = tmp; memcpy(w->drop.uri_list + w->drop.uri_list_sz - sz, data, sz); }
else { free(w->drop.uri_list); w->drop.uri_list = NULL; w->drop.uri_list_sz = 0; }
}
if (sz == 0) { drop_pop_request(w); drop_process_queue(w); }
}
}
// ---- Remote file / directory transfer ----
/* Return the nth (0-based) file path from a text/uri-list.
* On success returns true and *path_out is a malloc'd absolute resolved path.
* On failure returns false and *error_out points to a static error string. */
static bool
get_nth_file_url(const char *uri_list, size_t uri_list_sz, int n, char **path_out, const char **error_out) {
*path_out = NULL;
RAII_ALLOC(char, buf, malloc(uri_list_sz + 1));
if (!buf) { *error_out = "ENOMEM"; return false; }
memcpy(buf, uri_list, uri_list_sz);
buf[uri_list_sz] = 0;
const char *found_line = NULL;
char *p = buf;
while (*p) {
char *eol = p + strcspn(p, "\r\n");
char saved = *eol; *eol = 0;
/* trim trailing whitespace */
char *end = eol;
while (end > p && (end[-1] == ' ' || end[-1] == '\t')) { end--; *end = 0; }
if (*p && *p != '#') {
if (n <= 0) { found_line = p; break; }
n--;
}
if (saved == 0) break;
p = eol + 1;
while (*p == '\r' || *p == '\n') p++;
}
if (!found_line) { *error_out = "ENOENT"; return false; }
/* Must be a file:// URL */
if (strncmp(found_line, "file://", 7) != 0) { *error_out = "EUNKNOWN"; return false; }
const char *rest = found_line + 7;
const char *slash = strchr(rest, '/');
if (!slash) { *error_out = "EINVAL"; return false; }
/* Host part must be empty or "localhost" */
size_t host_len = (size_t)(slash - rest);
if (host_len > 0 && !(host_len == 9 && strncasecmp(rest, "localhost", 9) == 0)) {
*error_out = "EUNKNOWN"; return false;
}
RAII_ALLOC(char, path, strdup(slash));
if (!path) { *error_out = "ENOMEM"; return false; }
/* Strip any query (?...) or fragment (#...) from the path */
char *query_or_fragment_start = path + strcspn(path, "?#");
*query_or_fragment_start = 0;
url_decode_inplace(path);
if (path[0] != '/') { *error_out = "EINVAL"; return false; }
*path_out = strdup(path);
if (!*path_out) { *error_out = "ENOMEM"; return false; }
return true;
}
/* Send error using a literal string (for cases where we have a string, not int). */
static void
drop_send_error_str(Window *w, const char *err_name) {
char buf[128];
int header_size = snprintf(buf, sizeof(buf), "\x1b]%d;t=R", DND_CODE);
header_size += drop_append_request_keys(w, buf + header_size, sizeof(buf) - header_size);
queue_payload_to_child(w->id, w->drop.client_id, &w->drop.pending, buf, header_size, err_name, strlen(err_name), false);
}
/* Size of each file read chunk; fits in one base64 sub-chunk (≤4096 chars). */
#define FILE_CHUNK_SIZE 3072
/* Abort file transfer if no data has been sent to the child for this long. */
#define FILE_SEND_TIMEOUT_SECONDS 90
static void drop_send_file_chunks(Window *w);
static void
file_send_timer_callback(id_type timer_id UNUSED, void *x) {
id_type id = (uintptr_t)x;
Window *w = window_for_window_id(id);
if (!w || !w->drop.file_fd_plus_one) return;
w->drop.file_send_timer = 0;
if (monotonic() - w->drop.last_file_send_at > s_to_monotonic_t(FILE_SEND_TIMEOUT_SECONDS)) {
drop_close_file_fd(w);
drop_send_error(w, EIO, "timed out waiting for child to read file data");
drop_pop_request(w);
drop_process_queue(w);
return;
}
drop_send_file_chunks(w);
}
static void
drop_send_file_chunks(Window *w) {
if (!flush_pending(w->id, &w->drop.pending)) {
w->drop.file_send_timer = add_main_loop_timer(ms_to_monotonic_t(20), false, file_send_timer_callback, (void*)(uintptr_t)w->id, NULL);
return;
}
char hdr[128];
int hdr_sz = snprintf(hdr, sizeof(hdr), "\x1b]%d;t=r", DND_CODE);
hdr_sz += drop_append_request_keys(w, hdr + hdr_sz, sizeof(hdr) - hdr_sz);
while (1) {
char buf[FILE_CHUNK_SIZE];
ssize_t n;
do { n = read(w->drop.file_fd_plus_one - 1, buf, sizeof(buf)); } while (n < 0 && errno == EINTR);
if (n < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
/* No data available right now; retry via timer */
w->drop.file_send_timer = add_main_loop_timer(ms_to_monotonic_t(20), false, file_send_timer_callback, (void*)(uintptr_t)w->id, NULL);
return;
}
drop_close_file_fd(w);
drop_send_error(w, EIO, "failed ot read from drop data file");
drop_pop_request(w);
drop_process_queue(w);
return;
}
if (n == 0) {
/* EOF: close fd and send the empty end-of-data signal */
drop_close_file_fd(w);
queue_payload_to_child(w->id, w->drop.client_id, &w->drop.pending, hdr, hdr_sz, NULL, 0, true);
drop_pop_request(w);
drop_process_queue(w);
return;
}
size_t sent = send_payload_to_child(w->id, w->drop.client_id, hdr, hdr_sz, buf, (size_t)n, true);
if (sent > 0) w->drop.last_file_send_at = monotonic();
if (sent < (size_t)n) {
/* Partial send: rewind file pointer and retry via timer */
if (lseek(w->drop.file_fd_plus_one - 1, -(off_t)(((size_t)n) - sent), SEEK_CUR) < 0) {
drop_close_file_fd(w);
drop_send_error(w, EIO, "failed to seek() on drop data file");
drop_pop_request(w);
drop_process_queue(w);
return;
}
w->drop.file_send_timer = add_main_loop_timer(ms_to_monotonic_t(20), false, file_send_timer_callback, (void*)(uintptr_t)w->id, NULL);
return;
}
/* Full chunk sent: loop and read next chunk */
}
}
/* Open a regular file and begin sending its contents as t=r chunks followed
* by an empty end-of-data t=r, using chunked I/O to avoid large allocations.
* Returns true if completed synchronously (error), false if async I/O started. */
static bool
drop_send_file_data(Window *w, const char *path) {
drop_close_file_fd(w);
int fd = safe_open(path, O_RDONLY | O_CLOEXEC | O_NONBLOCK, 0);
if (fd < 0) {
switch (errno) {
case ENOENT: case ENOTDIR: drop_send_error(w, ENOENT, "drop data file does not exist"); break;
case EACCES: case EPERM: drop_send_error(w, EPERM, "opening drop data file permission denied"); break;
default: drop_send_error(w, EIO, "failed to open drop data file"); break;
}
return true;
}
struct stat st;
if (fstat(fd, &st) < 0) {
switch (errno) {
case ENOENT: case ENOTDIR: drop_send_error(w, ENOENT, "failed to stat drop data file"); break;
case EACCES: case EPERM: drop_send_error(w, EPERM, "failed to stat drop data file"); break;
default: drop_send_error(w, EIO, "failed to stat drop data file"); break;
}
safe_close(fd, __FILE__, __LINE__);
return true;
}
if (!S_ISREG(st.st_mode)) { drop_send_error(w, EINVAL, "drop data file is not a regular file"); safe_close(fd, __FILE__, __LINE__); return true; }
w->drop.file_fd_plus_one = fd + 1;
w->drop.last_file_send_at = monotonic();
drop_send_file_chunks(w);
return false; /* async: completion via drop_send_file_chunks */
}
/* Allocate a new DirHandle for the given path and entries (takes ownership of
* entries array and its strings). Returns the new handle id. */
static uint32_t
drop_alloc_dir_handle(Window *w, const char *path, char **entries, size_t num_entries) {
ensure_space_for(&w->drop, dir_handles, DirHandle, w->drop.num_dir_handles + 1, dir_handles_capacity, 4, true);
w->drop.next_dir_handle_id++;
/* Handles 0 and 1 are reserved (0 = absent, 1 = symlink indicator), so
* valid directory handles must be >= 2. */
if (w->drop.next_dir_handle_id < 2) w->drop.next_dir_handle_id = 2;
DirHandle *h = &w->drop.dir_handles[w->drop.num_dir_handles++];
zero_at_ptr(h);
h->id = w->drop.next_dir_handle_id;
h->path = strdup(path);
if (!h->path) fatal("Out of memory");
h->entries = entries;
h->num_entries = num_entries;
return h->id;
}
static DirHandle *
drop_find_dir_handle(Window *w, uint32_t id) {
for (size_t i = 0; i < w->drop.num_dir_handles; i++)
if (w->drop.dir_handles[i].id == id) return &w->drop.dir_handles[i];
return NULL;
}
/* Open a directory, build the null-separated listing, create a handle, and
* send the listing to the client as a t=d:x=handle_id response. */
static void
drop_send_dir_listing(Window *w, const char *path) {
DIR *dir = opendir(path);
if (!dir) {
switch (errno) {
case ENOENT: case ENOTDIR: drop_send_error(w, ENOENT, "drop data dir does not exist"); break;
case EACCES: case EPERM: drop_send_error(w, EPERM, "drop data dir permission denied"); break;
default: drop_send_error(w, EIO, "drop data dir opening failed"); break;
}
return;
}
/* Build null-separated payload: entry1\0entry2\0... */
size_t payload_cap = 4096, payload_sz = 0;
char *payload = malloc(payload_cap);
if (!payload) { closedir(dir); drop_send_error(w, ENOMEM, "out of memory reading drop data dir"); return; }
#define APPEND(s, n) do { \
size_t _n = (size_t)(n); \
size_t _need = payload_sz + _n + 1; \
if (_need > payload_cap) { \
while (payload_cap < _need) payload_cap *= 2; \
char *_np = realloc(payload, payload_cap); \
if (!_np) { free(payload); closedir(dir); drop_send_error(w, ENOMEM, "out of memory reading drop data dir"); return; } \
payload = _np; \
} \
memcpy(payload + payload_sz, (s), _n); \
payload_sz += _n; \
payload[payload_sz++] = 0; \
} while(0)
/* Collect directory entries */
size_t ents_cap = 16, ents_num = 0;
char **ents = malloc(sizeof(char *) * ents_cap);
if (!ents) { free(payload); closedir(dir); drop_send_error(w, ENOMEM, "out of memory reading directory contents"); return; }
struct dirent *de;
while ((de = readdir(dir)) != NULL) {
if (strcmp(de->d_name, ".") == 0 || strcmp(de->d_name, "..") == 0) continue;
unsigned char dtype = de->d_type;
if (dtype == DT_UNKNOWN) {
/* Fall back to lstat when d_type is unavailable */
char full[PATH_MAX];
if (snprintf(full, sizeof(full), "%s/%s", path, de->d_name) >= (int)sizeof(full)) continue;
struct stat est;
if (lstat(full, &est) < 0) continue;
if (S_ISREG(est.st_mode)) dtype = DT_REG;
else if (S_ISDIR(est.st_mode)) dtype = DT_DIR;
else if (S_ISLNK(est.st_mode)) dtype = DT_LNK;
else continue;
}
if (dtype != DT_REG && dtype != DT_DIR && dtype != DT_LNK) continue;
if (ents_num >= ents_cap) {
ents_cap *= 2;
char **ne = realloc(ents, sizeof(char *) * ents_cap);
if (!ne) {
for (size_t i = 0; i < ents_num; i++) free(ents[i]);
free(ents); free(payload); closedir(dir);
drop_send_error(w, ENOMEM, "out of memory reading drop data dir"); return;
}
ents = ne;
}
ents[ents_num] = strdup(de->d_name);
if (!ents[ents_num]) {
for (size_t i = 0; i < ents_num; i++) free(ents[i]);
free(ents); free(payload); closedir(dir);
drop_send_error(w, ENOMEM, "out of memory reading drop data dir"); return;
}
ents_num++;
APPEND(de->d_name, strlen(de->d_name));
}
closedir(dir);
#undef APPEND
uint32_t handle_id = drop_alloc_dir_handle(w, path, ents, ents_num);
char hdr[128];
int hdr_sz = snprintf(hdr, sizeof(hdr), "\x1b]%d;t=r", DND_CODE);
/* Echo all request keys (x, y, Y) so the client can unambiguously identify
* which filesystem object this listing corresponds to. For top-level URI
* file requests Y is absent; for sub-dir reads Y holds the parent handle
* and x holds the 1-based entry index. The new handle is X itself (a value
* > 1 distinguishes directories from regular files (X absent / X=0) and
* symlinks (X=1)). */
hdr_sz += drop_append_request_keys(w, hdr + hdr_sz, sizeof(hdr) - hdr_sz);
hdr_sz += snprintf(hdr + hdr_sz, sizeof(hdr) - hdr_sz, ":X=%u", (unsigned)handle_id);
/* payload_sz includes a trailing null; omit it the null-separated format
* does not require a trailing null after the last entry. */
size_t send_sz = payload_sz > 0 ? payload_sz - 1 : 0;
if (send_sz)
queue_payload_to_child(w->id, w->drop.client_id, &w->drop.pending, hdr, hdr_sz, payload, send_sz, true);
free(payload);
/* end-of-listing signal (empty payload) */
queue_payload_to_child(w->id, w->drop.client_id, &w->drop.pending, hdr, hdr_sz, NULL, 0, true);
}
static void
drop_send_symlink(Window *w, const char *path) {
char target[PATH_MAX]; ssize_t tgtsz;
if ((tgtsz = readlink(path, target, sizeof(target)-1)) < 0) { drop_send_error(w, EIO, "failed to read symlink for drop data"); return; }
char hdr[128];
int hdr_sz = snprintf(hdr, sizeof(hdr), "\x1b]%d;t=r", DND_CODE);
hdr_sz += drop_append_request_keys(w, hdr + hdr_sz, sizeof(hdr) - hdr_sz);
hdr_sz += snprintf(hdr + hdr_sz, sizeof(hdr) - hdr_sz, ":X=1");
queue_payload_to_child(w->id, w->drop.client_id, &w->drop.pending, hdr, hdr_sz, target, tgtsz, true);
queue_payload_to_child(w->id, w->drop.client_id, &w->drop.pending, hdr, hdr_sz, NULL, 0, true);
}
/* Send the file/directory at URI-list index idx.
* Returns true if completed synchronously, false if async file I/O started. */
static bool
do_drop_request_uri_data(Window *w, int32_t mime_idx, int32_t file_idx) {
if (!w->drop.uri_list || !w->drop.uri_list_sz) {
drop_send_error(w, EINVAL, "drop data uri list empty"); return true;
}
if (global_state.drag_source.from_window == w->id && w->drag_source.state != DRAG_SOURCE_NONE) {
drop_send_error(w, EPERM, "cannot drop into self window"); return true;
}
/* Verify mime_idx (1-based) points to text/uri-list */
if (mime_idx < 1 || !w->drop.offerred_mimes || (size_t)mime_idx > w->drop.num_offerred_mimes ||
strcmp(w->drop.offerred_mimes[mime_idx - 1], "text/uri-list") != 0) {
drop_send_error(w, EINVAL, "drop data mime index out of bounds"); return true;
}
/* file_idx is 1-based, convert to 0-based for get_nth_file_url */
if (file_idx < 1) { drop_send_error(w, EINVAL, "drop data file url index out of bounds"); return true; }
int file_n = file_idx - 1;
RAII_ALLOC(char, path, NULL);
const char *err = NULL;
if (!get_nth_file_url(w->drop.uri_list, w->drop.uri_list_sz, file_n, &path, &err)) {
drop_send_error_str(w, err);
return true;
}
struct stat st;
if (lstat(path, &st) < 0) {
switch (errno) {
case ENOENT: case ENOTDIR: drop_send_error(w, ENOENT, "drop data file does not exist"); break;
case EACCES: case EPERM: drop_send_error(w, EPERM, "permission denied for stat() on drop data file"); break;
default: drop_send_error(w, EIO, "stat() on drop data file failed"); break;
}
return true;
}
bool sync = true;
if (S_ISDIR(st.st_mode)) {
drop_send_dir_listing(w, path);
} else if (S_ISREG(st.st_mode)) {
sync = drop_send_file_data(w, path);
} else if (S_ISLNK(st.st_mode)) {
drop_send_symlink(w, path);
} else {
drop_send_error(w, EINVAL, "drop data file is neother a regular file, directory or symlink");
}
return sync;
}
/* Handle a directory request from the client.
* handle_id: the directory handle (Y= key).
* entry_num: 0 means close the handle; >=1 means read that entry (x= key, 1-based).
* Returns true if completed synchronously, false if async file I/O started. */
static bool
do_drop_handle_dir_request(Window *w, uint32_t handle_id, int32_t entry_num) {
if (!handle_id) { drop_send_error(w, EINVAL, "no parent directory handle specified"); return true; }
DirHandle *h = drop_find_dir_handle(w, handle_id);
if (!h) { drop_send_error(w, EINVAL, "parent directory handle not found"); return true; }
if (entry_num == 0) {
/* Close the handle */
size_t hidx = (size_t)(h - w->drop.dir_handles);
drop_free_dir_handle(h);
remove_i_from_array(w->drop.dir_handles, hidx, w->drop.num_dir_handles);
return true;
}
/* Read the entry at 1-based index */
size_t eidx = (size_t)(entry_num - 1);
if (eidx >= h->num_entries) { drop_send_error(w, EINVAL, "entry index out of bounds for parent directory"); return true; }
char full[PATH_MAX];
if (snprintf(full, sizeof(full), "%s/%s", h->path, h->entries[eidx]) >= (int)sizeof(full)) {
drop_send_error(w, EIO, "drop data file path too long"); return true;
}
struct stat lst;
if (lstat(full, &lst) < 0) {
switch (errno) {
case ENOENT: case ENOTDIR: case ELOOP: drop_send_error(w, ENOENT, "drop data entry does not exist"); break;
case EACCES: case EPERM: drop_send_error(w, EPERM, "parmission denied while trying to stat() drop data entry"); break;
default: drop_send_error(w, EIO, "stt() failed on drop data entry"); break;
}
return true;
}
if (S_ISLNK(lst.st_mode)) {
/* Symlink: send the symlink target as t=r:X=1 */
char target[PATH_MAX];
ssize_t tlen = readlink(full, target, sizeof(target) - 1);
if (tlen < 0) {
switch (errno) {
case ENOENT: case ENOTDIR: drop_send_error(w, ENOENT, "readlink() failed on drop data entry"); break;
case EACCES: case EPERM: drop_send_error(w, EPERM, "readlink() failed on drop data entry"); break;
default: drop_send_error(w, EIO, "readlink() failed on drop data entry"); break;
}
return true;
}
target[tlen] = '\0';
char hdr[128];
int hdr_sz = snprintf(hdr, sizeof(hdr), "\x1b]%d;t=r", DND_CODE);
hdr_sz += drop_append_request_keys(w, hdr + hdr_sz, sizeof(hdr) - hdr_sz);
hdr_sz += snprintf(hdr + hdr_sz, sizeof(hdr) - hdr_sz, ":X=1");
queue_payload_to_child(w->id, w->drop.client_id, &w->drop.pending, hdr, hdr_sz, target, (size_t)tlen, true);
queue_payload_to_child(w->id, w->drop.client_id, &w->drop.pending, hdr, hdr_sz, NULL, 0, true);
return true;
}
if (S_ISDIR(lst.st_mode)) {
drop_send_dir_listing(w, full);
return true;
} else if (S_ISREG(lst.st_mode)) {
return drop_send_file_data(w, full);
} else {
drop_send_error(w, EINVAL, "drop data entry is neither a regular file nor a directory");
return true;
}
}
void
drop_handle_dir_request(Window *w, uint32_t handle_id, int32_t entry_num) {
do_drop_handle_dir_request(w, handle_id, entry_num);
}
/* --- Request queue management --- */
/* Pop the head of the queue (the request that just completed). */
static void
drop_pop_request(Window *w) {
if (w->drop.num_data_requests == 0) return;
w->drop.num_data_requests--;
if (w->drop.num_data_requests > 0) {
memmove(w->drop.data_requests, w->drop.data_requests + 1,
w->drop.num_data_requests * sizeof(w->drop.data_requests[0]));
}
w->drop.current_request_x = 0;
w->drop.current_request_y = 0;
w->drop.current_request_Y = 0;
}
static void
drop_finish_and_clear_queue(Window *w) {
drop_close_file_fd(w);
drop_free_request_queue(w);
drop_finish(w);
}
/* Process queued requests in FIFO order.
* Must be called after popping a completed request, or after enqueuing
* the first request into an empty queue. */
static void
drop_process_queue(Window *w) {
while (w->drop.num_data_requests > 0) {
int32_t x = w->drop.data_requests[0].cell_x;
int32_t y = w->drop.data_requests[0].cell_y;
int32_t Y = w->drop.data_requests[0].pixel_y;
w->drop.current_request_x = x;
w->drop.current_request_y = y;
w->drop.current_request_Y = Y;
bool sync = true;
if (Y != 0) {
/* Directory request: Y=handle, x=entry_num */
sync = do_drop_handle_dir_request(w, (uint32_t)Y, x);
} else if (y != 0) {
/* URI file request: x=mime_idx, y=file_idx */
sync = do_drop_request_uri_data(w, x, y);
} else if (x != 0) {
/* MIME data request: x=idx */
sync = do_drop_request_data(w, x);
} else {
/* Finish: x=0, y=0, Y=0 */
drop_pop_request(w);
drop_finish_and_clear_queue(w);
return;
}
if (sync) {
drop_pop_request(w);
/* Loop continues to process next request */
} else {
/* Async operation in progress; completion will call drop_process_queue */
return;
}
}
}
void
drop_enqueue_request(Window *w, int32_t cell_x, int32_t cell_y, int32_t pixel_y, uint32_t operation) {
if (cell_x == 0 && cell_y == 0 && pixel_y == 0) { // drop finished
w->drop.accepted_operation = drop_operation_to_enum(operation);
last_drop_finish_operation = w->drop.accepted_operation;
drop_finish_and_clear_queue(w);
reset_drop(w);
return;
}
if (w->drop.num_data_requests >= arraysz(w->drop.data_requests)) {
/* Queue full: deny with EMFILE and end the drop */
int32_t saved_x = w->drop.current_request_x;
int32_t saved_y = w->drop.current_request_y;
int32_t saved_Y = w->drop.current_request_Y;
w->drop.current_request_x = cell_x;
w->drop.current_request_y = cell_y;
w->drop.current_request_Y = pixel_y;
drop_send_error(w, EMFILE, "too many drop data requests");
w->drop.current_request_x = saved_x;
w->drop.current_request_y = saved_y;
w->drop.current_request_Y = saved_Y;
drop_finish_and_clear_queue(w);
return;
}
size_t idx = w->drop.num_data_requests;
w->drop.data_requests[idx].cell_x = cell_x;
w->drop.data_requests[idx].cell_y = cell_y;
w->drop.data_requests[idx].pixel_y = pixel_y;
bool was_empty = (w->drop.num_data_requests == 0);
w->drop.num_data_requests++;
if (was_empty) drop_process_queue(w);
}
void
drop_left_child(Window *w) {
w->drop.hovered = false;
w->drop.dropped = false;
drop_free_offered_mimes(w);
if (w->drop.wanted) {
char buf[128];
int header_size = snprintf(buf, sizeof(buf), "\x1b]%d;t=m:x=-1:y=-1", DND_CODE);
queue_payload_to_child(w->id, w->drop.client_id, &w->drop.pending, buf, header_size, NULL, 0, false);
}
}
// }}}
// Dragging {{{
#define ds w->drag_source
static void
drag_free_remote_item(DragRemoteItem *x) {
free(x->dir_entry_name);
free(x->data);
if (x->fd_plus_one) safe_close(x->fd_plus_one-1, __FILE__, __LINE__);
if (x->top_level_parent_dir_fd_plus_one) safe_close(x->top_level_parent_dir_fd_plus_one-1, __FILE__, __LINE__);
if (x->children) {
for (size_t i = 0; i < x->children_sz; i++) drag_free_remote_item(x->children + i);
free(x->children);
}
zero_at_ptr(x);
}
void
drag_free_offer(Window *w) {
free(ds.mimes_buf); ds.mimes_buf = NULL; ds.bufsz = 0;
if (ds.items) {
for (size_t i=0; i < ds.num_mimes; i++) {
free(ds.items[i].optional_data);
if (ds.items[i].fd_plus_one > 0) safe_close(ds.items[i].fd_plus_one - 1, __FILE__, __LINE__);
if (ds.items[i].uri_list) {
for (size_t k = 0; k < ds.items[i].num_uris; k++) free(ds.items[i].uri_list[k]);
free(ds.items[i].uri_list);
}
if (ds.items[i].remote_items) {
for (size_t k = 0; k < ds.items[i].num_remote_items; k++) drag_free_remote_item(&ds.items[i].remote_items[k]);
free(ds.items[i].remote_items); ds.items[i].remote_items = NULL;
ds.items[i].num_remote_items = 0;
}
if (ds.items[i].base_dir_fd_plus_one) {
rmtree_best_effort(".", ds.items[i].base_dir_fd_plus_one - 1);
ds.items[i].base_dir_fd_plus_one = 0;
}
free(ds.items[i].base_dir_for_remote_items); ds.items[i].base_dir_for_remote_items = NULL;
}
free(ds.items);
ds.items = NULL;
}
ds.num_mimes = 0;
ds.total_remote_data_size = 0;
for (size_t i = 0; i < arraysz(ds.images); i++) {
if (ds.images[i].data) free(ds.images[i].data);
zero_at_ptr(ds.images + i);
}
free_pending(&ds.pending);
ds.allowed_operations = 0;
ds.state = DRAG_SOURCE_NONE;
ds.pre_sent_total_sz = 0;
ds.images_sent_total_sz = 0;
}
static void
drag_send_error(Window *w, int error_code, const char *details) {
char buf[128], details_buf[1024];
int n;
if (details && details[0]) n = snprintf(details_buf, sizeof(details_buf), "%s:%s", get_errno_name(error_code), details);
else n = snprintf(details_buf, sizeof(details_buf), "%s", get_errno_name(error_code));
int header_size = snprintf(buf, sizeof(buf), "\x1b]%d;t=E", DND_CODE);
queue_payload_to_child(
w->id, w->drag_source.client_id, &w->drag_source.pending, buf, header_size, details_buf, n, false);
}
static void
cancel_drag(Window *w, int error_code, const char *details) {
if (error_code) drag_send_error(w, error_code, details);
if (global_state.drag_source.is_active && global_state.drag_source.from_window == w->id) cancel_current_drag_source();
drag_free_offer(w);
}
#define abrt(code, details) { cancel_drag(w, code, details); return; }
void
drag_start_offerring(Window *w, const char *client_machine_id, size_t sz) {
ds.can_offer = true;
ds.is_remote_client = !is_same_machine(client_machine_id, sz);
}
void
drag_stop_offerring(Window *w) {
drag_free_offer(w);
ds.can_offer = false; ds.is_remote_client = false;
}
void
drag_add_mimes(Window *w, int allowed_operations, uint32_t client_id, const char *data, size_t sz, bool has_more) {
if (!ds.can_offer) abrt(EINVAL, "cannot add drag source mimes as not offerring drag");
if (allowed_operations && !ds.allowed_operations) ds.allowed_operations = allowed_operations;
if (!ds.allowed_operations || ds.state > DRAG_SOURCE_BEING_BUILT) abrt(EINVAL, !ds.allowed_operations ? "cannot add drag source mimes as allowed operations are not set" : "cannot add drag source mimes as drag source is not being built");
ds.state = DRAG_SOURCE_BEING_BUILT;
ds.client_id = client_id;
size_t new_sz = ds.bufsz + sz;
if (new_sz > MIME_LIST_SIZE_CAP) abrt(EFBIG, "drag source mimes size too large");
char *tmp = realloc(ds.mimes_buf, ds.bufsz + sz + 1);
if (!tmp) abrt(ENOMEM, "out of mmeory adding drag source mimes");
ds.mimes_buf = tmp;
memcpy(ds.mimes_buf + ds.bufsz, data, sz);
ds.bufsz = new_sz;
ds.mimes_buf[ds.bufsz] = 0;
if (!has_more) {
char *ptr = ds.mimes_buf;
size_t rough_count = 0;
while ((ptr = strchr(ptr, ' ')) != NULL) {
*ptr = 0; ptr++;
rough_count++;
}
ds.items = calloc(rough_count + 2, sizeof(ds.items[0]));
if (!ds.items) abrt(ENOMEM, "out of mmeory adding drag source mimes");
char *p = ds.mimes_buf, *end = ds.mimes_buf + ds.bufsz;
ds.num_mimes = 0;
while (p < end) {
if (*p) {
if (ds.num_mimes >= rough_count + 1) break;
ds.items[ds.num_mimes].is_uri_list = strcmp(p, "text/uri-list") == 0;
ds.items[ds.num_mimes++].mime_type = p;
p += strlen(p) + 1;
} else p++;
}
ds.pre_sent_total_sz = 0;
}
}
void
drag_add_pre_sent_data(Window *w, unsigned idx, const uint8_t *payload, size_t sz) {
if (ds.state != DRAG_SOURCE_BEING_BUILT || idx >= ds.num_mimes) abrt(EINVAL, idx >= ds.num_mimes ?
"pre-sent data item idx too large" : "drag source not being currently built, cannot add pre-sent data");
if (sz + ds.pre_sent_total_sz > PRESENT_DATA_CAP) abrt(EFBIG, "too much pre-sent data");
ds.pre_sent_total_sz += sz;
#define item ds.items[idx]
if (!item.data_decode_initialized) {
item.data_decode_initialized = true;
base64_init_stream_decoder(&item.base64_state);
}
if (item.data_capacity < sz + item.data_size) {
size_t newcap = MAX(item.data_capacity * 2, sz + item.data_size);
uint8_t *tmp = realloc(item.optional_data, newcap);
if (!tmp) abrt(ENOMEM, "");
item.optional_data = tmp;
item.data_capacity = newcap;
}
size_t outlen = item.data_capacity - item.data_size;
if (!base64_decode_stream(&item.base64_state, payload, sz, item.optional_data + item.data_size, &outlen)) abrt(EINVAL, "error while decoding base64 pre-sent data");
item.data_size += outlen;
#undef item
}
#define img ds.images[idx]
void
drag_add_image(Window *w, unsigned idx, int fmt, int width, int height, int opacity, const uint8_t *payload, size_t sz) {
if (ds.state != DRAG_SOURCE_BEING_BUILT) abrt(EINVAL, "cannot add drag thumbnail as drag source not currently being built");
if (idx + 1 >= arraysz(ds.images)) abrt(EFBIG, "too many drag thumbnails");
if (ds.images_sent_total_sz + sz > PRESENT_DATA_CAP) abrt(EFBIG, "drag thumbnails too large");
ds.images_sent_total_sz += sz;
if (!img.started) {
if (fmt != 0 && fmt != 24 && fmt != 32 && fmt != 100) abrt(EINVAL, "unknown drag thumbnail format");
if (fmt != 0 && (width < 1 || height < 1)) abrt(EINVAL, "invalid drag thumbnail image dimensions");
img.started = true;
img.width = width; img.height = height;
img.fmt = fmt;
img.opacity = opacity;
base64_init_stream_decoder(&img.base64_state);
}
if (img.capacity < MAX(32u, sz + img.sz)) {
size_t newcap = MAX(img.capacity * 2, MAX(32u, sz + img.sz));
uint8_t *tmp = realloc(img.data, newcap);
if (!tmp) abrt(ENOMEM, "out of memory processing drag thumbnails");
img.data = tmp;
img.capacity = newcap;
}
size_t outlen = img.capacity - img.sz;
if (!base64_decode_stream(&img.base64_state, payload, sz, img.data + img.sz, &outlen)) abrt(EINVAL, "could not base64 decode drag thumbnail data");
img.sz += outlen;
}
void
drag_change_image(Window *w, unsigned idx) {
ds.img_idx = idx;
if (ds.state == DRAG_SOURCE_STARTED) change_drag_image(idx);
}
static bool
expand_rgb_data(Window *w, size_t idx) {
#define fail(code, details) { cancel_drag(w, code, details); return false; }
if (img.sz != (size_t)img.width * (size_t)img.height * 3) fail(EINVAL, "drag thumbnail RGB data not correct size");
const size_t sz = (size_t)img.width * (size_t)img.height * 4u;
RAII_ALLOC(uint8_t, expanded, malloc(sz));
if (!expanded) fail(ENOMEM, "out of memory processing drag thumbnail RGB data");
memset(expanded, 0xff, sz);
for (int r = 0; r < img.height; r++) {
uint8_t *src_row = img.data + r * img.width * 3, *dest_row = expanded + r * img.width * 4;
for (int c = 0; c < img.width; c++) memcpy(dest_row + c * 4, src_row + c * 3, 3);
}
SWAP(img.data, expanded); img.sz = sz; img.fmt = 32;
return true;
}
static bool
expand_png_data(Window *w, size_t idx) {
png_read_data d = {0};
inflate_png_inner(&d, img.data, img.sz, 2000);
if (d.ok) {
free(img.data);
img.data = d.decompressed;
img.sz = d.sz;
img.width = d.width; img.height = d.height;
} else free(d.decompressed);
free(d.row_pointers);
return d.ok;
}
#undef fail
static size_t last_total_image_size = 0;
static char**
parse_uri_list(Window *w, char *data, const ssize_t sz, size_t *num_uris_out) {
*num_uris_out = 0;
// First pass: count non-comment, non-empty lines
size_t count = 0;
char *p = data;
while (p - data < sz) {
char *eol = p + strcspn(p, "\r\n");
char saved = *eol; *eol = '\0';
char *end = eol;
while (end > p && (end[-1] == ' ' || end[-1] == '\t')) end--;
char saved_end = *end; *end = '\0';
if (*p && *p != '#') count++;
*end = saved_end;
*eol = saved;
if (saved == '\0') break;
p = eol + 1;
while (*p == '\r' || *p == '\n') p++;
}
char **result = calloc((count + 1), sizeof(const char*));
if (!result) { cancel_drag(w, ENOMEM, "out of memory parsing uri list"); return NULL; }
// Second pass: fill in decoded URI strings
size_t idx = 0;
p = data;
while (p - data < sz && idx < count) {
char *eol = p + strcspn(p, "\r\n");
char saved = *eol; *eol = '\0';
char *end = eol;
while (end > p && (end[-1] == ' ' || end[-1] == '\t')) end--;
*end = '\0';
if (*p && *p != '#') {
char *decoded = strdup(p);
if (!decoded) {
for (size_t k = 0; k < idx; k++) free((char*)result[k]);
free(result); cancel_drag(w, ENOMEM, "out of memory parsing uri list"); return NULL;
}
result[idx++] = decoded;
}
*eol = saved;
if (saved == '\0') break;
p = eol + 1;
while (*p == '\r' || *p == '\n') p++;
}
*num_uris_out = idx;
return result;
}
void
drag_start(Window *w) {
if (ds.state != DRAG_SOURCE_BEING_BUILT) abrt(EINVAL, "cannot start drag as drag source is not being built");
size_t total_size = 0;
for (size_t idx = 0; idx < arraysz(ds.images); idx++) {
if (img.sz) {
switch (img.fmt) {
case 24:
if (!expand_rgb_data(w, idx)) return;
break;
case 100:
if (!expand_png_data(w, idx)) return;
break;
case 0: {
// Text format: render using draw_window_title
OSWindow *osw = os_window_for_kitty_window(w->id);
Screen *screen = w->render_data.screen;
if (!osw || !osw->fonts_data || !screen) break; // no fonts available, skip
int X = img.width > 0 ? img.width : 1;
int Y = img.height > 0 ? img.height : 1;
double adjusted_font_sz = osw->fonts_data->font_sz_in_pts * (double)X / (double)Y;
double ydpi = osw->fonts_data->logical_dpi_y;
double px_sz_d = adjusted_font_sz * ydpi / 72.0;
if (px_sz_d < 1.0) px_sz_d = 1.0;
size_t render_height = (size_t)(px_sz_d * 4.0 / 3.0 + 0.5);
if (render_height < 1) render_height = 1;
ColorProfile *cp = screen->color_profile;
const color_type fg_color = colorprofile_to_color(
cp, cp->overridden.default_fg, cp->configured.default_fg).rgb | 0xff000000;
static const unsigned DRAG_OPACITY_MAX = 1024u;
uint8_t bg_alpha = (uint8_t)(((uint32_t)img.opacity * 255u + DRAG_OPACITY_MAX / 2) / DRAG_OPACITY_MAX);
color_type bg_color = colorprofile_to_color(
cp, cp->overridden.default_bg, cp->configured.default_bg).rgb | (((uint32_t)bg_alpha) << 24);
// Add a null terminator for draw_window_title
uint8_t *txt = realloc(img.data, img.sz + 1);
if (!txt) { abrt(ENOMEM, "out of memory processing text based drag thumbnail"); return; }
img.data = txt; txt[img.sz] = '\0';
const size_t max_width = (size_t)screen->cell_size.width * screen->columns;
uint8_t *render_buf = malloc(max_width * render_height * 4);
if (!render_buf) { abrt(ENOMEM, "out of memory processing text based drag thumbnail"); return; }
size_t actual_width = max_width;
bool ok = draw_window_title(adjusted_font_sz, ydpi, (const char*)img.data,
fg_color, bg_color, render_buf,
max_width, render_height, &actual_width);
if (!ok || actual_width < 1) { free(render_buf); break; }
size_t final_sz = actual_width * render_height * 4;
// Un-premultiply alpha: draw_window_title output is pre-multiplied RGBA
for (size_t j = 0; j < actual_width * render_height; j++) {
uint8_t *px = render_buf + j * 4;
uint16_t a = px[3];
if (a > 0) {
px[0] = (uint8_t)(((uint16_t)px[0] * 255u + a / 2u) / a);
px[1] = (uint8_t)(((uint16_t)px[1] * 255u + a / 2u) / a);
px[2] = (uint8_t)(((uint16_t)px[2] * 255u + a / 2u) / a);
}
}
free(img.data);
img.data = render_buf;
img.sz = final_sz;
img.width = (int)actual_width;
img.height = (int)render_height;
img.fmt = 32;
} break;
}
total_size += img.sz;
if (total_size > 2 * PRESENT_DATA_CAP) abrt(EFBIG, "too large a drag thumbnail");
if (img.sz != (size_t)img.width * (size_t)img.height * 4u) abrt(EINVAL, "drag thumbnail size incorrect");
}
}
last_total_image_size = total_size;
int err = start_window_drag(w, dnd_is_test_mode());
if (err != 0) {
if (err == EPERM) abrt(err, "permission to start drag denied, this can happen if the user has already released the drag or if the mouse has moved out of the window");
abrt(err, "failed to start drag in OS");
} else {
// Free images and optional_data but keep the items array for later
// data requests from the drop target
for (size_t i = 0; i < ds.num_mimes; i++) {
if (ds.is_remote_client && ds.items[i].is_uri_list) {
if (ds.items[i].optional_data && ds.items[i].data_size) {
ds.items[i].uri_list = parse_uri_list(
w, (char*)ds.items[i].optional_data, ds.items[i].data_size, &ds.items[i].num_uris);
if (!ds.items[i].uri_list) return;
} else abrt(EINVAL, "remote client must pre-send text/uri-list data");
}
free(ds.items[i].optional_data);
ds.items[i].optional_data = NULL;
ds.items[i].data_size = 0;
ds.items[i].data_capacity = 0;
ds.items[i].data_decode_initialized = false;
}
for (size_t i = 0; i < arraysz(ds.images); i++) {
if (ds.images[i].data) free(ds.images[i].data);
zero_at_ptr(ds.images + i);
}
ds.state = DRAG_SOURCE_STARTED;
drag_send_error(w, 0, ""); // send OK
}
}
void
drag_notify(Window *w, DragNotifyType type) {
if (ds.state < DRAG_SOURCE_STARTED) return;
char buf[128];
size_t sz = snprintf(buf, sizeof(buf), "\x1b]%d;t=e:x=%d", DND_CODE, type + 1);
switch(type) {
case DRAG_NOTIFY_ACCEPTED:
if (global_state.drag_source.accepted_mime_type != NULL) {
for (size_t i = 0; i < ds.num_mimes; i++) {
if (strcmp(ds.items[i].mime_type, global_state.drag_source.accepted_mime_type) == 0) {
sz += snprintf(buf + sz, sizeof(buf) - sz, ":y=%zu", i); break;
}
}
} break;
case DRAG_NOTIFY_ACTION_CHANGED:
switch (global_state.drag_source.action) {
case GLFW_DRAG_OPERATION_MOVE:
sz += snprintf(buf + sz, sizeof(buf) - sz, ":o=2"); break;
default:
sz += snprintf(buf + sz, sizeof(buf) - sz, ":o=1"); break;
} break;
case DRAG_NOTIFY_DROPPED: ds.state = DRAG_SOURCE_DROPPED; break;
case DRAG_NOTIFY_FINISHED:
sz += snprintf(buf + sz, sizeof(buf) - sz, ":y=%d", global_state.drag_source.was_canceled ? 1 : 0); break;
}
queue_payload_to_child(w->id, w->drag_source.client_id, &w->drag_source.pending, buf, sz, NULL, 0, false);
if (type == DRAG_NOTIFY_FINISHED) drag_free_offer(w);
}
int
drag_free_data(Window *w, const char *mime_type, const char* data, size_t sz) {
(void)w; (void)mime_type; (void)sz;
free((void*)data);
return 0;
}
static bool
is_file_url(const char *url) {
return url != NULL && strlen(url) > sizeof("file://")-1 && memcmp(url, "file://", sizeof("file://")-1) == 0;
}
const char*
my_basename(const char *path) {
const char *base = strrchr(path, '/'); // Change to '\\' for Windows
return base ? base + 1 : path;
}
static bool
request_remote_files(Window *w, size_t i) {
#define mi ds.items[i]
char buf[128];
mi.remote_items = calloc(mi.num_uris, sizeof(mi.remote_items[0]));
if (!mi.remote_items) return false;
mi.num_remote_items = mi.num_uris;
for (size_t k = 0; k < mi.num_remote_items; k++) {
if (is_file_url(mi.uri_list[k])) {
int header_sz = snprintf(buf, sizeof(buf), "\x1b]%d;t=k:x=%zu", DND_CODE, k + 1);
queue_payload_to_child(
w->id, w->drag_source.client_id, &w->drag_source.pending, buf, header_sz, NULL, 0, false);
mi.remote_items[k].waiting_for_completion = true;
mi.remote_items[k].dir_entry_name = strdup(my_basename(mi.uri_list[k]));
}
}
return true;
#undef mi
}
const char*
drag_get_data(Window *w, const char *mime_type, size_t *sz, int *err_code) {
*err_code = ENOENT; *sz = 0;
if (!ds.items || ds.state < DRAG_SOURCE_STARTED) return NULL;
for (size_t i = 0; i < ds.num_mimes; i++) {
if (strcmp(ds.items[i].mime_type, mime_type) == 0) {
if (ds.items[i].fd_plus_one < 0) {
// Error was stored by drag_process_item_data
*err_code = -ds.items[i].fd_plus_one;
ds.items[i].fd_plus_one = 0;
return NULL;
}
if (ds.items[i].requested_remote_files) { // wait for remote files to be read
*err_code = EAGAIN;
return NULL;
}
if (ds.items[i].fd_plus_one > 0) {
// data_size = read position, data_capacity = bytes written to file
if (ds.items[i].data_capacity > ds.items[i].data_size) {
// Unread data available, use pread to read from read_pos
size_t available = ds.items[i].data_capacity - ds.items[i].data_size;
char *data = malloc(available);
if (!data) { *err_code = ENOMEM; return NULL; }
size_t total = 0;
while (total < available) {
ssize_t n = pread(ds.items[i].fd_plus_one - 1, data + total,
available - total,
(off_t)(ds.items[i].data_size + total));
if (n < 0) {
if (errno == EINTR) continue;
free(data);
*err_code = EIO;
return NULL;
}
if (n == 0) break;
total += (size_t)n;
}
ds.items[i].data_size += total;
*sz = total;
*err_code = 0;
return data;
}
// 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;
}
// Still receiving data from client, wait
*err_code = EAGAIN;
return NULL;
}
// No fd yet, request data from the client
if (!ds.items[i].data_requested_from_client) {
ds.items[i].data_requested_from_client = true;
ds.items[i].requested_remote_files = ds.is_remote_client && ds.items[i].is_uri_list;
if (ds.items[i].requested_remote_files) {
if (!request_remote_files(w, i)) { *err_code = ENOMEM; return NULL; }
} else {
char buf[128];
int header_sz = snprintf(buf, sizeof(buf),
"\x1b]%d;t=e:x=%d:y=%zu", DND_CODE, DRAG_NOTIFY_FINISHED + 2, i);
queue_payload_to_child(
w->id, w->drag_source.client_id, &w->drag_source.pending, buf, header_sz, NULL, 0, false);
}
}
*err_code = EAGAIN;
return NULL;
}
}
return NULL;
}
static int
parse_errno_name(const uint8_t *data, size_t sz) {
if (sz >= 6 && memcmp(data, "ENOENT", 6) == 0) return ENOENT;
if (sz >= 5 && memcmp(data, "EPERM", 5) == 0) return EPERM;
if (sz >= 6 && memcmp(data, "EINVAL", 6) == 0) return EINVAL;
if (sz >= 6 && memcmp(data, "ENOMEM", 6) == 0) return ENOMEM;
if (sz >= 5 && memcmp(data, "EFBIG", 5) == 0) return EFBIG;
if (sz >= 3 && memcmp(data, "EIO", 3) == 0) return EIO;
if (sz >= 6 && memcmp(data, "EMFILE", 6) == 0) return EMFILE;
return EIO;
}
static int
open_item_tmpfile(void) {
int fd = -1;
#ifdef O_TMPFILE
fd = safe_open("/tmp", O_TMPFILE | O_CLOEXEC | O_EXCL | O_RDWR, S_IRUSR | S_IWUSR);
#endif
if (fd < 0) {
char name[] = "/tmp/kitty-dnd-XXXXXXXXXXXX";
fd = safe_mkstemp(name);
if (fd >= 0) unlink(name);
}
return fd;
}
void
drag_process_item_data(Window *w, size_t idx, int has_more, const uint8_t *payload, size_t payload_sz) {
if ((ds.state < DRAG_SOURCE_STARTED) || idx >= ds.num_mimes || !ds.items) {
abrt(EINVAL, ds.state < DRAG_SOURCE_STARTED ? "cannot process drag source item data as drag has not been started" : "cannot process drag source item data as item index is out of bounds");
return;
}
if (has_more < 0) {
// Error from the client program
if (ds.items[idx].fd_plus_one > 0) {
safe_close(ds.items[idx].fd_plus_one - 1, __FILE__, __LINE__);
}
int err = parse_errno_name(payload, payload_sz);
ds.items[idx].fd_plus_one = -err;
ds.items[idx].data_decode_initialized = false;
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, "could not notify OS that drag source item data is available");
}
return;
}
// End of data: has_more == 0 and empty payload
if (has_more == 0 && payload_sz == 0) {
ds.items[idx].data_decode_initialized = false;
if (ds.items[idx].fd_plus_one > 0) {
if (!ds.items[idx].requested_remote_files) {
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, "could not notify OS that drag source item data is available");
}
}
}
return;
}
// Open temp file if not yet open
if (!ds.items[idx].fd_plus_one) {
int fd = open_item_tmpfile();
if (fd < 0) { cancel_drag(w, EIO, "failed to open temporary file to store drag source item data"); return; }
ds.items[idx].fd_plus_one = fd + 1;
ds.items[idx].data_decode_initialized = true;
ds.items[idx].data_size = 0; // read position for pread
ds.items[idx].data_capacity = 0; // bytes written to file
base64_init_stream_decoder(&ds.items[idx].base64_state);
}
// Decode and write payload data
if (payload_sz > 0) {
RAII_ALLOC(uint8_t, decoded, malloc(payload_sz));
if (!decoded) { cancel_drag(w, ENOMEM, "out of memory processing drag source item data"); return; }
size_t outlen = payload_sz;
if (!base64_decode_stream(&ds.items[idx].base64_state, payload, payload_sz, decoded, &outlen)) {
cancel_drag(w, EINVAL, "failed to base64 decode drag source item data");
return;
}
size_t written = 0;
while (written < outlen) {
ssize_t n = safe_write(ds.items[idx].fd_plus_one - 1, decoded + written, outlen - written);
if (n < 0) {
cancel_drag(w, EIO, "failed to write drag source item data to temp file");
return;
}
written += (size_t)n;
}
ds.items[idx].data_capacity += outlen;
// Notify as soon as any data is available
if (!ds.items[idx].requested_remote_files) {
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, "could not notify OS that drag source item data is available");
}
}
}
}
static int
write_all(int fd, const void *buf, size_t sz) {
size_t pos = 0; const char *p = buf;
while (pos < sz) {
ssize_t ret = safe_write(fd, p + pos, sz - pos);
if (ret < 0) return ret;
pos += ret;
}
return 0;
}
static void
finish_remote_data(Window *w, size_t item_idx) {
if (!ds.items[item_idx].fd_plus_one) {
int fd = open_item_tmpfile();
if (fd < 0) abrt(EIO, "failed to open temp file to store modified uri list");
ds.items[item_idx].fd_plus_one = fd + 1;
}
const int fd = ds.items[item_idx].fd_plus_one - 1;
ds.items[item_idx].requested_remote_files = false;
if (safe_ftruncate(fd, 0) != 0) abrt(errno, "error updating uri list after all remote data received");
if (lseek(fd, 0, SEEK_SET) == -1) abrt(errno, "error updating uri list after all remote data received");
size_t new_size = 0;
for (size_t i = 0; i < ds.items[item_idx].num_uris; i++) {
int ret = write_all(fd, ds.items[item_idx].uri_list[i], strlen(ds.items[item_idx].uri_list[i]));
new_size += strlen(ds.items[item_idx].uri_list[i]);
free((char*)ds.items[item_idx].uri_list[i]); ds.items[item_idx].uri_list[i] = NULL;
if (ret) abrt(ret, "error updating uri list after all remote data received");
if ((ret = write_all(fd, "\r\n", 2))) abrt(ret, "error updating uri list after all remote data received");
new_size += 2;
}
free(ds.items[item_idx].uri_list); ds.items[item_idx].uri_list = NULL; ds.items[item_idx].num_uris = 0;
// The fd has been completely rewritten with updated (cached) URIs; update the read tracking
// fields so drag_get_data returns the full new content starting from the beginning.
ds.items[item_idx].data_capacity = new_size;
ds.items[item_idx].data_size = 0;
int ret = dnd_is_test_mode() ? 0 : notify_drag_data_ready(global_state.drag_source.from_os_window, ds.items[item_idx].mime_type);
if (ret) abrt(ret, "could not notify OS that drag source item data is available");
}
#define mi ds.items[mime_item_idx]
static char*
lowercase_copy(const char *s) {
size_t len = strlen(s);
char *ans = malloc(len + 1);
if (!ans) return NULL;
for (size_t i = 0; i < len; i++) ans[i] = (char)tolower((unsigned char)s[i]);
ans[len] = '\0';
return ans;
}
static void
uniqify_dir_entries_for_case_insensitive_fs(DragRemoteItem *children, size_t count) {
if (!count) return;
lower_str_set seen;
vt_init(&seen);
for (size_t i = 0; i < count; i++) {
if (!children[i].dir_entry_name) continue;
const char *orig_name = children[i].dir_entry_name;
char *lower = lowercase_copy(orig_name);
if (!lower) continue;
if (vt_is_end(vt_get(&seen, lower))) {
if (vt_is_end(vt_insert(&seen, lower))) { free(lower); continue; } // OOM, skip tracking this entry
continue;
}
free(lower);
bool renamed = false;
for (int q = 1; q <= 1000; q++) {
size_t buflen = 26 + strlen(orig_name); // "case-conflict-" + 10 digits + "-" + name + null
char *new_name = malloc(buflen);
if (!new_name) break;
snprintf(new_name, buflen, "case-conflict-%d-%s", q, orig_name);
lower = lowercase_copy(new_name);
if (!lower) { free(new_name); break; }
if (vt_is_end(vt_get(&seen, lower))) {
free(children[i].dir_entry_name);
children[i].dir_entry_name = new_name;
if (vt_is_end(vt_insert(&seen, lower))) { free(lower); break; } // OOM
renamed = true;
break;
}
free(lower); free(new_name);
}
if (!renamed) {
lower = lowercase_copy(children[i].dir_entry_name);
if (lower) {
if (vt_is_end(vt_insert(&seen, lower))) free(lower); // OOM
}
}
}
vt_cleanup(&seen);
}
static void
populate_dir_entries(Window *w, DragRemoteItem *ri) {
size_t num = count_occurrences((char*)ri->data, ri->data_sz, 0) + 1;
ri->children = calloc(num + 1, sizeof(ri->children[0]));
if (!ri->children) abrt(ENOMEM, "out of memory processing drag source item directory entries");
ri->children_sz = 0;
const char *ptr = (char*)ri->data;
const char *end = (char*)ri->data + ri->data_sz;
while (ptr < end) {
const char *p = memchr(ptr, 0, (size_t)(end - ptr));
size_t len = p ? (size_t)(p - ptr) : (size_t)(end - ptr);
DragRemoteItem *child = ri->children + ri->children_sz++;
child->parent = ri;
if (len > 0) {
char *name = strndup(ptr, len);
if (!name) abrt(ENOMEM, "out of memory processing drag source item directory entries");
child->dir_entry_name = name;
}
ptr = p ? p + 1 : end;
}
if (tempdir_case_insensitive == 1)
uniqify_dir_entries_for_case_insensitive_fs(ri->children, ri->children_sz);
}
static void
add_payload(Window *w, DragRemoteItem *ri, bool has_more, const uint8_t *payload, size_t payload_sz, int dirfd) {
if (payload_sz && payload) {
if (payload_sz > 4096) abrt(EINVAL, "drag source item data chunk too large");
switch (ri->type) {
case 0: {
if (!ri->fd_plus_one) {
int fd = safe_openat(dirfd, ri->dir_entry_name, O_CREAT | O_WRONLY, file_permissions);
if (fd < 0) abrt(errno, "could not open drag source item data file");
ri->fd_plus_one = fd + 1;
}
uint8_t buf[4096];
size_t outlen = sizeof(buf);
if (!base64_decode_stream(&ri->base64_state, payload, payload_sz, buf, &outlen)) abrt(EINVAL, "could not base64 decode drag source item data");
ds.total_remote_data_size += outlen;
if (outlen && write_all(ri->fd_plus_one-1, buf, outlen) < 0) abrt(errno, "could not write drag source item data to file");
} break;
default: {
if (ri->data_sz + payload_sz > ri->data_capacity) {
size_t cap = MAX(ri->data_capacity * 2, ri->data_sz + payload_sz + 4096);
if (cap > PRESENT_DATA_CAP) abrt(EMFILE, "too much drag source item data");
uint8_t *tmp = realloc(ri->data, cap);
if (!tmp) abrt(ENOMEM, "out of memory processing drag source item data");
ri->data = tmp;
ri->data_capacity = cap;
}
size_t outlen = ri->data_capacity - ri->data_sz;
if (!base64_decode_stream(&ri->base64_state, payload, payload_sz, ri->data + ri->data_sz, &outlen)) abrt(EINVAL, "could not base64 decode drag source item data");
ds.total_remote_data_size += outlen;
ri->data_sz += outlen;
} break;
}
}
if (ds.total_remote_data_size > REMOTE_DRAG_LIMIT) abrt(EMFILE, "too much drag source item data");
if (!has_more && !payload_sz) { // all data received
switch (ri->type) {
case 0:
safe_close(ri->fd_plus_one-1, __FILE__, __LINE__);
ri->fd_plus_one = 0;
break;
case 1:
// Ensure room for the null terminator needed by symlinkat
if (ri->data_sz >= ri->data_capacity) {
uint8_t *tmp = realloc(ri->data, ri->data_sz + 1);
if (!tmp) abrt(ENOMEM, "out of memory processingdrag source symlink item");
ri->data = tmp;
ri->data_capacity = ri->data_sz + 1;
}
ri->data[ri->data_sz] = 0;
if (symlinkat((char*)ri->data, dirfd, ri->dir_entry_name) != 0) abrt(errno, "failed to create symlink for drag source item");
break;
default:
if (mkdirat(dirfd, ri->dir_entry_name, dir_permissions) != 0 && errno != EEXIST) abrt(errno, "failed to create directory for drag source item");
populate_dir_entries(w, ri);
break;
}
free(ri->data); ri->data = 0; ri->data_capacity = 0; ri->data_sz = 0;
}
}
static void
toplevel_data_for_drag(
Window *w, unsigned mime_item_idx, unsigned uri_item_idx, unsigned item_type,
bool has_more, const uint8_t *payload, size_t payload_sz
) {
if (!mi.base_dir_for_remote_items) {
int fd;
mi.base_dir_for_remote_items = mktempdir_in_cache("dnd-drag-", &fd);
if (!mi.base_dir_for_remote_items) abrt(errno, "failed to create temporary directory for drag source items");
mi.base_dir_fd_plus_one = fd + 1;
detect_tempdir_case_sensitivity(mi.base_dir_for_remote_items);
}
if (uri_item_idx >= mi.num_remote_items) abrt(EINVAL, "out of bounds uri list item index for drag source");
DragRemoteItem *ri = mi.remote_items + uri_item_idx;
if (!ri->started) {
ri->started = true;
ri->type = item_type;
base64_init_stream_decoder(&ri->base64_state);
if (uri_item_idx >= mi.num_uris) abrt(EINVAL, "out of bounds uri list item index for drag source");
const char *uri = mi.uri_list[uri_item_idx];
char *fname = sanitized_filename_from_url(uri);
if (!fname) abrt(EINVAL, "could not sanitize filename for URI in drag source uri-list");
ri->dir_entry_name = fname;
char path[32];
snprintf(path, sizeof(path), "%u", uri_item_idx);
if (mkdirat(mi.base_dir_fd_plus_one - 1, path, dir_permissions) != 0 && errno != EEXIST) abrt(errno, "failed to create directory for drag source item");
int fd = safe_openat(mi.base_dir_fd_plus_one - 1, path, O_RDONLY | O_DIRECTORY, 0);
if (fd < 0) abrt(errno, "failed to create directory for drag source item");
ri->top_level_parent_dir_fd_plus_one = fd + 1;
free(mi.uri_list[uri_item_idx]);
mi.uri_list[uri_item_idx] = as_file_url(mi.base_dir_for_remote_items, path, ri->dir_entry_name);
}
add_payload(w, ri, has_more, payload, payload_sz, ri->top_level_parent_dir_fd_plus_one - 1);
}
static DragRemoteItem*
find_by_handle(DragRemoteItem *parent, int handle, char *path_to_parent, size_t *path_len) {
if (parent->type == handle) return parent;
DragRemoteItem *x;
for (size_t i = 0; i < parent->children_sz; i++) {
DragRemoteItem *child = parent->children + i;
size_t before = *path_len;
size_t n = snprintf(path_to_parent + before, PATH_MAX - before, "/%s", child->dir_entry_name);
if (n + before + 1 >= PATH_MAX) return NULL;
*path_len += n;
if ((x = find_by_handle(parent->children + i, handle, path_to_parent, path_len))) return x;
*path_len = before;
}
return NULL;
}
static void
subdir_data_for_drag(
Window *w, unsigned mime_item_idx, unsigned uri_item_idx, int handle, unsigned entry_num, unsigned item_type,
bool has_more, const uint8_t *payload, size_t payload_sz, DragRemoteItem **ri
) {
if (!mi.remote_items || uri_item_idx >= mi.num_remote_items) abrt(EINVAL, "drag source sub directory item uri list index out of range");
DragRemoteItem *parent = NULL; *ri = NULL;
if (mi.currently_open_subdir) {
if (mi.currently_open_subdir->type == handle) parent = mi.currently_open_subdir;
else {
if (mi.currently_open_subdir->fd_plus_one) {
safe_close(mi.currently_open_subdir->fd_plus_one - 1, __FILE__, __LINE__);
mi.currently_open_subdir->fd_plus_one = 0;
}
mi.currently_open_subdir = NULL;
}
}
if (parent == NULL || !parent->fd_plus_one) {
char path[PATH_MAX+1]; path[PATH_MAX] = 0;
DragRemoteItem *root = mi.remote_items + uri_item_idx;
if (!root->dir_entry_name) abrt(EINVAL, "drag source sub directory parent dir does not exist");
size_t pos = snprintf(path, PATH_MAX, "%s/%u/%s",
mi.base_dir_for_remote_items, uri_item_idx, root->dir_entry_name);
parent = find_by_handle(root, handle, path, &pos);
if (!parent) abrt(EINVAL, "drag source sub directory parent dir handle does not exist");
mi.currently_open_subdir = parent;
if (!parent->fd_plus_one) {
int fd = safe_open(path, O_DIRECTORY | O_RDONLY, 0);
if (fd < 0) abrt(errno, "drag source failed to create sub directory");
parent->fd_plus_one = fd + 1;
}
}
if (entry_num >= parent->children_sz) abrt(EINVAL, "drag source sub diretory index out of bounds");
*ri = parent->children + entry_num;
if (!(*ri)->started) {
(*ri)->started = true;
(*ri)->type = item_type;
base64_init_stream_decoder(&(*ri)->base64_state);
}
add_payload(w, *ri, has_more, payload, payload_sz, parent->fd_plus_one - 1);
}
void
drag_offer_start_to_child(Window *w, int32_t cell_x, int32_t cell_y, int32_t pixel_x, int32_t pixel_y) {
char buf[256];
int header_size = snprintf(
buf, sizeof(buf), "\x1b]%d;t=o:x=%d:y=%d:X=%d:Y=%d", DND_CODE, cell_x, cell_y, pixel_x, pixel_y);
queue_payload_to_child(
w->id, w->drag_source.client_id, &w->drag_source.pending, buf, header_size, NULL, 0, false);
}
static void
finish_remote_data_if_all_items_received(Window *w, unsigned mime_item_idx) {
for (size_t i = 0; i < mi.num_remote_items; i++) {
if (mi.remote_items[i].waiting_for_completion && !mi.remote_items[i].completed) {
return;
}
}
finish_remote_data(w, mime_item_idx);
}
static bool
all_children_complete(DragRemoteItem *parent) {
for (size_t i = 0; i < parent->children_sz; i++) {
if (!parent->children[i].completed) return false;
}
return true;
}
void
drag_remote_file_data(
Window *w, int32_t x, int32_t y, int32_t X, int32_t Y, bool has_more, const uint8_t *payload, size_t payload_sz
) {
size_t mime_item_idx = ds.num_mimes;
for (size_t i = 0; i < ds.num_mimes; i++) {
if (ds.items[i].requested_remote_files) {
mime_item_idx = i; break;
}
}
if (mime_item_idx == ds.num_mimes) abrt(EINVAL, "drag source no text/uri-list MIME entry data was requested");
if (x < 1) abrt(EINVAL, "drag source remote item x index cannot be less than 1");
const bool all_data_received = !payload_sz && !has_more;
const unsigned uri_item_idx = x - 1;
if (uri_item_idx >= mi.num_remote_items) abrt(EINVAL, "drag source uri list index out of bounds");
DragRemoteItem *ri;
if (!Y) {
toplevel_data_for_drag(w, mime_item_idx, uri_item_idx, X, has_more, payload, payload_sz);
if (all_data_received) {
ri = mi.remote_items + uri_item_idx;
if (ri->waiting_for_completion && all_children_complete(ri)) {
ri->completed = true;
finish_remote_data_if_all_items_received(w, mime_item_idx);
}
}
} else {
if (y < 1) abrt(EINVAL, "drag source remote item y index cannot be less than 1");
subdir_data_for_drag(w, mime_item_idx, x - 1, Y, y - 1, X, has_more, payload, payload_sz, &ri);
if (all_data_received && ri && all_children_complete(ri)) {
ri->completed = true;
while (1) {
ri = ri->parent;
if (ri) {
if (all_children_complete(ri)) ri->completed = true;
else break;
} else {
finish_remote_data_if_all_items_received(w, mime_item_idx);
break;
}
}
}
}
}
#undef img
#undef abrt
#undef ds
#undef mi
// }}}
// DnD testing infrastructure {{{
static PyObject *
py_dnd_set_test_write_func(PyObject *self UNUSED, PyObject *args) {
PyObject *func = Py_None; unsigned mime_list_size_cap = 0, present_data_cap = 0, remote_drag_limit = 0;
if (!PyArg_ParseTuple(args, "|OIII", &func, &mime_list_size_cap, &present_data_cap, &remote_drag_limit)) return NULL;
// Pass None to clear the interceptor and restore normal operation.
dnd_set_test_write_func(func == Py_None ? NULL : func, mime_list_size_cap, present_data_cap, remote_drag_limit);
Py_RETURN_NONE;
}
static void
destroy_fake_window_contents(Window *w) {
// Free window resources without touching GPU objects (none allocated for fake windows).
drop_free_data(w);
drag_free_offer(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);
Py_CLEAR(w->title_bar_data.last_drawn_title_object_id);
free(w->title_bar_data.buf); w->title_bar_data.buf = NULL;
Py_CLEAR(w->url_target_bar_data.last_drawn_title_object_id);
free(w->url_target_bar_data.buf); w->url_target_bar_data.buf = NULL;
// render_data.vao_idx is -1 so release_gpu_resources_for_window is safe, but we skip it
// since we never allocated those resources.
}
static PyObject *
dnd_test_create_fake_window(PyObject *self UNUSED, PyObject *args UNUSED) {
// Create a minimal OS window + tab + window without any OpenGL/GPU resources.
// Returns (os_window_id, window_id).
ensure_space_for(&global_state, os_windows, OSWindow, global_state.num_os_windows + 1, capacity, 1, true);
OSWindow *osw = global_state.os_windows + global_state.num_os_windows++;
zero_at_ptr(osw);
osw->id = ++global_state.os_window_id_counter;
osw->tab_bar_render_data.vao_idx = -1;
osw->background_opacity.alpha = OPT(background_opacity);
osw->created_at = monotonic();
// osw->handle intentionally left NULL - no real GLFW window
ensure_space_for(osw, tabs, Tab, 1, capacity, 1, true);
Tab *tab = &osw->tabs[0];
zero_at_ptr(tab);
tab->id = ++global_state.tab_id_counter;
tab->border_rects.vao_idx = -1;
osw->num_tabs = 1;
osw->active_tab = 0;
ensure_space_for(tab, windows, Window, 1, capacity, 1, true);
Window *w = &tab->windows[0];
zero_at_ptr(w);
w->id = ++global_state.window_id_counter;
w->visible = true;
w->render_data.vao_idx = -1;
w->window_title_render_data.vao_idx = -1;
w->drop.wanted = true;
tab->num_windows = 1;
tab->active_window = 0;
global_state.mouse_hover_in_window = w->id;
return Py_BuildValue("KK", (unsigned long long)osw->id, (unsigned long long)w->id);
}
static PyObject *
dnd_test_cleanup_fake_window(PyObject *self UNUSED, PyObject *args) {
unsigned long long os_window_id;
if (!PyArg_ParseTuple(args, "K", &os_window_id)) return NULL;
for (size_t i = 0; i < global_state.num_os_windows; i++) {
if (global_state.os_windows[i].id == (id_type)os_window_id) {
OSWindow *osw = global_state.os_windows + i;
for (size_t t = 0; t < osw->num_tabs; t++) {
Tab *tab = osw->tabs + t;
for (size_t j = 0; j < tab->num_windows; j++) {
Window *win = tab->windows + j;
if (global_state.mouse_hover_in_window == win->id)
global_state.mouse_hover_in_window = 0;
destroy_fake_window_contents(win);
}
free(tab->border_rects.rect_buf); tab->border_rects.rect_buf = NULL;
free(tab->windows); tab->windows = NULL;
}
Py_CLEAR(osw->window_title);
Py_CLEAR(osw->tab_bar_render_data.screen);
free(osw->tabs); osw->tabs = NULL;
remove_i_from_array(global_state.os_windows, i, global_state.num_os_windows);
break;
}
}
Py_RETURN_NONE;
}
static PyObject *
dnd_test_set_mouse_pos(PyObject *self UNUSED, PyObject *args) {
unsigned long long window_id;
int cell_x, cell_y, pixel_x, pixel_y;
if (!PyArg_ParseTuple(args, "Kiiii", &window_id, &cell_x, &cell_y, &pixel_x, &pixel_y)) return NULL;
Window *w = window_for_window_id((id_type)window_id);
if (!w) { PyErr_SetString(PyExc_ValueError, "Window not found"); return NULL; }
w->mouse_pos.cell_x = (unsigned int)cell_x;
w->mouse_pos.cell_y = (unsigned int)cell_y;
w->mouse_pos.global_x = pixel_x;
w->mouse_pos.global_y = pixel_y;
Py_RETURN_NONE;
}
static PyObject *
dnd_test_fake_drop_event(PyObject *self UNUSED, PyObject *args) {
// Simulate a drop enter/move/drop event. mimes_seq must be a sequence of str, or
// None to simulate a leave event.
unsigned long long window_id;
int is_drop;
PyObject *mimes_seq = Py_None;
int x = -2, y = -2;
if (!PyArg_ParseTuple(args, "Kp|Oii", &window_id, &is_drop, &mimes_seq, &x, &y)) return NULL;
Window *w = window_for_window_id((id_type)window_id);
if (!w) { PyErr_SetString(PyExc_ValueError, "Window not found"); return NULL; }
if (mimes_seq == Py_None) {
drop_left_child(w);
Py_RETURN_NONE;
}
RAII_PyObject(fast_seq, PySequence_Fast(mimes_seq, "mimes must be a sequence"));
if (!fast_seq) return NULL;
Py_ssize_t num_mimes = PySequence_Fast_GET_SIZE(fast_seq);
RAII_ALLOC(const char*, mimes, malloc(sizeof(const char*) * (num_mimes ? num_mimes : 1)));
if (!mimes) return PyErr_NoMemory();
for (Py_ssize_t i = 0; i < num_mimes; i++) {
mimes[i] = PyUnicode_AsUTF8(PySequence_Fast_GET_ITEM(fast_seq, i));
if (!mimes[i]) return NULL;
}
if (x > -1) w->mouse_pos.cell_x = x;
if (y > -1) w->mouse_pos.cell_y = y;
drop_move_on_child(w, mimes, (size_t)num_mimes, is_drop ? true : false);
Py_RETURN_NONE;
}
static PyObject *
dnd_test_fake_drop_data(PyObject *self UNUSED, PyObject *args) {
// Simulate OS delivering drop data for the given MIME type.
// If error_code > 0, simulate an error (e.g. ENOENT=2, EIO=5, EPERM=1).
// Otherwise deliver data and the mandatory end-of-data signal.
unsigned long long window_id;
const char *mime;
RAII_PY_BUFFER(data);
int error_code = 0, no_eod = 0;
if (!PyArg_ParseTuple(args, "Ksy*|ip", &window_id, &mime, &data, &error_code, &no_eod)) return NULL;
Window *w = window_for_window_id((id_type)window_id);
if (!w) { PyErr_SetString(PyExc_ValueError, "Window not found"); return NULL; }
if (error_code > 0) {
drop_dispatch_data(w, mime, NULL, -(ssize_t)error_code);
} else if (data.len > 0) {
drop_dispatch_data(w, mime, (const char*)data.buf, (ssize_t)data.len);
if (!no_eod) drop_dispatch_data(w, mime, NULL, 0); // mandatory end-of-data signal
} else {
// Empty data: just the end-of-data signal (sz=0 is the sentinel for "no more data").
drop_dispatch_data(w, mime, NULL, 0);
}
Py_RETURN_NONE;
}
static PyObject *
dnd_test_force_drag_dropped(PyObject *self UNUSED, PyObject *args) {
// Force the drag source state to DROPPED for testing purposes.
// This simulates what would happen after start_window_drag() succeeds
// and the drop target receives the data.
unsigned long long window_id;
if (!PyArg_ParseTuple(args, "K", &window_id)) return NULL;
Window *w = window_for_window_id((id_type)window_id);
if (!w) { PyErr_SetString(PyExc_ValueError, "Window not found"); return NULL; }
// Simulate what drag_start does on success, without calling start_window_drag
#define ds w->drag_source
for (size_t i = 0; i < w->drag_source.num_mimes; i++) {
if (ds.is_remote_client && ds.items[i].is_uri_list) {
if (ds.items[i].optional_data && ds.items[i].data_size) {
ds.items[i].uri_list = parse_uri_list(
w, (char*)ds.items[i].optional_data, ds.items[i].data_size, &ds.items[i].num_uris);
}
}
free(ds.items[i].optional_data);
ds.items[i].optional_data = NULL;
ds.items[i].data_size = 0;
ds.items[i].data_capacity = 0;
ds.items[i].data_decode_initialized = false;
}
for (size_t i = 0; i < arraysz(w->drag_source.images); i++) {
if (ds.images[i].data) free(w->drag_source.images[i].data);
zero_at_ptr(ds.images + i);
}
ds.state = DRAG_SOURCE_DROPPED;
#undef ds
Py_RETURN_NONE;
}
static PyObject *
dnd_test_request_drag_data(PyObject *self UNUSED, PyObject *args) {
// Simulate what drag_get_data does initially: find the MIME item at the
// given index, set requested_remote_files if appropriate.
unsigned long long window_id;
unsigned idx;
if (!PyArg_ParseTuple(args, "KI", &window_id, &idx)) return NULL;
Window *w = window_for_window_id((id_type)window_id);
if (!w) { PyErr_SetString(PyExc_ValueError, "Window not found"); return NULL; }
if (w->drag_source.state < DRAG_SOURCE_DROPPED || idx >= w->drag_source.num_mimes || !w->drag_source.items) {
PyErr_SetString(PyExc_ValueError, "Invalid state or index"); return NULL;
}
w->drag_source.items[idx].requested_remote_files = w->drag_source.is_remote_client && w->drag_source.items[idx].is_uri_list;
if (w->drag_source.items[idx].requested_remote_files) request_remote_files(w, idx);
Py_RETURN_NONE;
}
static PyObject *
dnd_test_drag_finish(PyObject *self UNUSED, PyObject *args) {
unsigned long long window_id; int canceled_by_user; int errcode = 0;
if (!PyArg_ParseTuple(args, "Kp|i", &window_id, &canceled_by_user, &errcode)) return NULL;
Window *w = window_for_window_id((id_type)window_id);
if (!w) { PyErr_SetString(PyExc_ValueError, "Window not found"); return NULL; }
global_state.drag_source.was_canceled = canceled_by_user;
if (!errcode) drag_notify(w, DRAG_NOTIFY_FINISHED);
cancel_drag(w, errcode, "");
Py_RETURN_NONE;
}
static PyObject *
dnd_test_drag_notify(PyObject *self UNUSED, PyObject *args) {
// Call drag_notify with a specific type for testing the protocol output.
// type: 0=ACCEPTED, 1=ACTION_CHANGED, 2=DROPPED, 3=FINISHED
// accepted_mime: the MIME type to set in global_state.drag_source.accepted_mime_type (for ACCEPTED)
// action: the action to set in global_state.drag_source.action (for ACTION_CHANGED)
// was_canceled: whether the drag was canceled (for FINISHED)
unsigned long long window_id;
int type;
const char *accepted_mime = NULL;
int action = 0, was_canceled = 0;
if (!PyArg_ParseTuple(args, "Ki|sip", &window_id, &type, &accepted_mime, &action, &was_canceled)) return NULL;
Window *w = window_for_window_id((id_type)window_id);
if (!w) { PyErr_SetString(PyExc_ValueError, "Window not found"); return NULL; }
if (type < 0 || type > 3) { PyErr_SetString(PyExc_ValueError, "Invalid type"); return NULL; }
if (accepted_mime && *accepted_mime) {
free(global_state.drag_source.accepted_mime_type);
global_state.drag_source.accepted_mime_type = strdup(accepted_mime);
if (!global_state.drag_source.accepted_mime_type) { PyErr_NoMemory(); return NULL; }
}
global_state.drag_source.action = action;
global_state.drag_source.was_canceled = was_canceled;
drag_notify(w, (DragNotifyType)type);
Py_RETURN_NONE;
}
static PyObject*
dnd_test_probe_state(PyObject *self UNUSED, PyObject *args) {
const char *q; unsigned long long window_id;
if (!PyArg_ParseTuple(args, "Ks", &window_id, &q)) return NULL;
Window *w = window_for_window_id((id_type)window_id);
if (!w) { PyErr_SetString(PyExc_ValueError, "Window not found"); return NULL; }
if (strcmp(q, "drop_wanted") == 0) {
return Py_NewRef(w->drop.wanted ? Py_True : Py_False);
}
if (strcmp(q, "drag_can_offer") == 0) {
return Py_NewRef(w->drag_source.can_offer ? Py_True : Py_False);
}
if (strcmp(q, "drop_is_remote_client") == 0) {
return Py_NewRef(w->drop.is_remote_client ? Py_True : Py_False);
}
if (strcmp(q, "drag_is_remote_client") == 0) {
return Py_NewRef(w->drag_source.is_remote_client ? Py_True : Py_False);
}
if (strcmp(q, "drop_action") == 0) {
return PyLong_FromLong((long)w->drop.accepted_operation);
}
if (strcmp(q, "last_drop_action") == 0) {
return PyLong_FromLong((long)last_drop_finish_operation);
}
if (strcmp(q, "drop_mimes") == 0) {
if (w->drop.accepted_mimes == NULL) return PyUnicode_FromString("");
return PyUnicode_FromStringAndSize(w->drop.accepted_mimes, w->drop.accepted_mimes_sz);
}
if (strcmp(q, "drop_data_requests") == 0) {
PyObject *ans = PyTuple_New(w->drop.num_data_requests);
for (size_t i = 0; i < w->drop.num_data_requests; i++) {
#define item w->drop.data_requests[i]
PyObject *x = Py_BuildValue("iii", item.cell_x, item.cell_y, item.pixel_y);
PyTuple_SET_ITEM(ans, i, x);
#undef item
}
return ans;
}
if (strcmp(q, "drop_getting_data_for_mime") == 0) {
return PyUnicode_FromString(w->drop.getting_data_for_mime ? w->drop.getting_data_for_mime : "");
}
if (strcmp(q, "can_offer") == 0) {
return Py_NewRef(w->drag_source.can_offer ? Py_True : Py_False);
}
if (strcmp(q, "drag_operations") == 0) {
return PyLong_FromLong((long)w->drag_source.allowed_operations);
}
if (strcmp(q, "drag_mimes") == 0) {
PyObject *ans = PyTuple_New(w->drag_source.num_mimes);
for (size_t i = 0; i < w->drag_source.num_mimes; i++) PyTuple_SET_ITEM(
ans, i, PyUnicode_FromString(w->drag_source.items[i].mime_type));
return ans;
}
if (strcmp(q, "drag_thumbnail_size") == 0) {
return PyLong_FromSize_t(last_total_image_size);
}
if (strcmp(q, "drag_remote_data_complete") == 0) {
for (size_t idx = 0; idx < w->drag_source.num_mimes; idx++) {
#define mi w->drag_source.items[idx]
if (mi.is_uri_list && mi.requested_remote_files) {
for (size_t i = 0; i < mi.num_remote_items; i++) {
if (mi.remote_items[i].waiting_for_completion && !mi.remote_items[i].completed)
return PyUnicode_FromString(mi.remote_items[i].dir_entry_name ? mi.remote_items[i].dir_entry_name : "");
}
}
#undef mi
}
Py_RETURN_NONE;
}
Py_RETURN_NONE;
}
static PyObject*
dnd_test_start_drag_offer(PyObject *self UNUSED, PyObject *args) {
unsigned long long window_id; int x=0, y=0, X=0, Y=0;
if (!PyArg_ParseTuple(args, "K|iiii", &window_id, &x, &y, &X, &Y)) return NULL;
Window *w = window_for_window_id((id_type)window_id);
if (!w) { PyErr_SetString(PyExc_ValueError, "Window not found"); return NULL; }
drag_offer_start_to_child(w, x, y, X, Y);
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),
METHODB(dnd_test_cleanup_fake_window, METH_VARARGS),
METHODB(dnd_test_set_mouse_pos, METH_VARARGS),
METHODB(dnd_test_fake_drop_event, METH_VARARGS),
METHODB(dnd_test_fake_drop_data, METH_VARARGS),
METHODB(dnd_test_start_drag_offer, METH_VARARGS),
METHODB(dnd_test_force_drag_dropped, METH_VARARGS),
METHODB(dnd_test_request_drag_data, METH_VARARGS),
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}
};
bool
init_dnd(PyObject *m) {
if (PyModule_AddFunctions(m, dnd_methods) != 0) return false;
return true;
}
// }}}