From 4f7855aededa4d95a21333ebb76b7d83fbf31ce8 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 1 Apr 2026 12:01:04 +0530 Subject: [PATCH] More work on the DnD protocol --- docs/dnd-protocol.rst | 25 ++++++++++++++++--------- gen/apc_parsers.py | 2 +- kitty/boss.py | 25 +++++++++++++++++++++++++ kitty/dnd.c | 9 ++++++++- kitty/dnd.h | 2 +- kitty/glfw.c | 2 +- kitty/parse-dnd-command.h | 2 +- kitty/state.h | 1 + 8 files changed, 54 insertions(+), 14 deletions(-) diff --git a/docs/dnd-protocol.rst b/docs/dnd-protocol.rst index ca3072fc8..9e3620b9e 100644 --- a/docs/dnd-protocol.rst +++ b/docs/dnd-protocol.rst @@ -128,10 +128,13 @@ send an escape code of the form:: That is, it must send a request for data with no MIME type specified. The terminal emulator must then inform the OS that the drop is completed. +Dropping from remote machines +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + In order to support dropping of files from remote machines or to remote -machines, clients can first request the text/uri-list MIME type to get a list -of dropped URIs. For every ``file://`` URI they can send the terminal emulator -a data request of the form:: +machines, clients can first request the :rfc:`text/uri-list <2483>` MIME +type to get a list of dropped URIs. For every URI in the list, they can +send the terminal emulator a data request of the form:: OSC _dnd_code ; t=s ; text/uri-list:idx ST @@ -142,18 +145,20 @@ transmit the data as for a normal MIME data request. Terminals must reply with ``t=R ; ENOENT`` if the index is out of bounds. If the client does not first request the ``text/uri-list`` MIME type or that MIME type is not present in the drop, the terminal must reply with -``t=R ; EINVAL``. Similarly if the client requests an entry that is not a -``file://`` URI the terminal must reply with ``EUNKNOWN``. +``t=R ; EINVAL``. Terminals must support at least ``file://`` URIs. +If the client requests an entry that is not a supported URI type the +terminal must reply with ``t=R ; EUNKNOWN``. Terminals must ONLY send data for regular files. Symbolic links must be resolved and the corresponding file read. If the terminal does not have permission to read the file it must reply with ``t=R ; EPERM``. Terminals must respond with ``t=R ; EINVAL`` if the file is not a regular file after -resolving symlinks and ``t=R ; ENOENT`` if the file does not exist. +resolving symlinks and ``t=R ; ENOENT`` if the file does not exist. If an +I/O error occurs the terminal must send ``t=R ; EIO``. -Dropping directories -^^^^^^^^^^^^^^^^^^^^^^^^^ +Reading remote directories ++++++++++++++++++++++++++++ If the file is actually a directory the terminal must respond with ``t=d:x=idx ; payload``. Here payload is a null byte separated list of entries in the directory that are @@ -162,7 +167,8 @@ encoded and might be chunked if the directory has a lot of entries. The first entry in the list must be a unique identifier for the directory, to prevent symlink loops. Terminals may use whatever identifier is most suitable for their platforms, clients should not re-request the contents of a directory whose identifier they have seen -before. +before. On POSIX platforms the identifier would typically be device and inode +number. ``idx`` is an arbitrary 32 bit integer that acts as a handle to this directory. The client can now read the files in this directory using requests of the form @@ -194,6 +200,7 @@ Key Value Default Description ``M`` - a drop dropped event ``r`` - request dropped data ``R`` - report an error while retrieving data + ``s`` - request data from the URI list entry ``d`` - send directory contents ``m`` Chunking indicator ``0`` ``0`` or ``i`` diff --git a/gen/apc_parsers.py b/gen/apc_parsers.py index 1ef9987f5..fb3861d01 100755 --- a/gen/apc_parsers.py +++ b/gen/apc_parsers.py @@ -331,7 +331,7 @@ def parsers() -> None: write_header(text, 'kitty/parse-multicell-command.h') keymap = { - 't': ('type', flag('aAmMrRd')), + 't': ('type', flag('aAmMrRsd')), 'm': ('more', 'uint'), 'i': ('client_id', 'uint'), 'o': ('operation', 'uint'), diff --git a/kitty/boss.py b/kitty/boss.py index 829832432..d72daf53d 100644 --- a/kitty/boss.py +++ b/kitty/boss.py @@ -3716,3 +3716,28 @@ class Boss: def copy_or_noop(self) -> None: if w := self.active_window: w.copy_or_noop() + + def nth_decoded_file_url(self, n: int, text_uri_list: bytes) -> tuple[bool, str]: + ' Return the abspath of the nth url in text_uri_list if it is a valid file:// URL. Appropriate error code returned otherwise ' + from urllib.parse import unquote, urlparse + for line in text_uri_list.decode().splitlines(): + if line.startswith('#'): + continue + if n <= 0: + break + n -= 1 + else: + return False, 'ENOENT' + try: + purl = urlparse(line.strip()) + if purl.scheme != 'file': + return False, 'EUNKNOWN' + if not purl.path: + return False, 'EINVAL' + path = unquote(purl.path) + if not os.path.isabs(path): + return False, 'EINVAL' + path = os.path.abspath(os.path.realpath(path)) + return True, path + except Exception: + return False, 'EINVAL' diff --git a/kitty/dnd.c b/kitty/dnd.c index 1850aa156..0d459db16 100644 --- a/kitty/dnd.c +++ b/kitty/dnd.c @@ -41,6 +41,7 @@ drop_free_data(Window *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; } @@ -330,12 +331,18 @@ drop_request_data(Window *w, const char *mime) { } void -drop_dispatch_data(Window *w, const char *data, ssize_t sz) { +drop_dispatch_data(Window *w, const char *mime, const char *data, ssize_t sz) { if (sz < 0) drop_send_error(w, -sz); else { char buf[128]; int header_size = snprintf(buf, sizeof(buf), "\x1b]%d;t=r", DND_CODE); queue_payload_to_child(w->id, w->drop.client_id, &w->drop.pending, buf, header_size, sz ? data : NULL, sz, true); + if (strcmp(mime, "text/uri-list") == 0) { + w->drop.uri_list_sz += sz; + w->drop.uri_list = realloc(w->drop.uri_list, w->drop.uri_list_sz); + if (w->drop.uri_list) memcpy(w->drop.uri_list + w->drop.uri_list_sz - sz, data, sz); + else w->drop.uri_list_sz = 0; + } } } diff --git a/kitty/dnd.h b/kitty/dnd.h index f7aa9f564..53355e0e1 100644 --- a/kitty/dnd.h +++ b/kitty/dnd.h @@ -15,5 +15,5 @@ void drop_free_data(Window *w); void drop_request_data(Window *w, const char *mime); void drop_set_status(Window *w, int operation, const char *payload, size_t payload_sz, bool more); size_t drop_update_mimes(Window *w, const char **allowed_mimes, size_t allowed_mimes_count); -void drop_dispatch_data(Window *w, const char *data, ssize_t sz); +void drop_dispatch_data(Window *w, const char *mime_type, const char *data, ssize_t sz); void drop_finish(Window *w); diff --git a/kitty/glfw.c b/kitty/glfw.c index c5462c359..9b3062e03 100644 --- a/kitty/glfw.c +++ b/kitty/glfw.c @@ -857,7 +857,7 @@ on_drop(GLFWwindow *window, GLFWDropEvent *ev) { if (w->drop.getting_data_for_mime && strcmp(w->drop.getting_data_for_mime, ev->mimes[0]) == 0) { char buf[3072]; ssize_t ret = ev->read_data(window, ev, buf, sizeof(buf)); - drop_dispatch_data(w, buf, ret); + drop_dispatch_data(w, ev->mimes[0], buf, ret); if (ret < 0) ev->finish_drop(window, GLFW_DRAG_OPERATION_GENERIC); } } diff --git a/kitty/parse-dnd-command.h b/kitty/parse-dnd-command.h index 32902667f..fa7a10946 100644 --- a/kitty/parse-dnd-command.h +++ b/kitty/parse-dnd-command.h @@ -87,7 +87,7 @@ static inline void parse_dnd_code(PS *self, uint8_t *parser_buf, case type: { g.type = parser_buf[pos++]; if (g.type != 'A' && g.type != 'M' && g.type != 'R' && g.type != 'a' && - g.type != 'm' && g.type != 'r') { + g.type != 'd' && g.type != 'm' && g.type != 'r' && g.type != 's') { REPORT_ERROR("Malformed DnDCommand control block, unknown flag value " "for type: 0x%x", g.type); diff --git a/kitty/state.h b/kitty/state.h index dfd2312db..3567ac65a 100644 --- a/kitty/state.h +++ b/kitty/state.h @@ -261,6 +261,7 @@ typedef struct Window { bool wanted, hovered, dropped; uint32_t client_id; char *registered_mimes; + char *uri_list; size_t uri_list_sz; PendingData pending; const char **offerred_mimes; size_t num_offerred_mimes, offered_mimes_total_size;