From af3b604d8e031f4d988a2fb6c9693e5f10533286 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 26 Oct 2020 21:02:24 +0100 Subject: [PATCH 1/7] wayland: bind to data-device-manager version 3, for drag-and-drop support --- wayland.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wayland.c b/wayland.c index 2c82896a..e8a57c6a 100644 --- a/wayland.c +++ b/wayland.c @@ -849,7 +849,7 @@ handle_global(void *data, struct wl_registry *registry, } else if (strcmp(interface, wl_data_device_manager_interface.name) == 0) { - const uint32_t required = 1; + const uint32_t required = 3; if (!verify_iface_version(interface, version, required)) return; From 8e23b5b70d8622784e4856e5f14493a8893eab8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 26 Oct 2020 21:02:53 +0100 Subject: [PATCH 2/7] selection: implement support for drag-and-drop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We accept COPY and MOVE actions, for text/plain;charset=utf-8 mime-types. To implement DnD, we need to track the current DnD data offer *and* the terminal instance it is (currently) targeting. To do this, a seat has a new member, ‘dnd_term’. On a DnD enter event, we lookup the corresponding terminal instance and point ‘dnd_term’ to it. On a DnD leave event, ‘dnd_term’ is reset. The DnD data offer is tracked in the terminal’s wayland window instance. It is reset, along with the seat’s ‘dnd_term’ on a DnD leave event. On a drop event, we immediately clear the seat’s ‘dnd_term’, to ensure we don’t reset it, or destroy the offer before the drop has been completed. The drop’s ‘done()’ callback takes care of destroying and resetting the DnD offer in the terminal’s wayland window instance. Closes #175 --- CHANGELOG.md | 2 ++ selection.c | 96 +++++++++++++++++++++++++++++++++++++++++++++++++--- wayland.h | 5 +++ 3 files changed, 98 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a0cb347f..fdb6d97d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,8 @@ `foot.ini`. These options allow custom bold/italic fonts. They are unset by default, meaning the bold/italic version of the regular font is used (https://codeberg.org/dnkl/foot/issues/169). +* Drag & drop support; text, files and URLs can now be dropped in a + foot terminal window (https://codeberg.org/dnkl/foot/issues/175). ### Changed diff --git a/selection.c b/selection.c index c76a1608..278a892b 100644 --- a/selection.c +++ b/selection.c @@ -898,7 +898,7 @@ selection_stop_scroll_timer(struct terminal *term) static void target(void *data, struct wl_data_source *wl_data_source, const char *mime_type) { - LOG_WARN("TARGET: mime-type=%s", mime_type); + LOG_DBG("TARGET: mime-type=%s", mime_type); } struct clipboard_send { @@ -1008,19 +1008,23 @@ cancelled(void *data, struct wl_data_source *wl_data_source) clipboard->text = NULL; } +/* We don’t support dragging *from* */ static void dnd_drop_performed(void *data, struct wl_data_source *wl_data_source) { + //LOG_DBG("DnD drop performed"); } static void dnd_finished(void *data, struct wl_data_source *wl_data_source) { + //LOG_DBG("DnD finished"); } static void action(void *data, struct wl_data_source *wl_data_source, uint32_t dnd_action) { + //LOG_DBG("DnD action: %u", dnd_action); } static const struct wl_data_source_listener data_source_listener = { @@ -1464,17 +1468,20 @@ selection_from_primary(struct seat *seat, struct terminal *term) static void offer(void *data, struct wl_data_offer *wl_data_offer, const char *mime_type) { + LOG_INFO("OFFER: %s", mime_type); } static void source_actions(void *data, struct wl_data_offer *wl_data_offer, uint32_t source_actions) { + LOG_INFO("ACTIONS: 0x%08x", source_actions); } static void offer_action(void *data, struct wl_data_offer *wl_data_offer, uint32_t dnd_action) { + LOG_INFO("OFFER ACTION: 0x%08x", dnd_action); } static const struct wl_data_offer_listener data_offer_listener = { @@ -1488,6 +1495,7 @@ static void data_offer(void *data, struct wl_data_device *wl_data_device, struct wl_data_offer *id) { + //wl_data_offer_add_listener(id, &data_offer_listener, data); } static void @@ -1495,11 +1503,43 @@ enter(void *data, struct wl_data_device *wl_data_device, uint32_t serial, struct wl_surface *surface, wl_fixed_t x, wl_fixed_t y, struct wl_data_offer *id) { + struct seat *seat = data; + struct wayland *wayl = seat->wayl; + + assert(seat->dnd_term == NULL); + + /* Remember current DnD offer */ + + /* Remmeber _which_ terminal the current DnD offer is targetting */ + tll_foreach(wayl->terms, it) { + if (term_surface_kind(it->item, surface) == TERM_SURF_GRID && + !it->item->is_sending_paste_data) + { + wl_data_offer_accept(id, serial, "text/plain;charset=utf-8"); + wl_data_offer_set_actions( + id, + WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY | WL_DATA_DEVICE_MANAGER_DND_ACTION_MOVE, + WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY); + + seat->dnd_term = it->item; + seat->dnd_term->window->dnd_offer = id; + return; + } + } + + /* Either terminal is alraedy busy sending paste data, or mouse + * pointer isn’t over the grid */ + seat->dnd_term = NULL; + wl_data_offer_set_actions(id, 0, WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY); } static void leave(void *data, struct wl_data_device *wl_data_device) { + struct seat *seat = data; + if (seat->dnd_term != NULL) + seat->dnd_term->window->dnd_offer = NULL; + seat->dnd_term = NULL; } static void @@ -1508,9 +1548,59 @@ motion(void *data, struct wl_data_device *wl_data_device, uint32_t time, { } +static void +dnd_done(void *user) +{ + struct terminal *term = user; + struct wl_data_offer *offer = term->window->dnd_offer; + + assert(offer != NULL); + term->window->dnd_offer = NULL; + + wl_data_offer_finish(offer); + wl_data_offer_destroy(offer); + + from_clipboard_done(user); +} + static void drop(void *data, struct wl_data_device *wl_data_device) { + struct seat *seat = data; + + struct terminal *term = seat->dnd_term; + assert(term != NULL); + + struct wl_data_offer *offer = term->window->dnd_offer; + assert(offer != NULL); + seat->dnd_term = NULL; + + /* Prepare a pipe the other client can write its selection to us */ + int fds[2]; + if (pipe2(fds, O_CLOEXEC) == -1) { + LOG_ERRNO("failed to create pipe"); + goto err; + } + + int read_fd = fds[0]; + int write_fd = fds[1]; + + /* Give write-end of pipe to other client */ + wl_data_offer_receive(offer, "text/plain;charset=utf-8", write_fd); + + /* Don't keep our copy of the write-end open (or we'll never get EOF) */ + close(write_fd); + + term->is_sending_paste_data = true; + + if (term->bracketed_paste) + term_paste_data_to_slave(term, "\033[200~", 6); + + begin_receive_clipboard(term, read_fd, &from_clipboard_cb, &dnd_done, term); + return; + +err: + wl_data_offer_destroy(offer); } static void @@ -1526,10 +1616,6 @@ selection(void *data, struct wl_data_device *wl_data_device, wl_data_offer_destroy(clipboard->data_offer); clipboard->data_offer = id; -#if 0 - if (id != NULL) - wl_data_offer_add_listener(id, &data_offer_listener, term); -#endif } const struct wl_data_device_listener data_device_listener = { diff --git a/wayland.h b/wayland.h index 70c2098a..bf3db76c 100644 --- a/wayland.h +++ b/wayland.h @@ -205,6 +205,9 @@ struct seat { struct wl_clipboard clipboard; struct wl_primary primary; + + /* Drag ‘n’ drop */ + struct terminal *dnd_term; }; enum csd_surface { @@ -309,6 +312,8 @@ struct wl_window { struct wl_callback *frame_callback; + struct wl_data_offer *dnd_offer; + tll(const struct monitor *) on_outputs; /* Outputs we're mapped on */ bool is_configured; From 9580c04dd3522c562d8260c01c2f3ea2c2e69268 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 26 Oct 2020 21:19:07 +0100 Subject: [PATCH 3/7] selection: debug log of data offer mime-types and actions --- selection.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/selection.c b/selection.c index 278a892b..4486e765 100644 --- a/selection.c +++ b/selection.c @@ -1464,24 +1464,24 @@ selection_from_primary(struct seat *seat, struct terminal *term) text_from_primary(seat, term, &from_clipboard_cb, &from_clipboard_done, term); } -#if 0 +#if 1 static void offer(void *data, struct wl_data_offer *wl_data_offer, const char *mime_type) { - LOG_INFO("OFFER: %s", mime_type); + LOG_DBG("OFFER: %s", mime_type); } static void source_actions(void *data, struct wl_data_offer *wl_data_offer, uint32_t source_actions) { - LOG_INFO("ACTIONS: 0x%08x", source_actions); + LOG_DBG("ACTIONS: 0x%08x", source_actions); } static void offer_action(void *data, struct wl_data_offer *wl_data_offer, uint32_t dnd_action) { - LOG_INFO("OFFER ACTION: 0x%08x", dnd_action); + LOG_DBG("OFFER ACTION: 0x%08x", dnd_action); } static const struct wl_data_offer_listener data_offer_listener = { @@ -1495,7 +1495,7 @@ static void data_offer(void *data, struct wl_data_device *wl_data_device, struct wl_data_offer *id) { - //wl_data_offer_add_listener(id, &data_offer_listener, data); + wl_data_offer_add_listener(id, &data_offer_listener, data); } static void From 608cc746ad60bc2aa8db22814fe887fece4ea89b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 28 Oct 2020 19:10:44 +0100 Subject: [PATCH 4/7] uri: add uri_parse() - new function extracts components from an URI --- meson.build | 1 + uri.c | 274 ++++++++++++++++++++++++++++++++++++++++++++++++++++ uri.h | 11 +++ 3 files changed, 286 insertions(+) create mode 100644 uri.c create mode 100644 uri.h diff --git a/meson.build b/meson.build index 16a42150..0bf3a4ac 100644 --- a/meson.build +++ b/meson.build @@ -130,6 +130,7 @@ executable( 'spawn.c', 'spawn.h', 'terminal.c', 'terminal.h', 'tokenize.c', 'tokenize.h', + 'uri.c', 'uri.h', 'user-notification.h', 'vt.c', 'vt.h', 'wayland.c', 'wayland.h', diff --git a/uri.c b/uri.c new file mode 100644 index 00000000..1d46faf1 --- /dev/null +++ b/uri.c @@ -0,0 +1,274 @@ +#include "uri.h" + +#include +#include +#include +#include +#include +#include + +#define LOG_MODULE "uri" +#define LOG_ENABLE_DBG 0 +#include "log.h" +#include "xmalloc.h" + +static uint8_t +nibble2hex(char c) +{ + switch (c) { + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + return c - '0'; + + case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': + return c - 'a' + 10; + + case 'A': case 'B': case 'C': case 'D': case 'E': case 'F': + return c - 'A' + 10; + } + + assert(false); + return 0; +} + +bool +uri_parse(const char *uri, size_t len, + char **scheme, char **user, char **password, char **host, + uint16_t *port, char **path, char **query, char **fragment) +{ + LOG_DBG("parse URI: \"%.*s\"", (int)len, uri); + + if (scheme != NULL) *scheme = NULL; + if (user != NULL) *user = NULL; + if (password != NULL) *password = NULL; + if (host != NULL) *host = NULL; + if (port != NULL) *port = 0; + if (path != NULL) *path = NULL; + if (query != NULL) *query = NULL; + if (fragment != NULL) *fragment = NULL; + + size_t left = len; + const char *start = uri; + const char *end = NULL; + + if ((end = memchr(start, ':', left)) == NULL) + goto err; + + size_t scheme_len = end - start; + if (scheme_len == 0) + goto err; + + if (scheme != NULL) + *scheme = xstrndup(start, scheme_len); + + LOG_DBG("scheme: \"%.*s\"", (int)scheme_len, start); + + start = end + 1; + left = len - (start - uri); + + /* Authinfo */ + if (left >= 2 && start[0] == '/' && start[1] == '/') { + start += 2; + left -= 2; + + /* [user[:pasword]@]@host[:port] */ + + /* Find beginning of path segment (required component + * following the authinfo) */ + const char *path_segment = memchr(start, '/', left); + if (path_segment == NULL) + goto err; + + size_t auth_left = path_segment - start; + + /* Do we have a user (and optionally a password)? */ + const char *user_pw_end = memchr(start, '@', auth_left); + if (user_pw_end != NULL) { + size_t user_pw_len = user_pw_end - start; + + /* Do we have a password? */ + const char *user_end = memchr(start, ':', user_pw_end - start); + if (user_end != NULL) { + size_t user_len = user_end - start; + if (user_len == 0) + goto err; + + if (user != NULL) + *user = xstrndup(start, user_len); + + const char *pw = user_end + 1; + size_t pw_len = user_pw_end - pw; + if (pw_len == 0) + goto err; + + if (password != NULL) + *password = xstrndup(pw, pw_len); + + LOG_DBG("user: \"%.*s\"", (int)user_len, start); + LOG_DBG("password: \"%.*s\"", (int)pw_len, pw); + } else { + size_t user_len = user_pw_end - start; + if (user_len == 0) + goto err; + + if (user != NULL) + *user = xstrndup(start, user_len); + + LOG_DBG("user: \"%.*s\"", (int)user_len, start); + } + + start = user_pw_end + 1; + left = len - (start - uri); + auth_left -= user_pw_len + 1; + } + + const char *host_end = memchr(start, ':', auth_left); + if (host_end != NULL) { + size_t host_len = host_end - start; + if (host != NULL) + *host = xstrndup(start, host_len); + + const char *port_str = host_end + 1; + size_t port_len = path_segment - port_str; + if (port_len == 0) + goto err; + + uint16_t _port = 0; + for (size_t i = 0; i < port_len; i++) { + if (!(port_str[i] >= '0' && port_str[i] <= '9')) + goto err; + + _port *= 10; + _port += port_str[i] - '0'; + } + + if (port != NULL) + *port = _port; + + LOG_DBG("host: \"%.*s\"", (int)host_len, start); + LOG_DBG("port: \"%.*s\" (%hu)", (int)port_len, port_str, _port); + } else { + size_t host_len = path_segment - start; + if (host != NULL) + *host = xstrndup(start, host_len); + + LOG_DBG("host: \"%.*s\"", (int)host_len, start); + } + + start = path_segment; + left = len - (start - uri); + } + + /* Do we have a query? */ + const char *query_start = memchr(start, '?', left); + const char *fragment_start = memchr(start, '#', left); + + size_t path_len = + query_start != NULL ? query_start - start : + fragment_start != NULL ? fragment_start - start : + left; + + if (path_len == 0) + goto err; + + /* Path - decode %xx encoded characters */ + if (path != NULL) { + const char *encoded = start; + char *decoded = xmalloc(path_len + 1); + char *p = decoded; + + size_t encoded_len = path_len; + size_t decoded_len = 0; + + while (true) { + /* Find next '%' */ + const char *next = memchr(encoded, '%', encoded_len); + + if (next == NULL) { + strncpy(p, encoded, encoded_len); + decoded_len += encoded_len; + p += encoded_len; + break; + } + + /* Copy everything leading up to the '%' */ + size_t prefix_len = next - encoded; + memcpy(p, encoded, prefix_len); + + p += prefix_len; + encoded_len -= prefix_len; + decoded_len += prefix_len; + + if (isxdigit(next[1]) && isxdigit(next[2])) { + *p++ = nibble2hex(next[1]) << 4 | nibble2hex(next[2]); + decoded_len++; + encoded_len -= 3; + encoded = next + 3; + } else { + *p++ = *next; + decoded_len++; + encoded_len -= 1; + encoded = next + 1; + } + } + + *p = '\0'; + *path = decoded; + + LOG_DBG("path: encoded=\"%.*s\", decoded=\"%s\"", (int)path_len, start, decoded); + } else + LOG_DBG("path: encoded=\"%.*s\", decoded=", (int)path_len, start); + + start = query_start != NULL ? query_start + 1 : fragment_start != NULL ? fragment_start + 1 : uri + len; + left = len - (start - uri); + + if (query_start != NULL) { + size_t query_len = fragment_start != NULL + ? fragment_start - start : left; + + if (query_len == 0) + goto err; + + if (query != NULL) + *query = xstrndup(start, query_len); + + LOG_DBG("query: \"%.*s\"", (int)query_len, start); + + start = fragment_start != NULL ? fragment_start + 1 : uri + len; + left = len - (start - uri); + } + + if (fragment_start != NULL) { + if (left == 0) + goto err; + + if (fragment != NULL) + *fragment = xstrndup(start, left); + + LOG_DBG("fragment: \"%.*s\"", (int)left, start); + } + + return true; + +err: + if (scheme != NULL) free(*scheme); + if (user != NULL) free(*user); + if (password != NULL) free(*password); + if (host != NULL) free(*host); + if (path != NULL) free(*path); + if (query != NULL) free(*query); + if (fragment != NULL) free(*fragment); + return false; +} + +bool +hostname_is_localhost(const char *hostname) +{ + char this_host[HOST_NAME_MAX]; + if (gethostname(this_host, sizeof(this_host)) < 0) + this_host[0] = '\0'; + + return (strcmp(hostname, "") == 0 || + strcmp(hostname, "localhost") == 0 || + strcmp(hostname, this_host) == 0); +} diff --git a/uri.h b/uri.h new file mode 100644 index 00000000..b63290c5 --- /dev/null +++ b/uri.h @@ -0,0 +1,11 @@ +#pragma once + +#include +#include +#include + +bool uri_parse(const char *uri, size_t len, + char **scheme, char **user, char **password, char **host, + uint16_t *port, char **path, char **query, char **fragment); + +bool hostname_is_localhost(const char *hostname); From cad0ae957dbbd2363f1b9bbbc7c1d8034d367bb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 28 Oct 2020 19:11:22 +0100 Subject: [PATCH 5/7] osc: use new uri_parse() to parse an OSC7 PWD URI --- osc.c | 88 +++++++++-------------------------------------------------- 1 file changed, 13 insertions(+), 75 deletions(-) diff --git a/osc.c b/osc.c index d30d761c..bf0fff7e 100644 --- a/osc.c +++ b/osc.c @@ -3,8 +3,6 @@ #include #include #include -#include -#include #define LOG_MODULE "osc" #define LOG_ENABLE_DBG 0 @@ -14,6 +12,7 @@ #include "render.h" #include "selection.h" #include "terminal.h" +#include "uri.h" #include "vt.h" #include "xmalloc.h" @@ -99,7 +98,7 @@ struct clip_context { }; static void -from_clipboard_cb(const char *text, size_t size, void *user) +from_clipboard_cb(char *text, size_t size, void *user) { struct clip_context *ctx = user; struct terminal *term = ctx->term; @@ -352,87 +351,26 @@ parse_rgb(const char *string, uint32_t *color) return true; } -static uint8_t -nibble2hex(char c) -{ - switch (c) { - case '0': case '1': case '2': case '3': case '4': - case '5': case '6': case '7': case '8': case '9': - return c - '0'; - - case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': - return c - 'a' + 10; - - case 'A': case 'B': case 'C': case 'D': case 'E': case 'F': - return c - 'A' + 10; - } - - assert(false); - return 0; -} - static void osc_set_pwd(struct terminal *term, char *string) { LOG_DBG("PWD: URI: %s", string); - if (memcmp(string, "file://", 7) != 0) - return; - string += 7; - - const char *hostname = string; - char *hostname_end = strchr(string, '/'); - if (hostname_end == NULL) - return; - - char this_host[HOST_NAME_MAX]; - if (gethostname(this_host, sizeof(this_host)) < 0) - this_host[0] = '\0'; - - /* Ignore this CWD if the hostname isn't 'localhost' or our gethostname() */ - size_t hostname_len = hostname_end - hostname; - if (strncmp(hostname, "", hostname_len) != 0 && - strncmp(hostname, "localhost", hostname_len) != 0 && - strncmp(hostname, this_host, hostname_len) != 0) - { - LOG_DBG("ignoring OSC 7: hostname mismatch: %.*s != %s", - (int)hostname_len, hostname, this_host); + char *scheme, *host, *path; + if (!uri_parse(string, strlen(string), &scheme, NULL, NULL, &host, NULL, &path, NULL, NULL)) { + LOG_ERR("OSC7: invalid URI: %s", string); return; } - /* Decode %xx encoded characters */ - const char *path = hostname_end; - char *pwd = xmalloc(strlen(path) + 1); - char *p = pwd; + if (strcmp(scheme, "file") == 0 && hostname_is_localhost(host)) { + LOG_DBG("OSC7: pwd: %s", path); + free(term->cwd); + term->cwd = path; + } else + free(path); - while (true) { - /* Find next '%' */ - const char *next = strchr(path, '%'); - - if (next == NULL) { - strcpy(p, path); - break; - } - - /* Copy everything leading up to the '%' */ - size_t prefix_len = next - path; - memcpy(p, path, prefix_len); - p += prefix_len; - - if (isxdigit(next[1]) && isxdigit(next[2])) { - *p++ = nibble2hex(next[1]) << 4 | nibble2hex(next[2]); - *p = '\0'; - path = next + 3; - } else { - *p++ = *next; - *p = '\0'; - path = next + 1; - } - } - - LOG_DBG("PWD: decoded: %s", pwd); - free(term->cwd); - term->cwd = pwd; + free(scheme); + free(host); } #if 0 From be22fefdc73c8b2ca8375a1b3f19c8d05de18e24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 28 Oct 2020 19:16:04 +0100 Subject: [PATCH 6/7] selection: add support for different mime-types Add support for `text/plain`, `text/plain;charset=utf-8` and `text/uri-list` to regular copy operations (both from clipboard and primary selection) and drag-and-drop operations. --- selection.c | 398 +++++++++++++++++++++++++++++++++++++++++----------- selection.h | 4 +- wayland.h | 17 ++- 3 files changed, 329 insertions(+), 90 deletions(-) diff --git a/selection.c b/selection.c index 4486e765..4dd241dd 100644 --- a/selection.c +++ b/selection.c @@ -21,10 +21,18 @@ #include "grid.h" #include "misc.h" #include "render.h" +#include "uri.h" #include "util.h" #include "vt.h" #include "xmalloc.h" +static const char *const mime_type_map[] = { + [DATA_OFFER_MIME_UNSET] = NULL, + [DATA_OFFER_MIME_TEXT_PLAIN] = "text/plain", + [DATA_OFFER_MIME_TEXT_UTF8] = "text/plain;charset=utf-8", + [DATA_OFFER_MIME_URI_LIST] = "text/uri-list", +}; + bool selection_enabled(const struct terminal *term, struct seat *seat) { @@ -1096,7 +1104,7 @@ text_to_clipboard(struct seat *seat, struct terminal *term, char *text, uint32_t clipboard->text = text; /* Configure source */ - wl_data_source_offer(clipboard->data_source, "text/plain;charset=utf-8"); + wl_data_source_offer(clipboard->data_source, mime_type_map[DATA_OFFER_MIME_TEXT_UTF8]); wl_data_source_add_listener(clipboard->data_source, &data_source_listener, seat); wl_data_device_set_selection(seat->data_device, clipboard->data_source, serial); @@ -1124,7 +1132,7 @@ struct clipboard_receive { struct itimerspec timeout; /* Callback data */ - void (*cb)(const char *data, size_t size, void *user); + void (*cb)(char *data, size_t size, void *user); void (*done)(void *user); void *user; }; @@ -1195,7 +1203,7 @@ fdm_receive(struct fdm *fdm, int fd, int events, void *data) break; /* Call cb while at same time replacing \r\n with \n */ - const char *p = text; + char *p = text; size_t left = count; again: for (size_t i = 0; i < left - 1; i++) { @@ -1220,7 +1228,7 @@ done: static void begin_receive_clipboard(struct terminal *term, int read_fd, - void (*cb)(const char *data, size_t size, void *user), + void (*cb)(char *data, size_t size, void *user), void (*done)(void *user), void *user) { int timeout_fd = -1; @@ -1273,7 +1281,7 @@ err: void text_from_clipboard(struct seat *seat, struct terminal *term, - void (*cb)(const char *data, size_t size, void *user), + void (*cb)(char *data, size_t size, void *user), void (*done)(void *user), void *user) { struct wl_clipboard *clipboard = &seat->clipboard; @@ -1295,7 +1303,7 @@ text_from_clipboard(struct seat *seat, struct terminal *term, /* Give write-end of pipe to other client */ wl_data_offer_receive( - clipboard->data_offer, "text/plain;charset=utf-8", write_fd); + clipboard->data_offer, mime_type_map[clipboard->mime_type], write_fd); /* Don't keep our copy of the write-end open (or we'll never get EOF) */ close(write_fd); @@ -1303,18 +1311,94 @@ text_from_clipboard(struct seat *seat, struct terminal *term, begin_receive_clipboard(term, read_fd, cb, done, user); } +struct receive_offer_context { + struct terminal *term; + + /* URI state */ + bool add_space; + struct { + char *data; + size_t sz; + size_t idx; + } buf; + + union { + struct wl_data_offer *data_offer; + struct zwp_primary_selection_offer_v1 *primary_offer; + }; +}; + static void -from_clipboard_cb(const char *data, size_t size, void *user) +receive_offer_text(char *data, size_t size, void *user) { - struct terminal *term = user; - assert(term->is_sending_paste_data); - term_paste_data_to_slave(term, data, size); + struct receive_offer_context *ctx = user; + assert(ctx->term->is_sending_paste_data); + term_paste_data_to_slave(ctx->term, data, size); } static void -from_clipboard_done(void *user) +receive_offer_uri(char *data, size_t size, void *user) { - struct terminal *term = user; + struct receive_offer_context *ctx = user; + + while (ctx->buf.idx + size > ctx->buf.sz) { + size_t new_sz = ctx->buf.sz == 0 ? size : 2 * ctx->buf.sz; + ctx->buf.data = xrealloc(ctx->buf.data, new_sz); + ctx->buf.sz = new_sz; + } + + memcpy(&ctx->buf.data[ctx->buf.idx], data, size); + ctx->buf.idx += size; + + char *start = ctx->buf.data; + char *end = NULL; + + while ((end = memchr(start, '\n', ctx->buf.idx - (start - ctx->buf.data))) != NULL) { + const size_t len = end - start; + + LOG_DBG("URI: \"%.*s\"", (int)len, start); + + char *scheme, *host, *path; + if (!uri_parse(start, len, &scheme, NULL, NULL, &host, NULL, &path, NULL, NULL)) { + LOG_ERR("drag-and-drop: invalid URI: %.*s", (int)len, start); + start = end + 1; + continue; + } + + if (ctx->add_space) + receive_offer_text(" ", 1, ctx); + ctx->add_space = true; + + receive_offer_text("'", 1, ctx); + + if (strcmp(scheme, "file") == 0 && hostname_is_localhost(host)) + receive_offer_text(path, strlen(path), ctx); + else + receive_offer_text(start, len, ctx); + + receive_offer_text("'", 1, ctx); + start = end + 1; + + free(scheme); + free(host); + free(path); + } + + const size_t ofs = start - ctx->buf.data; + const size_t left = ctx->buf.idx - ofs; + + memmove(&ctx->buf.data[0], &ctx->buf.data[ofs], left); + ctx->buf.idx = left; +} + +static void +receive_offer_done(void *user) +{ + struct receive_offer_context *ctx = user; + struct terminal *term = ctx->term; + + free(ctx->buf.data); + free(ctx); if (term->bracketed_paste) term_paste_data_to_slave(term, "\033[201~", 6); @@ -1343,8 +1427,18 @@ selection_from_clipboard(struct seat *seat, struct terminal *term, uint32_t seri if (term->bracketed_paste) term_paste_data_to_slave(term, "\033[200~", 6); + struct receive_offer_context *ctx = xmalloc(sizeof(*ctx)); + *ctx = (struct receive_offer_context) { + .term = term, + .data_offer = clipboard->data_offer, + }; + text_from_clipboard( - seat, term, &from_clipboard_cb, &from_clipboard_done, term); + seat, term, + (clipboard->mime_type == DATA_OFFER_MIME_URI_LIST + ? &receive_offer_uri + : &receive_offer_text), + &receive_offer_done, ctx); } bool @@ -1383,7 +1477,7 @@ text_to_primary(struct seat *seat, struct terminal *term, char *text, uint32_t s primary->text = text; /* Configure source */ - zwp_primary_selection_source_v1_offer(primary->data_source, "text/plain;charset=utf-8"); + zwp_primary_selection_source_v1_offer(primary->data_source, mime_type_map[DATA_OFFER_MIME_TEXT_UTF8]); zwp_primary_selection_source_v1_add_listener(primary->data_source, &primary_selection_source_listener, seat); zwp_primary_selection_device_v1_set_selection(seat->primary_selection_device, primary->data_source, serial); @@ -1407,7 +1501,7 @@ selection_to_primary(struct seat *seat, struct terminal *term, uint32_t serial) void text_from_primary( struct seat *seat, struct terminal *term, - void (*cb)(const char *data, size_t size, void *user), + void (*cb)(char *data, size_t size, void *user), void (*done)(void *user), void *user) { if (term->wl->primary_selection_device_manager == NULL) { @@ -1434,7 +1528,7 @@ text_from_primary( /* Give write-end of pipe to other client */ zwp_primary_selection_offer_v1_receive( - primary->data_offer, "text/plain;charset=utf-8", write_fd); + primary->data_offer, mime_type_map[primary->mime_type], write_fd); /* Don't keep our copy of the write-end open (or we'll never get EOF) */ close(write_fd); @@ -1461,27 +1555,157 @@ selection_from_primary(struct seat *seat, struct terminal *term) if (term->bracketed_paste) term_paste_data_to_slave(term, "\033[200~", 6); - text_from_primary(seat, term, &from_clipboard_cb, &from_clipboard_done, term); + struct receive_offer_context *ctx = xmalloc(sizeof(*ctx)); + *ctx = (struct receive_offer_context){ + .term = term, + .primary_offer = primary->data_offer, + }; + + text_from_primary( + seat, term, + (primary->mime_type == DATA_OFFER_MIME_URI_LIST + ? &receive_offer_uri + : &receive_offer_text), + &receive_offer_done, ctx); +} + +static void +select_mime_type_for_offer(const char *_mime_type, + enum data_offer_mime_type *type) +{ + enum data_offer_mime_type mime_type = DATA_OFFER_MIME_UNSET; + + /* Translate offered mime type to our mime type enum */ + for (size_t i = 0; i < ALEN(mime_type_map); i++) { + if (mime_type_map[i] == NULL) + continue; + + if (strcmp(_mime_type, mime_type_map[i]) == 0) { + mime_type = i; + break; + } + } + + LOG_DBG("mime-type: %s -> %s (offered type was %s)", + mime_type_map[*type], mime_type_map[mime_type], _mime_type); + + /* Mime-type transition; if the new mime-type is "better" than + * previously offered types, use the new type */ + + switch (mime_type) { + case DATA_OFFER_MIME_TEXT_PLAIN: + /* text/plain is our least preferred type. Only use if current + * type is unset */ + switch (*type) { + case DATA_OFFER_MIME_UNSET: + *type = mime_type; + break; + + default: + break; + } + break; + + case DATA_OFFER_MIME_TEXT_UTF8: + /* text/plain;charset=utf-8 is preferred over text/plain */ + switch (*type) { + case DATA_OFFER_MIME_UNSET: + case DATA_OFFER_MIME_TEXT_PLAIN: + *type = mime_type; + break; + + default: + break; + } + break; + + case DATA_OFFER_MIME_URI_LIST: + /* text/uri-list is always used when offered */ + *type = mime_type; + break; + + case DATA_OFFER_MIME_UNSET: + break; + } +} + +static void +data_offer_reset(struct wl_clipboard *clipboard) +{ + if (clipboard->data_offer != NULL) { + wl_data_offer_destroy(clipboard->data_offer); + clipboard->data_offer = NULL; + } + + clipboard->window = NULL; + clipboard->mime_type = DATA_OFFER_MIME_UNSET; } -#if 1 static void offer(void *data, struct wl_data_offer *wl_data_offer, const char *mime_type) { - LOG_DBG("OFFER: %s", mime_type); + struct seat *seat = data; + select_mime_type_for_offer(mime_type, &seat->clipboard.mime_type); } static void source_actions(void *data, struct wl_data_offer *wl_data_offer, uint32_t source_actions) { - LOG_DBG("ACTIONS: 0x%08x", source_actions); +#if defined(_DEBUG) && LOG_ENABLE_DBG + char actions_as_string[1024]; + size_t idx = 0; + + actions_as_string[0] = '\0'; + actions_as_string[sizeof(actions_as_string) - 1] = '\0'; + + for (size_t i = 0; i < 31; i++) { + if (((source_actions >> i) & 1) == 0) + continue; + + enum wl_data_device_manager_dnd_action action = 1 << i; + + const char *s = NULL; + + switch (action) { + case WL_DATA_DEVICE_MANAGER_DND_ACTION_NONE: s = NULL; break; + case WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY: s = "copy"; break; + case WL_DATA_DEVICE_MANAGER_DND_ACTION_MOVE: s = "move"; break; + case WL_DATA_DEVICE_MANAGER_DND_ACTION_ASK: s = "ask"; break; + } + + if (s == NULL) + continue; + + strncat(actions_as_string, s, sizeof(actions_as_string) - idx - 1); + idx += strlen(s); + strncat(actions_as_string, ", ", sizeof(actions_as_string) - idx - 1); + idx += 2; + } + + /* Strip trailing ", " */ + if (strlen(actions_as_string) > 2) + actions_as_string[strlen(actions_as_string) - 2] = '\0'; + + LOG_DBG("DnD actions: %s (0x%08x)", actions_as_string, source_actions); +#endif } static void offer_action(void *data, struct wl_data_offer *wl_data_offer, uint32_t dnd_action) { - LOG_DBG("OFFER ACTION: 0x%08x", dnd_action); +#if defined(_DEBUG) && LOG_ENABLE_DBG + const char *s = NULL; + + switch (dnd_action) { + case WL_DATA_DEVICE_MANAGER_DND_ACTION_NONE: s = ""; break; + case WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY: s = "copy"; break; + case WL_DATA_DEVICE_MANAGER_DND_ACTION_MOVE: s = "move"; break; + case WL_DATA_DEVICE_MANAGER_DND_ACTION_ASK: s = "ask"; break; + } + + LOG_DBG("DnD offer action: %s (0x%08x)", s, dnd_action); +#endif } static const struct wl_data_offer_listener data_offer_listener = { @@ -1489,57 +1713,56 @@ static const struct wl_data_offer_listener data_offer_listener = { .source_actions = &source_actions, .action = &offer_action, }; -#endif static void data_offer(void *data, struct wl_data_device *wl_data_device, - struct wl_data_offer *id) + struct wl_data_offer *offer) { - wl_data_offer_add_listener(id, &data_offer_listener, data); + struct seat *seat = data; + data_offer_reset(&seat->clipboard); + seat->clipboard.data_offer = offer; + wl_data_offer_add_listener(offer, &data_offer_listener, seat); } static void enter(void *data, struct wl_data_device *wl_data_device, uint32_t serial, struct wl_surface *surface, wl_fixed_t x, wl_fixed_t y, - struct wl_data_offer *id) + struct wl_data_offer *offer) { struct seat *seat = data; struct wayland *wayl = seat->wayl; - assert(seat->dnd_term == NULL); - - /* Remember current DnD offer */ + assert(offer == seat->clipboard.data_offer); /* Remmeber _which_ terminal the current DnD offer is targetting */ + assert(seat->clipboard.window == NULL); tll_foreach(wayl->terms, it) { if (term_surface_kind(it->item, surface) == TERM_SURF_GRID && !it->item->is_sending_paste_data) { - wl_data_offer_accept(id, serial, "text/plain;charset=utf-8"); + wl_data_offer_accept( + offer, serial, mime_type_map[seat->clipboard.mime_type]); wl_data_offer_set_actions( - id, - WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY | WL_DATA_DEVICE_MANAGER_DND_ACTION_MOVE, + offer, + WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY, WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY); - seat->dnd_term = it->item; - seat->dnd_term->window->dnd_offer = id; + seat->clipboard.window = it->item->window; return; } } /* Either terminal is alraedy busy sending paste data, or mouse * pointer isn’t over the grid */ - seat->dnd_term = NULL; - wl_data_offer_set_actions(id, 0, WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY); + seat->clipboard.window = NULL; + wl_data_offer_set_actions(offer, 0, 0); } static void leave(void *data, struct wl_data_device *wl_data_device) { struct seat *seat = data; - if (seat->dnd_term != NULL) - seat->dnd_term->window->dnd_offer = NULL; - seat->dnd_term = NULL; + seat->clipboard.window = NULL; } static void @@ -1549,18 +1772,12 @@ motion(void *data, struct wl_data_device *wl_data_device, uint32_t time, } static void -dnd_done(void *user) +receive_dnd_done(void *user) { - struct terminal *term = user; - struct wl_data_offer *offer = term->window->dnd_offer; + struct receive_offer_context *ctx = user; - assert(offer != NULL); - term->window->dnd_offer = NULL; - - wl_data_offer_finish(offer); - wl_data_offer_destroy(offer); - - from_clipboard_done(user); + wl_data_offer_finish(ctx->data_offer); + receive_offer_done(user); } static void @@ -1568,25 +1785,33 @@ drop(void *data, struct wl_data_device *wl_data_device) { struct seat *seat = data; - struct terminal *term = seat->dnd_term; - assert(term != NULL); + assert(seat->clipboard.window != NULL); + struct terminal *term = seat->clipboard.window->term; - struct wl_data_offer *offer = term->window->dnd_offer; - assert(offer != NULL); - seat->dnd_term = NULL; + struct wl_clipboard *clipboard = &seat->clipboard; + + struct receive_offer_context *ctx = xmalloc(sizeof(*ctx)); + *ctx = (struct receive_offer_context){ + .term = term, + .data_offer = clipboard->data_offer, + }; /* Prepare a pipe the other client can write its selection to us */ int fds[2]; if (pipe2(fds, O_CLOEXEC) == -1) { LOG_ERRNO("failed to create pipe"); - goto err; + free(ctx); + return; } int read_fd = fds[0]; int write_fd = fds[1]; + LOG_DBG("DnD drop: mime-type=%s", mime_type_map[clipboard->mime_type]); + /* Give write-end of pipe to other client */ - wl_data_offer_receive(offer, "text/plain;charset=utf-8", write_fd); + wl_data_offer_receive( + clipboard->data_offer, mime_type_map[clipboard->mime_type], write_fd); /* Don't keep our copy of the write-end open (or we'll never get EOF) */ close(write_fd); @@ -1596,26 +1821,24 @@ drop(void *data, struct wl_data_device *wl_data_device) if (term->bracketed_paste) term_paste_data_to_slave(term, "\033[200~", 6); - begin_receive_clipboard(term, read_fd, &from_clipboard_cb, &dnd_done, term); - return; - -err: - wl_data_offer_destroy(offer); + begin_receive_clipboard( + term, read_fd, + (clipboard->mime_type == DATA_OFFER_MIME_URI_LIST + ? &receive_offer_uri + : &receive_offer_text), + &receive_dnd_done, ctx); } static void selection(void *data, struct wl_data_device *wl_data_device, - struct wl_data_offer *id) + struct wl_data_offer *offer) { /* Selection offer from other client */ - struct seat *seat = data; - struct wl_clipboard *clipboard = &seat->clipboard; - - if (clipboard->data_offer != NULL) - wl_data_offer_destroy(clipboard->data_offer); - - clipboard->data_offer = id; + if (offer == NULL) + data_offer_reset(&seat->clipboard); + else + assert(offer == seat->clipboard.data_offer); } const struct wl_data_device_listener data_device_listener = { @@ -1627,46 +1850,55 @@ const struct wl_data_device_listener data_device_listener = { .selection = &selection, }; -#if 0 static void primary_offer(void *data, struct zwp_primary_selection_offer_v1 *zwp_primary_selection_offer, const char *mime_type) { + LOG_DBG("primary offer: %s", mime_type); + struct seat *seat = data; + select_mime_type_for_offer(mime_type, &seat->primary.mime_type); } static const struct zwp_primary_selection_offer_v1_listener primary_selection_offer_listener = { .offer = &primary_offer, }; -#endif + +static void +primary_offer_reset(struct wl_primary *primary) +{ + if (primary->data_offer != NULL) { + zwp_primary_selection_offer_v1_destroy(primary->data_offer); + primary->data_offer = NULL; + } + + primary->mime_type = DATA_OFFER_MIME_UNSET; +} static void primary_data_offer(void *data, struct zwp_primary_selection_device_v1 *zwp_primary_selection_device, struct zwp_primary_selection_offer_v1 *offer) { + struct seat *seat = data; + primary_offer_reset(&seat->primary); + seat->primary.data_offer = offer; + zwp_primary_selection_offer_v1_add_listener( + offer, &primary_selection_offer_listener, seat); } static void primary_selection(void *data, struct zwp_primary_selection_device_v1 *zwp_primary_selection_device, - struct zwp_primary_selection_offer_v1 *id) + struct zwp_primary_selection_offer_v1 *offer) { /* Selection offer from other client, for primary */ struct seat *seat = data; - struct wl_primary *primary = &seat->primary; - - if (primary->data_offer != NULL) - zwp_primary_selection_offer_v1_destroy(primary->data_offer); - - primary->data_offer = id; -#if 0 - if (id != NULL) { - zwp_primary_selection_offer_v1_add_listener( - id, &primary_selection_offer_listener, term); - } -#endif + if (offer == NULL) + primary_offer_reset(&seat->primary); + else + assert(seat->primary.data_offer == offer); } const struct zwp_primary_selection_device_v1_listener primary_selection_device_listener = { diff --git a/selection.h b/selection.h index 888b2f05..1b74efb5 100644 --- a/selection.h +++ b/selection.h @@ -67,12 +67,12 @@ bool text_to_primary( */ void text_from_clipboard( struct seat *seat, struct terminal *term, - void (*cb)(const char *data, size_t size, void *user), + void (*cb)(char *data, size_t size, void *user), void (*done)(void *user), void *user); void text_from_primary( struct seat *seat, struct terminal *term, - void (*cb)(const char *data, size_t size, void *user), + void (*cb)(char *data, size_t size, void *user), void (*dont)(void *user), void *user); void selection_start_scroll_timer( diff --git a/wayland.h b/wayland.h index bf3db76c..98830d8f 100644 --- a/wayland.h +++ b/wayland.h @@ -101,9 +101,20 @@ struct key_binding_search { enum bind_action_search action; }; +/* Mime-types we support when dealing with data offers (e.g. copy-paste, or DnD) */ +enum data_offer_mime_type { + DATA_OFFER_MIME_UNSET, + DATA_OFFER_MIME_TEXT_PLAIN, + DATA_OFFER_MIME_TEXT_UTF8, + DATA_OFFER_MIME_URI_LIST, +}; + +struct wl_window; struct wl_clipboard { + struct wl_window *window; /* For DnD */ struct wl_data_source *data_source; struct wl_data_offer *data_offer; + enum data_offer_mime_type mime_type; char *text; uint32_t serial; }; @@ -111,6 +122,7 @@ struct wl_clipboard { struct wl_primary { struct zwp_primary_selection_source_v1 *data_source; struct zwp_primary_selection_offer_v1 *data_offer; + enum data_offer_mime_type mime_type; char *text; uint32_t serial; }; @@ -205,9 +217,6 @@ struct seat { struct wl_clipboard clipboard; struct wl_primary primary; - - /* Drag ‘n’ drop */ - struct terminal *dnd_term; }; enum csd_surface { @@ -312,8 +321,6 @@ struct wl_window { struct wl_callback *frame_callback; - struct wl_data_offer *dnd_offer; - tll(const struct monitor *) on_outputs; /* Outputs we're mapped on */ bool is_configured; From bb43695426a769f6e471e6753017b3e60fb6edae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 28 Oct 2020 19:34:49 +0100 Subject: [PATCH 7/7] codespell: fix misspelled words --- selection.c | 4 ++-- uri.c | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/selection.c b/selection.c index 4dd241dd..6e0d8d5a 100644 --- a/selection.c +++ b/selection.c @@ -1734,7 +1734,7 @@ enter(void *data, struct wl_data_device *wl_data_device, uint32_t serial, assert(offer == seat->clipboard.data_offer); - /* Remmeber _which_ terminal the current DnD offer is targetting */ + /* Remember _which_ terminal the current DnD offer is targeting */ assert(seat->clipboard.window == NULL); tll_foreach(wayl->terms, it) { if (term_surface_kind(it->item, surface) == TERM_SURF_GRID && @@ -1752,7 +1752,7 @@ enter(void *data, struct wl_data_device *wl_data_device, uint32_t serial, } } - /* Either terminal is alraedy busy sending paste data, or mouse + /* Either terminal is already busy sending paste data, or mouse * pointer isn’t over the grid */ seat->clipboard.window = NULL; wl_data_offer_set_actions(offer, 0, 0); diff --git a/uri.c b/uri.c index 1d46faf1..31bbc7b6 100644 --- a/uri.c +++ b/uri.c @@ -71,7 +71,7 @@ uri_parse(const char *uri, size_t len, start += 2; left -= 2; - /* [user[:pasword]@]@host[:port] */ + /* [user[:password]@]@host[:port] */ /* Find beginning of path segment (required component * following the authinfo) */